1 module fluid.slot;
2 
3 import std.traits;
4 
5 import fluid.node;
6 import fluid.utils;
7 import fluid.style;
8 import fluid.backend;
9 import fluid.structs;
10 
11 
12 @safe:
13 
14 
15 /// A "node slot" node, which displays the node given to it. Allows safely swapping nodes in the layout by reference,
16 /// even during drawing. Useful for creating tabs and menus.
17 ///
18 /// Because NodeSlot does not inherit from T, it uses the single-parameter overload of simpleConstructor.
19 alias nodeSlot(alias T) = simpleConstructor!(NodeSlot!T);
20 
21 /// ditto
22 class NodeSlot(T : Node) : Node {
23 
24     /// NodeSlot defines its own styles, which will only apply to the slot itself, not the contents. Most of the
25     /// styling options will have no effect, but padding and margin will.
26     mixin DefineStyles;
27 
28     public {
29 
30         /// Node placed in the slot.
31         T value;
32 
33     }
34 
35     /// Create a new node slot and optionally place a node within.
36     this(NodeParams params, T node = null) {
37 
38         super(params);
39         this.value = node;
40 
41     }
42 
43     deprecated("Use (NodeParams, T node) or simpleConstructor instead") {
44 
45         /// Create a new slot and place a node in it.
46         this(Layout layout, T node) {
47 
48             this.value = node;
49             this.layout = layout;
50 
51         }
52 
53         this(T node) {
54 
55             this.value = node;
56 
57         }
58 
59         /// Create a new empty slot.
60         this(Layout layout = Layout.init) {
61 
62             this.layout = layout;
63 
64         }
65 
66     }
67 
68     /// Change the node in the slot.
69     ///
70     /// This function is a little bit more convenient than setting the value directly, as it'll mark itself as
71     /// needing an update. Additionally, it returns the slot, not the given node, so you can assign a value to a
72     /// constructed slot while adding it to the scene tree.
73     typeof(this) opAssign(T value) {
74 
75         updateSize();
76 
77         this.value = value;
78 
79         return this;
80 
81     }
82 
83     /// Remove the assigned node from the slot.
84     void clear() {
85 
86         value = null;
87         updateSize();
88 
89     }
90 
91     protected override void resizeImpl(Vector2 space) {
92 
93         minSize = Vector2();
94 
95         // Don't resize if there's no child node
96         if (!value) return;
97 
98         // Remove the value if requested
99         if (value.toRemove) {
100             value = null;
101             return;
102         }
103 
104         value.resize(tree, theme, space);
105         minSize = value.minSize;
106 
107     }
108 
109     protected override void drawImpl(Rectangle paddingBox, Rectangle contentBox) {
110 
111         if (!value) return;
112 
113         value.draw(contentBox);
114 
115     }
116 
117     protected override bool hoveredImpl(Rectangle rect, Vector2 position) const {
118 
119         if (!value) return false;
120 
121         // If the child has ignoreMouse set, we should ignore it as well
122         if (value.ignoreMouse) return false;
123 
124         // hoveredImpl may be private... uhhh
125         return (cast(const Node) value).hoveredImpl(rect, position);
126 
127     }
128 
129     override inout(Style) pickStyle() inout {
130 
131         return style;
132 
133     }
134 
135     /// Swap contents of the two slots.
136     void swapSlots(Slot : Node)(Slot other) {
137 
138         static if (is(Slot : NodeSlot!U, U)) {
139 
140             import std.format;
141             import std.algorithm;
142 
143             updateSize();
144 
145             static if (is(T == U)) {
146 
147                 swap(value, other.value);
148 
149             }
150 
151             else static if (is(T : U) || is(U : T)) {
152 
153                 auto theirs = cast(T) other.value;
154                 auto ours   = cast(U) value;
155 
156                 const canAcceptTheirs = theirs || other.value is null;
157                 const canAcceptOurs   = ours   || value is null;
158 
159                 assert(canAcceptTheirs, format!"Can't swap: This slot doesn't accept %s"(typeid(theirs)));
160                 assert(canAcceptOurs,   format!"Can't swap: Other slot doesn't accept %s"(typeid(ours)));
161 
162                 // Perform the swap
163                 value = theirs;
164                 other.value = ours;
165 
166             }
167 
168             else static assert(false, "Slots given to swapSlots are not compatible");
169 
170         }
171 
172         else static assert(false, "The other item is not a node");
173 
174     }
175 
176     unittest {
177 
178         import fluid.label;
179         import fluid.space;
180         import fluid.button;
181 
182         NodeSlot!Label slot1, slot2;
183 
184         auto io = new HeadlessBackend;
185         auto root = hspace(
186             label("Hello, "),
187             slot1 = nodeSlot!Label(.layout!"fill"),
188             label(" and "),
189             slot2 = nodeSlot!Label(.layout!"fill"),
190         );
191 
192         slot1 = label("John");
193         slot2 = button("Jane", delegate {
194 
195             slot1.swapSlots(slot2);
196 
197         });
198 
199         root.theme = nullTheme.makeTheme!q{
200             Label.styleAdd.textColor = color!"000";
201         };
202         root.io = io;
203 
204         // First frame
205         {
206             root.draw();
207 
208             assert(slot1.value.text == "John");
209             assert(slot2.value.text == "Jane");
210             assert(slot1.minSize == slot1.value.minSize);
211             assert(slot2.minSize == slot2.value.minSize);
212         }
213 
214         // Focus the second button
215         {
216             io.nextFrame;
217             io.press(KeyboardKey.up);
218 
219             root.draw();
220 
221             assert(root.tree.focus.asNode is slot2.value);
222         }
223 
224         // Press it
225         {
226             io.nextFrame;
227             io.release(KeyboardKey.up);
228             io.press(KeyboardKey.enter);
229 
230             root.draw();
231 
232             assert(slot1.value.text == "Jane");
233             assert(slot2.value.text == "John");
234             assert(slot1.minSize == slot1.value.minSize);
235             assert(slot2.minSize == slot2.value.minSize);
236         }
237 
238         // Nodes can be unassigned
239         {
240             io.nextFrame;
241             io.release(KeyboardKey.enter);
242 
243             slot1.clear();
244 
245             root.draw();
246 
247             assert(slot1.value is null);
248             assert(slot2.value.text == "John");
249             assert(slot1.minSize == Vector2(0, 0));
250             assert(slot2.minSize == slot2.value.minSize);
251         }
252 
253         // toRemove should work as well
254         {
255             io.nextFrame;
256 
257             slot2.value.remove();
258 
259             root.draw();
260 
261             assert(slot1.value is null);
262             assert(slot2.value is null);
263             assert(slot1.minSize == Vector2(0, 0));
264             assert(slot2.minSize == Vector2(0, 0));
265         }
266 
267     }
268 
269 }
270 
271 ///
272 unittest {
273 
274     import fluid;
275 
276     NodeSlot!Label slot1, slot2;
277 
278     // Slots can be empty, with no node inside
279     auto root = vspace(
280         label("Hello, "),
281         slot1 = nodeSlot!Label(.layout!"fill"),
282         label(" and "),
283         slot2 = nodeSlot!Label(.layout!"fill"),
284     );
285 
286     // Slots can be assigned other nodes
287     slot1 = label("John");
288 
289     slot2 = button("Jane", delegate {
290 
291         // Slot contents can be swapped with a single call
292         slot1.swapSlots(slot2);
293 
294     });
295 
296     // Slots can be reassigned at any time
297     slot1 = label("Joe");
298 
299     // Their values can also be removed
300     slot1.clear();
301 
302 }