frame-of-mind/src/logic-scenes/board/draggable.gd

132 lines
3.9 KiB
GDScript3
Raw Normal View History

class_name Draggable
extends Area2D
## Base class for draggable UI elements (Cards and StickyNotes)
## Provides common dragging behavior and boundary protection
## 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
var is_dragged: bool = false:
set(dragged):
is_dragged = dragged
z_index = int(dragged)
## 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)
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-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-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
## 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
# 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)
## 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
# 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
# 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
)