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     public {
25 
26         /// Node placed in the slot.
27         T value;
28 
29     }
30 
31     /// Create a new node slot and optionally place a node within.
32     this(T node = null) {
33 
34         this.value = node;
35 
36     }
37 
38     /// Change the node in the slot.
39     ///
40     /// This function is a little bit more convenient than setting the value directly, as it'll mark itself as
41     /// needing an update. Additionally, it returns the slot, not the given node, so you can assign a value to a
42     /// constructed slot while adding it to the scene tree.
43     typeof(this) opAssign(T value) {
44 
45         updateSize();
46 
47         this.value = value;
48 
49         return this;
50 
51     }
52 
53     /// Remove the assigned node from the slot.
54     void clear() {
55 
56         value = null;
57         updateSize();
58 
59     }
60 
61     protected override void resizeImpl(Vector2 space) {
62 
63         minSize = Vector2();
64 
65         // Don't resize if there's no child node
66         if (!value) return;
67 
68         // Remove the value if requested
69         if (value.toRemove) {
70             value = null;
71             return;
72         }
73 
74         value.resize(tree, theme, space);
75         minSize = value.minSize;
76 
77     }
78 
79     protected override void drawImpl(Rectangle outer, Rectangle inner) {
80 
81         pickStyle().drawBackground(io, outer);
82 
83         if (!value) return;
84 
85         value.draw(inner);
86 
87     }
88 
89     protected override bool hoveredImpl(Rectangle rect, Vector2 position) {
90 
91         if (!value) return false;
92 
93         // If the child has ignoreMouse set, we should ignore it as well
94         if (value.ignoreMouse) return false;
95 
96         // hoveredImpl may be private... uhhh
97         return (cast(Node) value).hoveredImpl(rect, position);
98 
99     }
100 
101     /// Swap contents of the two slots.
102     void swapSlots(Slot : Node)(Slot other) {
103 
104         static if (is(Slot : NodeSlot!U, U)) {
105 
106             import std.format;
107             import std.algorithm;
108 
109             updateSize();
110 
111             static if (is(T == U)) {
112 
113                 swap(value, other.value);
114 
115             }
116 
117             else static if (is(T : U) || is(U : T)) {
118 
119                 auto theirs = cast(T) other.value;
120                 auto ours   = cast(U) value;
121 
122                 const canAcceptTheirs = theirs || other.value is null;
123                 const canAcceptOurs   = ours   || value is null;
124 
125                 assert(canAcceptTheirs, format!"Can't swap: This slot doesn't accept %s"(typeid(theirs)));
126                 assert(canAcceptOurs,   format!"Can't swap: Other slot doesn't accept %s"(typeid(ours)));
127 
128                 // Perform the swap
129                 value = theirs;
130                 other.value = ours;
131 
132             }
133 
134             else static assert(false, "Slots given to swapSlots are not compatible");
135 
136         }
137 
138         else static assert(false, "The other item is not a node");
139 
140     }
141 
142     unittest {
143 
144         import fluid.label;
145         import fluid.space;
146         import fluid.button;
147 
148         NodeSlot!Label slot1, slot2;
149 
150         auto io = new HeadlessBackend;
151         auto root = hspace(
152             label("Hello, "),
153             slot1 = nodeSlot!Label(.layout!"fill"),
154             label(" and "),
155             slot2 = nodeSlot!Label(.layout!"fill"),
156         );
157 
158         slot1 = label("John");
159         slot2 = button("Jane", delegate {
160 
161             slot1.swapSlots(slot2);
162 
163         });
164 
165         with (Rule)
166         root.theme = nullTheme.derive(
167             rule!Label(textColor = color!"000"),
168         );
169         root.io = io;
170 
171         // First frame
172         {
173             root.draw();
174 
175             assert(slot1.value.text == "John");
176             assert(slot2.value.text == "Jane");
177             assert(slot1.minSize == slot1.value.minSize);
178             assert(slot2.minSize == slot2.value.minSize);
179         }
180 
181         // Focus the second button
182         {
183             io.nextFrame;
184             io.press(KeyboardKey.up);
185 
186             root.draw();
187 
188             assert(root.tree.focus.asNode is slot2.value);
189         }
190 
191         // Press it
192         {
193             io.nextFrame;
194             io.release(KeyboardKey.up);
195             io.press(KeyboardKey.enter);
196 
197             root.draw();
198 
199             assert(slot1.value.text == "Jane");
200             assert(slot2.value.text == "John");
201             assert(slot1.minSize == slot1.value.minSize);
202             assert(slot2.minSize == slot2.value.minSize);
203         }
204 
205         // Nodes can be unassigned
206         {
207             io.nextFrame;
208             io.release(KeyboardKey.enter);
209 
210             slot1.clear();
211 
212             root.draw();
213 
214             assert(slot1.value is null);
215             assert(slot2.value.text == "John");
216             assert(slot1.minSize == Vector2(0, 0));
217             assert(slot2.minSize == slot2.value.minSize);
218         }
219 
220         // toRemove should work as well
221         {
222             io.nextFrame;
223 
224             slot2.value.remove();
225 
226             root.draw();
227 
228             assert(slot1.value is null);
229             assert(slot2.value is null);
230             assert(slot1.minSize == Vector2(0, 0));
231             assert(slot2.minSize == Vector2(0, 0));
232         }
233 
234     }
235 
236 }
237 
238 ///
239 unittest {
240 
241     import fluid;
242 
243     NodeSlot!Label slot1, slot2;
244 
245     // Slots can be empty, with no node inside
246     auto root = vspace(
247         label("Hello, "),
248         slot1 = nodeSlot!Label(.layout!"fill"),
249         label(" and "),
250         slot2 = nodeSlot!Label(.layout!"fill"),
251     );
252 
253     // Slots can be assigned other nodes
254     slot1 = label("John");
255 
256     slot2 = button("Jane", delegate {
257 
258         // Slot contents can be swapped with a single call
259         slot1.swapSlots(slot2);
260 
261     });
262 
263     // Slots can be reassigned at any time
264     slot1 = label("Joe");
265 
266     // Their values can also be removed
267     slot1.clear();
268 
269 }