1 /// 2 module fluid.switch_slot; 3 4 import fluid.node; 5 import fluid.utils; 6 import fluid.style; 7 import fluid.backend; 8 9 10 @safe: 11 12 13 /// A switch slot will try each of its children and pick the first one that fits the available space. If the a node 14 /// is too large to fit, it will try the next one in the list until it finds one that matches, or the last node in the 15 /// list. 16 /// 17 /// `null` is an acceptable value, indicating that no node should be drawn. 18 alias switchSlot = simpleConstructor!SwitchSlot; 19 20 /// ditto 21 class SwitchSlot : Node { 22 23 public { 24 25 Node[] availableNodes; 26 Node node; 27 28 /// If present, this node will only be drawn in case its principal node is hidden. In case the principal node is 29 /// another `SwitchSlot`, this might be because it failed to match any non-null node. 30 Node principalNode; 31 32 } 33 34 protected { 35 36 /// Last available space assigned to this node. 37 Vector2 _availableSpace; 38 39 } 40 41 @property { 42 43 alias isHidden = typeof(super).isHidden; 44 45 override bool isHidden() const return { 46 47 // Tree is available and resized 48 if (tree && !tree.resizePending) { 49 50 // Principal node is visible, hide self 51 if (principalNode && !principalNode.isHidden) 52 return true; 53 54 // Hide if no node was chosen 55 return super.isHidden || node is null; 56 57 } 58 59 return super.isHidden; 60 61 } 62 63 } 64 65 this(Node[] nodes...) { 66 67 this.availableNodes ~= nodes; 68 69 } 70 71 /// Create a new slot that will only draw if this slot is hidden or ends up with a `null` node. 72 SwitchSlot retry(Args...)(Args args) { 73 74 auto slot = switchSlot(args); 75 slot.principalNode = this; 76 return slot; 77 78 } 79 80 override void resizeImpl(Vector2 availableSpace) { 81 82 minSize = Vector2(); 83 this.node = null; 84 _availableSpace = availableSpace; 85 86 // Try each option 87 foreach (i, node; availableNodes) { 88 89 this.node = node; 90 91 // Null node reached, stop with no minSize 92 if (node is null) return; 93 94 auto previousTree = node.tree; 95 auto previousTheme = node.theme; 96 auto previousSize = node.minSize; 97 98 node.resize(tree, theme, availableSpace); 99 100 // Stop if it fits within available space 101 if (node.minSize.x <= availableSpace.x && node.minSize.y <= availableSpace.y) break; 102 103 // Restore previous info, unless this is the last node 104 if (i+1 != availableNodes.length && previousTree) { 105 106 // Resize the node again to recursively restore old parameters 107 node.tree = null; 108 node.theme = Theme.init; 109 node.resize(previousTree, previousTheme, previousSize); 110 111 } 112 113 } 114 115 // Copy minSize 116 minSize = node.minSize; 117 118 } 119 120 override void drawImpl(Rectangle outer, Rectangle inner) { 121 122 // No node to draw, stop 123 if (node is null) return; 124 125 // Draw the node 126 node.draw(inner); 127 128 } 129 130 override bool hoveredImpl(Rectangle, Vector2) const { 131 132 return false; 133 134 } 135 136 } 137 138 unittest { 139 140 import fluid.frame; 141 142 Frame bigFrame, smallFrame; 143 int bigDrawn, smallDrawn; 144 145 auto io = new HeadlessBackend; 146 auto slot = switchSlot( 147 bigFrame = new class Frame { 148 override void resizeImpl(Vector2) { 149 minSize = Vector2(300, 300); 150 } 151 override void drawImpl(Rectangle outer, Rectangle) { 152 io.drawRectangle(outer, color!"f00"); 153 bigDrawn++; 154 } 155 }, 156 smallFrame = new class Frame { 157 override void resizeImpl(Vector2) { 158 minSize = Vector2(100, 100); 159 } 160 override void drawImpl(Rectangle outer, Rectangle) { 161 io.drawRectangle(outer, color!"0f0"); 162 smallDrawn++; 163 } 164 }, 165 ); 166 167 slot.io = io; 168 169 // By default, there should be enough space to draw the big frame 170 slot.draw(); 171 172 assert(slot.node is bigFrame); 173 assert(bigDrawn == 1); 174 assert(smallDrawn == 0); 175 176 // Reduce the viewport, this time the small frame should be drawn 177 io.nextFrame; 178 io.windowSize = Vector2(200, 200); 179 slot.draw(); 180 181 assert(slot.node is smallFrame); 182 assert(bigDrawn == 1); 183 assert(smallDrawn == 1); 184 185 // Do it again, but make it so neither fit 186 io.nextFrame; 187 io.windowSize = Vector2(50, 50); 188 slot.draw(); 189 190 // The small one should be drawn regardless 191 assert(slot.node is smallFrame); 192 assert(bigDrawn == 1); 193 assert(smallDrawn == 2); 194 195 // Unless a null node is added 196 io.nextFrame; 197 slot.availableNodes ~= null; 198 slot.updateSize(); 199 slot.draw(); 200 201 assert(slot.node is null); 202 assert(bigDrawn == 1); 203 assert(smallDrawn == 2); 204 205 // Resize to fit the big node 206 io.nextFrame; 207 io.windowSize = Vector2(400, 400); 208 slot.draw(); 209 210 assert(slot.node is bigFrame); 211 assert(bigDrawn == 2); 212 assert(smallDrawn == 2); 213 214 } 215 216 unittest { 217 218 import fluid.frame; 219 import fluid.structs; 220 221 int principalDrawn, deputyDrawn; 222 223 auto io = new HeadlessBackend; 224 auto principal = switchSlot( 225 layout!(1, "fill"), 226 new class Frame { 227 override void resizeImpl(Vector2) { 228 minSize = Vector2(200, 200); 229 } 230 override void drawImpl(Rectangle outer, Rectangle) { 231 io.drawRectangle(outer, color!"f00"); 232 principalDrawn++; 233 } 234 }, 235 null 236 ); 237 auto deputy = principal.retry( 238 layout!(1, "fill"), 239 new class Frame { 240 override void resizeImpl(Vector2 space) { 241 minSize = Vector2(50, 200); 242 } 243 override void drawImpl(Rectangle outer, Rectangle) { 244 io.drawRectangle(outer, color!"f00"); 245 deputyDrawn++; 246 } 247 } 248 ); 249 auto root = vframe( 250 layout!(1, "fill"), 251 hframe( 252 layout!(1, "fill"), 253 deputy, 254 ), 255 hframe( 256 layout!(1, "fill"), 257 principal, 258 ), 259 ); 260 261 root.io = io; 262 263 // At the default size, the principal should be preferred 264 root.draw(); 265 266 assert(principalDrawn == 1); 267 assert(deputyDrawn == 0); 268 269 // Resize the window so that the principal can't fit 270 io.nextFrame; 271 io.windowSize = Vector2(300, 300); 272 273 root.draw(); 274 275 assert(principalDrawn == 1); 276 assert(deputyDrawn == 1); 277 278 } 279 280 unittest { 281 282 import std.algorithm; 283 284 import fluid.space; 285 import fluid.structs; 286 287 SwitchSlot slot; 288 289 auto checker = new class Node { 290 291 Vector2 size; 292 Vector2[] spacesGiven; 293 294 override void resizeImpl(Vector2 space) { 295 296 spacesGiven ~= space; 297 size = minSize = Vector2(500, 200); 298 299 } 300 301 override void drawImpl(Rectangle, Rectangle) { 302 303 } 304 305 }; 306 307 auto parentSlot = switchSlot(checker, null); 308 auto childSlot = parentSlot.retry(checker); 309 310 auto root = vspace( 311 layout!"fill", 312 nullTheme, 313 314 // Two slots: child slot that gets resized earlier 315 hspace( 316 layout!"fill", 317 childSlot, 318 ), 319 320 // Parent slot that doesn't give enough space for the child to fit 321 hspace( 322 layout!"fill", 323 vspace( 324 layout!(1, "fill"), 325 parentSlot, 326 ), 327 vspace( 328 layout!(3, "fill"), 329 ), 330 ), 331 ); 332 333 root.draw(); 334 335 // The principal slot gives the least space, namely the width of the window divided by 4 336 assert(checker.spacesGiven.map!"a.x".minElement == HeadlessBackend.defaultWindowSize.x / 4); 337 338 // The window size that is accepted is equal to its size, as it was assigned by the fallback slot 339 assert(checker.spacesGiven[$-1] == checker.size); 340 341 // A total of three resizes were performed: one by the fallback, one by the parent and one, final, by the parent 342 // using previous parameters 343 assert(checker.spacesGiven.length == 3); 344 345 // The first one (which should be the child's) has the largest width given, equal to the window width 346 assert(checker.spacesGiven[0].x == HeadlessBackend.defaultWindowSize.x); 347 348 }