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