class_name Draggable extends Area2D ## Base class for draggable UI elements (Cards and StickyNotes) ## Provides common dragging behavior and boundary protection ## 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 } ## 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") var is_dragged: bool = false: set(dragged): is_dragged = dragged z_index = int(dragged) ## Margin from screen edges when confining to screen bounds @export var screen_margin: float = 50.0 ## 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: # Skip if this is a sticky note attached to a card if self is StickyNote: var sticky := self as StickyNote if sticky.attached_to is Card: return # Try to get bounds from parent container var bounds := _get_container_bounds() # If no container bounds, use viewport/screen bounds if bounds == Rect2(): bounds = _get_viewport_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 ) # Check if parent is a Node2D with defined boundaries # (for future support of non-Control containers) if parent is Node2D: # For now, return empty rect - could be extended in the future # to check for custom boundary properties pass return Rect2() ## Gets the viewport bounds as fallback func _get_viewport_bounds() -> Rect2: var viewport := get_viewport() if viewport: var viewport_size := viewport.get_visible_rect().size return Rect2( screen_margin, screen_margin, viewport_size.x - screen_margin * 2, viewport_size.y - screen_margin * 2 ) return Rect2()