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.inheritTheme(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 @("SwitchSlot works") 139 unittest { 140 141 import fluid.frame; 142 143 Frame bigFrame, smallFrame; 144 int bigDrawn, smallDrawn; 145 146 auto io = new HeadlessBackend; 147 auto slot = switchSlot( 148 bigFrame = new class Frame { 149 override void resizeImpl(Vector2) { 150 minSize = Vector2(300, 300); 151 } 152 override void drawImpl(Rectangle outer, Rectangle) { 153 io.drawRectangle(outer, color!"f00"); 154 bigDrawn++; 155 } 156 }, 157 smallFrame = new class Frame { 158 override void resizeImpl(Vector2) { 159 minSize = Vector2(100, 100); 160 } 161 override void drawImpl(Rectangle outer, Rectangle) { 162 io.drawRectangle(outer, color!"0f0"); 163 smallDrawn++; 164 } 165 }, 166 ); 167 168 slot.io = io; 169 170 // By default, there should be enough space to draw the big frame 171 slot.draw(); 172 173 assert(slot.node is bigFrame); 174 assert(bigDrawn == 1); 175 assert(smallDrawn == 0); 176 177 // Reduce the viewport, this time the small frame should be drawn 178 io.nextFrame; 179 io.windowSize = Vector2(200, 200); 180 slot.draw(); 181 182 assert(slot.node is smallFrame); 183 assert(bigDrawn == 1); 184 assert(smallDrawn == 1); 185 186 // Do it again, but make it so neither fit 187 io.nextFrame; 188 io.windowSize = Vector2(50, 50); 189 slot.draw(); 190 191 // The small one should be drawn regardless 192 assert(slot.node is smallFrame); 193 assert(bigDrawn == 1); 194 assert(smallDrawn == 2); 195 196 // Unless a null node is added 197 io.nextFrame; 198 slot.availableNodes ~= null; 199 slot.updateSize(); 200 slot.draw(); 201 202 assert(slot.node is null); 203 assert(bigDrawn == 1); 204 assert(smallDrawn == 2); 205 206 // Resize to fit the big node 207 io.nextFrame; 208 io.windowSize = Vector2(400, 400); 209 slot.draw(); 210 211 assert(slot.node is bigFrame); 212 assert(bigDrawn == 2); 213 assert(smallDrawn == 2); 214 215 } 216 217 unittest { 218 219 import fluid.frame; 220 import fluid.structs; 221 222 int principalDrawn, deputyDrawn; 223 224 auto io = new HeadlessBackend; 225 auto principal = switchSlot( 226 layout!(1, "fill"), 227 new class Frame { 228 override void resizeImpl(Vector2) { 229 minSize = Vector2(200, 200); 230 } 231 override void drawImpl(Rectangle outer, Rectangle) { 232 io.drawRectangle(outer, color!"f00"); 233 principalDrawn++; 234 } 235 }, 236 null 237 ); 238 auto deputy = principal.retry( 239 layout!(1, "fill"), 240 new class Frame { 241 override void resizeImpl(Vector2 space) { 242 minSize = Vector2(50, 200); 243 } 244 override void drawImpl(Rectangle outer, Rectangle) { 245 io.drawRectangle(outer, color!"f00"); 246 deputyDrawn++; 247 } 248 } 249 ); 250 auto root = vframe( 251 layout!(1, "fill"), 252 hframe( 253 layout!(1, "fill"), 254 deputy, 255 ), 256 hframe( 257 layout!(1, "fill"), 258 principal, 259 ), 260 ); 261 262 root.io = io; 263 264 // At the default size, the principal should be preferred 265 root.draw(); 266 267 assert(principalDrawn == 1); 268 assert(deputyDrawn == 0); 269 270 // Resize the window so that the principal can't fit 271 io.nextFrame; 272 io.windowSize = Vector2(300, 300); 273 274 root.draw(); 275 276 assert(principalDrawn == 1); 277 assert(deputyDrawn == 1); 278 279 } 280 281 unittest { 282 283 import std.algorithm; 284 285 import fluid.space; 286 import fluid.structs; 287 288 SwitchSlot slot; 289 290 auto checker = new class Node { 291 292 Vector2 size; 293 Vector2[] spacesGiven; 294 295 override void resizeImpl(Vector2 space) { 296 297 spacesGiven ~= space; 298 size = minSize = Vector2(500, 200); 299 300 } 301 302 override void drawImpl(Rectangle, Rectangle) { 303 304 } 305 306 }; 307 308 auto parentSlot = switchSlot(checker, null); 309 auto childSlot = parentSlot.retry(checker); 310 311 auto root = vspace( 312 layout!"fill", 313 nullTheme, 314 315 // Two slots: child slot that gets resized earlier 316 hspace( 317 layout!"fill", 318 childSlot, 319 ), 320 321 // Parent slot that doesn't give enough space for the child to fit 322 hspace( 323 layout!"fill", 324 vspace( 325 layout!(1, "fill"), 326 parentSlot, 327 ), 328 vspace( 329 layout!(3, "fill"), 330 ), 331 ), 332 ); 333 334 root.draw(); 335 336 // The principal slot gives the least space, namely the width of the window divided by 4 337 assert(checker.spacesGiven.map!"a.x".minElement == HeadlessBackend.defaultWindowSize.x / 4); 338 339 // The window size that is accepted is equal to its size, as it was assigned by the fallback slot 340 assert(checker.spacesGiven[$-1] == checker.size); 341 342 // A total of three resizes were performed: one by the fallback, one by the parent and one, final, by the parent 343 // using previous parameters 344 assert(checker.spacesGiven.length == 3); 345 346 // The first one (which should be the child's) has the largest width given, equal to the window width 347 assert(checker.spacesGiven[0].x == HeadlessBackend.defaultWindowSize.x); 348 349 }