ImGui drag-and-drop implementation for the vendored ImGui wrapper. Implements drag/drop lifecycle: begin/end source and target, payload setting and acceptance, rendering target rect, and frame bookkeeping.
// Vendored from zeljkovranjes/sbox-imgui; see LICENSE and PROVENANCE.md in this folder.
using Sandbox;
#pragma warning disable CS0649 // Upstream reserves fields for optional widgets and features.
namespace SboxSuperuser.Engine.Vendor.ImGui;
internal static partial class ImGui
{
internal static void NewFrameDragDrop()
{
var g = G;
g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr;
g.DragDropAcceptIdCurr = 0;
g.DragDropAcceptIdCurrRectSurface = float.MaxValue;
g.DragDropWithinSource = false;
g.DragDropWithinTarget = false;
g.DragDropHoldJustPressedId = 0;
// Keep the source alive so even if the source disappears our state is consistent.
if ( g.DragDropActive && g.DragDropPayload.SourceId == g.ActiveId )
KeepAliveID( g.DragDropPayload.SourceId );
}
internal static void EndFrameDragDrop()
{
var g = G;
// Elapse payload (if delivered, or if the source stopped being submitted)
if ( g.DragDropActive )
{
bool isDelivered = g.DragDropPayload.Delivery;
bool isElapsed = g.DragDropSourceFrameCount + 1 < g.FrameCount
&& ((g.DragDropSourceFlags & ImGuiDragDropFlags.PayloadAutoExpire) != 0 || g.DragDropMouseButton == -1 || !IsMouseDown( (ImGuiMouseButton)g.DragDropMouseButton ));
if ( isDelivered || isElapsed )
ClearDragDrop();
}
// Fallback for a source that stopped submitting its tooltip.
if ( g.DragDropActive && g.DragDropSourceFrameCount + 1 < g.FrameCount && (g.DragDropSourceFlags & ImGuiDragDropFlags.SourceNoPreviewTooltip) == 0 )
{
g.DragDropWithinSource = true;
SetTooltip( "..." );
g.DragDropWithinSource = false;
}
}
internal static void ClearDragDrop()
{
var g = G;
g.DragDropActive = false;
g.DragDropPayload.Clear();
g.DragDropAcceptFlags = ImGuiDragDropFlags.None;
g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0;
g.DragDropAcceptIdCurrRectSurface = float.MaxValue;
g.DragDropAcceptFrameCount = -1;
g.DragDropMouseButton = -1;
}
public static bool IsDragDropActive() => G.DragDropActive;
/// <summary>
/// Call after submitting an item which may be dragged. When this returns true, you can call SetDragDropPayload() + EndDragDropSource().
/// </summary>
public static bool BeginDragDropSource( ImGuiDragDropFlags flags = ImGuiDragDropFlags.None )
{
var g = G;
var window = g.CurrentWindow;
int mouseButton = (int)ImGuiMouseButton.Left;
bool sourceDragActive;
int sourceId;
int sourceParentId = 0;
if ( (flags & ImGuiDragDropFlags.SourceExtern) == 0 )
{
sourceId = g.LastItemData.ID;
if ( sourceId != 0 )
{
// Common path: items with an ID
if ( g.ActiveId != sourceId )
return false;
if ( g.ActiveIdMouseButton != -1 )
mouseButton = g.ActiveIdMouseButton;
if ( !g.IO.MouseDown[mouseButton] || window.SkipItems )
return false;
g.ActiveIdAllowOverlap = false;
}
else
{
// Uncommon path: items without an ID (e.g. Text(), Image())
if ( !g.IO.MouseDown[mouseButton] || window.SkipItems )
return false;
if ( (g.LastItemData.StatusFlags & ImGuiItemStatusFlags.HoveredRect) == 0 && (g.ActiveId == 0 || g.ActiveIdWindow != window) )
return false;
if ( (flags & ImGuiDragDropFlags.SourceAllowNullID) == 0 )
{
Log.Warning( "ImGui: BeginDragDropSource() on an item without ID requires ImGuiDragDropFlags.SourceAllowNullID" );
return false;
}
// Build a throwaway ID from the ID stack + the item rectangle. It won't survive the item moving.
sourceId = g.LastItemData.ID = window.GetIDFromRectangle( g.LastItemData.Rect );
KeepAliveID( sourceId );
bool isHovered = ItemHoverable( g.LastItemData.Rect, sourceId, g.LastItemData.InFlags );
if ( isHovered && g.IO.MouseClicked[mouseButton] )
{
SetActiveID( sourceId, window );
g.ActiveIdMouseButton = mouseButton;
FocusWindow( window );
}
if ( g.ActiveId == sourceId )
g.ActiveIdAllowOverlap = isHovered;
}
if ( g.ActiveId != sourceId )
return false;
sourceParentId = window.IDStack[^1];
sourceDragActive = IsMouseDragging( (ImGuiMouseButton)mouseButton );
}
else
{
// External source (e.g. driven by the game): always active while the button is down.
window = null;
sourceId = ImHashStr( "#SourceExtern", 0 );
sourceDragActive = true;
mouseButton = g.IO.MouseDown[0] ? 0 : -1;
KeepAliveID( sourceId );
SetActiveID( sourceId, null );
}
if ( !sourceDragActive )
return false;
// Activate drag and drop
if ( !g.DragDropActive )
{
ClearDragDrop();
var payload = g.DragDropPayload;
payload.SourceId = sourceId;
payload.SourceParentId = sourceParentId;
g.DragDropActive = true;
g.DragDropSourceFlags = flags;
g.DragDropMouseButton = mouseButton;
if ( payload.SourceId == g.ActiveId )
g.ActiveIdNoClearOnFocusLoss = true;
}
g.DragDropSourceFrameCount = g.FrameCount;
g.DragDropWithinSource = true;
if ( (flags & ImGuiDragDropFlags.SourceNoPreviewTooltip) == 0 )
{
// Drag and drop tooltips are offset from the mouse cursor and semi-transparent.
var tooltipPos = g.IO.MousePos + new Vector2( 16, 10 ) * g.Style.MouseCursorScale;
SetNextWindowPos( tooltipPos );
SetNextWindowBgAlpha( g.Style.Colors[(int)ImGuiCol.PopupBg].w * 0.60f );
BeginTooltipEx();
if ( g.DragDropAcceptIdPrev != 0 && (g.DragDropAcceptFlags & ImGuiDragDropFlags.AcceptNoPreviewTooltip) != 0 )
{
// The target asked us not to display the tooltip: keep it alive but hidden.
g.CurrentWindow.Hidden = true;
g.CurrentWindow.HiddenFramesCanSkipItems = 1;
}
}
if ( (flags & ImGuiDragDropFlags.SourceNoDisableHover) == 0 && (flags & ImGuiDragDropFlags.SourceExtern) == 0 )
g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags.HoveredRect;
return true;
}
/// <summary>Only call EndDragDropSource() if BeginDragDropSource() returns true!</summary>
public static void EndDragDropSource()
{
var g = G;
if ( !g.DragDropActive || !g.DragDropWithinSource )
{
Log.Warning( "ImGui: EndDragDropSource() called without BeginDragDropSource()" );
return;
}
if ( (g.DragDropSourceFlags & ImGuiDragDropFlags.SourceNoPreviewTooltip) == 0 )
EndTooltip();
// Discard the drag if SetDragDropPayload() was never called.
if ( g.DragDropPayload.DataFrameCount == -1 )
ClearDragDrop();
g.DragDropWithinSource = false;
}
/// <summary>
/// Set the payload of the current drag source. type is a user defined tag (types starting with '_' are reserved).
/// Returns true when the payload has been accepted by a target.
/// </summary>
public static bool SetDragDropPayload( string type, object data, ImGuiCond cond = ImGuiCond.None )
{
var g = G;
var payload = g.DragDropPayload;
if ( cond == ImGuiCond.None )
cond = ImGuiCond.Always;
if ( cond == ImGuiCond.Always || payload.DataFrameCount == -1 )
{
payload.DataType = type;
payload.Data = data;
}
payload.DataFrameCount = g.FrameCount;
return g.DragDropAcceptFrameCount == g.FrameCount || g.DragDropAcceptFrameCount == g.FrameCount - 1;
}
public static bool SetDragDropPayload<T>( string type, T data, ImGuiCond cond = ImGuiCond.None )
=> SetDragDropPayload( type, (object)data, cond );
/// <summary>Call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget().</summary>
public static bool BeginDragDropTarget()
{
var g = G;
if ( !g.DragDropActive )
return false;
var window = g.CurrentWindow;
if ( (g.LastItemData.StatusFlags & ImGuiItemStatusFlags.HoveredRect) == 0 )
return false;
var hoveredWindow = g.HoveredWindowUnderMovingWindow;
if ( hoveredWindow is null || window.RootWindow != hoveredWindow.RootWindow || window.SkipItems )
return false;
var displayRect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags.HasDisplayRect) != 0 ? g.LastItemData.DisplayRect : g.LastItemData.Rect;
int id = g.LastItemData.ID;
if ( id == 0 )
{
id = window.GetIDFromRectangle( displayRect );
KeepAliveID( id );
}
if ( g.DragDropPayload.SourceId == id )
return false;
g.DragDropTargetRect = displayRect;
g.DragDropTargetClipRect = window.ClipRect;
g.DragDropTargetId = id;
g.DragDropWithinTarget = true;
return true;
}
/// <summary>
/// Accept contents of a given type. Returns the payload when it is delivered (or while previewing with AcceptBeforeDelivery), else null.
/// </summary>
public static ImGuiPayload AcceptDragDropPayload( string type, ImGuiDragDropFlags flags = ImGuiDragDropFlags.None )
{
var g = G;
var payload = g.DragDropPayload;
if ( !g.DragDropActive || payload.DataFrameCount == -1 )
return null;
if ( type is not null && !payload.IsDataType( type ) )
return null;
// Accept the smallest drag target bounding box, so drag targets can be nested.
bool wasAcceptedPreviously = g.DragDropAcceptIdPrev == g.DragDropTargetId;
var r = g.DragDropTargetRect;
float rSurface = r.Width * r.Height;
if ( rSurface > g.DragDropAcceptIdCurrRectSurface )
return null;
g.DragDropAcceptFlags = flags;
g.DragDropAcceptIdCurr = g.DragDropTargetId;
g.DragDropAcceptIdCurrRectSurface = rSurface;
payload.Preview = wasAcceptedPreviously;
flags |= g.DragDropSourceFlags & ImGuiDragDropFlags.AcceptNoDrawDefaultRect;
if ( (flags & ImGuiDragDropFlags.AcceptNoDrawDefaultRect) == 0 && payload.Preview )
RenderDragDropTargetRect( r, g.DragDropTargetClipRect );
g.DragDropAcceptFrameCount = g.FrameCount;
if ( (g.DragDropSourceFlags & ImGuiDragDropFlags.SourceExtern) != 0 && g.DragDropMouseButton == -1 )
payload.Delivery = wasAcceptedPreviously && g.DragDropSourceFrameCount < g.FrameCount;
else
payload.Delivery = wasAcceptedPreviously && g.DragDropMouseButton >= 0 && !IsMouseDown( (ImGuiMouseButton)g.DragDropMouseButton );
if ( !payload.Delivery && (flags & ImGuiDragDropFlags.AcceptBeforeDelivery) == 0 )
return null;
return payload;
}
internal static void RenderDragDropTargetRect( ImRect bb, ImRect itemClipRect )
{
var g = G;
var window = g.CurrentWindow;
var bbDisplay = bb;
bbDisplay.ClipWith( itemClipRect );
bbDisplay.Expand( 3.5f );
bool pushClipRect = !window.ClipRect.Contains( bbDisplay );
if ( pushClipRect )
window.DrawList.PushClipRectFullScreen();
window.DrawList.AddRect( bbDisplay.Min, bbDisplay.Max, GetColorU32Internal( ImGuiCol.DragDropTarget ), 0.0f, ImDrawFlags.None, 2.0f );
if ( pushClipRect )
window.DrawList.PopClipRect();
}
/// <summary>Only call EndDragDropTarget() if BeginDragDropTarget() returns true!</summary>
public static void EndDragDropTarget()
{
var g = G;
if ( !g.DragDropActive || !g.DragDropWithinTarget )
{
Log.Warning( "ImGui: EndDragDropTarget() called without BeginDragDropTarget()" );
return;
}
g.DragDropWithinTarget = false;
// Clear the payload right after delivery.
if ( g.DragDropPayload.Delivery )
ClearDragDrop();
}
/// <summary>Peek directly into the current payload from anywhere. Returns null when drag and drop is finished or inactive.</summary>
public static ImGuiPayload GetDragDropPayload()
{
var g = G;
return g.DragDropActive && g.DragDropPayload.DataFrameCount != -1 ? g.DragDropPayload : null;
}
}