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     // TODO: move keyboard input to FluidScrollBar.
50 
51     public {
52 
53         /// Scrollbar for the frame. Can be replaced with a customized one.
54         ScrollInput scrollBar;
55 
56     }
57 
58     private {
59 
60         /// minSize including the padding.
61         Vector2 paddingBoxSize;
62 
63     }
64 
65     this(T...)(T args) {
66 
67         super(args);
68         this.scrollBar = .vscrollInput();
69 
70     }
71 
72     /// Distance the node is scrolled by.
73     @property
74     ref inout(size_t) scroll() inout { return scrollBar.position; }
75 
76     /// Check if the underlying node is horizontal.
77     private bool isHorizontal() const {
78 
79         return mixin(horizontalExpression);
80 
81     }
82 
83     /// Scroll to the beginning of the node.
84     void scrollStart() {
85 
86         scroll = 0;
87 
88     }
89 
90     /// Scroll to the end of the node, requires the node to be drawn at least once.
91     void scrollEnd() {
92 
93         scroll = scrollMax;
94 
95     }
96 
97     /// Set the scroll to a value clamped between start and end.
98     void setScroll(ptrdiff_t value) {
99 
100         scrollBar.setScroll(value);
101 
102     }
103 
104     /// Get the maximum value this container can be scrolled to. Requires at least one draw.
105     size_t scrollMax() const {
106 
107         return scrollBar.scrollMax();
108 
109     }
110 
111     static if (is(typeof(this) : FluidContainer))
112     override Rectangle shallowScrollTo(const Node, Vector2, Rectangle parentBox, Rectangle childBox) {
113 
114         struct Position {
115 
116             float* start;
117             float end;
118             float viewportStart, viewportEnd;
119 
120         }
121 
122         // Get the data for the node
123         scope position = isHorizontal
124             ? Position(
125                 &childBox.x, childBox.x + childBox.width,
126                 parentBox.x, parentBox.x + parentBox.width
127             )
128             : Position(
129                 &childBox.y, childBox.y + childBox.height,
130                 parentBox.y, parentBox.y + parentBox.height
131             );
132 
133         auto scrollBefore = scroll();
134 
135         // Calculate the offset
136         auto offset
137 
138             // Need to scroll towards the end
139             = *position.start > position.viewportStart && position.end > position.viewportEnd
140             ? to!ptrdiff_t(position.end - position.viewportEnd)
141 
142             // Need to scroll towards the start
143             : *position.start < position.viewportStart && position.end < position.viewportEnd
144             ? to!ptrdiff_t(*position.start - position.viewportStart)
145 
146             // Already in viewport
147             : 0;
148 
149         // Perform the scroll
150         setScroll(scroll.to!ptrdiff_t + offset);
151 
152         // Adjust the offset
153         offset = scroll.to!ptrdiff_t - scrollBefore;
154 
155         // Apply child position
156         *position.start += offset;
157 
158         return childBox;
159 
160     }
161 
162     override void resizeImpl(Vector2 space) {
163 
164         assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable");
165         assert(theme !is null);
166         assert(tree !is null);
167 
168         /// Padding represented as a vector. This sums the padding on each axis.
169         const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum);
170 
171         /// Space with padding included
172         const paddingSpace = space + paddingVector;
173 
174         // Resize the scrollbar
175         with (scrollBar) {
176 
177             horizontal = isHorizontal;
178             layout = .layout!(1, "fill");
179             resize(this.tree, this.theme, paddingSpace);
180 
181         }
182 
183         /// Space without the scrollbar
184         const contentSpace = isHorizontal
185             ? space - Vector2(0, scrollBar.minSize.y)
186             : space - Vector2(scrollBar.minSize.x, 0);
187 
188         // Resize the frame while reserving some space for the scrollbar
189         super.resizeImpl(contentSpace);
190 
191         // Calculate the expected padding box size
192         paddingBoxSize = minSize + paddingVector;
193 
194         // Set scrollbar size and add the scrollbar to the result
195         if (isHorizontal) {
196 
197             scrollBar.availableSpace = cast(size_t) paddingBoxSize.x;
198             minSize.y += scrollBar.minSize.y;
199 
200         }
201 
202         else {
203 
204             scrollBar.availableSpace = cast(size_t) paddingBoxSize.y;
205             minSize.x += scrollBar.minSize.x;
206 
207         }
208 
209     }
210 
211     override void drawImpl(Rectangle outer, Rectangle inner) {
212 
213         // Note: Mouse input detection is primitive, awaiting #13 and #14 to help better identify when should the mouse
214         // affect this frame.
215 
216         // This node doesn't use InputNode because it doesn't take focus, and we don't want to cause related
217         // accessibility issues. It can function perfectly without it, or at least until above note gets fixed.
218         // Then, a "FluidHoverable" interface could possibly become a thing.
219 
220         // TODO Is the above still true?
221 
222         scrollBar.horizontal = isHorizontal;
223 
224         auto scrollBarRect = outer;
225 
226         if (isHovered) inputImpl();
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     /// Implementation of mouse input
281     private void inputImpl() @trusted {
282 
283         // TODO do this via input actions somehow https://git.samerion.com/Samerion/Fluid/issues/89
284         const isPlus = isHorizontal
285             ? io.isReleased(MouseButton.scrollRight)
286             : io.isReleased(MouseButton.scrollDown);
287         const isMinus = isHorizontal
288             ? io.isReleased(MouseButton.scrollLeft)
289             : io.isReleased(MouseButton.scrollUp);
290 
291         const speed = scrollBar.scrollSpeed;
292         const move
293             = isPlus  ? +speed
294             : isMinus ? -speed
295             : 0;
296 
297         scrollBar.setScroll(scroll.to!ptrdiff_t + move.to!ptrdiff_t);
298 
299     }
300 
301 }