1 modulefluid.popup_frame;
2 3 importoptional;
4 5 importstd.traits;
6 importstd.algorithm;
7 8 importfluid.node;
9 importfluid.tree;
10 importfluid.frame;
11 importfluid.input;
12 importfluid.style;
13 importfluid.utils;
14 importfluid.actions;
15 importfluid.backend;
16 17 importfluid.io.focus;
18 importfluid.io.action;
19 importfluid.io.overlay;
20 21 importfluid.future.action;
22 importfluid.future.context;
23 importfluid.future.branch_action;
24 25 @safe:
26 27 28 aliaspopupFrame = simpleConstructor!PopupFrame;
29 30 /// Spawn a new popup attached to the given tree.31 ///32 /// The popup automatically gains focus.33 voidspawnPopup(LayoutTree* tree, PopupFramepopup) {
34 35 popup.tree = tree;
36 37 // Set anchor38 popup.anchor = tree.focusBox;
39 popup._anchorVec = Vector2(
40 tree.focusBox.x,
41 tree.focusBox.y + tree.focusBox.height42 );
43 44 // Spawn the popup45 tree.queueAction(newPopupNodeAction(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 voidspawnChildPopup(PopupFrameparent, PopupFramepopup) {
54 55 autotree = parent.tree;
56 57 // Inherit theme from parent58 // TODO This may not work...59 if (!popup.theme)
60 popup.theme = parent.theme;
61 62 // Assign the child63 parent.childPopup = popup;
64 65 // Spawn the popup66 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 been73 /// 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 voidaddPopup(OverlayIOoverlayIO, PopupFramepopup, Rectangleanchor) {
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 can90 /// coexist with its parent. As long as the parent is open, so is the child. The child can be91 /// 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 voidaddChildPopup(OverlayIOoverlayIO, PopupFrameparent, PopupFramepopup, Rectangleanchor) {
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 from108 /// the node tree.109 classPopupFrame : InputNode!Frame, Overlayable, FocusIO, WithOrderedFocus, WithPositionalFocus {
110 111 mixinmakeHoverable;
112 mixinenableInputActions;
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 context118 /// menus, an action can spawn a submenu. Use `spawnChildPopup` to spawn child popups.119 PopupFramechildPopup;
120 121 /// Node that had focus before `popupFrame` took over. When the popup is closed using a keyboard shortcut, this122 /// 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 FluidFocusablepreviousFocus;
131 132 /// Node that was focused before the popup was opened. Using `restorePreviousFocus`, it133 /// can be given focus again, closing the popup. This is the default behavior for the134 /// 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 FocusablepreviousFocusable;
141 142 /// If true, the frame will claim focus on the next *resize*. This is used to give143 /// the popup focus when it is spawned, respecting currently active `FocusIO`.144 booltoTakeFocus;
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 boolchildHasFocus;
161 162 }
163 164 this(Node[] nodes...) {
165 166 importfluid.structs : layout;
167 168 super(nodes);
169 this.layout = layout!"fill";
170 this._orderedFocusAction = newOrderedFocusAction;
171 this._positionalFocusAction = newPositionalFocusAction;
172 this._findFocusBoxAction = newFindFocusBoxAction(this);
173 this._markPopupButtonsAction = newMarkPopupButtonsAction(this);
174 175 _findFocusBoxAction176 .then((Optional!Rectangleresult) => _lastFocusBox = result);
177 178 }
179 180 Optional!RectanglelastFocusBox() 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 match194 /// 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 finalrefinout(Vector2) anchor() inoutnothrowpure {
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 using208 /// `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 Rectangleanchor(Rectanglevalue) nothrow {
218 return_anchor = value;
219 }
220 221 /// Returns:222 /// Currently set rectangular anchor.223 /// See_Also:224 /// `anchor` for more information.225 finalRectanglegetAnchor() constnothrow {
226 return_anchor;
227 }
228 229 overridefinalRectanglegetAnchor(Rectangle) constnothrow {
230 returngetAnchor;
231 }
232 233 /// ditto234 voiddrawAnchored(Nodeparent) {
235 236 constrect = Rectangle(
237 anchoredStartCorner.tupleof,
238 minSize.tupleof239 );
240 241 // Draw the node within the defined rectangle242 parent.drawChild(this, rect);
243 244 }
245 246 privatevoidresizeInternal(Nodeparent, Vector2space) {
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 Vector2anchoredStartCorner() {
254 255 constviewportSize = io.windowSize;
256 257 // This method is very similar to MapSpace.getStartCorner, but simplified to handle the "automatic" case258 // only.259 260 // Define important points on the screen: center is our anchor, left is the other corner of the popup if we261 // extend it to the top-left, right is the other corner of the popup if we extend it to the bottom-right262 // x--| <- left263 // | |264 // |--o--| <- center (anchor)265 // | |266 // |--x <- right267 constleft = _anchorVec - minSize;
268 constcenter = _anchorVec;
269 constright = _anchorVec + minSize;
270 271 // Horizontal position272 constx273 274 // Default to extending towards the bottom-right, unless we overflow275 // |=============|276 // | ↓ center |277 // | O------| |278 // | | | |279 // | | | |280 // | |------| |281 // |=============|282 = right.x < viewportSize.x ? center.x283 284 // But in case we cannot fit the popup, we might need to reverse the direction285 // |=============| |=============|286 // | | ↓ right | ↓ left |287 // | O------> | <------O |288 // | | | | | | |289 // | | | | | | |290 // | |------| | |------| |291 // |=============| |=============|292 : left.x >= 0 ? left.x293 294 // However, if we overflow either way, it's best we center the popup on the screen295 : (viewportSize.x - minSize.x) / 2;
296 297 // Do the same for vertical position298 consty299 = right.y < viewportSize.y ? center.y300 : left.y >= 0 ? left.y301 : (viewportSize.y - minSize.y) / 2;
302 303 returnVector2(x, y);
304 305 }
306 307 protectedoverridevoidresizeImpl(Vector2space) {
308 309 // Load the parent's `focusIO`310 if (autofocusIO = use(this.focusIO)) {
311 312 {
313 autoio = this.implementIO();
314 super.resizeImpl(space);
315 }
316 317 // The above resizeImpl call sets `focusIO` to `this`, it now needs to be restored318 this.focusIO = focusIO;
319 }
320 321 // No `focusIO` in use322 elsesuper.resizeImpl(space);
323 324 // Immediately switch focus to self325 if (usingFocusIO && toTakeFocus) {
326 previousFocusable = focusIO.currentFocus;
327 focus();
328 toTakeFocus = false;
329 }
330 }
331 332 aliastoRemove = 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 this337 /// from the tree action.338 ///339 /// Returns:340 /// True if the `PopupFrame` was marked for removal, or if it has no focus.341 overridebooltoRemove() const {
342 if (!toTakeFocus && usingFocusIO && !this.isFocused) {
343 returntrue;
344 }
345 returnsuper.toRemove;
346 }
347 348 protectedoverridevoiddrawImpl(Rectangleouter, Rectangleinner) {
349 350 // Clear directional focus data; give the popup a separate context351 tree.focusDirection = FocusDirection(tree.focusDirection.lastFocusBox);
352 353 autoaction1 = this.startBranchAction(_findFocusBoxAction);
354 autoaction2 = this.startBranchAction(_markPopupButtonsAction);
355 super.drawImpl(outer, inner);
356 357 // Forcibly register previous & next focus if missing358 // The popup will register itself just after it gets drawn without this — and it'll be better if it doesn't359 if (tree.focusDirection.previousisnull) {
360 tree.focusDirection.previous = tree.focusDirection.last;
361 }
362 363 if (tree.focusDirection.nextisnull) {
364 tree.focusDirection.next = tree.focusDirection.first;
365 }
366 367 }
368 369 protectedoverrideboolactionImpl(IOio, intnumber, immutableInputActionIDactionID,
370 boolisActive)
371 do {
372 373 // Pass input events to whatever node is currently focused374 if (_currentFocus && _currentFocus.actionImpl(this, 0, actionID, isActive)) {
375 returntrue;
376 }
377 378 // Handle events locally otherwise379 returnthis.runInputActionHandler(io, number, actionID, isActive);
380 381 }
382 383 protectedoverridevoidmouseImpl() {
384 385 }
386 387 protectedoverrideboolfocusImpl() {
388 return_currentFocus && _currentFocus.focusImpl();
389 }
390 391 overridevoidfocus() {
392 393 // Set focus to self394 super.focus();
395 396 // Prefer if children get it, though397 this.focusRecurseChildren();
398 399 }
400 401 /// Give focus to whatever node had focus before this one.402 @(FluidInputAction.cancel)
403 voidrestorePreviousFocus() {
404 405 // Restore focus if possible406 if (previousFocusable) {
407 previousFocusable.focus();
408 }
409 elseif (previousFocus) {
410 previousFocus.focus();
411 }
412 413 // Clear focus414 elseif (usingFocusIO) {
415 focusIO.clearFocus();
416 }
417 elsetree.focus = null;
418 419 }
420 421 aliasisFocused = typeof(super).isFocused;
422 423 @property424 overrideboolisFocused() const {
425 426 returnchildHasFocus427 || super.isFocused428 || (childPopup && childPopup.isFocused);
429 430 }
431 432 aliasopEquals = typeof(super).opEquals;
433 434 overrideboolopEquals(constObjectother) const {
435 returnsuper.opEquals(other);
436 }
437 438 overridevoidemitEvent(InputEventevent) {
439 assert(focusIO, "FocusIO is not loaded");
440 focusIO.emitEvent(event);
441 }
442 443 overridevoidtypeText(scopeconstchar[] text) {
444 assert(focusIO, "FocusIO is not loaded");
445 focusIO.typeText(text);
446 }
447 448 overridechar[] readText(returnscopechar[] buffer, refintoffset) {
449 assert(focusIO, "FocusIO is not loaded");
450 returnfocusIO.readText(buffer, offset);
451 }
452 453 overrideinout(Focusable) currentFocus() inout {
454 if (usingFocusIO && !focusIO.isFocused(this)) {
455 returnnull;
456 }
457 return_currentFocus;
458 }
459 460 overrideFocusablecurrentFocus(FocusablenewValue) {
461 if (usingFocusIO) {
462 focusIO.currentFocus = this;
463 }
464 return_currentFocus = newValue;
465 }
466 467 privateboolusingFocusIO() constnothrow {
468 returnfocusIO && focusIO !isthis;
469 }
470 471 }
472 473 /// Tree action displaying a popup.474 classPopupNodeAction : TreeAction {
475 476 public {
477 478 PopupFramepopup;
479 480 }
481 482 protected {
483 484 /// Safety guard: Do not draw the popup if the tree hasn't resized.485 boolhasResized;
486 487 }
488 489 this(PopupFramepopup) {
490 491 this.startNode = this.popup = popup;
492 popup.show();
493 popup.toRemove = false;
494 495 }
496 497 overridevoidbeforeResize(Nodenode, Vector2viewportSize) {
498 499 // Only accept root resizes500 if (node !isnode.tree.root) return;
501 502 // Perform the resize503 popup.resizeInternal(node, viewportSize);
504 505 // First resize506 if (!hasResized) {
507 508 // Give that popup focus509 popup.previousFocus = node.tree.focus;
510 popup.focus();
511 hasResized = true;
512 513 }
514 515 }
516 517 /// Tree drawn, draw the popup now.518 overridevoidafterTree() {
519 520 // Don't draw without a resize521 if (!hasResized) return;
522 523 // Stop if the popup requested removal524 if (popup.toRemove) { stop; return; }
525 526 // Draw the popup527 popup.childHasFocus = false;
528 popup.drawAnchored(popup.tree.root);
529 530 // Remove the popup if it has no focus531 if (!popup.isFocused) {
532 popup.remove();
533 stop;
534 }
535 536 537 }
538 539 overridevoidafterDraw(Nodenode, Rectanglespace) {
540 541 importfluid.popup_button;
542 543 // Require at least one resize to search for focus544 if (!hasResized) return;
545 546 // Mark popup buttons547 if (autobutton = cast(PopupButton) node) {
548 549 button.parentPopup = popup;
550 551 }
552 553 // Ignore if a focused node has already been found554 if (popup.isFocused) return;
555 556 constfocusable = cast(FluidFocusable) node;
557 558 if (focusable && focusable.isFocused) {
559 560 popup.childHasFocus = focusable.isFocused;
561 562 }
563 564 }
565 566 overridevoidafterInput(refboolkeyboardHandled) {
567 568 // Require at least one resize569 if (!hasResized) return;
570 571 // Ignore if input was already handled572 if (keyboardHandled) return;
573 574 // Ignore input in child popups575 if (popup.childPopup && popup.childPopup.isFocused) return;
576 577 // Run actions for the popup578 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 with586 /// 0.8.0 popup frames should implement `LayoutIO` to detect child popups.587 privateclassMarkPopupButtonsAction : BranchAction {
588 589 PopupFrameparent;
590 591 this(PopupFrameparent) {
592 this.parent = parent;
593 }
594 595 overridevoidbeforeDraw(Nodenode, Rectangle) {
596 597 importfluid.popup_button;
598 599 if (autobutton = cast(PopupButton) node) {
600 button.parentPopup = parent;
601 }
602 603 }
604 605 }