2026-05-20 13:08:38 -04:00
|
|
|
@tool
|
2026-05-20 12:54:09 -04:00
|
|
|
extends Control
|
|
|
|
|
class_name DashboardGrid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
signal module_placed(module: Control, col: int, row: int)
|
|
|
|
|
signal module_removed(module: Control)
|
2026-05-21 08:39:15 -04:00
|
|
|
signal module_resized(module: Control, col: int, row: int, w: int, h: int)
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
@export var cell_min_size: Vector2 = Vector2(300, 240)
|
|
|
|
|
@export var cell_spacing: float = 8.0
|
|
|
|
|
@export var margin: float = 16.0
|
|
|
|
|
|
|
|
|
|
var columns: int = 0
|
|
|
|
|
var rows: int = 0
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Visual cells — shown as drag guides
|
|
|
|
|
var _cells: Array = [] # _cells[row][col] -> PanelContainer
|
|
|
|
|
|
|
|
|
|
# Occupancy: _grid[row][col] -> Control or null
|
|
|
|
|
var _grid: Array = []
|
|
|
|
|
|
|
|
|
|
# Ordered module list (row-major order for layout)
|
|
|
|
|
var _module_list: Array[Control] = []
|
|
|
|
|
|
|
|
|
|
# Cell styles cached for show/hide
|
|
|
|
|
var _cell_styles: Dictionary = {}
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
# 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
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Resize state
|
|
|
|
|
var _is_resizing: bool = false
|
|
|
|
|
var _resize_module: Control = null
|
|
|
|
|
var _resize_edge: int = 0 # bitfield: 1=left, 2=right, 4=top, 8=bottom
|
|
|
|
|
var _resize_origin_col: int = -1
|
|
|
|
|
var _resize_origin_row: int = -1
|
|
|
|
|
var _resize_origin_w: int = 1
|
|
|
|
|
var _resize_origin_h: int = 1
|
|
|
|
|
var _resize_preview: ColorRect = null # ghost showing new size
|
|
|
|
|
|
|
|
|
|
# Size of a single cell in pixels (computed during _layout_cells)
|
|
|
|
|
var _cell_w: float = 1.0
|
|
|
|
|
var _cell_h: float = 1.0
|
|
|
|
|
|
|
|
|
|
# Resize edge detection threshold (pixels)
|
|
|
|
|
const RESIZE_THRESHOLD: float = 10.0
|
|
|
|
|
const EDGE_LEFT: int = 1
|
|
|
|
|
const EDGE_RIGHT: int = 2
|
|
|
|
|
const EDGE_TOP: int = 4
|
|
|
|
|
const EDGE_BOTTOM: int = 8
|
2026-05-20 13:59:08 -04:00
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
func _ready() -> void:
|
2026-05-20 14:04:14 -04:00
|
|
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
2026-05-20 13:08:38 -04:00
|
|
|
resized.connect(_rebuild_grid)
|
2026-05-20 14:04:14 -04:00
|
|
|
gui_input.connect(_on_gui_input)
|
2026-05-20 16:18:33 -04:00
|
|
|
mouse_exited.connect(_on_mouse_exited)
|
2026-05-20 14:08:43 -04:00
|
|
|
_rebuild_grid()
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
func _on_mouse_exited() -> void:
|
|
|
|
|
if _is_dragging:
|
|
|
|
|
if _drag_module != null:
|
|
|
|
|
_drag_module.modulate = Color(1, 1, 1, 1)
|
|
|
|
|
_drop_to_source()
|
|
|
|
|
_layout_cells()
|
|
|
|
|
_finish_drag()
|
|
|
|
|
_show_cells(false)
|
|
|
|
|
elif _is_resizing:
|
|
|
|
|
_end_resize(Vector2.ZERO)
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 14:04:14 -04:00
|
|
|
func _on_gui_input(event: InputEvent) -> void:
|
2026-05-20 13:59:08 -04:00
|
|
|
if event is InputEventMouseButton:
|
|
|
|
|
var mb: InputEventMouseButton = event
|
2026-05-20 16:18:33 -04:00
|
|
|
if mb.button_index == MOUSE_BUTTON_LEFT and mb.double_click and mb.pressed:
|
|
|
|
|
_show_preset_popup(mb.position)
|
|
|
|
|
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.double_click:
|
2026-05-20 13:59:08 -04:00
|
|
|
if mb.pressed:
|
2026-05-20 16:18:33 -04:00
|
|
|
if not _try_begin_resize(mb.position):
|
|
|
|
|
_begin_drag(mb.position)
|
|
|
|
|
elif _is_resizing:
|
|
|
|
|
_end_resize(mb.position)
|
2026-05-20 13:59:08 -04:00
|
|
|
elif _is_dragging:
|
|
|
|
|
_end_drag(mb.position)
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
|
if _is_resizing:
|
|
|
|
|
_update_resize(event.position)
|
|
|
|
|
elif _is_dragging:
|
|
|
|
|
_update_drag(event.position)
|
|
|
|
|
else:
|
|
|
|
|
_update_hover_cursor(event.position)
|
2026-05-20 13:59:08 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
|
|
|
|
|
func _show_preset_popup(mouse_pos: Vector2) -> void:
|
|
|
|
|
var cell_pos := _cell_at_position(mouse_pos)
|
|
|
|
|
if cell_pos.x < 0 or cell_pos.y < 0:
|
|
|
|
|
return
|
|
|
|
|
var module := _get_module_at(cell_pos.x, cell_pos.y)
|
|
|
|
|
if module == null:
|
|
|
|
|
return
|
|
|
|
|
# Don't show popup if already dragging or resizing
|
|
|
|
|
if _is_dragging or _is_resizing:
|
2026-05-20 13:08:38 -04:00
|
|
|
return
|
2026-05-20 16:18:33 -04:00
|
|
|
|
|
|
|
|
var popup := PopupMenu.new()
|
|
|
|
|
popup.name = "PresetPopup"
|
|
|
|
|
var names := ShaderPresets.get_preset_names()
|
|
|
|
|
for i in names.size():
|
|
|
|
|
popup.add_item(names[i], i)
|
|
|
|
|
popup.position = Vector2i(get_screen_position()) + Vector2i(mouse_pos)
|
|
|
|
|
add_child(popup)
|
|
|
|
|
popup.popup()
|
|
|
|
|
|
|
|
|
|
var on_select := func(id: int):
|
|
|
|
|
if id >= 0 and id < names.size():
|
|
|
|
|
ShaderPresets.apply_preset(module, names[id])
|
|
|
|
|
if is_instance_valid(popup):
|
|
|
|
|
popup.queue_free()
|
|
|
|
|
|
|
|
|
|
popup.id_pressed.connect(on_select)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Public API
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func place_module(module: Control, col: int, row: int, span_w: int = 1, span_h: int = 1) -> void:
|
|
|
|
|
_ensure_occupancy_for_span(col, row, span_w, span_h)
|
|
|
|
|
_set_module_grid_data(module, col, row, span_w, span_h)
|
|
|
|
|
_occupy_cells(module, col, row, span_w, span_h)
|
|
|
|
|
|
|
|
|
|
# Add as direct child of the grid
|
|
|
|
|
add_child(module)
|
|
|
|
|
if module not in _module_list:
|
|
|
|
|
_module_list.append(module)
|
|
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
module_placed.emit(module, col, row)
|
2026-05-20 16:18:33 -04:00
|
|
|
_layout_cells()
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
func remove_module(module: Control) -> void:
|
|
|
|
|
if not is_instance_valid(module):
|
2026-05-20 13:08:38 -04:00
|
|
|
return
|
2026-05-20 16:18:33 -04:00
|
|
|
var col: int = module.get_meta("grid_col", -1)
|
|
|
|
|
var row: int = module.get_meta("grid_row", -1)
|
|
|
|
|
var w: int = module.get_meta("grid_w", 1)
|
|
|
|
|
var h: int = module.get_meta("grid_h", 1)
|
|
|
|
|
_clear_occupancy(col, row, w, h)
|
|
|
|
|
_module_list.erase(module)
|
|
|
|
|
if module.get_parent() == self:
|
|
|
|
|
remove_child(module)
|
|
|
|
|
module_removed.emit(module)
|
|
|
|
|
_layout_cells()
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-21 08:39:15 -04:00
|
|
|
func get_module_at(col: int, row: int) -> Control:
|
|
|
|
|
if not _is_valid_cell(col, row):
|
|
|
|
|
return null
|
|
|
|
|
return _grid[row][col] as Control
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
func get_grid_size() -> Vector2i:
|
2026-05-20 13:08:38 -04:00
|
|
|
return Vector2i(columns, rows)
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Module grid-data helpers
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _set_module_grid_data(module: Control, col: int, row: int, w: int, h: int) -> void:
|
|
|
|
|
module.set_meta("grid_col", col)
|
|
|
|
|
module.set_meta("grid_row", row)
|
|
|
|
|
module.set_meta("grid_w", w)
|
|
|
|
|
module.set_meta("grid_h", h)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _get_module_grid_data(module: Control) -> Dictionary:
|
|
|
|
|
return {
|
|
|
|
|
col = module.get_meta("grid_col", 0),
|
|
|
|
|
row = module.get_meta("grid_row", 0),
|
|
|
|
|
w = module.get_meta("grid_w", 1),
|
|
|
|
|
h = module.get_meta("grid_h", 1),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _occupy_cells(module: Control, col: int, row: int, w: int, h: int) -> void:
|
|
|
|
|
for r in range(row, row + h):
|
|
|
|
|
for c in range(col, col + w):
|
|
|
|
|
if r < _grid.size() and c < _grid[r].size():
|
|
|
|
|
_grid[r][c] = module
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _clear_occupancy(col: int, row: int, w: int, h: int) -> void:
|
|
|
|
|
for r in range(row, row + h):
|
|
|
|
|
for c in range(col, col + w):
|
|
|
|
|
if r < _grid.size() and c < _grid[r].size():
|
|
|
|
|
_grid[r][c] = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ensure_occupancy_for_span(col: int, row: int, w: int, h: int) -> void:
|
|
|
|
|
var needed_rows := row + h
|
|
|
|
|
var needed_cols := col + w
|
|
|
|
|
while _grid.size() < needed_rows:
|
|
|
|
|
_grid.append([])
|
|
|
|
|
for r in range(_grid.size()):
|
|
|
|
|
while _grid[r].size() < needed_cols:
|
|
|
|
|
_grid[r].append(null)
|
|
|
|
|
if columns < needed_cols:
|
|
|
|
|
columns = needed_cols
|
|
|
|
|
if rows < needed_rows:
|
|
|
|
|
rows = needed_rows
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
func _is_valid_cell(col: int, row: int) -> bool:
|
2026-05-20 16:18:33 -04:00
|
|
|
return row >= 0 and row < _grid.size() and col >= 0 and col < _grid[row].size()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Occupancy checks
|
|
|
|
|
# --------------------------------------------------------------------------
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
func _span_fits(col: int, row: int, w: int, h: int, exclude: Control = null) -> bool:
|
|
|
|
|
for r in range(row, row + h):
|
|
|
|
|
for c in range(col, col + w):
|
|
|
|
|
if r < 0 or c < 0:
|
|
|
|
|
return false
|
|
|
|
|
if r >= _grid.size() or c >= _grid[r].size():
|
|
|
|
|
continue # beyond grid is OK — will expand
|
|
|
|
|
var occupant: Control = _grid[r][c]
|
|
|
|
|
if occupant != null and occupant != exclude:
|
|
|
|
|
return false
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Cell / position helpers
|
|
|
|
|
# --------------------------------------------------------------------------
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
func _cell_at_position(pos: Vector2) -> Vector2i:
|
2026-05-20 16:18:33 -04:00
|
|
|
if columns == 0 or rows == 0 or _cell_w <= 0 or _cell_h <= 0:
|
2026-05-20 13:59:08 -04:00
|
|
|
return Vector2i(-1, -1)
|
|
|
|
|
|
|
|
|
|
var inner_x := margin + cell_spacing
|
|
|
|
|
var inner_y := margin + cell_spacing
|
2026-05-20 16:18:33 -04:00
|
|
|
var col := int((pos.x - inner_x) / (_cell_w + cell_spacing))
|
|
|
|
|
var row := int((pos.y - inner_y) / (_cell_h + cell_spacing))
|
2026-05-20 13:59:08 -04:00
|
|
|
col = clampi(col, 0, columns - 1)
|
|
|
|
|
row = clampi(row, 0, rows - 1)
|
|
|
|
|
return Vector2i(col, row)
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
func _get_module_at(col: int, row: int) -> Control:
|
2026-05-20 13:59:08 -04:00
|
|
|
if not _is_valid_cell(col, row):
|
|
|
|
|
return null
|
2026-05-20 16:18:33 -04:00
|
|
|
return _grid[row][col] as Control
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _get_module_in_grid() -> Array[Control]:
|
|
|
|
|
var result: Array[Control] = []
|
|
|
|
|
for r in range(rows):
|
|
|
|
|
for c in range(columns):
|
|
|
|
|
var mod := _get_module_at(c, r) as Control
|
|
|
|
|
if mod != null and mod not in result:
|
|
|
|
|
result.append(mod)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Hover cursor
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _update_hover_cursor(mouse_pos: Vector2) -> void:
|
|
|
|
|
var edge := _detect_resize_edge(mouse_pos)
|
|
|
|
|
mouse_default_cursor_shape = Control.CURSOR_ARROW
|
|
|
|
|
if edge != 0:
|
|
|
|
|
if edge & (EDGE_TOP | EDGE_BOTTOM):
|
|
|
|
|
mouse_default_cursor_shape = Control.CURSOR_VSIZE
|
|
|
|
|
elif edge & (EDGE_LEFT | EDGE_RIGHT):
|
|
|
|
|
mouse_default_cursor_shape = Control.CURSOR_HSIZE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _detect_resize_edge(mouse_pos: Vector2) -> int:
|
|
|
|
|
for module in _module_list:
|
|
|
|
|
if not is_instance_valid(module):
|
|
|
|
|
continue
|
|
|
|
|
if not module.visible:
|
|
|
|
|
continue
|
|
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
var rect := _get_module_rect(d.col, d.row, d.w, d.h)
|
|
|
|
|
if not rect.has_point(mouse_pos):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
var edge := 0
|
|
|
|
|
var dist_left: float = abs(mouse_pos.x - rect.position.x)
|
|
|
|
|
var dist_right: float = abs(mouse_pos.x - (rect.position.x + rect.size.x))
|
|
|
|
|
var dist_top: float = abs(mouse_pos.y - rect.position.y)
|
|
|
|
|
var dist_bottom: float = abs(mouse_pos.y - (rect.position.y + rect.size.y))
|
|
|
|
|
|
|
|
|
|
if dist_left < RESIZE_THRESHOLD:
|
|
|
|
|
edge |= EDGE_LEFT
|
|
|
|
|
if dist_right < RESIZE_THRESHOLD:
|
|
|
|
|
edge |= EDGE_RIGHT
|
|
|
|
|
if dist_top < RESIZE_THRESHOLD:
|
|
|
|
|
edge |= EDGE_TOP
|
|
|
|
|
if dist_bottom < RESIZE_THRESHOLD:
|
|
|
|
|
edge |= EDGE_BOTTOM
|
|
|
|
|
|
|
|
|
|
return edge
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Resize
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _try_begin_resize(mouse_pos: Vector2) -> bool:
|
|
|
|
|
var edge := _detect_resize_edge(mouse_pos)
|
|
|
|
|
if edge == 0:
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
# Find which module
|
|
|
|
|
for module in _module_list:
|
|
|
|
|
if not is_instance_valid(module):
|
|
|
|
|
continue
|
|
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
var rect := _get_module_rect(d.col, d.row, d.w, d.h)
|
|
|
|
|
if rect.has_point(mouse_pos):
|
|
|
|
|
_is_resizing = true
|
|
|
|
|
_resize_module = module
|
|
|
|
|
_resize_edge = edge
|
|
|
|
|
_resize_origin_col = d.col
|
|
|
|
|
_resize_origin_row = d.row
|
|
|
|
|
_resize_origin_w = d.w
|
|
|
|
|
_resize_origin_h = d.h
|
|
|
|
|
|
|
|
|
|
# Create preview ghost
|
|
|
|
|
_resize_preview = ColorRect.new()
|
|
|
|
|
_resize_preview.color = Color(0.3, 0.5, 1.0, 0.2)
|
|
|
|
|
_resize_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
|
|
|
add_child(_resize_preview)
|
|
|
|
|
_update_resize_preview(mouse_pos)
|
|
|
|
|
|
|
|
|
|
# Show cells
|
|
|
|
|
_show_cells(true)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _update_resize(mouse_pos: Vector2) -> void:
|
|
|
|
|
if _resize_preview == null:
|
|
|
|
|
return
|
|
|
|
|
_update_resize_preview(mouse_pos)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _update_resize_preview(mouse_pos: Vector2) -> void:
|
|
|
|
|
var d := _get_module_grid_data(_resize_module)
|
|
|
|
|
var cell_pos := _cell_at_position(mouse_pos)
|
|
|
|
|
if cell_pos.x < 0 or cell_pos.y < 0:
|
|
|
|
|
_resize_preview.visible = false
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var new_col: int = d.col
|
|
|
|
|
var new_row: int = d.row
|
|
|
|
|
var new_w: int = d.w
|
|
|
|
|
var new_h: int = d.h
|
|
|
|
|
|
|
|
|
|
if _resize_edge & EDGE_LEFT:
|
|
|
|
|
new_col = min(cell_pos.x, d.col + d.w - 1)
|
|
|
|
|
new_w = d.col + d.w - new_col
|
|
|
|
|
if _resize_edge & EDGE_RIGHT:
|
|
|
|
|
new_w = max(1, cell_pos.x - d.col + 1)
|
|
|
|
|
if _resize_edge & EDGE_TOP:
|
|
|
|
|
new_row = min(cell_pos.y, d.row + d.h - 1)
|
|
|
|
|
new_h = d.row + d.h - new_row
|
|
|
|
|
if _resize_edge & EDGE_BOTTOM:
|
|
|
|
|
new_h = max(1, cell_pos.y - d.row + 1)
|
|
|
|
|
|
|
|
|
|
# Clamp to grid
|
|
|
|
|
new_w = clampi(new_w, 1, columns - new_col)
|
|
|
|
|
new_h = clampi(new_h, 1, rows - new_row)
|
|
|
|
|
|
|
|
|
|
# Check for overlap (excluding ourselves)
|
|
|
|
|
var fits := _span_fits(new_col, new_row, new_w, new_h, _resize_module)
|
|
|
|
|
|
|
|
|
|
_resize_preview.visible = fits
|
|
|
|
|
if fits:
|
|
|
|
|
var prect := _get_module_rect(new_col, new_row, new_w, new_h)
|
|
|
|
|
_resize_preview.set_position(prect.position)
|
|
|
|
|
_resize_preview.set_size(prect.size)
|
|
|
|
|
|
|
|
|
|
# Store proposed span for end_resize
|
|
|
|
|
_resize_origin_col = new_col
|
|
|
|
|
_resize_origin_row = new_row
|
|
|
|
|
_resize_origin_w = new_w
|
|
|
|
|
_resize_origin_h = new_h
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _end_resize(_mouse_pos: Vector2) -> void:
|
|
|
|
|
if _resize_preview != null:
|
|
|
|
|
_resize_preview.queue_free()
|
|
|
|
|
_resize_preview = null
|
|
|
|
|
|
|
|
|
|
_show_cells(false)
|
|
|
|
|
|
|
|
|
|
if _resize_module != null and is_instance_valid(_resize_module):
|
|
|
|
|
# Clear old occupancy
|
|
|
|
|
var d := _get_module_grid_data(_resize_module)
|
|
|
|
|
_clear_occupancy(d.col, d.row, d.w, d.h)
|
|
|
|
|
|
|
|
|
|
# Apply new span
|
|
|
|
|
_set_module_grid_data(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
|
|
|
|
|
_occupy_cells(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
|
|
|
|
|
_layout_cells()
|
2026-05-21 08:39:15 -04:00
|
|
|
module_resized.emit(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
|
2026-05-20 16:18:33 -04:00
|
|
|
|
|
|
|
|
_is_resizing = false
|
|
|
|
|
_resize_module = null
|
|
|
|
|
_resize_edge = 0
|
2026-05-20 13:59:08 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# 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
|
2026-05-20 16:18:33 -04:00
|
|
|
var module := _get_module_at(col, row)
|
2026-05-20 13:59:08 -04:00
|
|
|
if module == null:
|
|
|
|
|
return
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
|
|
|
|
|
_drag_source_col = d.col
|
|
|
|
|
_drag_source_row = d.row
|
2026-05-20 14:04:14 -04:00
|
|
|
|
2026-05-20 14:54:11 -04:00
|
|
|
_show_cells(true)
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Module position in grid space
|
|
|
|
|
var module_rect := _get_module_rect(d.col, d.row, d.w, d.h)
|
|
|
|
|
_drag_mouse_offset = mouse_pos - module_rect.position
|
2026-05-20 14:04:14 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Reparent module to top of tree (already a child if direct child of grid)
|
|
|
|
|
_drag_module = module
|
|
|
|
|
move_child(_drag_module, get_child_count())
|
2026-05-20 13:59:08 -04:00
|
|
|
_is_dragging = true
|
|
|
|
|
|
|
|
|
|
_drag_module.position = mouse_pos - _drag_mouse_offset
|
|
|
|
|
_drag_module.modulate = Color(1, 1, 1, 0.7)
|
|
|
|
|
_clear_hover()
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Remove from occupancy temporarily
|
|
|
|
|
_clear_occupancy(d.col, d.row, d.w, d.h)
|
|
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
|
|
|
|
|
func _update_drag(mouse_pos: Vector2) -> void:
|
|
|
|
|
if _drag_module == null:
|
|
|
|
|
return
|
|
|
|
|
_drag_module.position = mouse_pos - _drag_mouse_offset
|
|
|
|
|
|
|
|
|
|
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()
|
2026-05-20 14:54:11 -04:00
|
|
|
_show_cells(false)
|
2026-05-20 14:44:12 -04:00
|
|
|
_drag_module.modulate = Color(1, 1, 1, 1)
|
2026-05-20 13:59:08 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
var d := _get_module_grid_data(_drag_module)
|
2026-05-20 13:59:08 -04:00
|
|
|
var cell_pos := _cell_at_position(mouse_pos)
|
|
|
|
|
var target_col := cell_pos.x
|
|
|
|
|
var target_row := cell_pos.y
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Check if mouse is actually inside the cell bounds
|
2026-05-20 14:54:11 -04:00
|
|
|
var on_cell := false
|
|
|
|
|
if _is_valid_cell(target_col, target_row):
|
|
|
|
|
var cell_ref: PanelContainer = _cells[target_row][target_col]
|
|
|
|
|
on_cell = Rect2(Vector2.ZERO, cell_ref.size).has_point(mouse_pos - cell_ref.position)
|
|
|
|
|
|
|
|
|
|
if not on_cell or not _is_valid_cell(target_col, target_row):
|
|
|
|
|
_drop_to_source()
|
2026-05-20 16:18:33 -04:00
|
|
|
_layout_cells()
|
2026-05-20 14:44:12 -04:00
|
|
|
_finish_drag()
|
2026-05-20 13:59:08 -04:00
|
|
|
return
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Check if target is occupied
|
|
|
|
|
var existing := _get_module_at(target_col, target_row)
|
2026-05-20 14:54:11 -04:00
|
|
|
if existing != null:
|
2026-05-20 16:18:33 -04:00
|
|
|
var ed := _get_module_grid_data(existing)
|
|
|
|
|
_swap_modules(_drag_module, d.col, d.row, d.w, d.h, existing, ed)
|
|
|
|
|
var ad := _get_module_grid_data(_drag_module)
|
|
|
|
|
module_placed.emit(_drag_module, ad.col, ad.row)
|
|
|
|
|
module_placed.emit(existing, d.col, d.row)
|
2026-05-20 13:59:08 -04:00
|
|
|
else:
|
2026-05-20 16:18:33 -04:00
|
|
|
# Place in new position
|
|
|
|
|
_clear_occupancy(d.col, d.row, d.w, d.h)
|
|
|
|
|
_set_module_grid_data(_drag_module, target_col, target_row, d.w, d.h)
|
|
|
|
|
if _span_fits(target_col, target_row, d.w, d.h, _drag_module):
|
|
|
|
|
_occupy_cells(_drag_module, target_col, target_row, d.w, d.h)
|
|
|
|
|
else:
|
|
|
|
|
_occupy_cells(_drag_module, d.col, d.row, d.w, d.h)
|
|
|
|
|
_set_module_grid_data(_drag_module, d.col, d.row, d.w, d.h)
|
2026-05-20 14:54:11 -04:00
|
|
|
module_placed.emit(_drag_module, target_col, target_row)
|
2026-05-20 13:59:08 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
_layout_cells()
|
2026-05-20 14:44:12 -04:00
|
|
|
_finish_drag()
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
func _swap_modules(a: Control, a_col: int, a_row: int, a_w: int, a_h: int,
|
|
|
|
|
b: Control, bd: Dictionary) -> void:
|
|
|
|
|
_clear_occupancy(a_col, a_row, a_w, a_h)
|
|
|
|
|
_clear_occupancy(bd.col, bd.row, bd.w, bd.h)
|
|
|
|
|
|
|
|
|
|
_set_module_grid_data(a, bd.col, bd.row, bd.w, bd.h)
|
|
|
|
|
_set_module_grid_data(b, a_col, a_row, a_w, a_h)
|
|
|
|
|
|
|
|
|
|
_occupy_cells(a, bd.col, bd.row, bd.w, bd.h)
|
|
|
|
|
_occupy_cells(b, a_col, a_row, a_w, a_h)
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 14:54:11 -04:00
|
|
|
func _drop_to_source() -> void:
|
|
|
|
|
if _drag_module == null:
|
|
|
|
|
return
|
2026-05-20 16:18:33 -04:00
|
|
|
var d := _get_module_grid_data(_drag_module)
|
|
|
|
|
_occupy_cells(_drag_module, d.col, d.row, d.w, d.h)
|
2026-05-20 14:54:11 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 14:44:12 -04:00
|
|
|
func _finish_drag() -> void:
|
2026-05-20 13:59:08 -04:00
|
|
|
_drag_module = null
|
|
|
|
|
_is_dragging = false
|
|
|
|
|
_drag_source_col = -1
|
|
|
|
|
_drag_source_row = -1
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Module rect in grid coordinates
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _get_module_rect(col: int, row: int, w: int, h: int) -> Rect2:
|
|
|
|
|
var x := margin + cell_spacing + col * (_cell_w + cell_spacing)
|
|
|
|
|
var y := margin + cell_spacing + row * (_cell_h + cell_spacing)
|
|
|
|
|
var mw := w * _cell_w + (w - 1) * cell_spacing
|
|
|
|
|
var mh := h * _cell_h + (h - 1) * cell_spacing
|
|
|
|
|
return Rect2(x, y, mw, mh)
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# 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]
|
2026-05-20 14:54:11 -04:00
|
|
|
var style: StyleBoxFlat = cell.get_meta("visible_style")
|
2026-05-20 13:59:08 -04:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Grid rebuild
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
func _rebuild_grid() -> void:
|
2026-05-20 13:59:08 -04:00
|
|
|
if _is_dragging:
|
|
|
|
|
_cancel_drag()
|
2026-05-20 16:18:33 -04:00
|
|
|
if _is_resizing:
|
|
|
|
|
_end_resize(Vector2.ZERO)
|
|
|
|
|
# Clean up orphaned popups from preset selection
|
|
|
|
|
for child in get_children():
|
|
|
|
|
if child is PopupMenu:
|
|
|
|
|
child.queue_free()
|
2026-05-20 13:59:08 -04:00
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
var avail := size - Vector2(margin * 2.0, margin * 2.0)
|
2026-05-20 14:08:43 -04:00
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
if avail.x < cell_min_size.x or avail.y < cell_min_size.y:
|
2026-05-20 14:08:43 -04:00
|
|
|
if _cells.size() > 0:
|
|
|
|
|
_layout_cells()
|
|
|
|
|
return
|
|
|
|
|
var new_cols := 1
|
|
|
|
|
var new_rows := 1
|
|
|
|
|
if new_cols == columns and new_rows == rows and _cells.size() > 0:
|
|
|
|
|
_layout_cells()
|
|
|
|
|
return
|
|
|
|
|
columns = new_cols
|
|
|
|
|
rows = new_rows
|
|
|
|
|
_save_modules()
|
2026-05-20 13:08:38 -04:00
|
|
|
_teardown_cells()
|
2026-05-20 14:08:43 -04:00
|
|
|
_build_cells()
|
|
|
|
|
_restore_modules()
|
|
|
|
|
_layout_cells()
|
2026-05-20 13:08:38 -04:00
|
|
|
return
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
var new_cols := maxi(1, int(avail.x / (cell_min_size.x + cell_spacing)))
|
2026-05-20 14:44:12 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
var module_count := _module_list.size()
|
2026-05-20 14:44:12 -04:00
|
|
|
var min_rows := maxi(1, ceili(float(module_count) / float(new_cols)))
|
|
|
|
|
var avail_rows := maxi(1, int(avail.y / (cell_min_size.y + cell_spacing)))
|
|
|
|
|
var new_rows := maxi(avail_rows, min_rows)
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
if new_cols == columns and new_rows == rows and _cells.size() > 0:
|
|
|
|
|
_layout_cells()
|
|
|
|
|
return
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
columns = new_cols
|
|
|
|
|
rows = new_rows
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 13:08:38 -04:00
|
|
|
_save_modules()
|
|
|
|
|
_teardown_cells()
|
|
|
|
|
_build_cells()
|
|
|
|
|
_restore_modules()
|
|
|
|
|
_layout_cells()
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
func _cancel_drag() -> void:
|
|
|
|
|
if _drag_module != null:
|
|
|
|
|
_drag_module.modulate = Color(1, 1, 1, 1)
|
2026-05-20 16:18:33 -04:00
|
|
|
var d := _get_module_grid_data(_drag_module)
|
|
|
|
|
_occupy_cells(_drag_module, d.col, d.row, d.w, d.h)
|
2026-05-20 14:54:11 -04:00
|
|
|
_show_cells(false)
|
|
|
|
|
_clear_hover()
|
2026-05-20 13:59:08 -04:00
|
|
|
_drag_module = null
|
|
|
|
|
_is_dragging = false
|
|
|
|
|
_drag_source_col = -1
|
|
|
|
|
_drag_source_row = -1
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Cell building / teardown / styling
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
func _build_cells() -> void:
|
2026-05-20 13:08:38 -04:00
|
|
|
_cells.clear()
|
|
|
|
|
for row in range(rows):
|
|
|
|
|
_cells.append([])
|
|
|
|
|
for col in range(columns):
|
|
|
|
|
var cell := PanelContainer.new()
|
|
|
|
|
cell.name = "Cell_%d_%d" % [col, row]
|
|
|
|
|
cell.mouse_filter = Control.MOUSE_FILTER_PASS
|
2026-05-20 13:59:08 -04:00
|
|
|
_style_cell(cell, col, row)
|
2026-05-20 13:08:38 -04:00
|
|
|
add_child(cell)
|
|
|
|
|
_cells[row].append(cell)
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 13:59:08 -04:00
|
|
|
func _style_cell(cell: PanelContainer, col: int, row: int) -> void:
|
2026-05-20 14:54:11 -04:00
|
|
|
var hidden := StyleBoxFlat.new()
|
|
|
|
|
hidden.bg_color = Color.TRANSPARENT
|
|
|
|
|
hidden.border_width_left = 1
|
|
|
|
|
hidden.border_width_top = 1
|
|
|
|
|
hidden.border_width_right = 1
|
|
|
|
|
hidden.border_width_bottom = 1
|
|
|
|
|
hidden.border_color = Color.TRANSPARENT
|
2026-05-20 15:03:47 -04:00
|
|
|
hidden.corner_radius_top_left = 12
|
|
|
|
|
hidden.corner_radius_top_right = 12
|
|
|
|
|
hidden.corner_radius_bottom_right = 12
|
|
|
|
|
hidden.corner_radius_bottom_left = 12
|
2026-05-20 14:54:11 -04:00
|
|
|
|
|
|
|
|
var visible := StyleBoxFlat.new()
|
|
|
|
|
visible.bg_color = Color(0.14, 0.14, 0.18, 1.0)
|
|
|
|
|
visible.border_width_left = 1
|
|
|
|
|
visible.border_width_top = 1
|
|
|
|
|
visible.border_width_right = 1
|
|
|
|
|
visible.border_width_bottom = 1
|
|
|
|
|
visible.border_color = Color(0.25, 0.25, 0.35, 1.0)
|
2026-05-20 15:03:47 -04:00
|
|
|
visible.corner_radius_top_left = 12
|
|
|
|
|
visible.corner_radius_top_right = 12
|
|
|
|
|
visible.corner_radius_bottom_right = 12
|
|
|
|
|
visible.corner_radius_bottom_left = 12
|
2026-05-20 14:54:11 -04:00
|
|
|
|
|
|
|
|
cell.add_theme_stylebox_override("panel", hidden)
|
|
|
|
|
cell.add_theme_stylebox_override("panel_focused", hidden)
|
|
|
|
|
cell.set_meta("hidden_style", hidden)
|
|
|
|
|
cell.set_meta("visible_style", visible)
|
|
|
|
|
_cell_styles["%d,%d" % [col, row]] = visible
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _show_cells(show: bool) -> void:
|
|
|
|
|
for row_idx in range(_cells.size()):
|
|
|
|
|
for col_idx in range(_cells[row_idx].size()):
|
|
|
|
|
var cell: PanelContainer = _cells[row_idx][col_idx]
|
|
|
|
|
if not is_instance_valid(cell):
|
|
|
|
|
continue
|
|
|
|
|
var style: StyleBoxFlat = cell.get_meta("visible_style") if show else cell.get_meta("hidden_style")
|
|
|
|
|
cell.add_theme_stylebox_override("panel", style)
|
|
|
|
|
cell.add_theme_stylebox_override("panel_focused", style)
|
2026-05-20 12:59:50 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
func _teardown_cells() -> void:
|
2026-05-20 13:59:08 -04:00
|
|
|
_cell_styles.clear()
|
2026-05-20 13:08:38 -04:00
|
|
|
for child in get_children():
|
2026-05-20 16:18:33 -04:00
|
|
|
# Keep modules — only remove cells
|
|
|
|
|
if child is PanelContainer and child.name.begins_with("Cell_"):
|
|
|
|
|
remove_child(child)
|
|
|
|
|
if is_instance_valid(child):
|
|
|
|
|
child.queue_free()
|
2026-05-20 13:08:38 -04:00
|
|
|
_cells.clear()
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Layout
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _ensure_cells_match_grid() -> void:
|
|
|
|
|
while _cells.size() < rows:
|
|
|
|
|
_cells.append([])
|
|
|
|
|
var new_row := _cells.size() - 1
|
|
|
|
|
for col in range(columns):
|
|
|
|
|
var cell := PanelContainer.new()
|
|
|
|
|
cell.name = "Cell_%d_%d" % [col, new_row]
|
|
|
|
|
cell.mouse_filter = Control.MOUSE_FILTER_PASS
|
|
|
|
|
_style_cell(cell, col, new_row)
|
|
|
|
|
add_child(cell)
|
|
|
|
|
_cells[new_row].append(cell)
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
func _layout_cells() -> void:
|
2026-05-20 13:08:38 -04:00
|
|
|
if rows == 0 or columns == 0:
|
|
|
|
|
return
|
2026-05-20 16:18:33 -04:00
|
|
|
_ensure_cells_match_grid()
|
2026-05-20 13:08:38 -04:00
|
|
|
|
2026-05-20 14:08:43 -04:00
|
|
|
var inner_w := maxf(0.0, size.x - margin * 2.0 - cell_spacing)
|
|
|
|
|
var inner_h := maxf(0.0, size.y - margin * 2.0 - cell_spacing)
|
2026-05-20 13:08:38 -04:00
|
|
|
var gap_x := cell_spacing * (columns - 1)
|
|
|
|
|
var gap_y := cell_spacing * (rows - 1)
|
2026-05-20 16:18:33 -04:00
|
|
|
_cell_w = maxf(1.0, (inner_w - gap_x) / columns)
|
|
|
|
|
_cell_h = maxf(1.0, (inner_h - gap_y) / rows)
|
2026-05-20 13:08:38 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Position cells
|
2026-05-20 13:08:38 -04:00
|
|
|
for row in range(rows):
|
|
|
|
|
for col in range(columns):
|
2026-05-20 16:18:33 -04:00
|
|
|
if row >= _cells.size() or col >= _cells[row].size():
|
|
|
|
|
continue
|
2026-05-20 13:08:38 -04:00
|
|
|
var cell: PanelContainer = _cells[row][col]
|
|
|
|
|
if not is_instance_valid(cell):
|
|
|
|
|
continue
|
2026-05-20 16:18:33 -04:00
|
|
|
var x := margin + cell_spacing + col * (_cell_w + cell_spacing)
|
|
|
|
|
var y := margin + cell_spacing + row * (_cell_h + cell_spacing)
|
2026-05-20 13:08:38 -04:00
|
|
|
cell.set_position(Vector2(x, y))
|
2026-05-20 16:18:33 -04:00
|
|
|
cell.set_size(Vector2(_cell_w, _cell_h))
|
|
|
|
|
|
|
|
|
|
# Position modules
|
|
|
|
|
for module in _module_list:
|
|
|
|
|
if not is_instance_valid(module):
|
|
|
|
|
continue
|
|
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
var rect := _get_module_rect(d.col, d.row, d.w, d.h)
|
|
|
|
|
module.set_position(rect.position)
|
|
|
|
|
module.set_size(rect.size)
|
|
|
|
|
|
2026-05-20 12:54:09 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Save / restore (used during grid rebuild)
|
|
|
|
|
# --------------------------------------------------------------------------
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
func _save_modules() -> void:
|
2026-05-20 16:18:33 -04:00
|
|
|
# Modules already have their grid data in metadata
|
|
|
|
|
pass
|
2026-05-20 12:54:09 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _restore_modules() -> void:
|
2026-05-20 16:18:33 -04:00
|
|
|
# Rebuild grid
|
|
|
|
|
_grid.clear()
|
|
|
|
|
for r in range(rows):
|
|
|
|
|
_grid.append([])
|
|
|
|
|
for c in range(columns):
|
|
|
|
|
_grid[r].append(null)
|
2026-05-20 14:44:12 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
if _module_list.is_empty():
|
2026-05-20 14:44:12 -04:00
|
|
|
return
|
|
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Ensure enough rows
|
|
|
|
|
while _grid.size() < rows:
|
|
|
|
|
_grid.append([])
|
|
|
|
|
for c in range(columns):
|
|
|
|
|
_grid[_grid.size() - 1].append(null)
|
|
|
|
|
|
|
|
|
|
# Re-place modules row-major
|
|
|
|
|
var reordered: Array[Control] = []
|
|
|
|
|
for r in range(rows):
|
|
|
|
|
for c in range(columns):
|
|
|
|
|
for module in _module_list:
|
|
|
|
|
if module in reordered:
|
|
|
|
|
continue
|
|
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
if d.col == c and d.row == r:
|
|
|
|
|
if _span_fits(c, r, d.w, d.h, null):
|
|
|
|
|
# Ensure grid is big enough
|
|
|
|
|
_ensure_occupancy_for_span(c, r, d.w, d.h)
|
|
|
|
|
_occupy_cells(module, c, r, d.w, d.h)
|
|
|
|
|
reordered.append(module)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Add any modules that didn't find their spot
|
|
|
|
|
for module in _module_list:
|
|
|
|
|
if module not in reordered:
|
|
|
|
|
reordered.append(module)
|
|
|
|
|
|
|
|
|
|
_module_list = reordered
|
|
|
|
|
|
|
|
|
|
# Place orphaned modules
|
|
|
|
|
for module in _module_list:
|
|
|
|
|
if module.get_parent() != self:
|
|
|
|
|
add_child(module)
|
|
|
|
|
# Check if it's in the grid
|
|
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
if _get_module_at(d.col, d.row) != module:
|
|
|
|
|
# Find a spot
|
|
|
|
|
_find_and_place(module)
|
|
|
|
|
|
|
|
|
|
# Rebuild columns/rows from grid occupancy
|
|
|
|
|
_ensure_occupancy_from_grid()
|
|
|
|
|
_layout_cells()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ensure_occupancy_from_grid() -> void:
|
|
|
|
|
var max_c := 0
|
|
|
|
|
var max_r := 0
|
|
|
|
|
for r in range(_grid.size()):
|
|
|
|
|
for c in range(_grid[r].size()):
|
|
|
|
|
if _grid[r][c] != null:
|
|
|
|
|
max_c = maxi(max_c, c + 1)
|
|
|
|
|
max_r = maxi(max_r, r + 1)
|
|
|
|
|
columns = maxi(columns, max_c)
|
|
|
|
|
rows = maxi(rows, max_r)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _find_and_place(module: Control) -> void:
|
|
|
|
|
var d := _get_module_grid_data(module)
|
|
|
|
|
# Try the desired position first
|
|
|
|
|
if _span_fits(d.col, d.row, d.w, d.h, null):
|
|
|
|
|
_ensure_occupancy_for_span(d.col, d.row, d.w, d.h)
|
|
|
|
|
_occupy_cells(module, d.col, d.row, d.w, d.h)
|
|
|
|
|
return
|
2026-05-20 14:44:12 -04:00
|
|
|
|
2026-05-20 16:18:33 -04:00
|
|
|
# Find first available position
|
|
|
|
|
for r in range(rows):
|
|
|
|
|
for c in range(columns):
|
|
|
|
|
if _span_fits(c, r, d.w, d.h, null):
|
|
|
|
|
_set_module_grid_data(module, c, r, d.w, d.h)
|
|
|
|
|
_ensure_occupancy_for_span(c, r, d.w, d.h)
|
|
|
|
|
_occupy_cells(module, c, r, d.w, d.h)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# No room — expand grid
|
|
|
|
|
var new_col := columns
|
|
|
|
|
var new_row := 0
|
|
|
|
|
_ensure_occupancy_for_span(new_col, new_row, d.w, d.h)
|
|
|
|
|
columns = new_col + d.w
|
|
|
|
|
_set_module_grid_data(module, new_col, new_row, d.w, d.h)
|
|
|
|
|
_occupy_cells(module, new_col, new_row, d.w, d.h)
|