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