1 module nodes.hover_transform; 2 3 import fluid; 4 import fluid.future.pipe; 5 6 import nodes.hover_chain; 7 8 @safe: 9 10 @("HoverTransform yields transformed pointers when iterated") 11 unittest { 12 13 auto content = sizeLock!vspace( 14 .sizeLimit(500, 500), 15 ); 16 auto transform = hoverTransform( 17 Rectangle(50, 50, 100, 100), 18 content 19 ); 20 auto hover = hoverChain( 21 .layout!(1, "fill"), 22 transform, 23 ); 24 auto root = testSpace( 25 hover 26 ); 27 28 root.draw(); 29 30 auto action = hover.point(50, 50); 31 foreach (HoverPointer pointer; transform) { 32 assert(pointer.position == Vector2(0, 0)); 33 assert(pointer.scroll == Vector2(0, 0)); 34 } 35 36 action.move(75, 150).scroll(10, 20); 37 foreach (HoverPointer pointer; transform) { 38 assert(pointer.position == Vector2(125, 500)); 39 assert(pointer.scroll == Vector2(10, 20)); 40 } 41 42 hover.point(0, 0); 43 auto index = 0; 44 foreach (HoverPointer pointer; transform) { 45 if (index++ == 0) { 46 assert(pointer.position == Vector2(125, 500)); 47 assert(pointer.scroll == Vector2(10, 20)); 48 } 49 else { 50 assert(pointer.position == Vector2(-250, -250)); 51 assert(pointer.scroll == Vector2(0, 0)); 52 } 53 } 54 55 } 56 57 @("HoverTransform can fetch and transform nodes") 58 unittest { 59 60 auto transform = hoverTransform( 61 Rectangle( 50, 50, 100, 100), 62 Rectangle(-100, -100, 100, 100), 63 ); 64 auto hover = hoverChain(transform); 65 66 hover.draw(); 67 68 auto action = hover.point(56, 56).scroll(2, 3); 69 auto pointer = transform.fetch(action.pointer.id); 70 assert(pointer.id == action.pointer.id); 71 assert(pointer.position == Vector2(-94, -94)); 72 assert(pointer.scroll == Vector2( 2, 3)); 73 74 } 75 76 @("HoverTransform affects child nodes") 77 unittest { 78 79 // Target's actual position is the first 50×50 rectangle 80 // Transform takes events from the 50×50 rectangle next to it. 81 auto tracker = sizeLock!hoverTracker( 82 .sizeLimit(50, 50), 83 ); 84 auto transform = hoverTransform(Rectangle(50, 0, 50, 50)); 85 auto hover = hoverChain(); 86 auto root = chain( 87 inputMapChain(.layout!"fill"), 88 hover, 89 transform, 90 tracker, 91 ); 92 93 hover.point(75, 25) 94 .then((a) { 95 assert(tracker.hoverImplCount == 1); 96 assert(tracker.pressHeldCount == 0); 97 a.press(false); 98 return a.stayIdle; 99 }) 100 .then((a) { 101 assert(tracker.hoverImplCount == 1); 102 assert(tracker.pressHeldCount == 1); 103 assert(tracker.pressCount == 0); 104 a.press(true); 105 return a.stayIdle; 106 }) 107 .runWhileDrawing(root, 3); 108 109 assert(tracker.hoverImplCount == 1); 110 assert(tracker.pressHeldCount == 2); 111 assert(tracker.pressCount == 1); 112 113 } 114 115 @("HoverTransform doesn't trigger active events when outside") 116 unittest { 117 118 auto tracker = sizeLock!hoverTracker( 119 .sizeLimit(50, 50), 120 ); 121 auto transform = hoverTransform(Rectangle(50, 0, 50, 50)); 122 auto hover = hoverChain(); 123 auto root = chain( 124 inputMapChain(.layout!"fill"), 125 hover, 126 transform, 127 tracker, 128 ); 129 130 root.draw(); 131 132 // Holding and clicking works inside 133 hover.point(75, 25) 134 .then((a) { 135 assert(hover.isHovered(transform)); 136 assert(transform.isHovered(tracker)); 137 assert(tracker.hoverImplCount == 1); 138 a.press(); 139 assert(tracker.pressHeldCount == 1); 140 assert(tracker.pressCount == 1); 141 return a.stayIdle; 142 }) 143 .then((a) { 144 a.press(false); 145 assert(tracker.pressHeldCount == 2); 146 assert(tracker.pressCount == 1); 147 return a.move(25, 25); 148 }) 149 150 // Outside the node, only holding works 151 .then((a) { 152 a.press(false); 153 assert(hover.isHovered(transform)); 154 assert(transform.isHovered(tracker)); 155 assert(tracker.pressHeldCount == 3); 156 assert(tracker.pressCount == 1); 157 return a.stayIdle; 158 }) 159 .then((a) { 160 a.press(true); 161 assert(tracker.pressHeldCount == 3); 162 assert(tracker.pressCount == 1); 163 assert(tracker.hoverImplCount == 1); 164 }) 165 .runWhileDrawing(root, 5); 166 167 } 168 169 @("HoverTransform supports scrolling") 170 unittest { 171 172 auto tracker = sizeLock!scrollTracker( 173 .sizeLimit(50, 50), 174 ); 175 auto transform = hoverTransform(Rectangle(50, 0, 50, 50)); 176 auto hover = hoverChain(); 177 auto root = chain( 178 inputMapChain(.layout!"fill"), 179 hover, 180 transform, 181 tracker, 182 ); 183 184 hover.point(75, 25).scroll(0, 40) 185 .then((a) { 186 assert(tracker.lastScroll == Vector2(0, 40)); 187 assert(tracker.totalScroll == Vector2(0, 40)); 188 }) 189 .runWhileDrawing(root, 2); 190 191 hover.point(25, 25).scroll(0, 60) 192 .then((a) { 193 assert(tracker.lastScroll == Vector2(0, 40)); 194 assert(tracker.totalScroll == Vector2(0, 40)); 195 }) 196 .runWhileDrawing(root, 2); 197 198 } 199 200 @("HoverTransform children can create pointers") 201 unittest { 202 203 auto tracker1 = sizeLock!hoverTracker( 204 .sizeLimit(50, 50), 205 ); 206 auto tracker2 = sizeLock!hoverTracker( 207 .sizeLimit(50, 50), 208 ); 209 auto innerDevice = myHover(); 210 auto outerDevice = myHover(); 211 auto transform = hoverTransform( 212 .layout!"fill", 213 Rectangle(50, 0, 100, 50), 214 hspace( 215 tracker1, 216 tracker2, 217 innerDevice, 218 ), 219 ); 220 auto hover = hoverChain(); 221 auto root = chain( 222 inputMapChain(.layout!"fill"), 223 hover, 224 vspace( 225 .layout!"fill", 226 transform, 227 outerDevice, 228 ), 229 ); 230 231 // Point both pointers at the same spot 232 outerDevice.pointers = [ 233 outerDevice.makePointer(0, Vector2(75, 25)), 234 ]; 235 innerDevice.pointers = [ 236 innerDevice.makePointer(0, Vector2(75, 25)), 237 ]; 238 root.draw(); 239 root.draw(); // 1 frame delay; need to wait for draw 240 241 const outerPointerID = hover.armedPointerID(outerDevice.pointers[0].id); 242 const innerPointerID = hover.armedPointerID(innerDevice.pointers[0].id); 243 auto outerPointer = hover.fetch(outerPointerID); 244 auto innerPointer = hover.fetch(innerPointerID); 245 246 // `outerDevice`, just like in other tests, gets transformed and hits `tracker1`. 247 assert(hover.hoverOf(outerPointer).opEquals(transform)); 248 assert(transform.hoverOf(outerPointer).opEquals(tracker1)); 249 250 // `innerDevice` exists within the transformed coordinate system, so, within the system, its 251 // position will stay unchanged. It should hit `tracker2` 252 assert(hover.hoverOf(innerPointer).opEquals(transform)); 253 assert(transform.hoverOf(innerPointer).opEquals(tracker2)); 254 255 // Press the inner device 256 innerDevice.emit(0, MouseIO.press.left); 257 root.draw(); 258 assert(tracker1.pressCount == 0); 259 assert(tracker2.pressCount == 1); 260 261 } 262 263 @("HoverTransform supports iterating on hovered items") 264 unittest { 265 266 auto tracker1 = sizeLock!hoverTracker( 267 .sizeLimit(50, 50), 268 ); 269 auto tracker2 = sizeLock!hoverTracker( 270 .sizeLimit(50, 50), 271 ); 272 auto tracker3 = sizeLock!hoverTracker( 273 .sizeLimit(50, 50), 274 ); 275 auto transform = hoverTransform(Rectangle(50, 0, 200, 50)); 276 auto hover = hoverChain(); 277 auto root = chain( 278 inputMapChain(.layout!"fill"), 279 hover, 280 transform, 281 hspace( 282 tracker1, 283 sizeLock!vspace( 284 .sizeLimit(50, 50) 285 ), 286 tracker2, 287 tracker3, 288 ), 289 ); 290 291 auto action1 = hover.point( 75, 25); 292 auto action2 = hover.point(125, 25); 293 auto action3 = hover.point(175, 25); 294 295 join(action1, action2, action3) 296 .runWhileDrawing(root, 2); 297 298 assert(action1.isHovered(transform)); 299 assert(action2.isHovered(transform)); 300 assert(action3.isHovered(transform)); 301 action1.stayIdle; // tracker1 302 action2.stayIdle; // blank space 303 action3.stayIdle; // tracker2 304 305 int matched1, matched2; 306 307 foreach (Hoverable hoverable; transform) { 308 if (hoverable.opEquals(tracker1)) matched1++; 309 else if (hoverable.opEquals(tracker2)) matched2++; 310 else assert(false); 311 } 312 313 assert(matched1 == 1); 314 assert(matched2 == 1); 315 316 } 317 318 @("HoverTransform can be nested inside a scrollable") 319 unittest { 320 321 auto innerScroll = sizeLock!scrollTracker( 322 .sizeLimit(100, 100), 323 ); 324 auto transform = hoverTransform( 325 .layout!(1, "fill"), 326 Rectangle(250, 250, 250, 250), 327 innerScroll, 328 ); 329 auto outerScroll = sizeLock!scrollTracker( 330 .sizeLimit(500, 500), 331 transform, 332 ); 333 auto hover = hoverChain(outerScroll); 334 auto root = testSpace(.nullTheme, hover); 335 336 // outer: (0, 0)–(500, 500) 337 // transform spans the entire area of outer, 338 // accepts input in (250, 250)–(500, 500) 339 // inner: (250, 250)–(350, 350) 340 341 // Scroll in inner 342 hover.point(300, 300).scroll(1, 2) 343 .then((a) { 344 const armed = hover.armedPointer(a.pointer); 345 346 assert(hover.hoverOf(a.pointer).opEquals(transform)); 347 assert(hover.scrollOf(a.pointer).opEquals(transform)); 348 assert(transform.scrollOf(armed).opEquals(innerScroll)); 349 assert(outerScroll.lastScroll == Vector2(0, 0)); 350 assert(innerScroll.lastScroll == Vector2(1, 2)); 351 352 // Scroll in outer 353 return a.move(200, 200).scroll(3, 4); 354 }) 355 .then((a) { 356 assert(hover.scrollOf(a.pointer).opEquals(outerScroll)); 357 assert(outerScroll.lastScroll == Vector2(3, 4)); 358 assert(innerScroll.lastScroll == Vector2(1, 2)); 359 }) 360 .runWhileDrawing(root, 3); 361 362 } 363 364 @("HoverTransform works with scrollIntoView") 365 unittest { 366 367 ScrollFrame outerFrame, innerFrame; 368 Button target; 369 370 auto ui = sizeLock!vspace( 371 .sizeLimit(250, 250), 372 .nullTheme, 373 outerFrame = vscrollFrame( 374 .layout!(1, "fill"), 375 sizeLock!vspace( 376 .sizeLimit(250, 250), 377 ), 378 hoverTransform( 379 Rectangle(0, 0, 100, 100), 380 innerFrame = sizeLock!vscrollFrame( 381 .sizeLimit(250, 250), 382 sizeLock!vspace( 383 .sizeLimit(250, 250), 384 ), 385 target = button("Make me visible!", delegate { }), 386 sizeLock!vspace( 387 .sizeLimit(250, 250), 388 ), 389 ), 390 ), 391 ) 392 ); 393 394 auto hover = hoverChain(ui); 395 auto root = testSpace(hover); 396 397 root.drawAndAssert( 398 outerFrame.isDrawn.at(0, 0, 250, 250), 399 innerFrame.isDrawn.at(0, 250), 400 target.isDrawn.at(0, 500), 401 ); 402 target.scrollToTop() 403 .runWhileDrawing(root, 1); 404 root.drawAndAssert( 405 outerFrame.isDrawn.at(0, 0, 250, 250), 406 innerFrame.isDrawn.at(0, 0), 407 target.isDrawn.at(0, 0), 408 ); 409 410 assert(outerFrame.scroll == 250); 411 assert(innerFrame.scroll == 250); 412 413 414 } 415 416 @("HoverTransform can switch between targets") 417 unittest { 418 419 Button[2] buttons; 420 421 auto content = resolutionOverride!vspace( 422 Vector2(400, 400), 423 buttons[0] = button(.layout!(1, "fill"), "One", delegate { }), 424 buttons[1] = button(.layout!(1, "fill"), "One", delegate { }), 425 ); 426 auto transform = hoverTransform( 427 Rectangle(0, 0, 100, 100), 428 content 429 ); 430 auto hover = hoverChain( 431 .layout!(1, "fill"), 432 transform, 433 ); 434 auto root = testSpace(hover); 435 436 hover.point(25, 25) 437 .then((a) { 438 assert(transform.isHovered(buttons[0])); 439 a.press(); 440 return a.stayIdle; 441 }) 442 .then((a) => a.move(75, 75)) 443 .then((a) { 444 assert(transform.isHovered(buttons[1])); 445 a.press(); 446 }) 447 .runWhileDrawing(root, 4); 448 449 } 450 451 @("HoverTransform supports holding scroll") 452 unittest { 453 454 auto innerButton = button(.layout!(1, "fill"), "Two", delegate { }); 455 auto tracker = scrollTracker( 456 .layout!(1, "fill"), 457 innerButton, 458 ); 459 auto content = resolutionOverride!vspace( 460 Vector2(400, 400), 461 button(.layout!(1, "fill"), "One", delegate { }), 462 tracker, 463 ); 464 auto transform = hoverTransform( 465 Rectangle(0, 0, 100, 100), 466 content 467 ); 468 auto hover = hoverChain( 469 .layout!(1, "fill"), 470 transform, 471 ); 472 auto root = testSpace(hover); 473 474 hover.point(75, 75).scroll(0, 25) 475 .then((a) { 476 const pointer = hover.armedPointer(a.pointer); 477 assert(transform.scrollOf(pointer).opEquals(tracker)); 478 assert(tracker.lastScroll == Vector2(0, 25)); 479 return a.move(25, 25).holdScroll(0, 5); 480 }) 481 .then((a) { 482 const pointer = hover.armedPointer(a.pointer); 483 assert(transform.scrollOf(pointer).opEquals(tracker)); 484 assert(tracker.lastScroll == Vector2(0, 5)); 485 assert(tracker.totalScroll == Vector2(0, 30)); 486 return a.scroll(0, 25); 487 }) 488 .runWhileDrawing(root, 4); 489 490 assert(tracker.totalScroll == Vector2(0, 30)); 491 492 } 493 494 @("HoverTransform passes focus on press") 495 unittest { 496 497 auto content = resolutionOverride!button( 498 Vector2(400, 400), 499 "One", 500 delegate { }, 501 ); 502 auto transform = hoverTransform( 503 Rectangle(0, 0, 100, 100), 504 vspace( 505 resolutionOverride!button( // This button should never be selected by hoverChain 506 Vector2(0, 0), 507 "Zero", 508 delegate { 509 assert(false); 510 } 511 ), 512 content 513 ), 514 ); 515 auto hover = hoverChain(transform); 516 auto focus = focusChain( 517 .layout!(1, "fill"), 518 hover, 519 ); 520 auto root = testSpace(focus); 521 522 hover.point(25, 25) 523 .then((a) { 524 a.press(); 525 return a.stayIdle(); 526 }) 527 .then((a) { }) 528 .runWhileDrawing(root, 2); 529 530 assert(content.isFocused); 531 assert(focus.isFocused(content)); 532 533 } 534 535 version (TODO) // https://git.samerion.com/Samerion/Fluid/issues/366 536 @("HoverTransform does not block focus") 537 unittest { 538 539 Button[4] buttons; 540 541 auto focus = focusChain( 542 vspace( 543 buttons[0] = button("One", delegate { }), 544 hoverTransform( 545 Rectangle(), 546 vspace( 547 buttons[1] = button("Two", delegate { }), 548 buttons[2] = button("Three", delegate { }), 549 ), 550 ), 551 buttons[3] = button("Four", delegate { }), 552 ), 553 ); 554 auto hover = hoverChain(focus); 555 auto root = hover; 556 focus.currentFocus = buttons[0]; 557 558 root.draw(); 559 focus.focusNext() 560 .thenAssertEquals(buttons[1]) 561 .runWhileDrawing(root, 1); 562 563 }