Files
2026-07-13 13:13:17 +08:00

236 lines
7.1 KiB
C#

#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using ImGuiNET;
using T3.Core.DataTypes.Vector;
namespace T3.Editor.Gui.UiHelpers;
/// <summary>
/// Implements UI and interaction for dragging or dropping various elements onto imgui items.
/// </summary>
internal static class DragAndDropHandling
{
/// <summary>
/// This should be called once per frame
/// </summary>
internal static void Update()
{
var cancelled = IsDragging && _stopRequested;
if (cancelled)
{
FreeData();
_stopRequested = false;
_activeDragType = DragTypes.None;
_externalDropJustHappened = false;
}
}
internal static void StartExternalDrag(DragTypes type, string data)
{
_activeDragType = type;
_dataString = data;
_stopRequested = false;
}
internal static void CancelExternalDrag()
{
_activeDragType = DragTypes.None;
_dataString = null;
}
internal static void CompleteExternalDrop(DragTypes type, string data)
{
_dataString = data;
_externalDropJustHappened = true;
}
/// <summary>
/// This should be called right after an ImGui item that is a drag source (e.g. a button).
/// </summary>
/// <returns>
/// True if dragging started
/// </returns>
internal static bool HandleDragSourceForLastItem(DragTypes dragType, string data)
{
if (ImGui.IsItemActive())
{
if (IsDragging || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceNoPreviewTooltip))
return false;
if (HasData)
FreeData();
_dataPtr = Marshal.StringToHGlobalUni(data);
_dataString = data;
_activeDragType = dragType;
ImGui.SetDragDropPayload(dragType.ToString(), _dataPtr, (uint)((data.Length + 1) * sizeof(char)));
ImGui.EndDragDropSource();
return true;
}
if (ImGui.IsItemDeactivated())
{
StopDragging();
}
return false;
}
/// <summary>
/// Can be called after a dropzone.
///
/// Checks if data matches <see cref="DragTypes"/>, adds an ImGuiDropDropTarget onto
/// the current ImGui item and show a drop target indicator.
///
/// It's also responsible for cancelling the drag action for events like Escape key presses or focus loss.
/// </summary>
/// <returns>
/// True if dropped
/// </returns>
internal static DragInteractionResult TryHandleDropOnItem(DragTypes dragType, out string? data, Action? drawTooltip = null)
{
data = string.Empty;
if (_activeDragType != dragType)
return DragInteractionResult.None;
if (!IsDragging && !_externalDropJustHappened)
{
_activeDragType = DragTypes.None;
return DragInteractionResult.None;
}
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
// We can't rely on imgui hovered after drop from external
var isHovered = _externalDropJustHappened
? new ImRect(min, max).Contains(ImGui.GetMousePos())
: ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
var color = Color.Orange.Fade(isHovered ? 1f : 0.5f);
var thickness = isHovered ? 2f : 1f;
ImGui.GetWindowDrawList().AddRect(min, max, color, 3, ImDrawFlags.None, thickness);
if (!isHovered)
return DragInteractionResult.None;
var result = DragInteractionResult.Hovering;
data = _dataString;
drawTooltip?.Invoke();
if (_externalDropJustHappened)
{
data = _dataString;
_stopRequested = true;
return DragInteractionResult.Dropped;
}
if (ImGui.BeginDragDropTarget())
{
// Check for manual cancel
if (ImGui.IsKeyPressed(ImGuiKey.Escape))
{
StopDragging();
ImGui.EndDragDropTarget();
return DragInteractionResult.None;
}
var payload = ImGui.AcceptDragDropPayload(dragType.ToString());
// Use MouseReleased(0) to ensure we catch the drop even if IDs are finicky
if (ImGui.IsMouseReleased(0))
{
if (HasData)
{
try
{
// If it was an internal ImGui-managed drag, the payload data might be relevant
// Otherwise, we fall back to our stored _dataString
var internalData = Marshal.PtrToStringAuto(payload.Data);
data = internalData ?? _dataString;
result = data != null
? DragInteractionResult.Dropped
: DragInteractionResult.Invalid;
}
catch (Exception e)
{
Log.Warning(" Failed to get drop data " + e.Message);
}
}
_stopRequested = true;
}
ImGui.EndDragDropTarget();
}
return result;
}
internal enum DragInteractionResult
{
None,
Invalid,
Hovering,
Dropped,
}
/// <summary>
/// To prevent inconsistencies related to the order of window processing,
/// we have to defer the end until beginning of
/// </summary>
private static void StopDragging()
{
_stopRequested = true;
}
private static void FreeData()
{
if (!HasData)
return;
Marshal.FreeHGlobal(_dataPtr);
_dataPtr = IntPtr.Zero; // Prevent double free
_dataString = null;
}
private static DragTypes _activeDragType = DragTypes.None;
internal static bool IsDragging => _activeDragType != DragTypes.None;
internal static bool IsDraggingWith(DragTypes dragType)
{
return _activeDragType == dragType;
}
/// <summary>
/// Peeks at the payload of an active drag without acting as a drop zone —
/// e.g. for drawing a drag indicator under the mouse.
/// </summary>
internal static bool TryGetDragData(DragTypes dragType, [NotNullWhen(true)] out string? data)
{
data = _activeDragType == dragType ? _dataString : null;
return data != null;
}
private static bool HasData => _dataPtr != IntPtr.Zero;
private static bool _externalDropJustHappened; // New flag
private static IntPtr _dataPtr = new(0);
private static string? _dataString;
private static bool _stopRequested;
internal enum DragTypes
{
None,
Symbol,
FileAsset,
ExternalFile,
SymbolNameSpace,
}
}