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 }