Dragging items between various UIs

I am trying to implement item dragging between various UI. The issue is that if I trigger a Press event and start dragging, the Motion and Release events only fire for the UI that the item originally belonged to, and not the target UI. I wonder how to deal with this issue.

Indeed.

To explain what / why happens:

If a TCastleUserInterface handles Press (returning true from Press or PreviewPress methods override) → it captures further events about the mouse (for this finger, in case of multi-touch), so that it is notified about motion/release even if mouse moves outside of it.

In case of Motion and Release, they pass events to (in order) :

  • ForceCaptureInput (and no further, if ForceCaptureInput event returned true)

  • current pointer lock control (and no further, if PointerLock.Controller event returned true)

  • current capturing control (from FCaptureInput) (and no further - we stop here, unconditionally for now, regardless if capturing control returned false or true from its event handler; proof: grep // if something is capturing the input, prevent other controls from getting the events in castleuicontrols_container.inc :slight_smile: )

  • and only then we proceed to pass event to other controls (everything in the hierarchy, except controls already handled above).

Possible solutions for you:

  • Do not allow the source control to capture events.

    • One way to do this, is to override Press and return false from it. (if you’re OK that other controls may also get press event).

    • Better way would be to call Container.ReleaseCapture(ControlOnWhichDraggingStarted) . Where? Hm, admittedly, it’s easiest to just call it from the view’s Update. (I considered various answers, but some would fail – e.g. calling Container.ReleaseCapture(Self) from control’s own Press would not work, as TCastleContainer.EventPress starts capturing after this control returned Press ).

  • Alternatively, don’t disable capturing at source, but make sure target sees it. So set TCastleContainer.ForceCaptureInput to the target component, so that it is notified about dragging. This makes sense if there’s only a single possible target (TCastleContainer.ForceCaptureInput is one component) – hm, that’s probably not the usual case:)

All in all, the simplest/reasonable/reliable answer for now is to track ControlOnWhichDraggingStarted and just call if ControlOnWhichDraggingStarted <> nil then Container.ReleaseCapture(ControlOnWhichDraggingStarted) from view’s Update.

TODO for engine: I admittedly don’t remember why we have this unconditional cancellation, for motion and release, from passing events further, if FCaptureInput for this is set. Maybe the right solution is just to not do this? I.e. right now we have this:

    if (Capture <> nil) and
       (not IsForceCaptureInput(Capture)) and
       (not IsPointerLockController(Capture)) then
    begin
      Result := NonRecursivePreviewAndRelease(Capture);
      Exit; // if something is capturing the input, prevent other controls from getting the events
    end;

Maybe we should just do this:

    if (Capture <> nil) and
       (not IsForceCaptureInput(Capture)) and
       (not IsPointerLockController(Capture)) then
    begin
      Result := NonRecursivePreviewAndRelease(Capture);
      if Result then Exit; // only if capturing control processed this, prevent other controls from getting the events
    end;

Test / PR of the latter solution at the engine side, to make Motion and Release just continue the job (if the capturing control returned false from its Motion / Release) are welcome:) You can also look at git blame around these lines – I honestly don’t remember now why I made it this way (doing Exit regardless of NonRecursivePreviewAndRelease(Capture) value), but maybe I had a reason and git blame will tell it:)

Container.ReleaseCapture works until the item reach the edge of the source UI then it stop updating :laughing: (The source UI is responsible for both the rendering and the positioning of the dragged item).

You could try to put the two on one background parent. That would prevent the world view below (the ground plane) from intercepting the mouse. Just sharing my idea.

Alternatively, you could use mouse click (or tap on touch screen) to “grab” the item from inventory, and use following next click to “release” it to the clicked inventory.

You only need to store the moved item ID somewhere, and use its icon to replace mouse pointer.

Advantage of it is that you don’t need to hold the button down, or keep finger movement on touch screen. This way you avoid mouse drag entirely. And it allows users to use any pointing device or game controller.

  • There will be more UIs on screen (i.e the Player and the NPC’s equipment slots)
  • Users can drop item to the ground via dragging as well.

“Replacing the mouse pointer” is what I am doing right now. Not really convenient since many logic need to be implemented on TCastleView side, which is what I am trying to avoid (game logic is mainly scripted using my own scripting language, and I prefer to expose those events from TCastleUserInterface’s descendents)

For example the inventory UI is currently exposing these following functions:
image
Which then can be implemented on the script side:

Done. A bit ugly on the TCastleView side.

Indeed, that is what ReleaseCapture means → “if a mouse is not above me, I’m no longer interested in events about it”.

If you want to go with ReleaseCapture approach, you have to handle motion events on a parent. Like a TCastleView, or any other TCastleUserInterface that is a parent of the other controls. (TCastleView is itself rather simple, it can be a parent of other controls, and it is TCastleUserInterface descendant, and it’s FullSize.) I guess that’s similar to your ultimate solution:)