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.

Extend 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 |
| OnCancel | - | Fired when dropped outside valid slots |
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.
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.
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.
// 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