Luna UI provides a flexible drag and drop system through the DragAndDropManipulator class. This abstract manipulator handles pointer events, visual feedback, and drop slot detection.

Drag and Drop

Features

  • Abstract Base Class: Extend to create custom drag visuals
  • Drop Slot Detection: Automatic overlap detection with drop targets
  • Visual Feedback: Apply USS classes to drop slots during drag
  • World Space Support: Works with both Screen Space and World Space UI
  • Drops into the scene: A release over no UI slot can land on a 3D target through the scene's PointerProbe
  • Event System: Events for drag start, move, drop, world drop, and cancel

Basic Usage

1. Create a Custom Manipulator

Extend DragAndDropManipulator and implement the abstract methods:

csharp
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; using CupkekGames.Luna; public class ItemDragManipulator : DragAndDropManipulator { private Sprite _itemSprite; private VisualElement _dragElement; // The optional base-constructor parameters (getDropSlots, worldSpace) // must be forwarded by your subclass to be usable. public ItemDragManipulator( LunaUIManager uiManager, VisualElement dragArea, int itemKey, List<VisualElement> dropSlots, Sprite itemSprite, Func<List<VisualElement>> getDropSlots = null, bool worldSpace = false ) : base(uiManager, dragArea, itemKey, dropSlots, getDropSlots, worldSpace) { _itemSprite = itemSprite; } public override VisualElement CreateDragElement() { _dragElement = new VisualElement(); _dragElement.AddToClassList("drag-item"); _dragElement.style.width = 64; _dragElement.style.height = 64; _dragElement.style.backgroundImage = new StyleBackground(_itemSprite); return _dragElement; } public override void DisposeDragElement() { _dragElement = null; } }

2. Add the Manipulator to Elements

csharp
// Get references — `root` is the visual tree delivered by your // PanelRenderer's reload callback (see Complete Example below) // or passed to UIViewComponent.OnUILoaded(root). LunaUIManager uiManager = LunaUIManager.Instance; VisualElement dragArea = root; List<VisualElement> dropSlots = dragArea.Query<VisualElement>(className: "inventory-slot").ToList(); // Create and add manipulator to draggable item var manipulator = new ItemDragManipulator( uiManager, dragArea, itemKey: 0, dropSlots, itemSprite ); // Subscribe to events manipulator.OnDrop += HandleDrop; manipulator.OnCancel += HandleCancel; // Add to the draggable element draggableItem.AddManipulator(manipulator);

3. Handle Drop Events

csharp
private void HandleDrop(int key, int dropSlotIndex, VisualElement dropSlot) { Debug.Log($"Item {key} dropped on slot {dropSlotIndex}"); // Move item to new slot } private void HandleCancel() { Debug.Log("Drag cancelled - no valid drop target"); }

Constructor

csharp
public DragAndDropManipulator( LunaUIManager uiElementManager, VisualElement dragArea, int key, List<VisualElement> dropSlots, Func<List<VisualElement>> getDropSlots = null, bool worldSpace = false )
ParameterTypeDescription
uiElementManagerLunaUIManagerReference to LunaUIManager for audio handling (can be null)
dragAreaVisualElementThe area where the drag clone will be added
keyintUnique identifier for the dragged item
dropSlotsList<VisualElement>Static list of valid drop targets
getDropSlotsFunc<List<VisualElement>>Optional function to get dynamic drop slots
worldSpaceboolSet to true for World Space UI

Abstract Methods

CreateDragElement

Called when drag starts. Must return the visual element to display during drag.

csharp
public abstract VisualElement CreateDragElement();

DisposeDragElement

Called when drag ends. Clean up any resources used by the drag element.

csharp
public abstract void DisposeDragElement();

Events

EventParametersDescription
OnStartVector3 positionFired when drag begins
OnMoveVector3 positionFired while dragging
OnDropint key, int slotIndex, VisualElement slotFired when dropped on valid slot
OnWorldDropIWorldDropTarget targetFired when dropped on a target in the scene (after its Drop)
OnCancel-Fired when dropped on neither a slot nor a world target

Key returns the key the manipulator was made with; IsDragging is true while the drag element follows the pointer.

Drop Slot Highlighting

Use SetDropSlotClasses to apply USS classes to drop slots during drag:

csharp
// Set classes to apply during drag manipulator.SetDropSlotClasses(new List<string> { "drop-highlight", "glow" });
css
/* USS styles */ .drop-highlight { border-color: var(--color-primary); border-width: 2px; } .glow { background-color: rgba(255, 255, 255, 0.1); }

Dynamic Drop Slots

For drop slots that change at runtime, use the getDropSlots parameter. It is a parameter of the base DragAndDropManipulator constructor — your subclass must forward it to base(...), as the ItemDragManipulator example above does:

csharp
var manipulator = new ItemDragManipulator( uiManager, dragArea, itemKey: 0, dropSlots: null, // No static slots itemSprite: itemSprite, getDropSlots: () => GetCurrentInventorySlots() // Dynamic function );

World Space UI

For World Space UI Toolkit panels, set worldSpace to true. Like getDropSlots, it is a base-constructor parameter that your subclass must forward to base(...) (see the ItemDragManipulator example above):

csharp
var manipulator = new ItemDragManipulator( uiManager, dragArea, itemKey: 0, dropSlots, itemSprite, worldSpace: true // Enable world space coordinate conversion );

The manipulator automatically handles coordinate conversion between screen space and panel local coordinates.

Dropping into the scene

A drag can end on something in the 3D scene: an ingredient dropped into a pot, a card dropped onto a field. Give the manipulator the scene's PointerProbe and put a component implementing IWorldDropTarget on the target's collider, or on a parent of it:

csharp
// the probe only casts against the layers its readers include probe.IncludeLayer(potLayer); manipulator.SetWorldDrop(probe); manipulator.OnWorldDrop += target => Debug.Log("landed in the scene");
csharp
public class Pot : MonoBehaviour, IWorldDropTarget { public bool CanDrop(DragAndDropManipulator drag) => !IsFull; // the drag came over the pot or left it (the release included): show it public void SetDropHover(DragAndDropManipulator drag, bool hovered) => _glow.enabled = hovered; public void Drop(DragAndDropManipulator drag) => Add(drag.Key); }

The order on release: a UI drop slot under the drag element wins; then the world target under the pointer, if its CanDrop takes this drag; otherwise the drag cancels. The target is asked every frame while the drag lasts, so a pointer resting on a scene that moves keeps the hover right. A target that refuses is never hovered or dropped on.

A target that is not a collider (a hex field resolved by its own math, a spot on a terrain) uses the resolver overload instead: SetWorldDrop(() => fieldUnderPointer ? fieldTarget : null). Return null for "nothing here". SetWorldDrop(null) turns world drops off.

The drag element never takes the pointer (it and its children ignore picking), so the probe sees the scene through it. A screen that lets the scene show through must leave that area unpicked too: the probe skips its cast while the pointer rests on any picking element of a screen-space view.

Inventory: InventoryViewPagination and InventoryViewList take the same call (SetWorldDrop(resolver, dragArea), inventory 0.4.7) and then make a drag for every item, even with no UI drop slot. The target reads the item from ItemDragAndDrop.StarterSlot.

Complete Example

csharp
using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; using CupkekGames.Luna; public class InventoryDragDrop : MonoBehaviour { [SerializeField] private PanelRenderer _panelRenderer; private List<ItemDragManipulator> _manipulators = new(); private void Awake() { // PanelRenderer delivers its visual tree asynchronously — // wire the manipulators in the reload callback, not in Start. _panelRenderer.RegisterUIReloadCallback(OnUIReload); } private void OnUIReload(PanelRenderer renderer, VisualElement root, int version) { if (_manipulators.Count > 0) return; // sentinel: only wire once SetupDragAndDrop(root); } private void SetupDragAndDrop(VisualElement root) { var dropSlots = root.Query<VisualElement>(className: "inventory-slot").ToList(); foreach (var slot in dropSlots) { var itemElement = slot.Q<VisualElement>("Item"); if (itemElement == null) continue; int slotIndex = dropSlots.IndexOf(slot); var manipulator = new ItemDragManipulator( LunaUIManager.Instance, root, slotIndex, dropSlots, GetItemSprite(slotIndex) ); manipulator.SetDropSlotClasses(new List<string> { "drop-target-active" }); manipulator.OnDrop += OnItemDropped; manipulator.OnCancel += OnDragCancelled; itemElement.AddManipulator(manipulator); _manipulators.Add(manipulator); } } private void OnItemDropped(int sourceSlot, int targetSlot, VisualElement targetElement) { Debug.Log($"Moved item from slot {sourceSlot} to slot {targetSlot}"); SwapItems(sourceSlot, targetSlot); RefreshUI(); } private void OnDragCancelled() { Debug.Log("Drag cancelled"); } private void OnDestroy() { _panelRenderer.UnregisterUIReloadCallback(OnUIReload); foreach (var manipulator in _manipulators) { manipulator.OnDrop -= OnItemDropped; manipulator.OnCancel -= OnDragCancelled; } } }

LunaUIManager Usage

LunaUIManager's purpose in DragAndDropManipulator is to prevent hover audio from playing when hovering over other buttons and interactable elements while dragging. When a drag operation starts, if LunaUIManager is not null, hover audio will be muted until the drag operation ends.

Note: For more advanced examples, refer to the Inventory implementation in the Game samples.

Tips

  • The drag element is positioned with absolute positioning within the drag area
  • Audio is automatically disabled during drag to prevent hover sounds (when LunaUIManager is provided)
  • The manipulator handles pointer capture automatically
  • UI drop slots are found by bounds overlap, not raycast; world targets come from the scene's PointerProbe raycast

Tooltip Integration

When using drag and drop with elements that have tooltips, you may want to disable tooltips during drag operations. Use AddTooltipReference to register tooltips that should be automatically disabled when dragging starts and re-enabled when dragging ends.

csharp
// Register tooltip to disable during drag manipulator.AddTooltipReference(tooltipController.Tooltip);

This prevents tooltips from interfering with drag operations and provides a cleaner user experience.

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL