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     deprecated("shallowScrollTo with a Vector2 argument has been deprecated and will be removed in Fluid 0.8.0.")
123     Rectangle shallowScrollTo(const Node child, Vector2, Rectangle parentBox, Rectangle childBox) {
124 
125         return shallowScrollTo(child, parentBox, childBox);
126 
127     }
128 
129     /// Scroll to the given node.
130     Rectangle shallowScrollTo(const Node, Rectangle parentBox, Rectangle childBox) {
131 
132         struct Position {
133 
134             float* start;
135             float end;
136             float viewportStart, viewportEnd;
137 
138         }
139 
140         // Get the data for the node
141         scope position = isHorizontal
142             ? Position(
143                 &childBox.x, childBox.x + childBox.width,
144                 parentBox.x, parentBox.x + parentBox.width
145             )
146             : Position(
147                 &childBox.y, childBox.y + childBox.height,
148                 parentBox.y, parentBox.y + parentBox.height
149             );
150 
151         auto scrollBefore = scroll();
152 
153         // Calculate the offset
154         auto offset
155 
156             // Need to scroll towards the end
157             = *position.start > position.viewportStart && position.end > position.viewportEnd
158             ? position.end - position.viewportEnd
159 
160             // Need to scroll towards the start
161             : *position.start < position.viewportStart && position.end < position.viewportEnd
162             ? *position.start - position.viewportStart
163 
164             // Already in viewport
165             : 0;
166 
167         // Perform the scroll
168         setScroll(scroll + offset);
169 
170         // Adjust the offset
171         offset = scroll - scrollBefore;
172 
173         // Apply child position
174         *position.start -= offset;
175 
176         return childBox;
177 
178     }
179 
180     override void resizeImpl(Vector2 space) {
181 
182         assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable");
183         assert(tree !is null);
184 
185         /// Padding represented as a vector. This sums the padding on each axis.
186         const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum);
187 
188         /// Space with padding included
189         const paddingSpace = space + paddingVector;
190 
191         // Resize the scrollbar
192         scrollBar.isHorizontal = this.isHorizontal;
193         scrollBar.resize(this.tree, this.theme, paddingSpace);
194 
195         /// Space without the scrollbar
196         const contentSpace = isHorizontal
197             ? space - Vector2(0, scrollBar.minSize.y)
198             : space - Vector2(scrollBar.minSize.x, 0);
199 
200         // Resize the frame while reserving some space for the scrollbar
201         super.resizeImpl(contentSpace);
202 
203         // Calculate the expected padding box size
204         paddingBoxSize = minSize + paddingVector;
205 
206         // Set scrollbar size and add the scrollbar to the result
207         if (isHorizontal) {
208 
209             scrollBar.availableSpace = paddingBoxSize.x;
210             minSize.y += scrollBar.minSize.y;
211 
212         }
213 
214         else {
215 
216             scrollBar.availableSpace = paddingBoxSize.y;
217             minSize.x += scrollBar.minSize.x;
218 
219         }
220 
221     }
222 
223     override void drawImpl(Rectangle mainOuter, Rectangle inner) {
224 
225         auto outer = mainOuter;
226         auto scrollBarRect = outer;
227 
228         scrollBar.horizontal = isHorizontal;
229 
230         // Scroll the given rectangle horizontally
231         if (isHorizontal) {
232 
233             // Calculate fake box sizes
234             outer.width = max(outer.width, paddingBoxSize.x);
235             inner = style.contentBox(outer);
236 
237             static foreach (rect; AliasSeq!(outer, inner)) {
238 
239                 // Perform the scroll
240                 rect.x -= scroll;
241 
242                 // Reduce both rects by scrollbar size
243                 rect.height -= scrollBar.minSize.y;
244 
245             }
246 
247             scrollBarRect.y += outer.height;
248             scrollBarRect.height = scrollBar.minSize.y;
249             mainOuter.height -= scrollBarRect.width;
250 
251         }
252 
253         // Vertically
254         else {
255 
256             // Calculate fake box sizes
257             outer.height = max(outer.height, paddingBoxSize.y);
258             inner = style.contentBox(outer);
259 
260             static foreach (rect; AliasSeq!(outer, inner)) {
261 
262                 // Perform the scroll
263                 rect.y -= scroll;
264 
265                 // Reduce both rects by scrollbar size
266                 rect.width -= scrollBar.minSize.x;
267 
268             }
269 
270             scrollBarRect.x += outer.width;
271             scrollBarRect.width = scrollBar.minSize.x;
272             mainOuter.width -= scrollBarRect.width;
273 
274         }
275 
276         // Draw the scrollbar
277         scrollBar.draw(scrollBarRect);
278 
279         // Draw the frame
280         super.drawImpl(mainOuter, inner);
281 
282     }
283 
284     bool canScroll(Vector2 valueVec) const {
285 
286         const speed = scrollBar.scrollSpeed;
287         const value = isHorizontal
288             ? valueVec.x
289             : valueVec.y;
290         const move = speed * value;
291 
292         return move.to!ptrdiff_t != 0;
293 
294     }
295 
296     void scrollImpl(Vector2 valueVec) {
297 
298         const speed = scrollBar.scrollSpeed;
299         const value = isHorizontal
300             ? valueVec.x
301             : valueVec.y;
302         const move = speed * value;
303         // io.deltaTime is irrelevant here
304 
305         scrollBar.setScroll(scroll + move);
306 
307     }
308 
309 }