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.

PointerProbeExtend DragAndDropManipulator and implement the abstract methods:
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;
}
}// 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);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");
}public DragAndDropManipulator(
LunaUIManager uiElementManager,
VisualElement dragArea,
int key,
List<VisualElement> dropSlots,
Func<List<VisualElement>> getDropSlots = null,
bool worldSpace = false
)| Parameter | Type | Description |
|---|---|---|
| uiElementManager | LunaUIManager | Reference to LunaUIManager for audio handling (can be null) |
| dragArea | VisualElement | The area where the drag clone will be added |
| key | int | Unique identifier for the dragged item |
| dropSlots | List<VisualElement> | Static list of valid drop targets |
| getDropSlots | Func<List<VisualElement>> | Optional function to get dynamic drop slots |
| worldSpace | bool | Set to true for World Space UI |
Called when drag starts. Must return the visual element to display during drag.
public abstract VisualElement CreateDragElement();Called when drag ends. Clean up any resources used by the drag element.
public abstract void DisposeDragElement();| Event | Parameters | Description |
|---|---|---|
| OnStart | Vector3 position | Fired when drag begins |
| OnMove | Vector3 position | Fired while dragging |
| OnDrop | int key, int slotIndex, VisualElement slot | Fired when dropped on valid slot |
| OnWorldDrop | IWorldDropTarget target | Fired 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.
Use SetDropSlotClasses to apply USS classes to drop slots during drag:
// Set classes to apply during drag
manipulator.SetDropSlotClasses(new List<string> { "drop-highlight", "glow" });/* USS styles */
.drop-highlight {
border-color: var(--color-primary);
border-width: 2px;
}
.glow {
background-color: rgba(255, 255, 255, 0.1);
}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:
var manipulator = new ItemDragManipulator(
uiManager,
dragArea,
itemKey: 0,
dropSlots: null, // No static slots
itemSprite: itemSprite,
getDropSlots: () => GetCurrentInventorySlots() // Dynamic function
);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):
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.
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:
// 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");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:
InventoryViewPaginationandInventoryViewListtake 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 fromItemDragAndDrop.StarterSlot.
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'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.
PointerProbe raycastWhen 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.
// 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)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction