1 ///
2 module glui.node;
3 
4 import raylib;
5 
6 import std.math;
7 import std.traits;
8 import std..string;
9 
10 import glui.style;
11 import glui.utils;
12 import glui.structs;
13 
14 @safe:
15 
16 private interface Styleable {
17 
18     /// Reload styles for the node. Triggered when the theme is changed.
19     ///
20     /// Use `mixin DefineStyles` to generate the styles.
21     final void reloadStyles() {
22 
23         // First load what we're given
24         reloadStylesImpl();
25 
26         // Then load the defaults
27         loadDefaultStyles();
28 
29     }
30 
31     // Internal:
32 
33     protected void reloadStylesImpl();
34     protected void loadDefaultStyles();
35 
36 }
37 
38 /// Represents a Glui node.
39 abstract class GluiNode : Styleable {
40 
41     /// This node defines a single style, `style`, which also works as a default style for all other nodes. However,
42     /// rather than for that, the purpose of this style is to define the convention of `style` being the node's default,
43     /// idle style.
44     ///
45     /// It should be noted the default `style` is the only style that affects a node's sizing — as the tree would have
46     /// to be resized in case they changed and secondary styles are assumed to change frequently (for example, on
47     /// hover). In practice, resizing the tree on those changes usually ends up horrible for the user, so it's advised
48     /// to stick to constant sizing in order to not hurt the accessibility.
49     mixin DefineStyles!(
50         "style", q{ Style.init },
51     );
52 
53     public {
54 
55         /// Tree data for the node. Note: requires at least one draw before this will work.
56         LayoutTree* tree;
57 
58         /// Layout for this node.
59         Layout layout;
60 
61         /// If true, this node will be removed from the tree on the next draw.
62         bool toRemove;
63 
64     }
65 
66     /// Minimum size of the node.
67     protected auto minSize = Vector2(0, 0);
68 
69     private {
70 
71         /// If true, this node must update its size.
72         bool _requiresResize = true;
73 
74         /// If true, this node is hidden and won't be rendered.
75         bool _hidden;
76 
77         /// If true, this node is currently hovered.
78         bool _hovered;
79 
80         /// Theme of this node.
81         Theme _theme;
82 
83     }
84 
85     @property {
86 
87         /// Get the current theme.
88         pragma(inline)
89         const(Theme) theme() const { return _theme; }
90 
91         /// Set the theme.
92         const(Theme) theme(const Theme value) @trusted {
93 
94             _theme = cast(Theme) value;
95             reloadStyles();
96             return _theme;
97 
98         }
99 
100     }
101 
102     @property {
103 
104         /// Check if the node is hidden.
105         bool hidden() const { return _hidden; }
106 
107         /// Set the visibility
108         bool hidden(bool value) {
109 
110             // If changed, trigger resize
111             if (_hidden != value) updateSize();
112 
113             return _hidden = value;
114 
115         }
116 
117     }
118 
119     /// Params:
120     ///     layout = Layout for this node.
121     ///     theme = Theme of this node.
122     this(Layout layout = Layout.init, const Theme theme = null) {
123 
124         this.layout = layout;
125         this.theme  = theme;
126         this.tree   = new LayoutTree(this);
127 
128     }
129 
130     /// Ditto
131     this(const Theme theme = null, Layout layout = Layout.init) {
132 
133         this(layout, theme);
134 
135     }
136 
137     /// Ditto
138     this() {
139 
140         this(Layout.init, null);
141 
142     }
143 
144     /// Show the node.
145     final GluiNode show() {
146 
147         hidden = false;
148         return this;
149 
150     }
151 
152     /// Hide the node.
153     final GluiNode hide() {
154 
155         hidden = true;
156         return this;
157 
158     }
159 
160     /// Toggle the node's visibility.
161     final void toggleShow() { hidden = !hidden; }
162 
163     /// Remove this node from the tree before the next draw.
164     final void remove() {
165 
166         hidden = true;
167         toRemove = true;
168 
169     }
170 
171     /// Check if this node is hovered.
172     @property
173     bool hovered() const { return _hovered; }
174 
175     /// Recalculate the window size before next draw.
176     ///
177     /// Note: should be called or root; in case of children, will only work after the first draw.
178     final void updateSize() {
179 
180         tree.root._requiresResize = true;
181 
182     }
183 
184     /// Draw this node as a root node.
185     final void draw() @trusted {
186 
187         // No theme set, set the default
188         if (!theme) {
189 
190             import glui.default_theme;
191             theme = gluiDefaultTheme;
192 
193         }
194 
195         const space = Vector2(GetScreenWidth, GetScreenHeight);
196 
197         // Clear mouse hover if LMB is up
198         if (!isLMBHeld) tree.hover = null;
199 
200 
201         // Resize if required
202         if (IsWindowResized || _requiresResize) {
203 
204             resize(space);
205             _requiresResize = false;
206 
207         }
208 
209         // Draw this node
210         draw(Rectangle(0, 0, space.x, space.y));
211 
212 
213         // Set mouse cursor to match hovered node
214         if (tree.hover) {
215 
216             if (auto style = tree.hover.pickStyle) {
217 
218                 SetMouseCursor(style.mouseCursor);
219 
220             }
221 
222         }
223 
224 
225         // Note: pressed, not released; released activates input events, pressed activates focus
226         const mousePressed = IsMouseButtonPressed(MouseButton.MOUSE_LEFT_BUTTON);
227 
228         // Mouse is hovering an input node
229         if (auto hoverInput = cast(GluiFocusable) tree.hover) {
230 
231             // Pass the input to it
232             hoverInput.mouseImpl();
233 
234             // If the left mouse button is pressed down, let it have focus
235             if (mousePressed && !hoverInput.isFocused) hoverInput.focus();
236 
237         }
238 
239         // Mouse pressed over a non-focusable node, remove focus
240         else if (mousePressed) tree.focus = null;
241 
242 
243         // Pass keyboard input to the currently focused node
244         if (tree.focus) tree.keyboardHandled = tree.focus.keyboardImpl();
245         else tree.keyboardHandled = false;
246 
247     }
248 
249     /// Draw this node at specified location.
250     final protected void draw(Rectangle space) @trusted {
251 
252         // Given "space" is the amount of space we're given and what we should use at max.
253         // Within this function, we deduce how much of the space we should actually use, and align the node
254         // within the space.
255 
256         import std.algorithm : all, min, max;
257 
258         assert(!toRemove, "A toRemove child wasn't removed from container.");
259 
260         // If hidden, don't draw anything
261         if (hidden) return;
262 
263         const spaceV = Vector2(space.width, space.height);
264 
265         // No style set? Reload styles, the theme might've been set through CTFE
266         if (!style) reloadStyles();
267 
268         // Get parameters
269         const size = Vector2(
270             layout.nodeAlign[0] == NodeAlign.fill ? space.width  : min(space.width,  minSize.x),
271             layout.nodeAlign[1] == NodeAlign.fill ? space.height : min(space.height, minSize.y),
272         );
273         const position = position(space, size);
274 
275         // Calculate the margin
276         const margin = style
277             ? Rectangle(
278                 style.margin[0], style.margin[2],
279                 style.margin[0] + style.margin[1], style.margin[2] + style.margin[3]
280             )
281             : Rectangle(0, 0, 0, 0);
282 
283         // Get the rectangle this node should occupy within the given space
284         const paddingBox = Rectangle(
285             position.x + margin.x, position.y + margin.y,
286             size.x - margin.w,     size.y - margin.h,
287         );
288 
289         // Get the visible part of the padding box — so overflowed content doesn't get mouse focus
290         const visibleBox = tree.intersectScissors(paddingBox);
291 
292         // Subtract padding to get the content box.
293         const contentBox = style.contentBox(paddingBox);
294 
295         // Check if hovered
296         _hovered = hoveredImpl(visibleBox, GetMousePosition);
297 
298         // Update global hover unless mouse is being held down
299         if (_hovered && !isLMBHeld) tree.hover = this;
300 
301         assert(
302             [size.tupleof].all!isFinite,
303             format!"Node %s resulting size is invalid: %s; given space = %s, minSize = %s"(
304                 typeid(this), size, space, minSize
305             ),
306         );
307         assert(
308             [paddingBox.tupleof, contentBox.tupleof].all!isFinite,
309             format!"Node %s size is invalid: paddingBox = %s, contentBox = %s"(
310                 typeid(this), paddingBox, contentBox
311             )
312         );
313 
314         tree.pushScissors(paddingBox);
315         scope (exit) tree.popScissors();
316 
317         // Draw the node
318         drawImpl(paddingBox, contentBox);
319 
320     }
321 
322     /// Recalculate the minimum node size and update the `minSize` property.
323     /// Params:
324     ///     space = Available space.
325     protected final void resize(Vector2 space) {
326 
327         // The node is hidden, reset size
328         if (hidden) minSize = Vector2(0, 0);
329 
330         // Otherwise perform like normal
331         else {
332 
333             import std.range, std.algorithm;
334 
335             const spacingX = style ? chain(style.margin[0..2], style.padding[0..2]).sum : 0;
336             const spacingY = style ? chain(style.margin[2..4], style.padding[2..4]).sum : 0;
337 
338             // Reduce space by margins
339             space.x = max(0, space.x - spacingX);
340             space.y = max(0, space.y - spacingY);
341 
342             // Resize the node
343             resizeImpl(space);
344 
345             // Add margins
346             minSize.x += spacingX;
347             minSize.y += spacingY;
348 
349         }
350 
351         assert(
352             minSize.x.isFinite && minSize.y.isFinite,
353             format!"Node %s returned invalid minSize %s"(typeid(this), minSize)
354         );
355 
356     }
357 
358     /// Ditto
359     ///
360     /// This is the implementation of resizing to be provided by children.
361     ///
362     /// If style margins/paddings are non-zero, they are automatically subtracted from space, so they are handled
363     /// automatically.
364     protected abstract void resizeImpl(Vector2 space);
365 
366     /// Draw this node.
367     ///
368     /// Note: Instead of directly accessing `style`, use `pickStyle` to enable temporarily changing styles as visual
369     /// feedback. `resize` should still use the normal style.
370     ///
371     /// Params:
372     ///     paddingBox = Area which should be used by the node. It should include styling elements such as background,
373     ///         but no content.
374     ///     contentBox = Area which should be filled with content of the node, such as child nodes, text, etc.
375     protected abstract void drawImpl(Rectangle paddingBox, Rectangle contentBox);
376 
377     /// Check if the node is hovered.
378     ///
379     /// This will be called right before drawImpl for each node in order to determine the which node should handle mouse
380     /// input.
381     ///
382     /// If your node fills the rectangle area its given in `drawImpl`, you may use `mixin ImplHoveredRect` to implement
383     /// this automatically.
384     ///
385     /// Params:
386     ///     rect          = Area the node should be drawn in, as provided by drawImpl.
387     ///     mousePosition = Current mouse position within the window.
388     protected abstract bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const;
389 
390     protected mixin template ImplHoveredRect() {
391 
392         private import raylib : Rectangle, Vector2;
393 
394         protected override bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const {
395 
396             import glui.utils : contains;
397 
398             return rect.contains(mousePosition);
399 
400         }
401 
402     }
403 
404     /// Get the current style.
405     protected abstract const(Style) pickStyle() const;
406 
407     /// Get the node position.
408     private Vector2 position(Rectangle space, Vector2 usedSpace) const {
409 
410         float positionImpl(NodeAlign align_, lazy float spaceLeft) {
411 
412             with (NodeAlign)
413             final switch (align_) {
414 
415                 case start, fill: return 0;
416                 case center: return spaceLeft / 2;
417                 case end: return spaceLeft;
418 
419             }
420 
421         }
422 
423         return Vector2(
424             space.x + positionImpl(layout.nodeAlign[0], space.width  - usedSpace.x),
425             space.y + positionImpl(layout.nodeAlign[1], space.height - usedSpace.y),
426         );
427 
428     }
429 
430     private bool isLMBHeld() @trusted {
431 
432         const lmb = MouseButton.MOUSE_LEFT_BUTTON;
433         return IsMouseButtonDown(lmb) || IsMouseButtonReleased(lmb);
434 
435     }
436 
437     override string toString() const {
438 
439         return format!"%s(%s)"(typeid(this), layout);
440 
441     }
442 
443 }