UI Toolbox logoUI Toolbox

Drop payloads and drop targets

The drop contract is in UIToolbox.Drop:

Type Role
IDropPayload Marker interface for dragged data.
WindowDropPayload A dragged window (Window).
TabDropPayload A dragged tab (Window, PointerOffset).
IAcceptsDropPayload<T> A target: QueryDrop, ShowFeedback, UpdateFeedback, HideFeedback.
DropTargetResult The answer of QueryDrop: ActionType, Action, InsertIndex, PreviewType. DropTargetResult.None rejects.
IDropAction The work done on drop: Execute().
DropActionType None, Merge, Reorder, TearOff.
DropPreviewType None, TabReorder, TabMerge, WindowMerge, TearOff.

Custom drop targets

A VisualElement that implements IAcceptsDropPayload<WindowDropPayload>, IAcceptsDropPayload<TabDropPayload>, or both, is a drop target. One hit-test serves the hover preview and the drop:

  1. The nearest custom target wins. The resolver picks the topmost element under the pointer, skipping the dragged window, and walks up to the workspace. The first element that implements IAcceptsDropPayload<T> for the dragged payload type is the target. This works inside window content too.
  2. Then the built-in targets. If the pointer is over a window (the dragged one excluded), the target is that window's tab group, or the window itself.
  3. Otherwise the workspace. It implements both payload types. It tears off tabs and refuses windows.

A custom target inside a window blocks the merge onto that window while the pointer is over it. Keep such targets small, or refuse payloads you do not handle. Return DropTargetResult.None from QueryDrop to refuse a payload; the drop then does nothing. The ActionType and PreviewType values are only passed back to your feedback methods.

This bin closes any window dropped on it:

#nullable enable
using UIToolbox;
using UIToolbox.Drop;
using UIToolbox.Utils;
using UnityEngine;
using UnityEngine.UIElements;

[UxmlElement]
public partial class TrashBin : VisualElement, IAcceptsDropPayload<WindowDropPayload>
{
    private const string HOT_CLASS = "trash-bin--hot";

    public TrashBin() => AddToClassList("trash-bin");

    public DropTargetResult QueryDrop(WindowDropPayload payload, Vector2 worldPosition) =>
        new DropTargetResult(DropActionType.Merge, new CloseWindowAction(payload.Window));

    public void ShowFeedback(WindowDropPayload payload, DropTargetResult result, Vector2 mousePos) => AddToClassList(HOT_CLASS);

    public void UpdateFeedback(WindowDropPayload payload, DropTargetResult result, Vector2 mousePos) { }

    public void HideFeedback() => RemoveFromClassList(HOT_CLASS);

    private sealed class CloseWindowAction : IDropAction
    {
        private readonly WindowDescriptor _window;

        public CloseWindowAction(WindowDescriptor window) => _window = window;

        public void Execute() => AsyncRunner.Run(() => _window.CloseAsync(), $"Trash '{_window.Id}'");
    }
}
<tb:Workspace>
  <tb:WorkspaceToolbar>
    <TrashBin />
  </tb:WorkspaceToolbar>
</tb:Workspace>

Limits:

Styling

Class Element
tb-tab-ghost The tab image that follows the pointer during a tab drag.
tb-snap-indicator The snap preview rectangle.
tb-tab-insert-indicator The line that shows where a tab goes in a strip.
tb-tear-off-preview The preview of a new window during a tab tear-off.
drop-target-active Added to a window or tab group that accepts the current drop.
dock-zone-active Added to a dock zone that accepts the current drop.