1 /// 2 module fluid.hover_chain; 3 4 import std.array; 5 import std.algorithm; 6 7 import fluid.node; 8 import fluid.types; 9 import fluid.utils; 10 import fluid.node_chain; 11 12 import fluid.io.hover; 13 import fluid.io.focus; 14 import fluid.io.action; 15 16 import fluid.future.arena; 17 import fluid.future.action; 18 19 @safe: 20 21 alias hoverChain = nodeBuilder!HoverChain; 22 23 /// A hover chain can be used to separate hover in different areas of the user interface, effectively treating them 24 /// like separate windows. A device node (like a mouse) can be placed to control nodes inside. 25 /// 26 /// `HoverChain` has to be placed inside `FocusIO` to enable switching focus by pressing nodes. 27 /// 28 /// For focus-based nodes like keyboard and gamepad, see `FocusChain`. 29 /// 30 /// `HoverChain` only works with nodes compatible with the new I/O system introduced in Fluid 0.7.2. 31 class HoverChain : NodeChain, ActionHoverIO { 32 33 mixin controlIO; 34 35 ActionIO actionIO; 36 FocusIO focusIO; 37 38 private { 39 40 struct Pointer { 41 42 /// The stored pointer. This is the last pointer assigned by `load`, as given by the device node. 43 /// Event handlers are given `armedValue` instead. 44 HoverPointer value; 45 46 /// Pointer passed to event handlers. Associated with a negative ID, i.e. if the pointer's ID is `0`, 47 /// the ID of `armedValue` is `-1`, if the main ID is `1`, the ID of `armedValue` is `-2` and so on. 48 HoverPointer armedValue; 49 50 /// Branch action associated with the pointer; finds the associated node. 51 FindHoveredNodeAction action; 52 53 /// Node last matched to the pointer. "Hovered" node. 54 Node node; 55 56 /// Node that is being held, placed under the cursor at the time a button has been pressed. 57 /// Input actions won't fire if the hovered node, the one under the cursor, is different from the one 58 /// that is being held. 59 Node heldNode; 60 61 /// Scrollable hovered by this pointer, if any. 62 HoverScrollable scrollable; 63 64 /// If true, any button related to the pointer is being held. 65 /// 66 /// `heldNode` will not be updated while this is true, and current focus will be updated to match 67 /// the hovered node. 68 bool isHeld; 69 70 /// If true, the pointer already handled incoming input events. 71 bool isHandled; 72 73 bool opEquals(const HoverPointer pointer) const { 74 return this.value.isSame(pointer); 75 } 76 77 } 78 79 ResourceArena!Pointer _pointers; 80 81 } 82 83 this() { 84 85 } 86 87 this(Node next) { 88 super(next); 89 } 90 91 /// Each `HoverPointer` loaded into `HoverChain` has two values, under two different ID numbers. 92 /// This function converts either number into the original one. 93 /// 94 /// Since the IDs are assigned in a consistent, deterministic manner, 95 /// the pointer does not need to be loaded for this function to work. 96 /// 97 /// See_Also: 98 /// `fetch` for information on the difference between the values. 99 /// `armedPointerID` for a function to get the ID of the armed pointer. 100 /// Params: 101 /// number = Pointer ID to normalize, negative or not. 102 /// Returns: 103 /// The normalized, non-negative pointer number. 104 /// Returns the same ID as given if it was already normalized. 105 int normalizedPointerID(int number) const { 106 107 if (number < 0) { 108 return -number - 1; 109 } 110 else { 111 return number; 112 } 113 114 } 115 116 /// Performs the opposite of `normalizedPointerID`; gets the ID of the armed pointer, the one made available 117 /// to event handling nodes. 118 /// 119 /// Since the IDs are assigned in a consistent, deterministic manner, 120 /// the pointer does not need to be loaded for this function to work. 121 /// 122 /// See_Also: 123 /// `normalizedPointerID` 124 /// Params: 125 /// number = ID of the pointer, either negative or not. 126 /// Returns: 127 /// The ID of the armed pointer. 128 /// Returns the same ID as given if it was already armed. 129 int armedPointerID(int number) const { 130 131 if (number >= 0) { 132 return -number - 1; 133 } 134 else { 135 return number; 136 } 137 138 } 139 140 /// Get the armed variant of the pointer. 141 /// Params: 142 /// pointer = A regular, or armed pointer. 143 /// Returns: 144 /// Armed pointer corresponding to the given pointer. 145 /// If the pointer is already an armed variant, no conversion is performed. 146 inout(HoverPointer) armedPointer(HoverPointer pointer) inout { 147 const armed = armedPointerID(pointer.id); 148 return fetch(armed); 149 } 150 151 override int load(HoverPointer pointer) 152 out(r) { 153 import std.format; 154 debug assert(_pointers.allResources.count(pointer) == 1, 155 format!"Duplicate pointers created: %(\n %s%)"(_pointers.allResources)); 156 } 157 do { 158 159 const index = cast(int) _pointers.allResources.countUntil(pointer); 160 161 // No such pointer 162 if (index == -1) { 163 Pointer newPointer; 164 newPointer.value = pointer; 165 newPointer.armedValue.isDisabled = true; 166 newPointer.action = new FindHoveredNodeAction; 167 newPointer.action.stop(); // Temporarily mark as inactive 168 169 const newIndex = _pointers.load(newPointer); 170 _pointers[newIndex].value.load(this, newIndex); 171 return newIndex; 172 } 173 174 // Found, update the pointer 175 else { 176 auto updatedPointer = _pointers[index]; 177 updatedPointer.value.update(pointer); 178 updatedPointer.value.load(this, index); 179 updatedPointer.armedValue.clickCount = pointer.clickCount; 180 _pointers.reload(index, updatedPointer); 181 return index; 182 } 183 184 } 185 186 /// Fetch a pointer by the number assigned to it when loading. 187 /// 188 /// Under the hood, `HoverChain` creates two pointers for each load. 189 /// One has a number of zero or more (the original pointer), and one has a negative number (armed pointer). 190 /// The original pointer reflects the changes made when loading and updating exactly, 191 /// while the armed pointer is updated only when a new frame starts. 192 /// This makes it possible to update the pointer, while it is in use by `FindHoveredNodeAction`. 193 /// Otherwise, the values given to the could be out of date by the time the relevant node is found. 194 /// 195 /// See_Also: 196 /// `normalizedPointerID` and `armedPointerID` for converting between pointer IDs. 197 /// Returns: 198 /// Pointer associated with the node. 199 override inout(HoverPointer) fetch(int number) inout { 200 201 // Armed variant 202 if (number < 0) { 203 const trueNumber = normalizedPointerID(number); 204 assert(_pointers.isActive(trueNumber), "Pointer is not active"); 205 return _pointers[trueNumber].armedValue; 206 } 207 208 // Original variant 209 else { 210 assert(_pointers.isActive(number), "Pointer is not active"); 211 return _pointers[number].value; 212 } 213 214 } 215 216 override void emitEvent(HoverPointer pointer, InputEvent event) { 217 218 const id = normalizedPointerID(pointer.id); 219 220 assert(_pointers.isActive(id), "Pointer is not active"); 221 222 // Mark the pointer as held 223 _pointers[id].isHeld = true; 224 225 // Emit the event 226 if (actionIO) { 227 actionIO.emitEvent(event, id, &runInputAction); 228 } 229 230 } 231 232 override bool isHovered(const Hoverable hoverable) const { 233 foreach (pointer; _pointers.activeResources) { 234 if (hoverable.opEquals(pointer.heldNode)) { 235 return true; 236 } 237 } 238 return false; 239 } 240 241 /// List all active pointers controlled by this `HoverChain`. 242 /// 243 /// A copy of each pointer is maintained to pass to event handlers. While iterating, 244 /// only one version will be passed of each pointer: if while drawing, 245 /// the "armed" copy is used, otherwise the regular versions will be returned. 246 /// 247 /// The above distinction makes it possible for nodes to process the same pointers 248 /// as they're given in event handlers, while outsiders are given the usual versions. 249 override int opApply(int delegate(HoverPointer) @safe yield) { 250 251 foreach (pointer; _pointers.activeResources) { 252 253 auto value = pointer.action.toStop 254 ? pointer.value 255 : pointer.armedValue; 256 257 // List each pointer 258 if (auto result = yield(value)) { 259 return result; 260 } 261 262 } 263 264 return 0; 265 266 } 267 268 override int opApply(int delegate(Hoverable) @safe yield) { 269 270 foreach (pointer; _pointers.activeResources) { 271 272 // Skip disabled pointers 273 if (pointer.value.isDisabled) continue; 274 275 // List each hoverable 276 if (auto hoverable = cast(Hoverable) pointer.heldNode) { 277 if (auto result = yield(hoverable)) { 278 return result; 279 } 280 } 281 282 } 283 284 return 0; 285 286 } 287 288 override void beforeResize(Vector2) { 289 use(actionIO); 290 use(focusIO); 291 _pointers.startCycle(); 292 startIO(); 293 } 294 295 override void afterResize(Vector2) { 296 stopIO(); 297 } 298 299 override void beforeDraw(Rectangle outer, Rectangle inner) { 300 301 foreach (resource; _pointers.activeResources) { 302 303 const id = resource.value.id; 304 const armedID = armedPointerID(id); 305 306 // Update the pointer when done 307 scope (exit) _pointers[id] = resource; 308 309 // Arm the pointer 310 resource.armedValue = resource.value; 311 resource.armedValue.load(this, armedID); 312 313 if (resource.value.isDisabled) continue; 314 315 // Start the tree action 316 resource.action.pointer = resource.armedValue; 317 auto frame = controlBranchAction(resource.action); 318 frame.start(); 319 frame.release(); 320 321 } 322 323 } 324 325 override void afterDraw(Rectangle outer, Rectangle inner) { 326 327 auto frame = controlBranchAction(_pointers.activeResources.map!"a.action"); 328 frame.stop(); 329 330 // Update hover data 331 foreach (resource; _pointers.activeResources) { 332 333 auto pointer = resource.armedValue; 334 335 // Ignore disabled pointers 336 if (pointer.isDisabled) continue; 337 338 const id = resource.value.id; 339 const armedID = pointer.id; 340 assert(armedID < 0); 341 342 scope (exit) _pointers[id] = resource; 343 344 // Keep the same hovered node if the pointer is being held, 345 // otherwise switch. 346 resource.node = resource.action.result; 347 if (!resource.isHeld) { 348 resource.heldNode = resource.node; 349 } 350 351 // Switch focus to hovered node if holding 352 else if (focusIO) { 353 if (auto focusable = resource.heldNode.castIfAcceptsInput!Focusable) { 354 if (!focusable.isFocused) { 355 focusable.focus(); 356 } 357 } 358 else { 359 focusIO.clearFocus(); 360 } 361 } 362 363 // Update scroll and send new events 364 if (!pointer.isScrollHeld) { 365 resource.scrollable = resource.action.scrollable; 366 } 367 if (resource.scrollable) { 368 resource.scrollable.scrollImpl(pointer); 369 } 370 371 // Reset state 372 resource.isHeld = false; 373 resource.isHandled = false; 374 375 // Send a frame event to trigger hoverImpl 376 if (actionIO) { 377 actionIO.emitEvent(ActionIO.frameEvent, armedID, &runInputAction); 378 } 379 else if (auto hoverable = resource.heldNode.castIfAcceptsInput!Hoverable) { 380 resource.isHandled = hoverable.hoverImpl(pointer); 381 } 382 383 } 384 385 } 386 387 override inout(Hoverable) hoverOf(HoverPointer pointer) inout { 388 const id = normalizedPointerID(pointer.id); 389 debug assert(_pointers.isActive(id), "Given pointer wasn't loaded"); 390 return _pointers[id].heldNode.castIfAcceptsInput!Hoverable; 391 } 392 393 override inout(HoverScrollable) scrollOf(HoverPointer pointer) inout { 394 const id = normalizedPointerID(pointer.id); 395 debug assert(_pointers.isActive(id), "Given pointer wasn't loaded"); 396 return _pointers[id].scrollable; 397 } 398 399 override bool runInputAction(HoverPointer pointer, immutable InputActionID actionID, 400 bool isActive = true) 401 do { 402 403 const id = normalizedPointerID(pointer.id); 404 const armedID = -id - 1; 405 const isFrameAction = actionID == inputActionID!(ActionIO.CoreAction.frame); 406 407 auto hover = hoverOf(pointer); 408 auto meta = _pointers[id]; 409 410 // Active input actions can only fire if `heldNode` is still hovered 411 if (isActive) { 412 if (meta.node is null || !meta.node.opEquals(meta.heldNode)) { 413 return false; 414 } 415 } 416 417 // Mark pointer as held 418 if (!isFrameAction) { 419 _pointers[id].isHeld = true; 420 } 421 422 // Try to handle the action 423 const handled = 424 425 // Try to run the action 426 (hover && hover.actionImpl(this, armedID, actionID, isActive)) 427 428 // Run local input actions as fallback 429 || runLocalInputActions(pointer, actionID, isActive) 430 431 // Run hoverImpl as a last resort 432 || (isFrameAction && hover && hover.hoverImpl(pointer)); 433 434 // Mark as handled, if so 435 _pointers[id].isHandled = meta.isHandled || handled; 436 437 return handled; 438 439 } 440 441 /// ditto 442 bool runInputAction(alias action)(HoverPointer pointer, bool isActive = true) { 443 const id = inputActionID!action; 444 return runInputAction(pointer, id, isActive); 445 } 446 447 /// ditto 448 protected final bool runInputAction(InputActionID actionID, bool isActive, int number) { 449 450 auto pointer = fetch(number); 451 return runInputAction(pointer, actionID, isActive); 452 453 } 454 455 /// Run an input action implemented by this node. `HoverChain` does not implement any by default. 456 /// Params: 457 /// pointer = Pointer associated with the event. 458 /// actionID = ID of the input action to perform. 459 /// isActive = If true, the action has been activated during this frame. 460 /// Returns: 461 /// True if the action was handled, false if not. 462 protected bool runLocalInputActions(HoverPointer pointer, InputActionID actionID, 463 bool isActive = true) 464 do { 465 466 return false; 467 468 } 469 470 }