1 /// Enables transforming the mouse position from one space to another by translating and scaling. 2 /// 3 /// Requires the new I/O system introduced in Fluid 0.7.2 to be used. 4 /// 5 /// History: 6 /// * Introduced in Fluid 0.7.2. 7 module fluid.hover_transform; 8 9 import std.array; 10 import std.string; 11 import std.algorithm; 12 13 import fluid.node; 14 import fluid.types; 15 import fluid.utils; 16 import fluid.structs; 17 import fluid.node_chain; 18 19 import fluid.future.arena; 20 import fluid.future.context; 21 22 import fluid.io.hover; 23 import fluid.io.focus; 24 import fluid.io.action; 25 26 @safe: 27 28 /// 29 alias hoverTransform = nodeBuilder!HoverTransform; 30 31 /// Implements `HoverIO` by transforming inputs from a "host" Hover I/O system. `HoverTransform` 32 /// must be placed as a child of a host I/O system to function. 33 /// 34 /// This node is most useful when Fluid's graphical output is transformed in post-processing. 35 /// For example, Raylib users may render the user interface inside a render texture. In such 36 /// situation, the mouse input would not match what is seen by the user. 37 /// 38 /// `HoverTransform` creates a barrier between inside — its own children — and nodes outside. 39 /// Inputs received from the host are transformed for all of its children, but remain unmodified 40 /// outside. On the other hand, inputs created inside are untouched for other transformed nodes, 41 /// but are inversely transformed for nodes outside. 42 /// 43 /// --- 44 /// hoverChain( 45 /// vspace( 46 /// .layout!"fill", 47 /// button("I receive unmodified inputs", delegate { }), 48 /// hoverTransform( 49 /// .layout!(1, "fill"), 50 /// Rectangle(0, 0, 100, 100), 51 /// button("I'm transformed", delegate { }) 52 /// ), 53 /// ), 54 /// ), 55 /// --- 56 /// 57 /// Instead of managing its own set of `HoverPointer` instances, `HoverTransform` uses the host 58 /// Hover I/O system for this task. For every pointer inside the host, a transformed version 59 /// exists in this system. Conversely, a pointer created inside `HoverTransform` will have 60 /// an inversely transformed version in the host system. 61 /// 62 /// Note that the host system will not know that the transform has taken place. If a transformed 63 /// node is hovered, the host will report that the *hover transform* node itself is hovered. 64 /// To see which node is hovered, use `HoverTransform`'s `hoverOf`. 65 class HoverTransform : NodeChain, HoverIO, Focusable, Hoverable, HoverScrollable { 66 67 // This node is WAY too complex right now 68 69 mixin controlIO; 70 71 HoverIO hoverIO; 72 FocusIO focusIO; 73 74 public { 75 76 /// By default, the destination rectangle is automatically updated to match the padding 77 /// box of the transform's child node. If toggled on, it is instead static, and can be 78 /// manually updated. 79 bool isDestinationManual; 80 81 } 82 83 private { 84 85 Rectangle _sourceRectangle; 86 Rectangle _destinationRectangle; 87 88 /// Pointers received from the host. 89 ResourceArena!Pointer _pointers; 90 91 /// Pool of actions that are used to find matching nodes. 92 FindHoveredNodeAction[] _actions; 93 94 } 95 96 /// Params: 97 /// sourceRectangle = Rectangle the input is expected to fit in. 98 /// destinationRectangle = Rectangle to map the input to. If omitted, chosen automatically 99 /// so that input is remapped to the content of this node. 100 /// next = Node to be affected by the transform. 101 this(Rectangle sourceRectangle, Node next = null) { 102 this._sourceRectangle = sourceRectangle; 103 this.isDestinationManual = false; 104 super(next); 105 } 106 107 /// ditto 108 this(Rectangle sourceRectangle, Rectangle destinationRectangle, Node next = null) { 109 this._sourceRectangle = sourceRectangle; 110 this._destinationRectangle = destinationRectangle; 111 this.isDestinationManual = true; 112 super(next); 113 } 114 115 /// Returns: 116 /// Rectangle for hover input. This input will be transformed to match 117 /// `destinationRectangle`. 118 Rectangle sourceRectangle() const { 119 return _sourceRectangle; 120 } 121 122 /// Change the source rectangle. 123 /// Params: 124 /// newValue = New value for the rectangle. 125 /// Returns: 126 /// Same value as passed. 127 Rectangle sourceRectangle(Rectangle newValue) { 128 return _sourceRectangle = newValue; 129 } 130 131 /// Returns: 132 /// Rectangle for output. By default, this should match the padding box of this node, 133 /// unless explicitly changed to something else. 134 Rectangle destinationRectangle() const { 135 if (tree) 136 return _destinationRectangle; 137 else 138 return sourceRectangle; 139 } 140 141 /// Change the destination rectangle, disabling automatic destination selection. 142 /// 143 /// Changing destination rectangle sets `isDestinationManual` to `true`. Set it to false if 144 /// you want the destination rectangle to match the node's padding box instead. 145 /// 146 /// See_Also: 147 /// `isDestinationManual` 148 Rectangle destinationRectangle(Rectangle newValue) { 149 isDestinationManual = true; 150 return _destinationRectangle = newValue; 151 } 152 153 /// Transform a point in `sourceRectangle` onto `destinationRectangle`. 154 /// See_Also: 155 /// `pointToHost` for the reverse transformation. 156 /// Params: 157 /// point = Point, in host space, to transform. 158 /// Returns: 159 /// Point transformed into local space. 160 Vector2 pointToLocal(Vector2 point) const { 161 return point.viewportTransform(sourceRectangle, destinationRectangle); 162 } 163 164 /// Transform a point in `destinationRectangle` onto `sourceRectangle`. 165 /// See_Also: 166 /// `pointToLocal` for the reverse transformation. 167 /// Params: 168 /// point = Point, in local space, to transform. 169 /// Returns: 170 /// Point transformed into host space. 171 Vector2 pointToHost(Vector2 point) const { 172 return point.viewportTransform(destinationRectangle, sourceRectangle); 173 } 174 175 /// Transform a pointer into a new position. 176 /// 177 /// This will convert the pointer into a pointer within this node. The pointer *must* 178 /// be loaded in the host `HoverIO`. 179 /// 180 /// Params: 181 /// pointer = Pointer to transform. 182 /// Returns: 183 /// Transformed pointer. 184 inout(HoverPointer) pointerToLocal(inout HoverPointer pointer) inout @trusted { 185 HoverPointer result; 186 result.update(pointer); 187 result.device = cast() pointer.device; 188 result.number = pointer.number; 189 result.position = pointToLocal(pointer.position); 190 return cast(inout) result.loadCopy(this, pointer.id); 191 } 192 193 /// Reverse pointer transform. Transform pointers from the local, transformed space, into the 194 /// space of the host. 195 /// 196 /// This is used when loading pointers through `HoverTransform.load`. This way, devices placed 197 /// inside the transform exist within the transformed space. 198 /// 199 /// Params: 200 /// pointer = Pointer to transform. 201 /// Returns: 202 /// Transformed pointer. 203 inout(HoverPointer) pointerToHost(inout HoverPointer pointer) inout @trusted { 204 HoverPointer result; 205 result.update(pointer); 206 result.device = cast() pointer.device; 207 result.number = pointer.number; 208 result.position = pointToHost(pointer.position); 209 return cast(inout) result.loadCopy(hoverIO, pointer.id); 210 } 211 212 override void beforeResize(Vector2) { 213 require(hoverIO); 214 use(focusIO); 215 startIO(); 216 } 217 218 override void afterResize(Vector2) { 219 stopIO(); 220 } 221 222 /// `HoverTransform` saves all the pointers it receives from the host `HoverIO` 223 /// and creates local copies. It then transforms those, and checks its children for 224 /// matching nodes. 225 override void beforeDraw(Rectangle outer, Rectangle inner) { 226 227 if (!isDestinationManual && next) { 228 _destinationRectangle = next.paddingBoxForSpace(inner); 229 } 230 231 size_t actionIndex; 232 233 foreach (HoverPointer pointer; hoverIO) { 234 if (pointer.isDisabled) continue; 235 236 auto transformed = pointerToLocal(pointer); 237 const localID = cast(int) _pointers.allResources.countUntil(pointer.id); 238 239 // Allocate a branch action for each pointer 240 if (actionIndex >= _actions.length) { 241 _actions.length = actionIndex + 1; 242 _actions[actionIndex] = new FindHoveredNodeAction; 243 } 244 245 auto action = _actions[actionIndex++]; 246 action.pointer = transformed; 247 controlBranchAction(action).startAndRelease(); 248 249 // Create or update pointer entries 250 if (localID == -1) { 251 const newLocalID = _pointers.load(Pointer(pointer.id, 0, action)); 252 _pointers[newLocalID].localID = newLocalID; 253 } 254 else { 255 auto resource = _pointers[localID]; 256 resource.action = action; 257 resource.localID = localID; 258 _pointers.reload(localID, resource); 259 } 260 } 261 262 } 263 264 override void afterDraw(Rectangle outer, Rectangle inner) { 265 foreach (pointer; _pointers.activeResources) { 266 controlBranchAction(pointer.action).stop(); 267 268 // Read the result of each action into the local pointer 269 pointer.hoveredNode = pointer.action.result; 270 271 if (!pointer.isHeld) { 272 pointer.heldNode = pointer.hoveredNode; 273 } 274 275 // Switch focus if holding 276 else if (focusIO) { 277 if (auto focusable = pointer.heldNode.castIfAcceptsInput!Focusable) { 278 if (!focusable.isFocused) { 279 focusable.focus(); 280 } 281 } 282 else { 283 focusIO.clearFocus(); 284 } 285 } 286 if (!pointer.action.pointer.isScrollHeld) { 287 pointer.scrollable = pointer.action.scrollable; 288 } 289 290 pointer.isHeld = false; 291 _pointers[pointer.localID] = pointer; 292 } 293 } 294 295 override int load(HoverPointer pointer) { 296 auto hostPointer = pointerToHost(pointer); 297 return hoverIO.load(hostPointer); 298 } 299 300 override inout(HoverPointer) fetch(int number) inout { 301 auto pointer = hoverIO.fetch(number); 302 return pointerToLocal(pointer); 303 } 304 305 override void emitEvent(HoverPointer pointer, InputEvent event) { 306 auto hostPointer = pointerToHost(pointer); 307 hoverIO.emitEvent(hostPointer, event); 308 } 309 310 private int hostToLocalID(int id) const { 311 const localID = cast(int) _pointers.allResources.countUntil(id); 312 assert(localID >= 0, format!"Pointer %s isn't loaded"(id)); 313 return localID; 314 } 315 316 override inout(Hoverable) hoverOf(HoverPointer pointer) inout { 317 return hoverOf(pointer.id); 318 } 319 320 inout(Hoverable) hoverOf(int pointerID) inout { 321 const localID = hostToLocalID(pointerID); 322 return _pointers[localID].heldNode.castIfAcceptsInput!Hoverable; 323 } 324 325 override inout(HoverScrollable) scrollOf(const HoverPointer pointer) inout { 326 return scrollOf(pointer.id); 327 } 328 329 inout(HoverScrollable) scrollOf(int pointerID) inout { 330 const localID = hostToLocalID(pointerID); 331 return _pointers[localID].scrollable; 332 } 333 334 override bool isHovered(const Hoverable hoverable) const { 335 foreach (pointer; _pointers.activeResources) { 336 if (hoverable.opEquals(pointer.heldNode)) { 337 return true; 338 } 339 } 340 return false; 341 } 342 343 override int opApply(int delegate(HoverPointer) @safe yield) { 344 foreach (HoverPointer pointer; hoverIO) { 345 346 auto transformed = pointerToLocal(pointer); 347 if (auto result = yield(transformed)) { 348 return result; 349 } 350 351 } 352 return 0; 353 } 354 355 override int opApply(int delegate(Hoverable) @safe yield) { 356 foreach (pointer; _pointers.activeResources) { 357 if (auto hoverable = cast(Hoverable) pointer.heldNode) { 358 if (auto result = yield(hoverable)) { 359 return result; 360 } 361 } 362 } 363 return 0; 364 } 365 366 override bool blocksInput() const { 367 return isDisabled || isDisabledInherited; 368 } 369 370 override bool actionImpl(IO io, int hostID, immutable InputActionID actionID, bool isActive) { 371 372 const localID = hostToLocalID(hostID); 373 auto resource = _pointers[localID]; 374 const isFrameAction = actionID == inputActionID!(ActionIO.CoreAction.frame); 375 376 // Active input actions can only fire if `heldNode` is still hovered 377 if (isActive) { 378 const isNotHovered = resource.hoveredNode is null 379 || !resource.hoveredNode.opEquals(resource.heldNode); 380 381 if (isNotHovered) { 382 return false; 383 } 384 } 385 386 // Mark pointer as held 387 if (!isFrameAction) { 388 _pointers[localID].isHeld = true; 389 } 390 391 // Dispatch the event 392 if (auto target = resource.heldNode.castIfAcceptsInput!Hoverable) { 393 return target.actionImpl(this, hostID, actionID, isActive); 394 } 395 return false; 396 397 } 398 399 override bool hoverImpl(HoverPointer pointer) { 400 if (auto target = hoverOf(pointer)) { 401 auto transformed = pointerToLocal(pointer); 402 return target.hoverImpl(transformed); 403 } 404 return false; 405 } 406 407 override IsOpaque inBoundsImpl(Rectangle outer, Rectangle inner, Vector2 position) { 408 if (super.inBoundsImpl(outer, inner, position).inSelf) { 409 return IsOpaque.onlySelf; 410 } 411 return IsOpaque.no; 412 } 413 414 override bool isHovered() const { 415 return hoverIO.isHovered(this); 416 } 417 418 override bool canScroll(const HoverPointer pointer) const { 419 if (auto scroll = scrollOf(pointer)) { 420 auto transformed = pointerToLocal(pointer); 421 return scroll.canScroll(transformed); 422 } 423 return false; 424 } 425 426 override bool scrollImpl(HoverPointer pointer) { 427 if (auto scroll = scrollOf(pointer)) { 428 auto transformed = pointerToLocal(pointer); 429 return scroll.scrollImpl(transformed); 430 } 431 else return false; 432 } 433 434 override Rectangle shallowScrollTo(const(Node) child, Rectangle parentBox, Rectangle childBox) { 435 return childBox; 436 } 437 438 override bool focusImpl() { 439 return false; 440 } 441 442 override void focus() { 443 // NOOP, can't focus 444 } 445 446 bool isFocused() const { 447 return false; 448 } 449 450 alias opEquals = typeof(super).opEquals; 451 override bool opEquals(const Object other) const { 452 return super.opEquals(other); 453 } 454 455 } 456 457 private struct Pointer { 458 int hostID; 459 int localID; 460 FindHoveredNodeAction action; 461 Node heldNode; 462 Node hoveredNode; 463 HoverScrollable scrollable; 464 bool isHeld; 465 466 /// Find a pointer by its host ID 467 bool opEquals(int id) const { 468 return this.hostID == id; 469 } 470 }