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 }