1 module fluid.popup_frame; 2 3 import optional; 4 5 import std.traits; 6 import std.algorithm; 7 8 import fluid.node; 9 import fluid.tree; 10 import fluid.frame; 11 import fluid.input; 12 import fluid.style; 13 import fluid.utils; 14 import fluid.actions; 15 import fluid.backend; 16 17 import fluid.io.focus; 18 import fluid.io.action; 19 import fluid.io.overlay; 20 21 import fluid.future.action; 22 import fluid.future.context; 23 import fluid.future.branch_action; 24 25 @safe: 26 27 28 alias popupFrame = simpleConstructor!PopupFrame; 29 30 /// Spawn a new popup attached to the given tree. 31 /// 32 /// The popup automatically gains focus. 33 void spawnPopup(LayoutTree* tree, PopupFrame popup) { 34 35 popup.tree = tree; 36 37 // Set anchor 38 popup.anchor = tree.focusBox; 39 popup._anchorVec = Vector2( 40 tree.focusBox.x, 41 tree.focusBox.y + tree.focusBox.height 42 ); 43 44 // Spawn the popup 45 tree.queueAction(new PopupNodeAction(popup)); 46 tree.root.updateSize(); 47 48 } 49 50 /// Spawn a new popup, as a child of another. While the child is active, the parent will also remain so. 51 /// 52 /// The newly spawned popup automatically gains focus. 53 void spawnChildPopup(PopupFrame parent, PopupFrame popup) { 54 55 auto tree = parent.tree; 56 57 // Inherit theme from parent 58 // TODO This may not work... 59 if (!popup.theme) 60 popup.theme = parent.theme; 61 62 // Assign the child 63 parent.childPopup = popup; 64 65 // Spawn the popup 66 spawnPopup(tree, popup); 67 68 } 69 70 /// Spawn a popup using `OverlayIO`. 71 /// 72 /// This function can be used to add new popups, or to open them again after they have been 73 /// closed. 74 /// 75 /// Params: 76 /// overlayIO = `OverlayIO` instance to control to popup. 77 /// popup = Popup to draw. 78 /// anchor = Box to attach the frame to; 79 /// likely a 0×0 rectangle at the mouse position for hover events, 80 /// and the relevant `focusBox` for keyboard events. 81 void addPopup(OverlayIO overlayIO, PopupFrame popup, Rectangle anchor) { 82 popup.anchor = anchor; 83 popup.toTakeFocus = true; 84 overlayIO.addOverlay(popup, OverlayIO.types.context); 85 } 86 87 /// Spawn a new child popup using `OverlayIO`. 88 /// 89 /// Regular popups are mutually exclusive; only one can be open at a time. A child popup can 90 /// coexist with its parent. As long as the parent is open, so is the child. The child can be 91 /// closed without closing the parent popup, but closing the parent popup will close the child. 92 /// 93 /// Params: 94 /// overlayIO = `OverlayIO` instance to control to popup. 95 /// parent = Parent popup. 96 /// popup = Popup to draw. 97 /// anchor = Box to attach the frame to; 98 /// likely a 0×0 rectangle at the mouse position for hover events, 99 /// and the relevant `focusBox` for keyboard events. 100 void addChildPopup(OverlayIO overlayIO, PopupFrame parent, PopupFrame popup, Rectangle anchor) { 101 popup.anchor = anchor; 102 popup.toTakeFocus = true; 103 parent.childPopup = popup; 104 overlayIO.addChildOverlay(parent, popup, OverlayIO.types.context); 105 } 106 107 /// This is an override of Frame to simplify creating popups: if clicked outside of it, it will disappear from 108 /// the node tree. 109 class PopupFrame : InputNode!Frame, Overlayable, FocusIO, WithOrderedFocus, WithPositionalFocus { 110 111 mixin makeHoverable; 112 mixin enableInputActions; 113 114 public { 115 116 /// A child popup will keep this focus alive while focused. 117 /// Typically, child popups are spawned as a result of actions within the popup itself, for example in context 118 /// menus, an action can spawn a submenu. Use `spawnChildPopup` to spawn child popups. 119 PopupFrame childPopup; 120 121 /// Node that had focus before `popupFrame` took over. When the popup is closed using a keyboard shortcut, this 122 /// node will take focus again. 123 /// 124 /// Assigned automatically if `spawnPopup` or `spawnChildPopup` is used, but otherwise not. 125 /// 126 /// Used if `FocusIO` is not available; for old backend only. 127 /// 128 /// See_Also: 129 /// `previousFocusable`, which is used with the new I/O system. 130 FluidFocusable previousFocus; 131 132 /// Node that was focused before the popup was opened. Using `restorePreviousFocus`, it 133 /// can be given focus again, closing the popup. This is the default behavior for the 134 /// escape key while a popup is open. 135 /// 136 /// Used if `FocusIO` is available. 137 /// 138 /// See_Also: 139 /// `previousFocus`, which is used with the old backend system. 140 Focusable previousFocusable; 141 142 /// If true, the frame will claim focus on the next *resize*. This is used to give 143 /// the popup focus when it is spawned, respecting currently active `FocusIO`. 144 bool toTakeFocus; 145 146 } 147 148 private { 149 150 Rectangle _anchor; 151 Vector2 _anchorVec; 152 Focusable _currentFocus; 153 Optional!Rectangle _lastFocusBox; 154 155 OrderedFocusAction _orderedFocusAction; 156 PositionalFocusAction _positionalFocusAction; 157 FindFocusBoxAction _findFocusBoxAction; 158 MarkPopupButtonsAction _markPopupButtonsAction; 159 160 bool childHasFocus; 161 162 } 163 164 this(Node[] nodes...) { 165 166 import fluid.structs : layout; 167 168 super(nodes); 169 this.layout = layout!"fill"; 170 this._orderedFocusAction = new OrderedFocusAction; 171 this._positionalFocusAction = new PositionalFocusAction; 172 this._findFocusBoxAction = new FindFocusBoxAction(this); 173 this._markPopupButtonsAction = new MarkPopupButtonsAction(this); 174 175 _findFocusBoxAction 176 .then((Optional!Rectangle result) => _lastFocusBox = result); 177 178 } 179 180 Optional!Rectangle lastFocusBox() const { 181 return _lastFocusBox; 182 } 183 184 inout(OrderedFocusAction) orderedFocusAction() inout { 185 return _orderedFocusAction; 186 } 187 188 inout(PositionalFocusAction) positionalFocusAction() inout { 189 return _positionalFocusAction; 190 } 191 192 /// Returns: 193 /// Position the frame is "anchored" to. A corner of the frame will be chosen to match 194 /// this position. 195 deprecated("`Vector2 anchor` has been deprecated in favor of `Rectangle getAnchor` and " 196 ~ "will be removed in Fluid 0.8.0.") 197 final ref inout(Vector2) anchor() inout nothrow pure { 198 return _anchorVec; 199 } 200 201 /// Set a new rectangular anchor. 202 /// 203 /// The popup is used to specify the popup's position. The popup may appear below the `anchor`, 204 /// above, next to it, or it may cover the anchor. The exact behavior depends on the `OverlayIO` 205 /// system drawing the frame. Usually the direction is covered by the `layout` node property. 206 /// 207 /// For backwards compatibility, to getting the rectangular anchor is currently done using 208 /// `getAnchor`. 209 /// 210 /// See_Also: 211 /// `getAnchor` to get the current anchor value. 212 /// `fluid.io.overlay` for information about how overlays and popups work in Fluid. 213 /// Params: 214 /// value = Anchor to set. 215 /// Returns: 216 /// Newly set anchor; same as passed in. 217 Rectangle anchor(Rectangle value) nothrow { 218 return _anchor = value; 219 } 220 221 /// Returns: 222 /// Currently set rectangular anchor. 223 /// See_Also: 224 /// `anchor` for more information. 225 final Rectangle getAnchor() const nothrow { 226 return _anchor; 227 } 228 229 override final Rectangle getAnchor(Rectangle) const nothrow { 230 return getAnchor; 231 } 232 233 /// ditto 234 void drawAnchored(Node parent) { 235 236 const rect = Rectangle( 237 anchoredStartCorner.tupleof, 238 minSize.tupleof 239 ); 240 241 // Draw the node within the defined rectangle 242 parent.drawChild(this, rect); 243 244 } 245 246 private void resizeInternal(Node parent, Vector2 space) { 247 248 parent.resizeChild(this, space); 249 250 } 251 252 /// Get start (top-left) corner of the popup if `drawAnchored` is to be used. 253 Vector2 anchoredStartCorner() { 254 255 const viewportSize = io.windowSize; 256 257 // This method is very similar to MapSpace.getStartCorner, but simplified to handle the "automatic" case 258 // only. 259 260 // Define important points on the screen: center is our anchor, left is the other corner of the popup if we 261 // extend it to the top-left, right is the other corner of the popup if we extend it to the bottom-right 262 // x--| <- left 263 // | | 264 // |--o--| <- center (anchor) 265 // | | 266 // |--x <- right 267 const left = _anchorVec - minSize; 268 const center = _anchorVec; 269 const right = _anchorVec + minSize; 270 271 // Horizontal position 272 const x 273 274 // Default to extending towards the bottom-right, unless we overflow 275 // |=============| 276 // | ↓ center | 277 // | O------| | 278 // | | | | 279 // | | | | 280 // | |------| | 281 // |=============| 282 = right.x < viewportSize.x ? center.x 283 284 // But in case we cannot fit the popup, we might need to reverse the direction 285 // |=============| |=============| 286 // | | ↓ right | ↓ left | 287 // | O------> | <------O | 288 // | | | | | | | 289 // | | | | | | | 290 // | |------| | |------| | 291 // |=============| |=============| 292 : left.x >= 0 ? left.x 293 294 // However, if we overflow either way, it's best we center the popup on the screen 295 : (viewportSize.x - minSize.x) / 2; 296 297 // Do the same for vertical position 298 const y 299 = right.y < viewportSize.y ? center.y 300 : left.y >= 0 ? left.y 301 : (viewportSize.y - minSize.y) / 2; 302 303 return Vector2(x, y); 304 305 } 306 307 protected override void resizeImpl(Vector2 space) { 308 309 // Load the parent's `focusIO` 310 if (auto focusIO = use(this.focusIO)) { 311 312 { 313 auto io = this.implementIO(); 314 super.resizeImpl(space); 315 } 316 317 // The above resizeImpl call sets `focusIO` to `this`, it now needs to be restored 318 this.focusIO = focusIO; 319 } 320 321 // No `focusIO` in use 322 else super.resizeImpl(space); 323 324 // Immediately switch focus to self 325 if (usingFocusIO && toTakeFocus) { 326 previousFocusable = focusIO.currentFocus; 327 focus(); 328 toTakeFocus = false; 329 } 330 } 331 332 alias toRemove = typeof(super).toRemove; 333 334 /// `PopupFrame` will automatically be marked for removal if not focused. 335 /// 336 /// For the new I/O, this is done by overriding the `toRemove` mark; the old backend does this 337 /// from the tree action. 338 /// 339 /// Returns: 340 /// True if the `PopupFrame` was marked for removal, or if it has no focus. 341 override bool toRemove() const { 342 if (!toTakeFocus && usingFocusIO && !this.isFocused) { 343 return true; 344 } 345 return super.toRemove; 346 } 347 348 protected override void drawImpl(Rectangle outer, Rectangle inner) { 349 350 // Clear directional focus data; give the popup a separate context 351 tree.focusDirection = FocusDirection(tree.focusDirection.lastFocusBox); 352 353 auto action1 = this.startBranchAction(_findFocusBoxAction); 354 auto action2 = this.startBranchAction(_markPopupButtonsAction); 355 super.drawImpl(outer, inner); 356 357 // Forcibly register previous & next focus if missing 358 // The popup will register itself just after it gets drawn without this — and it'll be better if it doesn't 359 if (tree.focusDirection.previous is null) { 360 tree.focusDirection.previous = tree.focusDirection.last; 361 } 362 363 if (tree.focusDirection.next is null) { 364 tree.focusDirection.next = tree.focusDirection.first; 365 } 366 367 } 368 369 protected override bool actionImpl(IO io, int number, immutable InputActionID actionID, 370 bool isActive) 371 do { 372 373 // Pass input events to whatever node is currently focused 374 if (_currentFocus && _currentFocus.actionImpl(this, 0, actionID, isActive)) { 375 return true; 376 } 377 378 // Handle events locally otherwise 379 return this.runInputActionHandler(io, number, actionID, isActive); 380 381 } 382 383 protected override void mouseImpl() { 384 385 } 386 387 protected override bool focusImpl() { 388 return _currentFocus && _currentFocus.focusImpl(); 389 } 390 391 override void focus() { 392 393 // Set focus to self 394 super.focus(); 395 396 // Prefer if children get it, though 397 this.focusRecurseChildren(); 398 399 } 400 401 /// Give focus to whatever node had focus before this one. 402 @(FluidInputAction.cancel) 403 void restorePreviousFocus() { 404 405 // Restore focus if possible 406 if (previousFocusable) { 407 previousFocusable.focus(); 408 } 409 else if (previousFocus) { 410 previousFocus.focus(); 411 } 412 413 // Clear focus 414 else if (usingFocusIO) { 415 focusIO.clearFocus(); 416 } 417 else tree.focus = null; 418 419 } 420 421 alias isFocused = typeof(super).isFocused; 422 423 @property 424 override bool isFocused() const { 425 426 return childHasFocus 427 || super.isFocused 428 || (childPopup && childPopup.isFocused); 429 430 } 431 432 alias opEquals = typeof(super).opEquals; 433 434 override bool opEquals(const Object other) const { 435 return super.opEquals(other); 436 } 437 438 override void emitEvent(InputEvent event) { 439 assert(focusIO, "FocusIO is not loaded"); 440 focusIO.emitEvent(event); 441 } 442 443 override void typeText(scope const char[] text) { 444 assert(focusIO, "FocusIO is not loaded"); 445 focusIO.typeText(text); 446 } 447 448 override char[] readText(return scope char[] buffer, ref int offset) { 449 assert(focusIO, "FocusIO is not loaded"); 450 return focusIO.readText(buffer, offset); 451 } 452 453 override inout(Focusable) currentFocus() inout { 454 if (usingFocusIO && !focusIO.isFocused(this)) { 455 return null; 456 } 457 return _currentFocus; 458 } 459 460 override Focusable currentFocus(Focusable newValue) { 461 if (usingFocusIO) { 462 focusIO.currentFocus = this; 463 } 464 return _currentFocus = newValue; 465 } 466 467 private bool usingFocusIO() const nothrow { 468 return focusIO && focusIO !is this; 469 } 470 471 } 472 473 /// Tree action displaying a popup. 474 class PopupNodeAction : TreeAction { 475 476 public { 477 478 PopupFrame popup; 479 480 } 481 482 protected { 483 484 /// Safety guard: Do not draw the popup if the tree hasn't resized. 485 bool hasResized; 486 487 } 488 489 this(PopupFrame popup) { 490 491 this.startNode = this.popup = popup; 492 popup.show(); 493 popup.toRemove = false; 494 495 } 496 497 override void beforeResize(Node node, Vector2 viewportSize) { 498 499 // Only accept root resizes 500 if (node !is node.tree.root) return; 501 502 // Perform the resize 503 popup.resizeInternal(node, viewportSize); 504 505 // First resize 506 if (!hasResized) { 507 508 // Give that popup focus 509 popup.previousFocus = node.tree.focus; 510 popup.focus(); 511 hasResized = true; 512 513 } 514 515 } 516 517 /// Tree drawn, draw the popup now. 518 override void afterTree() { 519 520 // Don't draw without a resize 521 if (!hasResized) return; 522 523 // Stop if the popup requested removal 524 if (popup.toRemove) { stop; return; } 525 526 // Draw the popup 527 popup.childHasFocus = false; 528 popup.drawAnchored(popup.tree.root); 529 530 // Remove the popup if it has no focus 531 if (!popup.isFocused) { 532 popup.remove(); 533 stop; 534 } 535 536 537 } 538 539 override void afterDraw(Node node, Rectangle space) { 540 541 import fluid.popup_button; 542 543 // Require at least one resize to search for focus 544 if (!hasResized) return; 545 546 // Mark popup buttons 547 if (auto button = cast(PopupButton) node) { 548 549 button.parentPopup = popup; 550 551 } 552 553 // Ignore if a focused node has already been found 554 if (popup.isFocused) return; 555 556 const focusable = cast(FluidFocusable) node; 557 558 if (focusable && focusable.isFocused) { 559 560 popup.childHasFocus = focusable.isFocused; 561 562 } 563 564 } 565 566 override void afterInput(ref bool keyboardHandled) { 567 568 // Require at least one resize 569 if (!hasResized) return; 570 571 // Ignore if input was already handled 572 if (keyboardHandled) return; 573 574 // Ignore input in child popups 575 if (popup.childPopup && popup.childPopup.isFocused) return; 576 577 // Run actions for the popup 578 keyboardHandled = popup.runFocusInputActions; 579 580 } 581 582 } 583 584 /// This tree action will walk the branch to mark PopupButtons with the parent PopupFrame. 585 /// This is a temporary workaround to fill `PopupButton.parentPopup` in new I/O; starting with 586 /// 0.8.0 popup frames should implement `LayoutIO` to detect child popups. 587 private class MarkPopupButtonsAction : BranchAction { 588 589 PopupFrame parent; 590 591 this(PopupFrame parent) { 592 this.parent = parent; 593 } 594 595 override void beforeDraw(Node node, Rectangle) { 596 597 import fluid.popup_button; 598 599 if (auto button = cast(PopupButton) node) { 600 button.parentPopup = parent; 601 } 602 603 } 604 605 }