1 module glui.scroll;
2 
3 import raylib;
4 
5 import std.meta;
6 import std.conv;
7 import std.algorithm;
8 
9 import glui.node;
10 import glui.frame;
11 import glui.space;
12 import glui.utils;
13 import glui.input;
14 import glui.style;
15 import glui.structs;
16 import glui.scrollbar;
17 
18 private extern(C) float GetMouseWheelMove();
19 
20 
21 @safe:
22 
23 
24 alias GluiScrollFrame = GluiScrollable!GluiFrame;
25 alias GluiScrollable(T : GluiSpace) = GluiScrollable!(T, "directionHorizontal");
26 
27 /// Create a new vertical scroll frame.
28 alias vscrollFrame = simpleConstructor!GluiScrollFrame;
29 
30 /// Create a new horizontal scroll frame.
31 alias hscrollFrame = simpleConstructor!(GluiScrollFrame, (a) {
32 
33     a.directionHorizontal = true;
34 
35 });
36 
37 /// Implement scrolling for the given node.
38 ///
39 /// This only supports scrolling in one side.
40 class GluiScrollable(T : GluiNode, string horizontalExpression) : T {
41 
42     mixin DefineStyles;
43 
44     // TODO: move keyboard input to GluiScrollBar.
45 
46     public {
47 
48         /// Scrollbar for the frame. Can be replaced with a customized one.
49         GluiScrollBar 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 = .vscrollBar();
64 
65     }
66 
67     /// Distance the node is scrolled by.
68     @property
69     ref inout(size_t) scroll() inout { return scrollBar.position; }
70 
71     /// Check if the underlying node is horizontal.
72     private bool isHorizontal() const {
73 
74         return mixin(horizontalExpression);
75 
76     }
77 
78     /// Scroll to the beginning of the node.
79     void scrollStart() {
80 
81         scroll = 0;
82 
83     }
84 
85     /// Scroll to the end of the node, requires the node to be drawn at least once.
86     void scrollEnd() {
87 
88         scroll = scrollMax;
89 
90     }
91 
92     /// Set the scroll to a value clamped between start and end.
93     void setScroll(ptrdiff_t value) {
94 
95         scrollBar.setScroll(value);
96 
97     }
98 
99     /// Get the maximum value this container can be scrolled to. Requires at least one draw.
100     size_t scrollMax() const {
101 
102         return scrollBar.scrollMax();
103 
104     }
105 
106     override void resizeImpl(Vector2 space) {
107 
108         assert(scrollBar !is null, "No scrollbar has been set for GluiScrollable");
109         assert(theme !is null);
110         assert(tree !is null);
111 
112         /// Padding represented as a vector. This sums the padding on each axis.
113         const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum);
114 
115         /// Space with padding included
116         const paddingSpace = space + paddingVector;
117 
118         // Resize the scrollbar
119         with (scrollBar) {
120 
121             horizontal = isHorizontal;
122             layout = .layout!(1, "fill");
123             resize(this.tree, this.theme, paddingSpace);
124 
125         }
126 
127         /// Space without the scrollbar
128         const contentSpace = isHorizontal
129             ? space - Vector2(0, scrollBar.minSize.y)
130             : space - Vector2(scrollBar.minSize.x, 0);
131 
132         // Resize the frame while reserving some space for the scrollbar
133         super.resizeImpl(contentSpace);
134 
135         // Calculate the expected padding box size
136         paddingBoxSize = minSize + paddingVector;
137 
138         // Set scrollbar size and add the scrollbar to the result
139         if (isHorizontal) {
140 
141             scrollBar.availableSpace = cast(size_t) paddingBoxSize.x;
142             minSize.y += scrollBar.minSize.y;
143 
144         }
145 
146         else {
147 
148             scrollBar.availableSpace = cast(size_t) paddingBoxSize.y;
149             minSize.x += scrollBar.minSize.x;
150 
151         }
152 
153     }
154 
155     override void drawImpl(Rectangle outer, Rectangle inner) {
156 
157         // Note: Mouse input detection is primitive, awaiting #13 and #14 to help better identify when should the mouse
158         // affect this frame.
159 
160         // This node doesn't use GluiInput because it doesn't take focus, and we don't want to cause related
161         // accessibility issues. It can function perfectly without it, or at least until above note gets fixed.
162         // Then, a "GluiHoverable" interface could possibly become a thing.
163 
164         // TODO Is the above still true?
165 
166         scrollBar.horizontal = isHorizontal;
167 
168         auto scrollBarRect = outer;
169 
170         if (isHovered) inputImpl();
171 
172         // Scroll the given rectangle horizontally
173         if (isHorizontal) {
174 
175             // Calculate fake box sizes
176             outer.width = max(outer.width, paddingBoxSize.x);
177             inner = style.contentBox(outer);
178 
179             static foreach (rect; AliasSeq!(outer, inner)) {
180 
181                 // Perform the scroll
182                 rect.x -= scroll;
183 
184                 // Reduce both rects by scrollbar size
185                 rect.height -= scrollBar.minSize.y;
186 
187             }
188 
189             scrollBarRect.y += outer.height;
190             scrollBarRect.height = scrollBar.minSize.y;
191 
192         }
193 
194         // Vertically
195         else {
196 
197             // Calculate fake box sizes
198             outer.height = max(outer.height, paddingBoxSize.y);
199             inner = style.contentBox(outer);
200 
201             static foreach (rect; AliasSeq!(outer, inner)) {
202 
203                 // Perform the scroll
204                 rect.y -= scroll;
205 
206                 // Reduce both rects by scrollbar size
207                 rect.width -= scrollBar.minSize.x;
208 
209             }
210 
211             scrollBarRect.x += outer.width;
212             scrollBarRect.width = scrollBar.minSize.x;
213 
214         }
215 
216         // Draw the scrollbar
217         scrollBar.draw(scrollBarRect);
218 
219         // Draw the frame
220         super.drawImpl(outer, inner);
221 
222     }
223 
224     /// Implementation of mouse input
225     private void inputImpl() @trusted {
226 
227         const float move = -GetMouseWheelMove;
228         const float totalChange = move * scrollBar.scrollSpeed;
229 
230         scrollBar.setScroll(scroll.to!ptrdiff_t + totalChange.to!ptrdiff_t);
231 
232     }
233 
234 }