1 /// This module implements actions that require the new I/O system to work correctly. 2 module fluid.future.action; 3 4 import fluid.node; 5 import fluid.tree; 6 import fluid.types; 7 import fluid.style; 8 import fluid.actions; 9 10 import fluid.io.focus; 11 import fluid.io.action; 12 13 import fluid.future.pipe; 14 import fluid.future.branch_action; 15 16 @safe: 17 18 19 /// Focus next or previous focusable node relative to the point of reference. 20 /// This function only works with nodes compatible with the new I/O system introduced in Fluid 0.7.2. 21 /// 22 /// Params: 23 /// node = Node to use for reference. 24 /// branch = Branch to search. Nodes that are not children of this node will not be matched. 25 /// Default to the whole tree. 26 /// wrap = If true, if no node remains to focus, focus the first or last node found. 27 OrderedFocusAction focusNext(Node node, bool wrap = true) { 28 auto action = new OrderedFocusAction(node, false, wrap); 29 node.tree.queueAction(action); 30 return action; 31 } 32 33 /// ditto 34 OrderedFocusAction focusPrevious(Node node, bool wrap = true) { 35 auto action = new OrderedFocusAction(node, true, wrap); 36 node.tree.queueAction(action); 37 return action; 38 } 39 40 /// ditto 41 OrderedFocusAction focusNext(Node node, Node branch, bool wrap = true) { 42 auto action = new OrderedFocusAction(node, false, wrap); 43 branch.queueAction(action); 44 return action; 45 } 46 47 /// ditto 48 OrderedFocusAction focusPrevious(Node node, Node branch, bool wrap = true) { 49 auto action = new OrderedFocusAction(node, true, wrap); 50 branch.queueAction(action); 51 return action; 52 } 53 54 final class OrderedFocusAction : FocusSearchAction { 55 56 public { 57 58 /// Node to use as reference. The action will either select the next node that follows, or the previous. 59 Node target; 60 61 /// If true, the action finds the previous node. If false, the action finds the next one. 62 bool isReverse; 63 64 /// If true, does nothing if the target node is the last (going forward) or the first (going backwards). 65 /// Otherwise goes back to the top or bottom respectively. 66 bool isWrapDisabled; 67 68 } 69 70 private { 71 72 /// Last focusable node in the branch, first focusable node in the branch. Updates as the node iterates. 73 Node _last, _first; 74 75 /// Previous and next focusable relative to the target. 76 Node _previous, _next; 77 78 } 79 80 this() { 81 82 } 83 84 this(Node target, bool isReverse = false, bool wrap = true) { 85 reset(target, isReverse, wrap); 86 } 87 88 /// Re-arm the action. 89 void reset(Node target, bool isReverse = false, bool wrap = true) { 90 this.target = target; 91 this.isReverse = isReverse; 92 this.isWrapDisabled = !wrap; 93 clearSubscribers(); 94 } 95 96 override void beforeTree(Node node, Rectangle rect) { 97 98 super.beforeTree(node, rect); 99 this._last = null; 100 this._first = null; 101 this._previous = null; 102 this._next = null; 103 104 } 105 106 override void beforeDraw(Node node, Rectangle) { 107 108 // The start node is not a valid subject 109 if (startNode && node.opEquals(startNode)) return; 110 111 // Found the target 112 if (node.opEquals(target)) { 113 114 // Going backwards: Mark the last focusable as the previous node 115 if (isReverse) { 116 _previous = _last; 117 } 118 119 // Going forwards: Clear the next focusable so it can be overriden by a correct value 120 else { 121 _next = null; 122 } 123 124 return; 125 126 } 127 128 // Ignore nodes that are not focusable 129 if (node.castIfAcceptsInput!Focusable is null) return; 130 131 // Set first and next node to this node 132 if (_first is null) { 133 _first = node; 134 } 135 if (_next is null) { 136 _next = node; 137 } 138 139 // Mark as the last found focusable 140 _last = node; 141 142 } 143 144 override void afterTree() { 145 146 // Selecting previous or next node 147 result = isReverse 148 ? _previous 149 : _next; 150 151 // No such node, try first/last 152 if (!isWrapDisabled && result is null) { 153 result = isReverse 154 ? _last 155 : _first; 156 } 157 158 // Found a result! 159 if (auto focusable = cast(Focusable) result) { 160 focusable.focus(); 161 } 162 163 stop; 164 165 } 166 167 } 168 169 170 /// Find and focus a focusable node based on its visual position; above, below, to the left or to the right 171 /// of a chosen node. 172 /// 173 /// Using this function requires knowing the last position of the node, which isn't usually stored. Depending on 174 /// the usecase, you may need to use `FindFocusBoxAction` earlier. 175 /// 176 /// Nodes are chosen based on semantical weight — nodes within the same container will be prioritized over 177 /// nodes in another. Only if the weight is the same, they will be compared based on their visual distance. 178 /// 179 /// Params: 180 /// node = Node to use as reference. 181 /// focusBox = Last known `focusBox` of the node. 182 /// direction = Direction to switch to, if calling `focusDirection`. 183 /// Returns: 184 /// A tree action which will run during the next frame. You can attach a callback using its `then` method 185 /// to process the found node. 186 PositionalFocusAction focusAbove(Node node, Rectangle focusBox) { 187 return focusDirection(node, focusBox, Style.Side.top); 188 } 189 190 /// ditto 191 PositionalFocusAction focusBelow(Node node, Rectangle focusBox) { 192 return focusDirection(node, focusBox, Style.Side.bottom); 193 } 194 195 /// ditto 196 PositionalFocusAction focusToLeft(Node node, Rectangle focusBox) { 197 return focusDirection(node, focusBox, Style.Side.left); 198 } 199 200 /// ditto 201 PositionalFocusAction focusToRight(Node node, Rectangle focusBox) { 202 return focusDirection(node, focusBox, Style.Side.right); 203 } 204 205 /// ditto 206 PositionalFocusAction focusDirection(Node node, Rectangle focusBox, Style.Side direction) { 207 208 auto action = new PositionalFocusAction(node, focusBox, direction); 209 node.startAction(action); 210 return action; 211 212 } 213 214 final class PositionalFocusAction : FocusSearchAction { 215 216 public { 217 218 /// Node to use as reference. The action will either select the next node that follows, or the previous. 219 Node target; 220 221 /// Focus box of the target node. 222 Rectangle focusBox; 223 224 /// Direction of search. 225 Style.Side direction; 226 227 /// Focus box of the located node. 228 Rectangle resultFocusBox; 229 230 } 231 232 private { 233 234 // Properties for the match 235 int resultPriority; /// Priority assigned to the match. 236 float resultDistance2; /// Distance 237 238 /// Priority assigned to the next node, based on the current tree position. 239 int priority; 240 241 /// Multiplier for changes to priority; +1 when moving towards the target, -1 when moving away from it. 242 /// This assigns higher priority for nodes that are semantically closer to the match. 243 /// 244 /// Priority changes only when depth changes; if two nodes are drawn and they're siblings, priority 245 /// won't change. Priority will only change if the relation is different, e.g. child, cousin, etc. 246 int priorityDirection = 1; 247 248 /// Current depth. 249 int depth; 250 251 /// Depth of the last node drawn. 252 int lastDepth; 253 254 } 255 256 this() { 257 258 } 259 260 this(Node target, Rectangle focusBox, Style.Side direction) { 261 reset(target, focusBox, direction); 262 } 263 264 /// Re-arm the action. 265 void reset(Node target, Rectangle focusBox, Style.Side direction) { 266 this.result = null; 267 this.target = target; 268 this.focusBox = focusBox; 269 this.direction = direction; 270 this.resultFocusBox = focusBox; 271 clearSubscribers(); 272 } 273 274 override void beforeTree(Node node, Rectangle rectangle) { 275 this.result = null; 276 this.priority = 0; 277 this.priorityDirection = 1; 278 this.depth = 0; 279 this.lastDepth = 0; 280 } 281 282 override void beforeDraw(Node node, Rectangle) { 283 284 depth++; 285 286 } 287 288 override void afterDraw(Node node, Rectangle, Rectangle, Rectangle inner) { 289 290 import std.math : abs; 291 292 depth--; 293 294 auto focusable = node.castIfAcceptsInput!Focusable; 295 296 // Set priority 297 priority += priorityDirection * abs(depth - lastDepth); 298 lastDepth = depth; 299 300 // Ignore nodes that don't accept focus 301 if (!focusable) return; 302 303 // Found the target, reverse priority direction 304 if (node.opEquals(target)) { 305 priorityDirection = -1; 306 return; 307 } 308 309 const box = node.focusBox(inner); 310 const dist = distance2(box); 311 312 313 // Check if this node matches the direction 314 if (!box.isBeyond(focusBox, direction)) return; 315 316 // Compare against previous best match 317 if (result) { 318 319 // Ignore if the other match has higher priority 320 if (resultPriority > priority) return; 321 322 // If priorities are equal, compare distance 323 if (resultPriority == priority 324 && resultDistance2 < dist) return; 325 326 } 327 328 // Replace the node 329 result = node; 330 resultPriority = priority; 331 resultDistance2 = dist; 332 resultFocusBox = box; 333 334 } 335 336 override void stopped() { 337 338 if (auto focusable = cast(Focusable) result) { 339 focusable.focus(); 340 } 341 342 super.stopped(); 343 344 } 345 346 /// Get the square of the distance between given box and the target's `focusBox`. 347 private float distance2(Rectangle box) { 348 349 /// Get the center of given rectangle on the axis opposite to the results of getSide. 350 float center(Rectangle rect) { 351 352 return direction == Style.Side.left || direction == Style.Side.right 353 ? rect.y + rect.height 354 : rect.x + rect.width; 355 356 } 357 358 // Distance between box sides facing each other, see `checkDirection` 359 const distanceExternal = focusBox.getSide(direction) - box.getSide(direction.reverse); 360 361 /// Distance between centers of the boxes on the other axis 362 const distanceOpposite = center(box) - center(focusBox); 363 364 return distanceExternal^^2 + distanceOpposite^^2; 365 366 } 367 368 }