1 module glui.scrollbar;
2 
3 import raylib;
4 
5 import std.algorithm;
6 
7 import glui.node;
8 import glui.utils;
9 import glui.input;
10 import glui.style;
11 
12 alias vscrollBar = simpleConstructor!GluiScrollBar;
13 
14 @safe:
15 
16 GluiScrollBar hscrollBar(Args...)(Args args) {
17 
18     auto bar = vscrollBar(args);
19     bar.horizontal = true;
20     return bar;
21 
22 }
23 
24 ///
25 class GluiScrollBar : GluiInput!GluiNode {
26 
27     /// Styles defined by this node:
28     ///
29     /// `backgroundStyle` — style defined for the background part of the scrollbar,
30     ///
31     /// `pressStyle` — style to activate while the scrollbar is pressed.
32     mixin DefineStyles!(
33         "backgroundStyle", q{ Style.init },
34         "pressStyle", q{ style },
35     );
36 
37     mixin ImplHoveredRect;
38 
39     public {
40 
41         /// If true, the scrollbar will be horizontal.
42         bool horizontal;
43 
44         /// Amount of pixels the page is scrolled down.
45         size_t position;
46 
47         /// Available space to scroll.
48         ///
49         /// Note: page length, and therefore scrollbar handle length, are determined from the space occupied by the
50         /// scrollbar.
51         size_t availableSpace;
52 
53         /// Multipler of the scroll speed; applies to keyboard scroll only.
54         ///
55         /// This is actually number of pixels per mouse wheel event, as `GluiScrollable` determines mouse scroll speed
56         /// based on this.
57         enum scrollSpeed = 15.0;
58 
59     }
60 
61     protected {
62 
63         /// If true, the inner part of the scrollbar is hovered.
64         bool innerHovered;
65 
66         /// Page length as determined in drawImpl.
67         double pageLength;
68 
69         /// Length of the scrollbar as determined in drawImpl.
70         double scrollbarLength;
71 
72         /// Length of the handle.
73         double handleLength;
74 
75         /// Position of the scrollbar on the screen.
76         Vector2 scrollbarPosition;
77 
78         /// Position where the mouse grabbed the scrollbar.
79         Vector2 grabPosition;
80 
81         /// Start position of the mouse at the beginning of the grab.
82         size_t startPosition;
83 
84     }
85 
86     this(Args...)(Args args) {
87 
88         super(args);
89 
90     }
91 
92     /// Set the scroll to a value clamped between start and end.
93     void setScroll(ptrdiff_t value) {
94 
95         position = cast(size_t) value.clamp(0, scrollMax);
96 
97     }
98 
99     /// Ditto
100     void setScroll(float value) {
101 
102         position = cast(size_t) value.clamp(0, scrollMax);
103 
104     }
105 
106     /// Get the maximum value this container can be scrolled to. Requires at least one draw.
107     size_t scrollMax() const {
108 
109         return cast(size_t) max(0, availableSpace - pageLength);
110 
111     }
112 
113     /// Set the total size of the scrollbar. Will always fill the available space in the target direction.
114     override protected void resizeImpl(Vector2 space) {
115 
116         // Get minSize
117         minSize = horizontal
118             ? Vector2(space.x, 10)
119             : Vector2(10, space.y);
120 
121         // Get the expected page length
122         pageLength = horizontal
123             ? space.x + style.padding.sideX[].sum + style.margin.sideX[].sum
124             : space.y + style.padding.sideY[].sum + style.margin.sideY[].sum;
125 
126     }
127 
128     override protected void drawImpl(Rectangle paddingBox, Rectangle contentBox) @trusted {
129 
130         // Clamp the values first
131         setScroll(position);
132 
133         // Draw the background
134         backgroundStyle.drawBackground(paddingBox);
135 
136         // Calculate the size of the scrollbar
137         scrollbarPosition = Vector2(contentBox.x, contentBox.y);
138         scrollbarLength = horizontal ? contentBox.width : contentBox.height;
139         handleLength = availableSpace
140             ? max(50, scrollbarLength^^2 / availableSpace)
141             : 0;
142 
143         const handlePosition = (scrollbarLength - handleLength) * position / scrollMax;
144 
145         // Now get the size of the inner rect
146         auto innerRect = contentBox;
147 
148         if (horizontal) {
149 
150             innerRect.x += handlePosition;
151             innerRect.w  = handleLength;
152 
153         }
154 
155         else {
156 
157             innerRect.y += handlePosition;
158             innerRect.h  = handleLength;
159 
160         }
161 
162         // Check if the inner part is hovered
163         innerHovered = innerRect.contains(GetMousePosition);
164 
165         // Get the inner style
166         const innerStyle = pickStyle();
167 
168         innerStyle.drawBackground(innerRect);
169 
170     }
171 
172     override protected const(Style) pickStyle() const {
173 
174         const up = super.pickStyle();
175 
176         // The outer part is being hovered...
177         if (up is hoverStyle) {
178 
179             // Check if the inner part is
180             return innerHovered
181                 ? hoverStyle
182                 : style;
183 
184         }
185 
186         return up;
187 
188     }
189 
190     override protected void mouseImpl() @trusted {
191 
192         const triggerButton = MouseButton.MOUSE_LEFT_BUTTON;
193 
194         // Ignore if we can't scroll
195         if (availableSpace == 0) return;
196 
197         // Pressed the scrollbar
198         if (IsMouseButtonPressed(triggerButton)) {
199 
200             // Remember the grab position
201             grabPosition = GetMousePosition;
202             scope (exit) startPosition = position;
203 
204             // Didn't press the handle
205             if (!innerHovered) {
206 
207                 // Get the position
208                 const posdir = horizontal ? scrollbarPosition.x : scrollbarPosition.y;
209                 const grabdir = horizontal ? grabPosition.x : grabPosition.y;
210                 const screenPos = grabdir - posdir - handleLength/2;
211 
212                 // Move it to this position
213                 setScroll(screenPos * availableSpace / scrollbarLength);
214 
215             }
216 
217         }
218 
219         // Mouse is held down
220         else if (IsMouseButtonDown(triggerButton)) {
221 
222             const mouse = GetMousePosition;
223 
224             const float move = horizontal
225                 ? mouse.x - grabPosition.x
226                 : mouse.y - grabPosition.y;
227 
228             // Move the scrollbar
229             setScroll(startPosition + move * availableSpace / scrollbarLength);
230 
231         }
232 
233     }
234 
235     override protected bool keyboardImpl() @trusted {
236 
237         const plusKey = horizontal
238             ? KeyboardKey.KEY_RIGHT
239             : KeyboardKey.KEY_DOWN;
240         const minusKey = horizontal
241             ? KeyboardKey.KEY_LEFT
242             : KeyboardKey.KEY_UP;
243 
244         const arrowSpeed = scrollSpeed * 20 * GetFrameTime;
245         const pageSpeed = scrollbarLength * 3/4;
246 
247         const move = IsKeyPressed(KeyboardKey.KEY_PAGE_DOWN) ? +pageSpeed
248             : IsKeyPressed(KeyboardKey.KEY_PAGE_UP) ? -pageSpeed
249             : IsKeyDown(plusKey) ? +arrowSpeed
250             : IsKeyDown(minusKey) ? -arrowSpeed
251             : 0;
252 
253         if (move != 0) {
254 
255             setScroll(position + move);
256 
257             if (changed) changed();
258 
259             return true;
260 
261         }
262 
263         return false;
264 
265     }
266 
267 }