2026-01-16 15:38:26 +00:00
|
|
|
class_name Draggable
|
|
|
|
|
extends Area2D
|
|
|
|
|
|
|
|
|
|
## Base class for draggable UI elements (Cards and StickyNotes)
|
|
|
|
|
## Provides common dragging behavior and boundary protection
|
|
|
|
|
|
2026-01-17 11:11:21 +00:00
|
|
|
## Margin from screen edges when confining to screen bounds
|
|
|
|
|
@export var screen_margin: float = 50.0
|
|
|
|
|
|
2026-01-16 19:46:16 +00:00
|
|
|
## Drop result codes for DropTarget pattern
|
|
|
|
|
enum DropResult {
|
|
|
|
|
ACCEPTED, # Drop successful, item is now owned by target
|
|
|
|
|
REJECTED, # Drop refused, item stays with previous owner
|
|
|
|
|
EXCHANGED # Swap occurred, exchanged item needs handling
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 09:48:03 +00:00
|
|
|
var mouse_over: bool = false
|
|
|
|
|
|
2026-01-16 15:38:26 +00:00
|
|
|
var is_dragged: bool = false:
|
|
|
|
|
set(dragged):
|
|
|
|
|
is_dragged = dragged
|
|
|
|
|
z_index = int(dragged)
|
|
|
|
|
|
2026-01-16 21:40:28 +00:00
|
|
|
## Internal highlighted state - do not set directly, use set_highlight()
|
|
|
|
|
var _highlighted: bool = false
|
|
|
|
|
|
|
|
|
|
## Public highlighted property - use for reading state
|
|
|
|
|
var highlighted: bool:
|
|
|
|
|
get: return _highlighted
|
|
|
|
|
set(value): set_highlight(value)
|
|
|
|
|
|
|
|
|
|
## Sets the highlight state - override in subclasses for visual feedback
|
|
|
|
|
## Base implementation just updates the internal state
|
|
|
|
|
func set_highlight(value: bool) -> void:
|
|
|
|
|
_highlighted = value
|
|
|
|
|
|
2026-01-16 19:46:16 +00:00
|
|
|
## Drag state tracking
|
|
|
|
|
var _drag_start_position: Vector2
|
|
|
|
|
var _mouse_drag_offset: Vector2
|
|
|
|
|
var _drag_source: Node = null # Where the drag started from
|
|
|
|
|
|
2026-01-18 09:48:03 +00:00
|
|
|
## === SETUP ###
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
mouse_entered.connect(_on_mouse_entered)
|
|
|
|
|
mouse_exited.connect(_on_mouse_exited)
|
2026-01-18 16:32:31 +00:00
|
|
|
input_event.connect(_on_input_event)
|
2026-01-18 09:48:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _get_hover_handler() -> Node:
|
|
|
|
|
var parent := get_parent()
|
|
|
|
|
while parent and not parent.has_method("handle_hover"):
|
|
|
|
|
parent = parent.get_parent()
|
|
|
|
|
return parent
|
|
|
|
|
|
2026-01-18 16:32:31 +00:00
|
|
|
|
|
|
|
|
## Walks up the scene tree to find the CardBoard
|
|
|
|
|
func _get_board() -> Node:
|
|
|
|
|
var node := get_parent()
|
|
|
|
|
while node:
|
|
|
|
|
if node.has_method("handle_mouse_button"):
|
|
|
|
|
return node
|
|
|
|
|
node = node.get_parent()
|
|
|
|
|
return null
|
|
|
|
|
|
2026-01-16 19:46:16 +00:00
|
|
|
## === DRAG LIFECYCLE METHODS ===
|
|
|
|
|
## Override these in Card and StickyNote for specific behavior
|
|
|
|
|
|
2026-01-18 09:48:03 +00:00
|
|
|
func _on_mouse_entered() -> void:
|
|
|
|
|
prints("Draggable[base]._on_mouse_entered", self, self.name)
|
|
|
|
|
mouse_over = true
|
|
|
|
|
var handler := _get_hover_handler()
|
|
|
|
|
if handler: handler.handle_hover(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_mouse_exited() -> void:
|
|
|
|
|
prints("Draggable[base]._on_mouse_exited", self, self.name)
|
|
|
|
|
mouse_over = false
|
|
|
|
|
var handler := _get_hover_handler()
|
|
|
|
|
if handler: handler.handle_hover(self)
|
|
|
|
|
|
|
|
|
|
|
2026-01-18 16:32:31 +00:00
|
|
|
## Handles global input events (used to catch mouse release during drag)
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
|
|
|
if event is InputEventMouseButton:
|
|
|
|
|
if event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
|
|
|
|
|
if is_dragged:
|
|
|
|
|
is_dragged = false
|
|
|
|
|
# Trigger the drop logic
|
|
|
|
|
var board := _get_board()
|
|
|
|
|
if board and board.has_method("_end_drag"):
|
|
|
|
|
board._end_drag(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Handles input events on this Area2D (used to start drag)
|
|
|
|
|
func _on_input_event(_viewport, event, _shape_idx):
|
|
|
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
|
|
|
|
if highlighted:
|
|
|
|
|
var board := _get_board()
|
|
|
|
|
if board and board.has_method("handle_mouse_button"):
|
2026-01-18 16:52:04 +00:00
|
|
|
board.handle_mouse_button(event, self)
|
2026-01-18 16:32:31 +00:00
|
|
|
|
|
|
|
|
|
2026-01-16 19:46:16 +00:00
|
|
|
## Starts a drag operation
|
|
|
|
|
func start_drag(mouse_offset: Vector2) -> void:
|
|
|
|
|
_drag_start_position = global_position
|
|
|
|
|
_mouse_drag_offset = mouse_offset
|
|
|
|
|
_drag_source = get_parent()
|
|
|
|
|
is_dragged = true
|
|
|
|
|
|
|
|
|
|
## Updates position during drag (call from _process or manual update)
|
|
|
|
|
func update_drag_position(mouse_pos: Vector2) -> void:
|
|
|
|
|
global_position = mouse_pos - _mouse_drag_offset
|
|
|
|
|
confine_to_screen()
|
|
|
|
|
|
|
|
|
|
## Finds the best drop target for this draggable
|
|
|
|
|
## Override in subclasses for specific drop target logic
|
|
|
|
|
## Returns the node that should receive the drop, or null for no valid target
|
|
|
|
|
func find_drop_target() -> Node:
|
|
|
|
|
# Base implementation: return parent (board)
|
|
|
|
|
return get_parent()
|
|
|
|
|
|
|
|
|
|
## Called after drop to clean up drag state
|
2026-01-18 09:48:03 +00:00
|
|
|
func end_drag() -> Node:
|
2026-01-16 19:46:16 +00:00
|
|
|
is_dragged = false
|
|
|
|
|
_drag_source = null
|
2026-01-18 09:48:03 +00:00
|
|
|
return null
|
2026-01-16 19:46:16 +00:00
|
|
|
|
2026-01-17 11:11:21 +00:00
|
|
|
|
2026-01-16 15:38:26 +00:00
|
|
|
## Confines this draggable element to stay within screen or container bounds
|
|
|
|
|
## Skip this check if a sticky note is attached to a card
|
|
|
|
|
func confine_to_screen() -> void:
|
|
|
|
|
# Try to get bounds from parent container
|
|
|
|
|
var bounds := _get_container_bounds()
|
2026-01-18 09:48:03 +00:00
|
|
|
|
2026-01-16 15:38:26 +00:00
|
|
|
# If we have valid bounds, clamp position
|
|
|
|
|
if bounds != Rect2():
|
|
|
|
|
position.x = clampf(position.x, bounds.position.x, bounds.position.x + bounds.size.x)
|
|
|
|
|
position.y = clampf(position.y, bounds.position.y, bounds.position.y + bounds.size.y)
|
|
|
|
|
|
2026-01-17 11:11:21 +00:00
|
|
|
|
2026-01-16 15:38:26 +00:00
|
|
|
## Gets the bounds of the parent container if it exists and is a Control node
|
|
|
|
|
func _get_container_bounds() -> Rect2:
|
|
|
|
|
var parent := get_parent()
|
2026-01-18 09:48:03 +00:00
|
|
|
|
2026-01-16 15:38:26 +00:00
|
|
|
# Check if parent is a Control node with a defined rect
|
|
|
|
|
if parent is Control:
|
|
|
|
|
var control := parent as Control
|
|
|
|
|
# Return the usable area with margins
|
|
|
|
|
return Rect2(
|
|
|
|
|
screen_margin,
|
|
|
|
|
screen_margin,
|
|
|
|
|
control.size.x - screen_margin * 2,
|
|
|
|
|
control.size.y - screen_margin * 2
|
|
|
|
|
)
|
2026-01-18 09:48:03 +00:00
|
|
|
|
2026-01-17 11:11:21 +00:00
|
|
|
# Default: whole screen
|
|
|
|
|
var viewport_size := get_viewport().get_visible_rect().size
|
|
|
|
|
return Rect2(
|
|
|
|
|
screen_margin,
|
|
|
|
|
screen_margin,
|
|
|
|
|
viewport_size.x - screen_margin * 2,
|
|
|
|
|
viewport_size.y - screen_margin * 2
|
|
|
|
|
)
|