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 }