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