1 module rpui.scroll; 2 3 import std.math; 4 import std.conv : to; 5 6 import rpui.primitives; 7 import rpui.input; 8 import rpui.events; 9 import rpui.math; 10 11 /** 12 * Describe interface for scrollable widgets, this controller computes content 13 * offsets and scroll button size and offset depending of the `contentSize` and 14 * and `visibleSize`. See how to use this controller in `rpui.widgets.panel.Panel`. 15 */ 16 struct ScrollController { 17 float buttonMaxOffset = 0; 18 float buttonMinSize = 20; 19 float buttonMaxSize; 20 bool buttonClick = false; /// If true, then user clicked button and hold it. 21 float contentMaxOffset = 0; 22 float contentSize = 0; /// Full content size. 23 float visibleSize = 0; /// Visible area size. 24 25 /// Calculated button size. 26 @property float buttonSize() { return ceil(buttonSize_); } 27 private float buttonSize_ = 0; 28 29 /// Calculated button offset. 30 @property float buttonOffset() { return ceil(buttonOffset_); } 31 private float buttonOffset_ = 0; 32 33 /// Calculated content offset. 34 @property float contentOffset() { return ceil(contentOffset_); } 35 private float contentOffset_ = 0; 36 37 private Orientation orientation; 38 private float buttonClickOffset; 39 40 /// Create controller with orientation - vertical or horizontal. 41 this(in Orientation orientation) { 42 this.orientation = orientation; 43 } 44 45 /// Calculates `buttonSize`, `buttonOffset` and `contentOffset`. 46 void pollButton(in vec2i mousePos, in vec2i mouseClickPos) { 47 const float buttonRatio = visibleSize / contentSize; 48 buttonSize_ = buttonMaxSize * buttonRatio; 49 50 if (buttonSize_ < buttonMinSize) 51 buttonSize_ = buttonMinSize; 52 53 if (!buttonClick) { 54 clampValues(); 55 return; 56 } 57 58 float delta = 0; 59 60 if (orientation == Orientation.horizontal) 61 delta = mousePos.x - mouseClickPos.x; 62 63 if (orientation == Orientation.vertical) 64 delta = mousePos.y - mouseClickPos.y; 65 66 buttonOffset_ = buttonClickOffset + delta; 67 clampValues(); 68 const float contentRatio = buttonOffset_ / (buttonMaxSize - buttonSize_); 69 contentOffset_ = contentMaxOffset * contentRatio; 70 } 71 72 /// Update parameters when widget update size. 73 void onResize() { 74 if (contentMaxOffset == 0) { 75 buttonOffset_ = 0; 76 } else { 77 const float ratio = contentOffset_ / contentMaxOffset; 78 buttonOffset_ = (buttonMaxSize - buttonSize_) * ratio; 79 } 80 81 clampValues(); 82 } 83 84 void onMouseDown(in MouseDownEvent event) { 85 buttonClickOffset = buttonOffset_; 86 } 87 88 bool addOffsetInPx(in float delta) { 89 if (visibleSize >= contentSize) 90 return false; 91 92 const float lastScrollOffset = contentOffset_; 93 contentOffset_ += delta; 94 onResize(); 95 return lastScrollOffset != contentOffset_; 96 } 97 98 bool setOffsetInPx(in float pixels) { 99 const float lastScrollOffset = contentOffset_; 100 contentOffset_ = pixels; 101 onResize(); 102 return lastScrollOffset != contentOffset_; 103 } 104 105 /// Set content offset in `percent`, value should be in 0..1 range. 106 void setOffsetInPercent(in float percent) 107 in { 108 assert(percent <= 1.0f, "percent should be in range 0..1"); 109 } 110 body { 111 contentOffset_ = contentMaxOffset * percent; 112 onResize(); 113 } 114 115 private void clampValues() { 116 buttonOffset_ = unsafeClamp(buttonOffset_, 0, buttonMaxOffset - buttonSize_); 117 contentOffset_ = unsafeClamp(contentOffset_, 0, contentMaxOffset); 118 } 119 }