1 module fluid.popup_frame;
2 
3 import std.traits;
4 import std.algorithm;
5 
6 import fluid.node;
7 import fluid.tree;
8 import fluid.frame;
9 import fluid.input;
10 import fluid.style;
11 import fluid.utils;
12 import fluid.actions;
13 import fluid.backend;
14 
15 
16 @safe:
17 
18 
19 deprecated("popup has been renamed to popupFrame")
20 alias popup = popupFrame;
21 
22 deprecated("FluidPopup has been renamed to PopupFrame")
23 alias FluidPopup = PopupFrame;
24 
25 alias popupFrame = simpleConstructor!PopupFrame;
26 
27 /// Spawn a new popup attached to the given tree.
28 ///
29 /// The popup automatically gains focus.
30 void spawnPopup(LayoutTree* tree, PopupFrame popup) {
31 
32     popup.tree = tree;
33 
34     // Set anchor
35     popup.anchor = Vector2(
36         tree.focusBox.x,
37         tree.focusBox.y + tree.focusBox.height
38     );
39 
40     // Spawn the popup
41     tree.queueAction(new PopupNodeAction(popup));
42     tree.root.updateSize();
43 
44 }
45 
46 /// Spawn a new popup, as a child of another. While the child is active, the parent will also remain so.
47 ///
48 /// The newly spawned popup automatically gains focus.
49 void spawnChildPopup(PopupFrame parent, PopupFrame popup) {
50 
51     auto tree = parent.tree;
52 
53     // Assign the child
54     parent.childPopup = popup;
55 
56     // Spawn the popup
57     spawnPopup(tree, popup);
58 
59 }
60 
61 /// This is an override of Frame to simplify creating popups: if clicked outside of it, it will disappear from
62 /// the node tree.
63 class PopupFrame : Frame, FluidFocusable {
64 
65     mixin defineStyles;
66     mixin makeHoverable;
67     mixin enableInputActions;
68 
69     public {
70 
71         /// Position the frame is "anchored" to. A corner of the frame will be chosen to match this position.
72         Vector2 anchor;
73 
74         /// A child popup will keep this focus alive while focused.
75         /// Typically, child popups are spawned as a result of actions within the popup itself, for example in context
76         /// menus, an action can spawn a submenu. Use `spawnChildPopup` to spawn child popups.
77         PopupFrame childPopup;
78 
79         /// Node that had focus before `popupFrame` took over. When the popup is closed using a keyboard shortcut, this
80         /// node will take focus again.
81         ///
82         /// Assigned automatically if `spawnPopup` or `spawnChildPopup` is used, but otherwise not.
83         FluidFocusable previousFocus;
84 
85     }
86 
87     private {
88 
89         bool childHasFocus;
90 
91     }
92 
93     this(NodeParams params, Node[] nodes...) {
94 
95         super(params, nodes);
96 
97     }
98 
99     /// Draw the popup using the assigned anchor position.
100     void drawAnchored() {
101 
102         const rect = Rectangle(
103             anchoredStartCorner.tupleof,
104             minSize.tupleof
105         );
106 
107         // Draw the node within the defined rectangle
108         draw(rect);
109 
110     }
111 
112     private void resizeInternal(LayoutTree* tree, Theme theme, Vector2 space) {
113 
114         resize(tree, theme, space);
115 
116     }
117 
118     /// Get start (top-left) corner of the popup if `drawAnchored` is to be used.
119     Vector2 anchoredStartCorner() {
120 
121         const viewportSize = io.windowSize;
122 
123         // This method is very similar to MapSpace.getStartCorner, but simplified to handle the "automatic" case
124         // only.
125 
126         // Define important points on the screen: center is our anchor, left is the other corner of the popup if we
127         // extend it to the top-left, right is the other corner of the popup if we extend it to the bottom-right
128         //  x--|    <- left
129         //  |  |
130         //  |--o--| <- center (anchor)
131         //     |  |
132         //     |--x <- right
133         const left = anchor - minSize;
134         const center = anchor;
135         const right = anchor + minSize;
136 
137         // Horizontal position
138         const x
139 
140             // Default to extending towards the bottom-right, unless we overflow
141             // |=============|
142             // |   ↓ center  |
143             // |   O------|  |
144             // |   |      |  |
145             // |   |      |  |
146             // |   |------|  |
147             // |=============|
148             = right.x < viewportSize.x ? center.x
149 
150             // But in case we cannot fit the popup, we might need to reverse the direction
151             // |=============|          |=============|
152             // |             | ↓ right  | ↓ left
153             // |        O------>        | <------O    |
154             // |        |      |        | |      |    |
155             // |        |      |        | |      |    |
156             // |        |------|        | |------|    |
157             // |=============|          |=============|
158             : left.x >= 0 ? left.x
159 
160             // However, if we overflow either way, it's best we center the popup on the screen
161             : (viewportSize.x - minSize.x) / 2;
162 
163         // Do the same for vertical position
164         const y
165             = right.y < viewportSize.y ? center.y
166             : left.y >= 0 ? left.y
167             : (viewportSize.y - minSize.y) / 2;
168 
169         return Vector2(x, y);
170 
171     }
172 
173     protected override void drawImpl(Rectangle outer, Rectangle inner) {
174 
175         // Clear directional focus data; give the popup a separate context
176         tree.focusDirection = FocusDirection(tree.focusDirection.lastFocusBox);
177 
178         super.drawImpl(outer, inner);
179 
180         // Forcibly register previous & next focus if missing
181         // The popup will register itself just after it gets drawn without this — and it'll be better if it doesn't
182         if (tree.focusDirection.previous is null) {
183 
184             tree.focusDirection.previous = tree.focusDirection.last;
185 
186         }
187 
188         if (tree.focusDirection.next is null) {
189 
190             tree.focusDirection.next = tree.focusDirection.first;
191 
192         }
193 
194     }
195 
196     protected void mouseImpl() {
197 
198     }
199 
200     protected bool focusImpl() {
201 
202         return false;
203 
204     }
205 
206     void focus() {
207 
208         // Set focus to self
209         tree.focus = this;
210 
211         // Prefer if children get it, though
212         this.focusRecurseChildren();
213 
214     }
215 
216     /// Give focus to whatever node had focus before this one.
217     @(FluidInputAction.cancel)
218     void restorePreviousFocus() {
219 
220         // Restore focus if possible
221         if (previousFocus) {
222 
223             previousFocus.focus();
224 
225         }
226 
227         // Clear focus
228         else tree.focus = null;
229 
230     }
231 
232     bool isFocused() const {
233 
234         return childHasFocus
235             || tree.focus is this
236             || (childPopup && childPopup.isFocused);
237 
238     }
239 
240 }
241 
242 /// Tree action displaying a popup.
243 class PopupNodeAction : TreeAction {
244 
245     public {
246 
247         PopupFrame popup;
248 
249     }
250 
251     protected {
252 
253         /// Safety guard: Do not draw the popup if the tree hasn't resized.
254         bool hasResized;
255 
256     }
257 
258     this(PopupFrame popup) {
259 
260         this.startNode = this.popup = popup;
261         popup.show();
262         popup.toRemove = false;
263 
264     }
265 
266     override void beforeResize(Node root, Vector2 viewportSize) {
267 
268         // Perform the resize
269         popup.resizeInternal(root.tree, root.theme, viewportSize);
270 
271         // First resize
272         if (!hasResized) {
273 
274             // Give that popup focus
275             popup.previousFocus = root.tree.focus;
276             popup.focus();
277             hasResized = true;
278 
279         }
280 
281     }
282 
283     /// Tree drawn, draw the popup now.
284     override void afterTree() {
285 
286         // Don't draw without a resize
287         if (!hasResized) return;
288 
289         // Stop if the popup requested removal
290         if (popup.toRemove) { stop; return; }
291 
292         // Draw the popup
293         popup.childHasFocus = false;
294         popup.drawAnchored();
295 
296         // Remove the popup if it has no focus
297         if (!popup.isFocused) {
298             popup.remove();
299             stop;
300         }
301 
302 
303     }
304 
305     override void afterDraw(Node node, Rectangle space) {
306 
307         import fluid.popup_button;
308 
309         // Require at least one resize to search for focus
310         if (!hasResized) return;
311 
312         // Mark popup buttons
313         if (auto button = cast(PopupButton) node) {
314 
315             button.parentPopup = popup;
316 
317         }
318 
319         // Ignore if a focused node has already been found
320         if (popup.isFocused) return;
321 
322         const focusable = cast(FluidFocusable) node;
323 
324         if (focusable && focusable.isFocused) {
325 
326             popup.childHasFocus = focusable.isFocused;
327 
328         }
329 
330     }
331 
332     override void afterInput(ref bool keyboardHandled) {
333 
334         // Require at least one resize
335         if (!hasResized) return;
336 
337         // Ignore if input was already handled
338         if (keyboardHandled) return;
339 
340         // Ignore input in child popups
341         if (popup.childPopup && popup.childPopup.isFocused) return;
342 
343         // Run actions for the popup
344         keyboardHandled = popup.runFocusInputActions;
345 
346     }
347 
348 }