1 module nodes.drag_slot; 2 3 import fluid; 4 5 @safe: 6 7 alias resizable = nodeBuilder!Resizable; 8 9 class Resizable : Node { 10 11 Vector2 size; 12 13 this(Vector2 size) { 14 this.size = size; 15 } 16 17 override void resizeImpl(Vector2) { 18 minSize = size; 19 } 20 21 override void drawImpl(Rectangle, Rectangle) { 22 23 } 24 25 override IsOpaque inBoundsImpl(Rectangle, Rectangle, Vector2) { 26 return IsOpaque.no; 27 } 28 29 } 30 31 Theme testTheme; 32 33 const frameColor = color("#fdc798"); 34 35 static this() { 36 import fluid.theme; 37 testTheme = nullTheme.derive( 38 rule!Frame( 39 backgroundColor = frameColor, 40 ), 41 ); 42 } 43 44 @("DragSlot ignores gap if the handle is hidden") 45 unittest { 46 47 import std.algorithm; 48 import fluid.theme; 49 50 auto theme = nullTheme.derive( 51 rule!DragSlot(gap = 4), 52 ); 53 auto content = label("a"); 54 auto slot = dragSlot(content); 55 auto root = testSpace(theme, slot); 56 slot.handle.hide(); 57 root.draw(); 58 assert(slot.getMinSize == content.getMinSize); 59 root.drawAndAssert( 60 content.drawsImage(content.text.texture.chunks[0].image) 61 .at(0, 0) 62 ); 63 64 } 65 66 @("DragSlot can be dragged") 67 unittest { 68 69 auto content = label(.ignoreMouse, "a"); 70 auto slot = dragSlot(.nullTheme, content); 71 auto hover = hoverChain(); 72 auto root = chain( 73 hover, 74 overlayChain(), 75 slot 76 ); 77 78 root.draw(); 79 hover.point(4, 4) 80 .then((a) { 81 assert(a.isHovered(slot)); 82 assert(!slot.dragAction); 83 a.press(false); 84 return a.move(100, 100); 85 }) 86 .then((a) { 87 a.press(false); 88 assert(a.isHovered(slot)); 89 assert(slot.dragAction.offset == Vector2(96, 96)); 90 return a.move(50, -50); 91 }) 92 .then((a) { 93 a.press(false); 94 assert(slot.dragAction.offset == Vector2(46, -54)); 95 return root.nextFrame; 96 }) 97 .runWhileDrawing(root); 98 99 assert(!slot.dragAction); 100 101 } 102 103 @("DragSlot allows the dragged node to be resized while dragged") 104 unittest { 105 106 auto content = resizable(Vector2(10, 10)); 107 auto slot = dragSlot(.nullTheme, content); 108 auto hover = hoverChain(); 109 auto root = chain( 110 hover, 111 overlayChain(), 112 slot 113 ); 114 115 root.draw(); 116 hover.point(5, 5) 117 .then((a) { 118 assert(a.isHovered(slot)); 119 assert(!slot.dragAction); 120 assert(content.getMinSize == Vector2(10, 10)); 121 a.press(false); 122 return a.move(105, 5); 123 }) 124 125 // Resize the node 126 .then((a) { 127 a.press(false); 128 content.size = Vector2(0, 0); 129 content.updateSize(); 130 assert(slot.dragAction.offset == Vector2(100, 0)); 131 assert(content.getMinSize == Vector2(10, 10)); 132 return a.move(205, 5); 133 }) 134 .then((a) { 135 a.press(false); 136 assert(slot.dragAction.offset == Vector2(200, 0)); 137 assert(content.getMinSize == Vector2(0, 0)); 138 return a.move(305, 5); 139 }) 140 .then((a) { 141 a.press(false); 142 assert(slot.dragAction.offset == Vector2(300, 0)); 143 assert(content.getMinSize == Vector2(0, 0)); 144 return root.nextFrame; 145 }) 146 .runWhileDrawing(root); 147 148 assert(!slot.dragAction); 149 assert(content.getMinSize == Vector2(0, 0)); 150 151 } 152 153 @("DragSlot contents can load I/O systems while dragged") 154 unittest { 155 156 static class IOTracker : Node { 157 158 HoverIO hoverIO; 159 CanvasIO canvasIO; 160 161 override void resizeImpl(Vector2) { 162 use(hoverIO); 163 use(canvasIO); 164 } 165 166 override void drawImpl(Rectangle, Rectangle) { 167 168 } 169 170 } 171 172 alias ioTracker = nodeBuilder!IOTracker; 173 174 auto slot = dragSlot(); 175 auto overlay = overlayChain(); 176 auto hover = hoverChain(); 177 auto root = testSpace( 178 chain( 179 focusChain(), 180 hover, 181 overlay, 182 slot, 183 ) 184 ); 185 186 root.drawAndAssert( 187 overlay.drawsChild(slot), 188 ); 189 assert(slot.hoverIO.opEquals(hover)); 190 auto action = hover.point(0, 0); 191 slot.drag(action.pointer); 192 assert(slot.dragAction); 193 root.drawAndAssert( 194 overlay.drawsChild(slot.overlay), 195 slot.isDrawn, 196 ); 197 root.drawAndAssertFailure( 198 overlay.drawsChild(slot), 199 ); 200 assert(slot.dragAction); 201 assert(slot.hoverIO.opEquals(hover)); 202 203 // Place the tracker in the slot, continue dragging 204 auto tracker = ioTracker(); 205 slot = tracker; 206 slot.drag(action.pointer); 207 root.drawAndAssert( 208 overlay.drawsChild(slot.overlay), 209 slot.value.isDrawn, 210 ); 211 assert(slot.dragAction); 212 assert(slot.hoverIO.opEquals(hover)); 213 assert(slot.canvasIO.opEquals(root)); 214 assert(tracker.hoverIO.opEquals(hover)); 215 assert(tracker.canvasIO.opEquals(root)); 216 217 } 218 219 @("Droppable nodes can be nested") 220 unittest { 221 222 DragSlot slot; 223 Frame inner; 224 Label[2] dummies; 225 226 const targets = [ 227 Vector2(0, 450), // Control sample 228 Vector2(0, 0), // Drop into outer 229 Vector2(0, 300), // Drop into inner 230 ]; 231 232 foreach (index, dropTarget; targets) { 233 234 auto outer = sizeLock!vframe( 235 .layout!(1, "fill"), 236 .sizeLimit(600, 600), 237 .acceptDrop, 238 dummies[0] = label( 239 .layout!1, 240 "Dummy 1", 241 ), 242 inner = vframe( 243 .layout!(1, "fill"), 244 .acceptDrop, 245 dummies[1] = label( 246 .layout!1, 247 "Dummy 2" 248 ), 249 slot = sizeLock!dragSlot( 250 .layout!1, 251 .sizeLimit(100, 100), 252 label( 253 .ignoreMouse, 254 "Drag me" 255 ), 256 ), 257 ) 258 ); 259 auto overlay = overlayChain(.layout!"fill"); 260 auto hover = hoverChain(.testTheme, .layout!"fill"); 261 auto root = testSpace( 262 chain(hover, overlay, outer) 263 ); 264 265 root.drawAndAssert( 266 slot.isDrawn().at(0, 450), 267 ), 268 269 hover.point(1, 451) 270 .then((a) { 271 a.press(false); 272 return a.move(dropTarget); 273 }) 274 275 // Hover over the target 276 .then((a) { 277 a.press(false); // Wait 1 more frame to trigger `afterKeyboard` 278 root.draw(); // TODO fix this in 0.8.0 279 280 // Control sample 281 a.press(false); 282 if (index == 0) { 283 root.drawAndAssert( 284 dummies[0].isDrawn().at(0, 0), 285 dummies[1].isDrawn().at(0, 300), 286 ); 287 } 288 // Drop into outer 289 else if (index == 1) { 290 root.drawAndAssert( 291 dummies[0].isDrawn().at(0, 100), // TODO correct expanding behavior 292 dummies[1].isDrawn().at(0, 400), 293 ); 294 } 295 // Drop into inner 296 else if (index == 2) { 297 root.drawAndAssert( 298 dummies[0].isDrawn().at(0, 000), 299 dummies[1].isDrawn().at(0, 400), 300 ); 301 } 302 a.press(false); 303 root.drawAndAssert( 304 overlay.drawsChild(slot.overlay), 305 ); 306 return a.stayIdle; 307 }) 308 309 // Drop it 310 .then((a) { 311 a.press(true); 312 root.draw(); 313 314 if (index == 0) { 315 root.drawAndAssert( 316 dummies[0].isDrawn().at(0, 0), 317 dummies[1].isDrawn().at(0, 300), 318 slot .isDrawn().at(0, 450), 319 ); 320 } 321 // Drop into outer 322 else if (index == 1) { 323 root.drawAndAssert( 324 slot .isDrawn().at(0, 0), 325 dummies[0].isDrawn().at(0, 200), 326 dummies[1].isDrawn().at(0, 400), 327 ); 328 } 329 // Drop into inner 330 else if (index == 2) { 331 root.drawAndAssert( 332 dummies[0].isDrawn().at(0, 0), 333 slot .isDrawn().at(0, 300), 334 dummies[1].isDrawn().at(0, 450), 335 ); 336 } 337 }) 338 .runWhileDrawing(root, 4); 339 340 } 341 342 }