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 ## 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 } 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 ## Drag state tracking var _drag_start_position: Vector2 var _mouse_drag_offset: Vector2 var _drag_source: Node = null # Where the drag started from ## === DRAG LIFECYCLE METHODS === ## Override these in Card and StickyNote for specific behavior ## 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 func end_drag() -> void: is_dragged = false _drag_source = null ## 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() # 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() # 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 ) # 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 ) # === HELPERS === ## Static helper to check if a node implements DropTarget pattern ## DropTarget pattern requires: can_accept_drop(draggable) and handle_drop(draggable) static func is_drop_target(node: Node) -> bool: return node != null and node.has_method("can_accept_drop") and node.has_method("handle_drop")