1 modulefluid.popup_frame;
2 3 importstd.traits;
4 importstd.algorithm;
5 6 importfluid.node;
7 importfluid.tree;
8 importfluid.frame;
9 importfluid.input;
10 importfluid.style;
11 importfluid.utils;
12 importfluid.actions;
13 importfluid.backend;
14 15 16 @safe:
17 18 19 aliaspopupFrame = simpleConstructor!PopupFrame;
20 21 /// Spawn a new popup attached to the given tree.22 ///23 /// The popup automatically gains focus.24 voidspawnPopup(LayoutTree* tree, PopupFramepopup) {
25 26 popup.tree = tree;
27 28 // Set anchor29 popup.anchor = Vector2(
30 tree.focusBox.x,
31 tree.focusBox.y + tree.focusBox.height32 );
33 34 // Spawn the popup35 tree.queueAction(newPopupNodeAction(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 voidspawnChildPopup(PopupFrameparent, PopupFramepopup) {
44 45 autotree = parent.tree;
46 47 // Inherit theme from parent48 // TODO This may not work...49 if (!popup.theme)
50 popup.theme = parent.theme;
51 52 // Assign the child53 parent.childPopup = popup;
54 55 // Spawn the popup56 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 from61 /// the node tree.62 classPopupFrame : InputNode!Frame {
63 64 mixinmakeHoverable;
65 mixinenableInputActions;
66 67 public {
68 69 /// Position the frame is "anchored" to. A corner of the frame will be chosen to match this position.70 Vector2anchor;
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 context74 /// menus, an action can spawn a submenu. Use `spawnChildPopup` to spawn child popups.75 PopupFramechildPopup;
76 77 /// Node that had focus before `popupFrame` took over. When the popup is closed using a keyboard shortcut, this78 /// node will take focus again.79 ///80 /// Assigned automatically if `spawnPopup` or `spawnChildPopup` is used, but otherwise not.81 FluidFocusablepreviousFocus;
82 83 }
84 85 private {
86 87 boolchildHasFocus;
88 89 }
90 91 this(Node[] nodes...) {
92 93 super(nodes);
94 95 }
96 97 /// Draw the popup using the assigned anchor position.98 voiddrawAnchored() {
99 100 constrect = Rectangle(
101 anchoredStartCorner.tupleof,
102 minSize.tupleof103 );
104 105 // Draw the node within the defined rectangle106 draw(rect);
107 108 }
109 110 privatevoidresizeInternal(LayoutTree* tree, Themetheme, Vector2space) {
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 Vector2anchoredStartCorner() {
118 119 constviewportSize = io.windowSize;
120 121 // This method is very similar to MapSpace.getStartCorner, but simplified to handle the "automatic" case122 // only.123 124 // Define important points on the screen: center is our anchor, left is the other corner of the popup if we125 // extend it to the top-left, right is the other corner of the popup if we extend it to the bottom-right126 // x--| <- left127 // | |128 // |--o--| <- center (anchor)129 // | |130 // |--x <- right131 constleft = anchor - minSize;
132 constcenter = anchor;
133 constright = anchor + minSize;
134 135 // Horizontal position136 constx137 138 // Default to extending towards the bottom-right, unless we overflow139 // |=============|140 // | ↓ center |141 // | O------| |142 // | | | |143 // | | | |144 // | |------| |145 // |=============|146 = right.x < viewportSize.x ? center.x147 148 // But in case we cannot fit the popup, we might need to reverse the direction149 // |=============| |=============|150 // | | ↓ right | ↓ left |151 // | O------> | <------O |152 // | | | | | | |153 // | | | | | | |154 // | |------| | |------| |155 // |=============| |=============|156 : left.x >= 0 ? left.x157 158 // However, if we overflow either way, it's best we center the popup on the screen159 : (viewportSize.x - minSize.x) / 2;
160 161 // Do the same for vertical position162 consty163 = right.y < viewportSize.y ? center.y164 : left.y >= 0 ? left.y165 : (viewportSize.y - minSize.y) / 2;
166 167 returnVector2(x, y);
168 169 }
170 171 protectedoverridevoiddrawImpl(Rectangleouter, Rectangleinner) {
172 173 // Clear directional focus data; give the popup a separate context174 tree.focusDirection = FocusDirection(tree.focusDirection.lastFocusBox);
175 176 super.drawImpl(outer, inner);
177 178 // Forcibly register previous & next focus if missing179 // The popup will register itself just after it gets drawn without this — and it'll be better if it doesn't180 if (tree.focusDirection.previousisnull) {
181 182 tree.focusDirection.previous = tree.focusDirection.last;
183 184 }
185 186 if (tree.focusDirection.nextisnull) {
187 188 tree.focusDirection.next = tree.focusDirection.first;
189 190 }
191 192 }
193 194 protectedoverridevoidmouseImpl() {
195 196 }
197 198 protectedoverrideboolfocusImpl() {
199 200 returnfalse;
201 202 }
203 204 overridevoidfocus() {
205 206 // Set focus to self207 tree.focus = this;
208 209 // Prefer if children get it, though210 this.focusRecurseChildren();
211 212 }
213 214 /// Give focus to whatever node had focus before this one.215 @(FluidInputAction.cancel)
216 voidrestorePreviousFocus() {
217 218 // Restore focus if possible219 if (previousFocus) {
220 221 previousFocus.focus();
222 223 }
224 225 // Clear focus226 elsetree.focus = null;
227 228 }
229 230 aliasisFocused = typeof(super).isFocused;
231 232 @property233 overrideboolisFocused() const {
234 235 returnchildHasFocus236 || tree.focusisthis237 || (childPopup && childPopup.isFocused);
238 239 }
240 241 }
242 243 /// Tree action displaying a popup.244 classPopupNodeAction : TreeAction {
245 246 public {
247 248 PopupFramepopup;
249 250 }
251 252 protected {
253 254 /// Safety guard: Do not draw the popup if the tree hasn't resized.255 boolhasResized;
256 257 }
258 259 this(PopupFramepopup) {
260 261 this.startNode = this.popup = popup;
262 popup.show();
263 popup.toRemove = false;
264 265 }
266 267 overridevoidbeforeResize(Nodenode, Vector2viewportSize) {
268 269 // Only accept root resizes270 if (node !isnode.tree.root) return;
271 272 // Perform the resize273 popup.resizeInternal(node.tree, node.theme, viewportSize);
274 275 // First resize276 if (!hasResized) {
277 278 // Give that popup focus279 popup.previousFocus = node.tree.focus;
280 popup.focus();
281 hasResized = true;
282 283 }
284 285 }
286 287 /// Tree drawn, draw the popup now.288 overridevoidafterTree() {
289 290 // Don't draw without a resize291 if (!hasResized) return;
292 293 // Stop if the popup requested removal294 if (popup.toRemove) { stop; return; }
295 296 // Draw the popup297 popup.childHasFocus = false;
298 popup.drawAnchored();
299 300 // Remove the popup if it has no focus301 if (!popup.isFocused) {
302 popup.remove();
303 stop;
304 }
305 306 307 }
308 309 overridevoidafterDraw(Nodenode, Rectanglespace) {
310 311 importfluid.popup_button;
312 313 // Require at least one resize to search for focus314 if (!hasResized) return;
315 316 // Mark popup buttons317 if (autobutton = cast(PopupButton) node) {
318 319 button.parentPopup = popup;
320 321 }
322 323 // Ignore if a focused node has already been found324 if (popup.isFocused) return;
325 326 constfocusable = cast(FluidFocusable) node;
327 328 if (focusable && focusable.isFocused) {
329 330 popup.childHasFocus = focusable.isFocused;
331 332 }
333 334 }
335 336 overridevoidafterInput(refboolkeyboardHandled) {
337 338 // Require at least one resize339 if (!hasResized) return;
340 341 // Ignore if input was already handled342 if (keyboardHandled) return;
343 344 // Ignore input in child popups345 if (popup.childPopup && popup.childPopup.isFocused) return;
346 347 // Run actions for the popup348 keyboardHandled = popup.runFocusInputActions;
349 350 }
351 352 }