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 } var mouse_over: bool = false var is_dragged: bool = false: set(dragged): is_dragged = dragged z_index = int(dragged) # local coordinates home position, where the draggable can try to animate_home to var home : Vector2 = Vector2.ZERO ## 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 ## === SETUP ### func _ready() -> void: mouse_entered.connect(_on_mouse_entered) mouse_exited.connect(_on_mouse_exited) input_event.connect(_on_input_event) func _get_hover_handler() -> Node: var parent := get_parent() while parent and not parent.has_method("handle_hover"): parent = parent.get_parent() return parent ## 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 ## === DRAG LIFECYCLE METHODS === ## Override these in Card and StickyNote for specific behavior var tween : Tween = null func animate_home() -> void: if tween: tween.kill() tween = create_tween().set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_BACK) tween.tween_property(self, "position", home, 0.5) 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) ## 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"): board.handle_mouse_button(event, self) ## 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() ## End drag operation and return the node we want to be accepted by (if any) func end_drag() -> Node: is_dragged = false _drag_source = null return 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 )