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 pixel 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         /// Length of the scrollbar as determined in drawImpl.
67         double scrollbarLength;
68 
69         /// Length of the handle.
70         double handleLength;
71 
72         /// Position of the scrollbar on the screen.
73         Vector2 scrollbarPosition;
74 
75         /// Position where the mouse grabbed the scrollbar.
76         Vector2 grabPosition;
77 
78         /// Start position of the mouse at the beginning of the grab.
79         size_t startPosition;
80 
81     }
82 
83     this(Args...)(Args args) {
84 
85         super(args);
86 
87     }
88 
89     /// Set the scroll to a value clamped between start and end.
90     void setScroll(ptrdiff_t value) {
91 
92         position = cast(size_t) value.clamp(0, scrollMax);
93 
94     }
95 
96     /// Ditto
97     void setScroll(float value) {
98 
99         position = cast(size_t) value.clamp(0, scrollMax);
100 
101     }
102 
103     /// Get the maximum value this container can be scrolled to. Requires at least one draw.
104     size_t scrollMax() const {
105 
106         return cast(size_t) max(0, availableSpace - scrollbarLength);
107 
108     }
109 
110     /// Set the total size of the scrollbar. Will always fill the available space in the target direction.
111     override protected void resizeImpl(Vector2 space) {
112 
113         minSize = horizontal
114             ? Vector2(space.x, 10)
115             : Vector2(10, space.y);
116 
117     }
118 
119     override protected void drawImpl(Rectangle paddingBox, Rectangle contentBox) @trusted {
120 
121         setScroll(position);
122 
123         // Draw the background
124         backgroundStyle.drawBackground(paddingBox);
125 
126         // Calculate the size of the scrollbar
127         scrollbarPosition = Vector2(contentBox.x, contentBox.y);
128         scrollbarLength = horizontal ? contentBox.width : contentBox.height;
129         handleLength = availableSpace
130             ? max(50, scrollbarLength^^2 / availableSpace)
131             : 0;
132 
133         const handlePosition = (scrollbarLength - handleLength) * position / scrollMax;
134 
135         // Now get the size of the inner rect
136         auto innerRect = contentBox;
137 
138         if (horizontal) {
139 
140             innerRect.x += handlePosition;
141             innerRect.w  = handleLength;
142 
143         }
144 
145         else {
146 
147             innerRect.y += handlePosition;
148             innerRect.h  = handleLength;
149 
150         }
151 
152         // Check if the inner part is hovered
153         innerHovered = innerRect.contains(GetMousePosition);
154 
155         // Get the inner style
156         const innerStyle = pickStyle();
157 
158         innerStyle.drawBackground(innerRect);
159 
160     }
161 
162     override protected const(Style) pickStyle() const {
163 
164         const up = super.pickStyle();
165 
166         // The outer part is being hovered...
167         if (up is hoverStyle) {
168 
169             // Check if the inner part is
170             return innerHovered
171                 ? hoverStyle
172                 : style;
173 
174         }
175 
176         return up;
177 
178     }
179 
180     override protected void mouseImpl() @trusted {
181 
182         const triggerButton = MouseButton.MOUSE_LEFT_BUTTON;
183 
184         // Ignore if we can't scroll
185         if (availableSpace == 0) return;
186 
187         // Pressed the scrollbar
188         if (IsMouseButtonPressed(triggerButton)) {
189 
190             // Remember the grab position
191             grabPosition = GetMousePosition;
192             scope (exit) startPosition = position;
193 
194             // Didn't press the handle
195             if (!innerHovered) {
196 
197                 // Get the position
198                 const posdir = horizontal ? scrollbarPosition.x : scrollbarPosition.y;
199                 const grabdir = horizontal ? grabPosition.x : grabPosition.y;
200                 const screenPos = grabdir - posdir - handleLength/2;
201 
202                 // Move it to this position
203                 setScroll(screenPos * availableSpace / scrollbarLength);
204 
205             }
206 
207         }
208 
209         // Mouse is held down
210         else if (IsMouseButtonDown(triggerButton)) {
211 
212             const mouse = GetMousePosition;
213 
214             const float move = horizontal
215                 ? mouse.x - grabPosition.x
216                 : mouse.y - grabPosition.y;
217 
218             // Move the scrollbar
219             setScroll(startPosition + move * availableSpace / scrollbarLength);
220 
221         }
222 
223     }
224 
225     override protected bool keyboardImpl() @trusted {
226 
227         const plusKey = horizontal
228             ? KeyboardKey.KEY_RIGHT
229             : KeyboardKey.KEY_DOWN;
230         const minusKey = horizontal
231             ? KeyboardKey.KEY_LEFT
232             : KeyboardKey.KEY_UP;
233 
234         const arrowSpeed = scrollSpeed * 20 * GetFrameTime;
235         const pageSpeed = scrollbarLength * 3/4;
236 
237         const move = IsKeyPressed(KeyboardKey.KEY_PAGE_DOWN) ? +pageSpeed
238             : IsKeyPressed(KeyboardKey.KEY_PAGE_UP) ? -pageSpeed
239             : IsKeyDown(plusKey) ? +arrowSpeed
240             : IsKeyDown(minusKey) ? -arrowSpeed
241             : 0;
242 
243         if (move != 0) {
244 
245             setScroll(position + move);
246 
247             if (changed) changed();
248 
249             return true;
250 
251         }
252 
253         return false;
254 
255     }
256 
257 }