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 }