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 }