1 ///
2 module fluid.field_slot;
3 
4 import fluid.node;
5 import fluid.utils;
6 import fluid.input;
7 import fluid.actions;
8 import fluid.backend;
9 
10 
11 @safe:
12 
13 
14 /// A field slot is a node meant to hold an input node along with associated nodes, like labels. It's functionally
15 /// equivalent to the [`<label>` element in HTML](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label).
16 ///
17 /// Fields expand the interactable (clickable) area of input nodes by other nodes that are placed inside the slot. For
18 /// example, in the code snippet below, if the user clicks on the "username" label, the text input underneath will gain
19 /// focus.
20 ///
21 /// ---
22 /// fieldSlot!vframe(
23 ///     label("Username"),
24 ///     textInput(),
25 /// )
26 /// ---
27 alias fieldSlot(alias node) = simpleConstructor!(FieldSlot, node);
28 
29 /// ditto
30 class FieldSlot(T : Node) : T, FluidHoverable {
31 
32     mixin makeHoverable;
33     mixin enableInputActions;
34 
35     this(Args...)(Args args) {
36         super(args);
37     }
38 
39     /// Pass focus to the field contained by this slot.
40     @(FluidInputAction.press)
41     void focus() {
42 
43         auto action = this.focusRecurseChildren();
44 
45         // Press the target when found
46         action.finished = (node) {
47             if (node) {
48                 node.runInputAction!(FluidInputAction.press);
49             }
50         };
51 
52     }
53 
54     override void drawImpl(Rectangle outer, Rectangle inner) {
55 
56         const previousHover = tree.hover;
57 
58         // Draw children
59         super.drawImpl(outer, inner);
60 
61         // Test if hover has switched to any of them
62         const isChildHovered = tree.hover !is previousHover;
63 
64         // If the node doesn't handle hover itself, take over
65         // (pun not intended)
66         if (isChildHovered && !cast(FluidHoverable) tree.hover) {
67 
68             tree.hover = this;
69 
70         }
71 
72     }
73 
74     // implements FluidHoverable
75     override bool isHovered() const {
76         return tree.hover is this
77             || super.isHovered();
78     }
79 
80     // implements FluidHoverable
81     void mouseImpl() {
82 
83     }
84 
85 }
86 
87 unittest {
88 
89     import fluid.frame;
90     import fluid.label;
91     import fluid.structs;
92     import fluid.text_input;
93 
94     TextInput input;
95 
96     auto io = new HeadlessBackend;
97     auto root = fieldSlot!vframe(
98         layout!"fill",
99         label("Hello, World!"),
100         input = textInput(),
101     );
102 
103     root.io = io;
104     root.draw();
105 
106     assert(!input.isFocused);
107 
108     // In this case, clicking anywhere should give the textInput focus
109     io.nextFrame;
110     io.mousePosition = Vector2(200, 200);
111     io.press;
112     root.draw();
113 
114     assert(root.tree.hover is root);
115 
116     // Trigger the event
117     io.nextFrame;
118     io.release;
119     root.draw();
120 
121     // Focus should be transferred once actions have been processed
122     io.nextFrame;
123     root.draw();
124 
125     assert(input.isFocused);
126 
127 }
128 
129 unittest {
130 
131     import fluid.space;
132     import fluid.label;
133     import fluid.structs;
134     import fluid.text_input;
135     import fluid.default_theme;
136 
137     Label theLabel;
138     TextInput input;
139 
140     // This time around use a vspace, so it won't trigger hover events when pressed outside
141     auto io = new HeadlessBackend;
142     auto root = fieldSlot!vspace(
143         layout!"fill",
144         nullTheme,
145         theLabel = label("Hello, World!"),
146         input = textInput(),
147     );
148 
149     root.io = io;
150     root.draw();
151 
152     assert(!input.isFocused);
153 
154     // Hover outside
155     io.nextFrame;
156     io.mousePosition = Vector2(500, 500);
157     root.draw();
158 
159     assert(root.tree.hover is null);
160 
161     // Hover the label
162     io.nextFrame;
163     io.mousePosition = Vector2(5, 5);
164     io.press;
165     root.draw();
166 
167     // The root should take the hover
168     assert(theLabel.isHovered);
169     assert(root.tree.hover is root);
170 
171     // Trigger the event
172     io.nextFrame;
173     io.release;
174     root.draw();
175 
176     // Focus should be transferred once actions have been processed
177     io.nextFrame;
178     root.draw();
179 
180     assert(input.isFocused);
181 
182 }