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 }