From b44312fe6bb9ecf66a4fa70987bdc6123678c7c1 Mon Sep 17 00:00:00 2001 From: Eric Smith <5d@fifthdread.com> Date: Wed, 20 May 2026 13:59:08 -0400 Subject: [PATCH] add drag-and-drop module reordering in grid with hover highlight and swap --- scripts/dashboard_grid.gd | 272 +++++++++++++++++++++++++++++++++++--- 1 file changed, 255 insertions(+), 17 deletions(-) diff --git a/scripts/dashboard_grid.gd b/scripts/dashboard_grid.gd index 8a01ef3..09ebc2f 100644 --- a/scripts/dashboard_grid.gd +++ b/scripts/dashboard_grid.gd @@ -16,42 +16,255 @@ var rows: int = 0 var _cells: Array = [] # 2D: _cells[row][col] -> PanelContainer var _module_positions: Dictionary = {} # "col,row" -> Control +# Drag state +var _is_dragging: bool = false +var _drag_source_col: int = -1 +var _drag_source_row: int = -1 +var _drag_module: Control = null +var _drag_mouse_offset: Vector2 = Vector2.ZERO +var _hover_col: int = -1 +var _hover_row: int = -1 + +# Styles cached per-cell so we can modify them during drag +var _cell_styles: Dictionary = {} # cell -> StyleBoxFlat + func _ready() -> void: resized.connect(_rebuild_grid) _rebuild_grid() +func _gui_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + var mb: InputEventMouseButton = event + if mb.button_index == MOUSE_BUTTON_LEFT and not mb.double_click: + if mb.pressed: + _begin_drag(mb.position) + elif _is_dragging: + _end_drag(mb.position) + elif event is InputEventMouseMotion and _is_dragging: + _update_drag(event.position) + + func place_module(module: Control, col: int, row: int) -> void: if not _is_valid_cell(col, row): return - var cell: PanelContainer = _cells[row][col] - _set_cell_child(cell, module) - _module_positions["%d,%d" % [col, row]] = module + _place_module_in_cell(module, col, row) module_placed.emit(module, col, row) func remove_module(col: int, row: int) -> void: if not _is_valid_cell(col, row): return - var cell: PanelContainer = _cells[row][col] - var key := "%d,%d" % [col, row] - for child in cell.get_children(): - cell.remove_child(child) - if is_instance_valid(child): - module_removed.emit(child) - child.queue_free() - _module_positions.erase(key) + var module := _take_module_from_cell(col, row) + if module != null: + module_removed.emit(module) + module.queue_free() func get_grid_size() -> Vector2i: return Vector2i(columns, rows) +func get_cell_rect(col: int, row: int) -> Rect2: + if not _is_valid_cell(col, row): + return Rect2() + var cell: PanelContainer = _cells[row][col] + return Rect2(cell.position, cell.size) + + func _is_valid_cell(col: int, row: int) -> bool: return row >= 0 and row < _cells.size() and col >= 0 and col < _cells[row].size() +func _cell_at_position(pos: Vector2) -> Vector2i: + if columns == 0 or rows == 0: + return Vector2i(-1, -1) + + var inner_x := margin + cell_spacing + var inner_y := margin + cell_spacing + var inner_w := size.x - margin * 2.0 - cell_spacing + var inner_h := size.y - margin * 2.0 - cell_spacing + var gap_x := cell_spacing * (columns - 1) + var gap_y := cell_spacing * (rows - 1) + var cell_w := (inner_w - gap_x) / columns + var cell_h := (inner_h - gap_y) / rows + + var col := int((pos.x - inner_x) / (cell_w + cell_spacing)) + var row := int((pos.y - inner_y) / (cell_h + cell_spacing)) + col = clampi(col, 0, columns - 1) + row = clampi(row, 0, rows - 1) + + return Vector2i(col, row) + + +func _get_cell_module(col: int, row: int) -> Control: + if not _is_valid_cell(col, row): + return null + var cell: PanelContainer = _cells[row][col] + if cell.get_child_count() > 0: + return cell.get_child(0) + return null + + +# -------------------------------------------------------------------------- +# Drag & Drop +# -------------------------------------------------------------------------- + +func _begin_drag(mouse_pos: Vector2) -> void: + var cell_pos := _cell_at_position(mouse_pos) + if cell_pos.x < 0 or cell_pos.y < 0: + return + + var col := cell_pos.x + var row := cell_pos.y + var module := _get_cell_module(col, row) + if module == null: + return + + _drag_source_col = col + _drag_source_row = row + _drag_module = _take_module_from_cell(col, row) + _drag_mouse_offset = mouse_pos - _drag_module.position + _is_dragging = true + + # Reparent to grid so it renders above all cells + add_child(_drag_module) + _drag_module.position = mouse_pos - _drag_mouse_offset + + # Make the dragged module semi-transparent + _drag_module.modulate = Color(1, 1, 1, 0.7) + + # Clear hover state + _clear_hover() + + +func _update_drag(mouse_pos: Vector2) -> void: + if _drag_module == null: + return + + _drag_module.position = mouse_pos - _drag_mouse_offset + + # Update hover highlight + var cell_pos := _cell_at_position(mouse_pos) + if cell_pos.x != _hover_col or cell_pos.y != _hover_row: + _clear_hover() + if cell_pos.x >= 0 and cell_pos.y >= 0: + _hover_col = cell_pos.x + _hover_row = cell_pos.y + _highlight_cell(_hover_col, _hover_row, true) + + +func _end_drag(mouse_pos: Vector2) -> void: + if _drag_module == null: + return + + _clear_hover() + + var cell_pos := _cell_at_position(mouse_pos) + var target_col := cell_pos.x + var target_row := cell_pos.y + + # Dropped on the same cell — just put it back + if target_col == _drag_source_col and target_row == _drag_source_row: + _drag_module.modulate = Color(1, 1, 1, 1) + _place_module_in_cell(_drag_module, target_col, target_row) + module_placed.emit(_drag_module, target_col, target_row) + _drag_module = null + _is_dragging = false + _drag_source_col = -1 + _drag_source_row = -1 + return + + # Remove the module from the grid (we'll reparent it below) + _drag_module.get_parent().remove_child(_drag_module) + + if _is_valid_cell(target_col, target_row): + var existing := _get_cell_module(target_col, target_row) + if existing != null: + # Swap: put existing module in source cell, drag module in target + existing.get_parent().remove_child(existing) + var was_source_valid := _is_valid_cell(_drag_source_col, _drag_source_row) + if was_source_valid: + _place_module_in_cell(existing, _drag_source_col, _drag_source_row) + module_placed.emit(existing, _drag_source_col, _drag_source_row) + else: + existing.queue_free() + + # Place dragged module in target cell + _drag_module.modulate = Color(1, 1, 1, 1) + _place_module_in_cell(_drag_module, target_col, target_row) + module_placed.emit(_drag_module, target_col, target_row) + else: + # Dropped outside the grid — return to source if still valid + if _is_valid_cell(_drag_source_col, _drag_source_row): + _drag_module.modulate = Color(1, 1, 1, 1) + _place_module_in_cell(_drag_module, _drag_source_col, _drag_source_row) + module_placed.emit(_drag_module, _drag_source_col, _drag_source_row) + else: + _drag_module.queue_free() + + _drag_module = null + _is_dragging = false + _drag_source_col = -1 + _drag_source_row = -1 + + +# -------------------------------------------------------------------------- +# Cell highlights +# -------------------------------------------------------------------------- + +func _clear_hover() -> void: + if _hover_col >= 0 and _hover_row >= 0 and _is_valid_cell(_hover_col, _hover_row): + _highlight_cell(_hover_col, _hover_row, false) + _hover_col = -1 + _hover_row = -1 + + +func _highlight_cell(col: int, row: int, highlighted: bool) -> void: + if not _is_valid_cell(col, row): + return + var cell: PanelContainer = _cells[row][col] + var style_key: String = "%d,%d" % [col, row] + var style: StyleBoxFlat = _cell_styles.get(style_key) + if style == null: + return + if highlighted: + style.border_color = Color(0.5, 0.6, 1.0, 1.0) + else: + style.border_color = Color(0.25, 0.25, 0.35, 1.0) + cell.add_theme_stylebox_override("panel", style) + cell.add_theme_stylebox_override("panel_focused", style) + + +# -------------------------------------------------------------------------- +# Module placement helpers +# -------------------------------------------------------------------------- + +func _take_module_from_cell(col: int, row: int) -> Control: + if not _is_valid_cell(col, row): + return null + var cell: PanelContainer = _cells[row][col] + for child in cell.get_children(): + cell.remove_child(child) + _module_positions.erase("%d,%d" % [col, row]) + return child + return null + + +func _place_module_in_cell(module: Control, col: int, row: int) -> void: + if not _is_valid_cell(col, row): + return + var cell: PanelContainer = _cells[row][col] + # Clear existing children + for child in cell.get_children(): + cell.remove_child(child) + if is_instance_valid(child): + child.queue_free() + cell.add_child(module) + _module_positions["%d,%d" % [col, row]] = module + + func _set_cell_child(cell: PanelContainer, child: Control) -> void: for existing in cell.get_children(): cell.remove_child(existing) @@ -60,7 +273,15 @@ func _set_cell_child(cell: PanelContainer, child: Control) -> void: cell.add_child(child) +# -------------------------------------------------------------------------- +# Grid rebuild +# -------------------------------------------------------------------------- + func _rebuild_grid() -> void: + # Cancel any active drag + if _is_dragging: + _cancel_drag() + var avail := size - Vector2(margin * 2.0, margin * 2.0) if avail.x < cell_min_size.x or avail.y < cell_min_size.y: _teardown_cells() @@ -69,7 +290,6 @@ func _rebuild_grid() -> void: var new_cols := maxi(1, int(avail.x / (cell_min_size.x + cell_spacing))) var new_rows := maxi(1, int(avail.y / (cell_min_size.y + cell_spacing))) - # Bail out if grid dimensions haven't changed if new_cols == columns and new_rows == rows and _cells.size() > 0: _layout_cells() return @@ -84,6 +304,23 @@ func _rebuild_grid() -> void: _layout_cells() +func _cancel_drag() -> void: + if _drag_module != null: + _drag_module.modulate = Color(1, 1, 1, 1) + if _is_valid_cell(_drag_source_col, _drag_source_row): + var parent := _drag_module.get_parent() + if parent != null: + parent.remove_child(_drag_module) + _place_module_in_cell(_drag_module, _drag_source_col, _drag_source_row) + else: + _drag_module.queue_free() + _drag_module = null + _is_dragging = false + _drag_source_col = -1 + _drag_source_row = -1 + _clear_hover() + + func _build_cells() -> void: _cells.clear() for row in range(rows): @@ -92,12 +329,12 @@ func _build_cells() -> void: var cell := PanelContainer.new() cell.name = "Cell_%d_%d" % [col, row] cell.mouse_filter = Control.MOUSE_FILTER_PASS - _style_cell(cell) + _style_cell(cell, col, row) add_child(cell) _cells[row].append(cell) -func _style_cell(cell: PanelContainer) -> void: +func _style_cell(cell: PanelContainer, col: int, row: int) -> void: var style := StyleBoxFlat.new() style.bg_color = Color(0.14, 0.14, 0.18, 1.0) style.border_width_left = 1 @@ -111,9 +348,11 @@ func _style_cell(cell: PanelContainer) -> void: style.corner_radius_bottom_left = 6 cell.add_theme_stylebox_override("panel", style) cell.add_theme_stylebox_override("panel_focused", style) + _cell_styles["%d,%d" % [col, row]] = style func _teardown_cells() -> void: + _cell_styles.clear() for child in get_children(): remove_child(child) if is_instance_valid(child): @@ -162,13 +401,12 @@ func _restore_modules() -> void: var module: Control = _module_positions[key] if _is_valid_cell(col, row): - _set_cell_child(_cells[row][col], module) + _place_module_in_cell(module, col, row) else: - # Place in the last available cell if original position no longer exists if _cells.size() > 0 and _cells[0].size() > 0: var last_row := _cells.size() - 1 var last_col: int = _cells[last_row].size() - 1 - _set_cell_child(_cells[last_row][last_col], module) + _place_module_in_cell(module, last_col, last_row) else: module.queue_free()