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         /// If true, mouse focus will be disabled for this node, so mouse signals will "go through" to its parents, as
65         /// if the node wasn't there. The mouse will still detect hover like normal.
66         bool mousePass;
67 
68     }
69 
70     /// Minimum size of the node.
71     protected auto minSize = Vector2(0, 0);
72 
73     private {
74 
75         /// If true, this node must update its size.
76         bool _requiresResize = true;
77 
78         /// If true, this node is hidden and won't be rendered.
79         bool _hidden;
80 
81         /// If true, this node is currently hovered.
82         bool _hovered;
83 
84         /// If true, this node is currently disabled.
85         bool _disabled;
86 
87         /// Theme of this node.
88         Theme _theme;
89 
90     }
91 
92     @property {
93 
94         /// Get the current theme.
95         pragma(inline)
96         const(Theme) theme() const { return _theme; }
97 
98         /// Set the theme.
99         const(Theme) theme(const Theme value) @trusted {
100 
101             _theme = cast(Theme) value;
102             reloadStyles();
103             return _theme;
104 
105         }
106 
107     }
108 
109     @property {
110 
111         /// Check if the node is hidden.
112         bool hidden() const { return _hidden; }
113 
114         /// Set the visibility
115         bool hidden(bool value) {
116 
117             // If changed, trigger resize
118             if (_hidden != value) updateSize();
119 
120             return _hidden = value;
121 
122         }
123 
124     }
125 
126     /// Params:
127     ///     layout = Layout for this node.
128     ///     theme = Theme of this node.
129     this(Layout layout = Layout.init, const Theme theme = null) {
130 
131         this.layout = layout;
132         this.theme  = theme;
133 
134     }
135 
136     /// Ditto
137     this(const Theme theme = null, Layout layout = Layout.init) {
138 
139         this(layout, theme);
140 
141     }
142 
143     /// Ditto
144     this() {
145 
146         this(Layout.init, null);
147 
148     }
149 
150     /// Show the node.
151     final GluiNode show() {
152 
153         hidden = false;
154         return this;
155 
156     }
157 
158     /// Hide the node.
159     final GluiNode hide() {
160 
161         hidden = true;
162         return this;
163 
164     }
165 
166     /// Toggle the node's visibility.
167     final void toggleShow() { hidden = !hidden; }
168 
169     /// Remove this node from the tree before the next draw.
170     final void remove() {
171 
172         hidden = true;
173         toRemove = true;
174 
175     }
176 
177     /// Check if this node is hovered.
178     ///
179     /// Returns false if the node or some of its ancestors are disabled.
180     @property
181     bool hovered() const { return _hovered && !_disabled && !tree.disabledDepth; }
182 
183     /// Check if this node is disabled.
184     ref inout(bool) isDisabled() inout { return _disabled; }
185 
186     /// Check if this node is disabled.
187     deprecated("`disabled` will be removed in Glui 0.6.0. Use isDisabled instead.")
188     ref inout(bool) disabled() inout { return _disabled; }
189 
190     /// Checks if the node is disabled, either by self, or by any of its ancestors. Only works while the node is being
191     /// drawn.
192     protected bool isDisabledInherited() const { return tree.disabledDepth != 0; }
193 
194     /// Recalculate the window size before next draw.
195     final void updateSize() {
196 
197         if (tree) tree.root._requiresResize = true;
198         // Tree might be null — if so, the node will be resized regardless
199 
200     }
201 
202     /// Draw this node as a root node.
203     final void draw() @trusted {
204 
205         // No tree set
206         if (tree is null) {
207 
208             // Create one
209             tree = new LayoutTree(this);
210 
211             // Workaround for a HiDPI scissors mode glitch, which breaks Glui
212             SetWindowSize(GetScreenWidth, GetScreenHeight);
213 
214         }
215 
216         // No theme set, set the default
217         if (!theme) {
218 
219             import glui.default_theme;
220             theme = gluiDefaultTheme;
221 
222         }
223 
224         // Windows scales scissors mode regardless if we report that we support it or not
225         version (Windows) const scale = GetWindowScaleDPI;
226         else const scale = hidpiScale();
227 
228         const space = Vector2(GetScreenWidth / scale.x, GetScreenHeight / scale.y);
229 
230         // Clear mouse hover if LMB is up
231         if (!isLMBHeld) tree.hover = null;
232 
233 
234         // Resize if required
235         if (IsWindowResized || _requiresResize) {
236 
237             resize(tree, theme, space);
238             _requiresResize = false;
239 
240         }
241 
242         // Draw this node
243         draw(Rectangle(0, 0, space.x, space.y));
244 
245 
246         // Set mouse cursor to match hovered node
247         if (tree.hover) {
248 
249             if (auto style = tree.hover.pickStyle) {
250 
251                 SetMouseCursor(style.mouseCursor);
252 
253             }
254 
255         }
256 
257 
258         // Note: pressed, not released; released activates input events, pressed activates focus
259         const mousePressed = IsMouseButtonPressed(MouseButton.MOUSE_LEFT_BUTTON);
260 
261         // TODO: remove hover from disabled nodes (specifically to handle edgecase — node disabled while hovered and LMB
262         // down)
263         // TODO: move focus away from disabled nodes into neighbors along with #8
264 
265         // Mouse is hovering an input node
266         if (auto hoverInput = cast(GluiFocusable) tree.hover) {
267 
268             // Pass the input to it
269             hoverInput.mouseImpl();
270 
271             // If the left mouse button is pressed down, let it have focus
272             if (mousePressed && !hoverInput.isFocused) hoverInput.focus();
273 
274         }
275 
276         // Mouse pressed over a non-focusable node, remove focus
277         else if (mousePressed) tree.focus = null;
278 
279 
280         // Pass keyboard input to the currently focused node
281         if (tree.focus && !tree.focus.isDisabled) tree.keyboardHandled = tree.focus.keyboardImpl();
282         else tree.keyboardHandled = false;
283 
284     }
285 
286     /// Draw this node at specified location.
287     final protected void draw(Rectangle space) @trusted {
288 
289         // Given "space" is the amount of space we're given and what we should use at max.
290         // Within this function, we deduce how much of the space we should actually use, and align the node
291         // within the space.
292 
293         import std.algorithm : all, min, max;
294 
295         assert(!toRemove, "A toRemove child wasn't removed from container.");
296 
297         // If hidden, don't draw anything
298         if (hidden) return;
299 
300         const spaceV = Vector2(space.width, space.height);
301 
302         // No style set? Reload styles, the theme might've been set through CTFE
303         if (!style) reloadStyles();
304 
305         // Get parameters
306         const size = Vector2(
307             layout.nodeAlign[0] == NodeAlign.fill ? space.width  : min(space.width,  minSize.x),
308             layout.nodeAlign[1] == NodeAlign.fill ? space.height : min(space.height, minSize.y),
309         );
310         const position = position(space, size);
311 
312         // Calculate the margin
313         const margin = style
314             ? Rectangle(
315                 style.margin[0], style.margin[2],
316                 style.margin[0] + style.margin[1], style.margin[2] + style.margin[3]
317             )
318             : Rectangle(0, 0, 0, 0);
319 
320         // Get the rectangle this node should occupy within the given space
321         const paddingBox = Rectangle(
322             position.x + margin.x, position.y + margin.y,
323             size.x - margin.w,     size.y - margin.h,
324         );
325 
326         // Get the visible part of the padding box — so overflowed content doesn't get mouse focus
327         const visibleBox = tree.intersectScissors(paddingBox);
328 
329         // Subtract padding to get the content box.
330         const contentBox = style.contentBox(paddingBox);
331 
332         // Check if hovered
333         _hovered = hoveredImpl(visibleBox, GetMousePosition);
334 
335         // Update global hover unless mouse is being held down or mouse focus is disabled for this node
336         if (hovered && !isLMBHeld && !mousePass) tree.hover = this;
337 
338         assert(
339             [size.tupleof].all!isFinite,
340             format!"Node %s resulting size is invalid: %s; given space = %s, minSize = %s"(
341                 typeid(this), size, space, minSize
342             ),
343         );
344         assert(
345             [paddingBox.tupleof, contentBox.tupleof].all!isFinite,
346             format!"Node %s size is invalid: paddingBox = %s, contentBox = %s"(
347                 typeid(this), paddingBox, contentBox
348             )
349         );
350 
351         // Descending into a disabled tree
352         const incrementDisabled = isDisabled || tree.disabledDepth;
353 
354         // Count if disabled or not
355         if (incrementDisabled) tree.disabledDepth++;
356         scope (exit) if (incrementDisabled) tree.disabledDepth--;
357 
358         // Draw the node cropped
359         // Note: minSize includes margin!
360         if (minSize.x > space.width || minSize.y > space.height) {
361 
362             tree.pushScissors(paddingBox);
363             scope (exit) tree.popScissors();
364 
365             drawImpl(paddingBox, contentBox);
366 
367         }
368 
369         // Draw the node
370         else drawImpl(paddingBox, contentBox);
371 
372     }
373 
374     /// Recalculate the minimum node size and update the `minSize` property.
375     /// Params:
376     ///     tree  = The parent's tree to pass down to this node.
377     ///     theme = Theme to inherit from the parent.
378     ///     space = Available space.
379     protected final void resize(LayoutTree* tree, const Theme theme, Vector2 space)
380     in(tree, "Tree for Node.resize() must not be null.")
381     in(theme, "Theme for Node.resize() must not be null.")
382     do {
383 
384         // Inherit tree and theme
385         this.tree = tree;
386         if (this.theme is null) this.theme = theme;
387 
388         // The node is hidden, reset size
389         if (hidden) minSize = Vector2(0, 0);
390 
391         // Otherwise perform like normal
392         else {
393 
394             import std.range, std.algorithm;
395 
396             const spacingX = style ? chain(style.margin.sideX[], style.padding.sideX[]).sum : 0;
397             const spacingY = style ? chain(style.margin.sideY[], style.padding.sideY[]).sum : 0;
398 
399             // Reduce space by margins
400             space.x = max(0, space.x - spacingX);
401             space.y = max(0, space.y - spacingY);
402 
403             // Resize the node
404             resizeImpl(space);
405 
406             // Add margins
407             minSize.x += spacingX;
408             minSize.y += spacingY;
409 
410         }
411 
412         assert(
413             minSize.x.isFinite && minSize.y.isFinite,
414             format!"Node %s returned invalid minSize %s"(typeid(this), minSize)
415         );
416 
417     }
418 
419     /// Ditto
420     ///
421     /// This is the implementation of resizing to be provided by children.
422     ///
423     /// If style margins/paddings are non-zero, they are automatically subtracted from space, so they are handled
424     /// automatically.
425     protected abstract void resizeImpl(Vector2 space);
426 
427     /// Draw this node.
428     ///
429     /// Note: Instead of directly accessing `style`, use `pickStyle` to enable temporarily changing styles as visual
430     /// feedback. `resize` should still use the normal style.
431     ///
432     /// Params:
433     ///     paddingBox = Area which should be used by the node. It should include styling elements such as background,
434     ///         but no content.
435     ///     contentBox = Area which should be filled with content of the node, such as child nodes, text, etc.
436     protected abstract void drawImpl(Rectangle paddingBox, Rectangle contentBox);
437 
438     /// Check if the node is hovered.
439     ///
440     /// This will be called right before drawImpl for each node in order to determine the which node should handle mouse
441     /// input.
442     ///
443     /// If your node fills the rectangle area its given in `drawImpl`, you may use `mixin ImplHoveredRect` to implement
444     /// this automatically.
445     ///
446     /// Params:
447     ///     rect          = Area the node should be drawn in, as provided by drawImpl.
448     ///     mousePosition = Current mouse position within the window.
449     protected abstract bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const;
450 
451     protected mixin template ImplHoveredRect() {
452 
453         private import raylib : Rectangle, Vector2;
454 
455         protected override bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const {
456 
457             import glui.utils : contains;
458 
459             return rect.contains(mousePosition);
460 
461         }
462 
463     }
464 
465     /// Get the current style.
466     protected abstract const(Style) pickStyle() const;
467 
468     /// Get the node position.
469     private Vector2 position(Rectangle space, Vector2 usedSpace) const {
470 
471         float positionImpl(NodeAlign align_, lazy float spaceLeft) {
472 
473             with (NodeAlign)
474             final switch (align_) {
475 
476                 case start, fill: return 0;
477                 case center: return spaceLeft / 2;
478                 case end: return spaceLeft;
479 
480             }
481 
482         }
483 
484         return Vector2(
485             space.x + positionImpl(layout.nodeAlign[0], space.width  - usedSpace.x),
486             space.y + positionImpl(layout.nodeAlign[1], space.height - usedSpace.y),
487         );
488 
489     }
490 
491     private bool isLMBHeld() @trusted {
492 
493         const lmb = MouseButton.MOUSE_LEFT_BUTTON;
494         return IsMouseButtonDown(lmb) || IsMouseButtonReleased(lmb);
495 
496     }
497 
498     override string toString() const {
499 
500         return format!"%s(%s)"(typeid(this), layout);
501 
502     }
503 
504 }