Quick answer: Godot 4 renamed the drag-and-drop virtuals to _get_drag_data(), _can_drop_data() and _drop_data(). A drag only starts if _get_drag_data() returns non-null, and a drop only happens if the target's _can_drop_data() returns true. Both Controls must receive mouse input, so check their mouse_filter.
Built-in drag and drop is three methods on Control. When it fails, it fails silently, so the fix is usually spotting which of the three is not being called.
The three methods
# On the item being dragged (e.g. an inventory slot)
func _get_drag_data(at_position: Vector2) -> Variant:
if item == null:
return null # null = no drag
var preview := TextureRect.new()
preview.texture = item.icon
set_drag_preview(preview)
return {"item": item, "from": self}
# On the drop target
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
return data is Dictionary and data.has("item")
func _drop_data(at_position: Vector2, data: Variant) -> void:
accept_item(data.item)
data.from.clear_item()
Cause 1: Godot 3 method names
Godot 3 used get_drag_data, can_drop_data and drop_data without the leading underscore. In Godot 4 those are just ordinary methods that the engine never calls. Rename them and add the parameter types shown above.
Cause 2: _get_drag_data returns null
If the method has an early return, or returns null because the slot looked empty, no drag begins. Note that a drag only starts once the mouse moves a few pixels with the button held — a click on its own will not call it.
Cause 3: _can_drop_data returns false
Print data inside _can_drop_data(). A type check against the wrong class or a missing dictionary key is the usual reason. While it returns false, the cursor shows the forbidden icon and _drop_data() is never called.
Cause 4: mouse_filter blocks the events
Both the source and the target must receive mouse input. A Control with mouse_filter set to Ignore cannot start or accept a drag. Watch out for children too: if a TextureRect icon covers the slot with filter Stop, *it* receives the events instead of the slot that has the script. Set child visuals to Ignore, or Pass.
Cause 5: the preview hides the target
The drag preview follows the mouse. Previews are not hit-tested, but a custom overlay Control you show during dragging can be. Keep anything you add during a drag at MOUSE_FILTER_IGNORE.
Without subclassing: drag forwarding
To keep the logic in a parent script, forward the callbacks from child Controls:
func _ready():
for slot in $Grid.get_children():
slot.set_drag_forwarding(
_get_slot_drag.bind(slot),
_can_drop_slot.bind(slot),
_drop_slot.bind(slot))
In Godot 4 set_drag_forwarding() takes three Callables. The bound slot arrives as the last argument, after the position and data.
Detecting a cancelled drag
Listen for NOTIFICATION_DRAG_END in _notification() and check get_viewport().gui_is_drag_successful() to restore an item when it was dropped somewhere invalid.