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:
- 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. - 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.
- 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:
- The gesture recognizer creates only
WindowDropPayloadandTabDropPayload. You cannot add a payload type. - A custom target inside a window's content never receives a drop, because the window is found first. For drag and drop inside a window, use UI Toolkit
PointerDownEvent,PointerMoveEventandPointerUpEventin your own content.
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. |