1 module fluid.scroll;
2 
3 import std.meta;
4 import std.conv;
5 import std.algorithm;
6 
7 import fluid.node;
8 import fluid.frame;
9 import fluid.space;
10 import fluid.utils;
11 import fluid.input;
12 import fluid.style;
13 import fluid.backend;
14 import fluid.structs;
15 
16 import fluid.io.hover;
17 
18 public import fluid.scroll_input;
19 
20 
21 @safe:
22 
23 
24 alias ScrollFrame = Scrollable!Frame;
25 alias Scrollable(T : Space) = Scrollable!(T, "directionHorizontal");
26 
27 /// Create a new vertical scroll frame.
28 alias vscrollFrame = simpleConstructor!ScrollFrame;
29 
30 /// Create a new horizontal scroll frame.
31 alias hscrollFrame = simpleConstructor!(ScrollFrame, (a) {
32 
33     a.directionHorizontal = true;
34 
35 });
36 
37 /// Create a new scrollable node.
38 alias vscrollable(alias T) = simpleConstructor!(ApplyRight!(ScrollFrame, "false"), T);
39 
40 /// Create a new horizontally scrollable node.
41 alias hscrollable(alias T) = simpleConstructor!(ApplyRight!(ScrollFrame, "true"), T);
42 
43 /// Implement scrolling for the given node.
44 ///
45 /// This only supports scrolling in one axis.
46 class Scrollable(T : Node, string horizontalExpression) : T, FluidScrollable, HoverScrollable {
47 
48     HoverIO hoverIO;
49 
50     public {
51 
52         /// Scrollbar for the frame. Can be replaced with a customized one.
53         ScrollInput scrollBar;
54 
55     }
56 
57     private {
58 
59         /// minSize including the padding.
60         Vector2 paddingBoxSize;
61 
62     }
63 
64     this(T...)(T args) {
65 
66         super(args);
67         this.scrollBar = .vscrollInput(.layout!(1, "fill"));
68 
69     }
70 
71     alias opEquals = Node.opEquals;
72     override bool opEquals(const Object other) const {
73         return super.opEquals(other);
74     }
75 
76     /// Distance the node is scrolled by.
77     ref inout(float) scroll() inout {
78 
79         return scrollBar.position;
80 
81     }
82 
83     float scroll() const {
84 
85         return scrollBar.position;
86 
87     }
88 
89     float scroll(float value) {
90 
91         setScroll(value);
92         return value;
93 
94     }
95 
96     /// Check if the underlying node is horizontal.
97     private bool isHorizontal() const {
98 
99         return mixin(horizontalExpression);
100 
101     }
102 
103     /// Scroll to the beginning of the node.
104     void scrollStart() {
105 
106         scroll = 0;
107 
108     }
109 
110     /// Scroll to the end of the node, requires the node to be drawn at least once.
111     void scrollEnd() {
112 
113         scroll = scrollMax;
114 
115     }
116 
117     /// Set the scroll to a value clamped between start and end.
118     void setScroll(float value) {
119 
120         scrollBar.setScroll(value);
121 
122     }
123 
124     /// Get the maximum value this container can be scrolled to. Requires at least one draw.
125     float scrollMax() const {
126 
127         return scrollBar.scrollMax();
128 
129     }
130 
131     alias maxScroll = scrollMax;
132 
133     deprecated("shallowScrollTo with a Vector2 argument has been deprecated and will be removed in Fluid 0.8.0.")
134     Rectangle shallowScrollTo(const Node child, Vector2, Rectangle parentBox, Rectangle childBox) {
135 
136         return shallowScrollTo(child, parentBox, childBox);
137 
138     }
139 
140     /// Scroll to the given node.
141     Rectangle shallowScrollTo(const Node, Rectangle parentBox, Rectangle childBox) {
142 
143         struct Position {
144 
145             float* start;
146             float end;
147             float viewportStart, viewportEnd;
148 
149         }
150 
151         // Get the data for the node
152         scope position = isHorizontal
153             ? Position(
154                 &childBox.x, childBox.x + childBox.width,
155                 parentBox.x, parentBox.x + parentBox.width
156             )
157             : Position(
158                 &childBox.y, childBox.y + childBox.height,
159                 parentBox.y, parentBox.y + parentBox.height
160             );
161 
162         auto scrollBefore = scroll();
163 
164         // Calculate the offset
165         auto offset
166 
167             // Need to scroll towards the end
168             = *position.start > position.viewportStart && position.end > position.viewportEnd
169             ? position.end - position.viewportEnd
170 
171             // Need to scroll towards the start
172             : *position.start < position.viewportStart && position.end < position.viewportEnd
173             ? *position.start - position.viewportStart
174 
175             // Already in viewport
176             : 0;
177 
178         // Perform the scroll
179         setScroll(scroll + offset);
180 
181         // Adjust the offset
182         offset = scroll - scrollBefore;
183 
184         // Apply child position
185         *position.start -= offset;
186 
187         return childBox;
188 
189     }
190 
191     override void resizeImpl(Vector2 space) {
192 
193         assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable");
194         assert(tree !is null);
195 
196         use(hoverIO);
197 
198         /// Padding represented as a vector. This sums the padding on each axis.
199         const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum);
200 
201         /// Space with padding included
202         const paddingSpace = space + paddingVector;
203 
204         // Resize the scrollbar
205         scrollBar.isHorizontal = this.isHorizontal;
206         resizeChild(scrollBar, paddingSpace);
207 
208         /// Space without the scrollbar
209         const contentSpace = isHorizontal
210             ? space - Vector2(0, scrollBar.minSize.y)
211             : space - Vector2(scrollBar.minSize.x, 0);
212 
213         // Resize the frame while reserving some space for the scrollbar
214         super.resizeImpl(contentSpace);
215 
216         // Calculate the expected padding box size
217         paddingBoxSize = minSize + paddingVector;
218 
219         // Set scrollbar size and add the scrollbar to the result
220         if (isHorizontal) {
221 
222             scrollBar.availableSpace = paddingBoxSize.x;
223             minSize.y += scrollBar.minSize.y;
224 
225         }
226 
227         else {
228 
229             scrollBar.availableSpace = paddingBoxSize.y;
230             minSize.x += scrollBar.minSize.x;
231 
232         }
233 
234     }
235 
236     override void drawImpl(Rectangle mainOuter, Rectangle inner) {
237 
238         auto outer = mainOuter;
239         auto scrollBarRect = outer;
240 
241         scrollBar.horizontal = isHorizontal;
242 
243         // Scroll the given rectangle horizontally
244         if (isHorizontal) {
245 
246             // Calculate fake box sizes
247             outer.width = max(outer.width, paddingBoxSize.x);
248             inner = style.contentBox(outer);
249 
250             static foreach (rect; AliasSeq!(outer, inner)) {
251 
252                 // Perform the scroll
253                 rect.x -= scroll;
254 
255                 // Reduce both rects by scrollbar size
256                 rect.height -= scrollBar.minSize.y;
257 
258             }
259 
260             scrollBarRect.y += outer.height;
261             scrollBarRect.height = scrollBar.minSize.y;
262             mainOuter.height -= scrollBarRect.height;
263 
264         }
265 
266         // Vertically
267         else {
268 
269             // Calculate fake box sizes
270             outer.height = max(outer.height, paddingBoxSize.y);
271             inner = style.contentBox(outer);
272 
273             static foreach (rect; AliasSeq!(outer, inner)) {
274 
275                 // Perform the scroll
276                 rect.y -= scroll;
277 
278                 // Reduce both rects by scrollbar size
279                 rect.width -= scrollBar.minSize.x;
280 
281             }
282 
283             scrollBarRect.x += outer.width;
284             scrollBarRect.width = scrollBar.minSize.x;
285             mainOuter.width -= scrollBarRect.width;
286 
287         }
288 
289         // Draw the scrollbar
290         drawChild(scrollBar, scrollBarRect);
291 
292         // Draw the frame
293         if (canvasIO) {
294             const lastArea = canvasIO.intersectCrop(mainOuter);
295             scope (exit) canvasIO.cropArea = lastArea;
296             super.drawImpl(mainOuter, inner);
297         }
298         else {
299             super.drawImpl(mainOuter, inner);
300         }
301 
302     }
303 
304     bool canScroll(Vector2 valueVec) const {
305 
306         const speed = scrollBar.scrollSpeed;
307         const value = isHorizontal
308             ? valueVec.x
309             : valueVec.y;
310         const move = speed * value;
311         const maxMoveBackward = -scroll;
312         const maxMoveForward  = maxScroll - scroll;
313 
314         return move.clamp(maxMoveBackward, maxMoveForward) != 0;
315 
316     }
317 
318     void scrollImpl(Vector2 valueVec) {
319 
320         const speed = scrollBar.scrollSpeed;
321         const value = isHorizontal
322             ? valueVec.x
323             : valueVec.y;
324         const move = speed * value;
325         // io.deltaTime is irrelevant here
326 
327         scrollBar.setScroll(scroll + move);
328 
329     }
330 
331     bool canScroll(const HoverPointer pointer) const {
332 
333         const value = isHorizontal
334             ? pointer.scroll.x
335             : pointer.scroll.y;
336         const maxMoveBackward = -scroll;
337         const maxMoveForward  = maxScroll - scroll;
338 
339         return value.clamp(maxMoveBackward, maxMoveForward) != 0;
340 
341     }
342 
343     bool scrollImpl(HoverPointer pointer) {
344         const value = isHorizontal
345             ? pointer.scroll.x
346             : pointer.scroll.y;
347         scrollBar.setScroll(scroll + value);
348         return true;
349     }
350 
351 }