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