using Robust.Client.Interfaces.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Utility
{
///
/// Helper for implementing drag and drop interactions.
///
/// The basic flow for a drag drop interaction as per this helper is:
/// 1. User presses mouse down on something (using class should communicate this to helper by calling MouseDown()).
/// 2. User continues to hold the mouse down and moves the mouse outside of the defined
/// deadzone. OnBeginDrag is invoked to see if a drag should be initiated. If so, initiates a drag.
/// If user didn't move the mouse beyond the deadzone the drag is not initiated (OnEndDrag invoked).
/// 3. Every Update/FrameUpdate, OnContinueDrag is invoked.
/// 4. User lifts mouse up. This is not handled by DragDropHelper. The using class of the helper should
/// do whatever they want and then end the drag by calling EndDrag() (which invokes OnEndDrag).
///
/// If for any reason the drag is ended, OnEndDrag is invoked.
///
/// thing being dragged and dropped
public class DragDropHelper
{
private const float DefaultDragDeadzone = 2f;
private readonly IInputManager _inputManager;
private readonly OnBeginDrag _onBeginDrag;
private readonly OnEndDrag _onEndDrag;
private readonly OnContinueDrag _onContinueDrag;
private readonly float _deadzone;
///
/// Convenience method, current mouse screen position as provided by inputmanager.
///
public Vector2 MouseScreenPosition => _inputManager.MouseScreenPosition;
///
/// True if initiated a drag and currently dragging something.
/// I.e. this will be false if we've just had a mousedown over something but the mouse
/// has not moved outside of the drag deadzone.
///
public bool IsDragging => _state == DragState.Dragging;
///
/// Current thing being dragged or which mouse button is being held down on.
///
public T Dragged { get; private set; }
// screen pos where the mouse down began for the drag
private Vector2 _mouseDownScreenPos;
private DragState _state = DragState.NotDragging;
private enum DragState : byte
{
NotDragging,
// not dragging yet, waiting to see
// if they hold for long enough
MouseDown,
// currently dragging something
Dragging,
}
///
///
///
/// drag will be triggered when mouse leaves
/// this deadzone around the mousedown position
public DragDropHelper(OnBeginDrag onBeginDrag, OnContinueDrag onContinueDrag,
OnEndDrag onEndDrag, float deadzone = DefaultDragDeadzone)
{
_deadzone = deadzone;
_inputManager = IoCManager.Resolve();
_onBeginDrag = onBeginDrag;
_onEndDrag = onEndDrag;
_onContinueDrag = onContinueDrag;
}
///
/// Tell the helper that the mouse button was pressed down on
/// a target, thus a drag has the possibility to begin for this target.
/// Assumes current mouse screen position is the location the mouse was clicked.
///
/// EndDrag should be called when the drag is done.
///
public void MouseDown(T target)
{
if (_state != DragState.NotDragging)
{
EndDrag();
}
Dragged = target;
_state = DragState.MouseDown;
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
}
///
/// Stop the current drag / drop operation no matter what state it is in.
///
public void EndDrag()
{
Dragged = default;
_state = DragState.NotDragging;
_onEndDrag.Invoke();
}
private void StartDragging()
{
if (_onBeginDrag.Invoke())
{
_state = DragState.Dragging;
}
else
{
EndDrag();
}
}
///
/// Should be invoked by using class every FrameUpdate or Update.
///
public void Update(float frameTime)
{
switch (_state)
{
// check if dragging should begin
case DragState.MouseDown:
{
var screenPos = _inputManager.MouseScreenPosition;
if ((_mouseDownScreenPos - screenPos).Length > _deadzone)
{
StartDragging();
}
break;
}
case DragState.Dragging:
{
if (!_onContinueDrag.Invoke(frameTime))
{
EndDrag();
}
break;
}
}
}
}
///
/// Invoked when a drag is confirmed and going to be initiated. Implementation should
/// typically set the drag shadow texture based on the target.
///
/// true if drag should begin, false to end.
public delegate bool OnBeginDrag();
///
/// Invoked every frame when drag is ongoing. Typically implementation should
/// make the drag shadow follow the mouse position.
///
/// true if drag should continue, false to end.
public delegate bool OnContinueDrag(float frameTime);
///
/// invoked when
/// the drag drop is ending for any reason. This
/// should typically just clear the drag shadow.
///
public delegate void OnEndDrag();
}