add drag-and-drop module reordering in grid with hover highlight and swap
This commit is contained in:
parent
9ec671d4c9
commit
b44312fe6b
1 changed files with 255 additions and 17 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue