Weird behaviour with custom draggable control

Both elements have been added from the UI Builder Library, the first behaves correctly.

The second however is the same element but behaves in a strange manner:
While not clicked, nothing happens to it, but when it is clicked it starts behaving like in the below gif, even if the MouseUpEvent is called.

Does anyone know what I am doing wrong that might be causing this behaviour?

public class MovableElement : VisualElement
{
     private UIDocument doc;
     private VisualElement selected;
    
     public new class UxmlFactory : UxmlFactory<MovableElement, UxmlTraits> {}
     public MovableElement()
     {
         RegisterCallback<MouseDownEvent>(evt => { RegisterCallback<MouseMoveEvent>(MoveSelected); });
         RegisterCallback<MouseUpEvent>(evt => { UnregisterCallback<MouseMoveEvent>(MoveSelected); });
     }
     private void MoveSelected(MouseMoveEvent evt)
     {
         style.top = evt.mousePosition.y - layout.height / 2;
         style.left = evt.mousePosition.x - layout.width / 2;
     }
}

7631413--949825--fhmtptfeQ0.gif

I figured out whats going on… The position of the object needs to be absolute for this to work properly.

Hey, just so you know, you might want to consider having the element capture the pointer in your MouseDownEvent handler and having it release the pointer in your MouseUpEvent handler. You see how, with the second element, it would stop moving until your cursor entered it? This same behavior may occur if you drag close to the edge and drag too quickly: the cursor will leave the bounds of the element before the event fires, resulting in your handler not being called.

If you call VisualElement.CapturePointer(evt.pointerId) in your MouseDown handler and call VisualElement.ReleasePointer(evt.pointerId) in your MouseUp handler, you’ll avoid this.

2 Likes

I’d also recommend using pointers instead of mice, since pointers are a broader term that encompass both mice and touchscreen interfaces.

1 Like

This has actually happened and I wondered why, well spotted!

If you keep some internal state that needs to be reset on Pointer/MouseUpEvent, make sure you also react to a Pointer/MouseCaptureOutEvent as well as capture might be lost when focus changes. We’ll provide a proper sample for this, but in the meantime you can have a look at this snippet:

      var myRoot = root.Q<VisualElement>("myRoot");
        myRoot.AddManipulator(new ElementDragManipulator());
    }

    class ElementDragManipulator : PointerManipulator
    {
        private bool enabled;
        private Vector2 startLayoutPosition;
        private Vector3 startPointerPosition;

        protected override void RegisterCallbacksOnTarget()
        {
            this.target.RegisterCallback<PointerDownEvent>(OnPointerDown);
            this.target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
            this.target.RegisterCallback<PointerUpEvent>(OnPointerUp);
            this.target.RegisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
        }

        protected override void UnregisterCallbacksFromTarget()
        {
            this.target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
            this.target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
            this.target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
            this.target.UnregisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
        }

        private void OnPointerDown(PointerDownEvent e)
        {
            DebugEvent(e, e.pointerId);
            if (this.enabled)
            {
                // Here we choose to Stop all PointerDown, regardless of the pointerId 
               // You might want a different behavior
                e.StopImmediatePropagation();
                return;
            }

            this.startLayoutPosition = this.target.transform.position;
            this.startPointerPosition = e.position;

            this.target.CapturePointer(e.pointerId);
            e.StopPropagation();

            this.enabled = true;
        }

        private void OnPointerMove(PointerMoveEvent e)
        {
            DebugEvent(e, e.pointerId);
            if (!this.enabled || !this.target.HasPointerCapture(e.pointerId))
            {
                return;
            }

            var pointerDelta = e.position - this.startPointerPosition;
            var margin = this.target.layout.size * 0.5f;

            var newPosition = new Vector2(Mathf.Clamp(this.startLayoutPosition.x + pointerDelta.x, -margin.x, this.target.panel.visualTree.worldBound.width - margin.x),
                                          Mathf.Clamp(this.startLayoutPosition.y + pointerDelta.y, -margin.y, this.target.panel.visualTree.worldBound.height - margin.y));
           
            this.target.transform.position = newPosition;

            e.StopPropagation();
        }

        private void OnPointerUp(PointerUpEvent e)
        {
            DebugEvent(e, e.pointerId);
            if (!this.enabled || !this.target.HasPointerCapture(e.pointerId))
            {
                return;
            }

            this.target.ReleasePointer(e.pointerId);
            this.enabled = false;
        }

        private void OnPointerCaptureOut(PointerCaptureOutEvent e)
        {
            DebugEvent(e, e.pointerId);
            this.enabled = false;
        }
        void DebugEvent(EventBase evt, int pointerId)
        {
            Debug.Log($"{evt.GetType().Name}: enabled: {this.enabled}  pointerCapture:{this.target.HasPointerCapture(pointerId)}");
        }
    }

This encapsulates the dragging functionality in an external Manipulator object that you can add to any element. (Props to @Kichang-Kim for the original implementation!)

1 Like

Thank you for that, this is definitly a much cleaner and better way of handling the case :slight_smile: