@tool extends Control class_name DashboardGrid signal module_placed(module: Control, col: int, row: int) signal module_removed(module: Control) @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 var _cells: Array = [] # 2D: _cells[row][col] -> PanelContainer var _module_positions: Dictionary = {} # "col,row" -> Control func _ready() -> void: resized.connect(_rebuild_grid) _rebuild_grid() 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 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) func get_grid_size() -> Vector2i: return Vector2i(columns, rows) 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 _set_cell_child(cell: PanelContainer, child: Control) -> void: for existing in cell.get_children(): cell.remove_child(existing) if is_instance_valid(existing): existing.queue_free() cell.add_child(child) func _rebuild_grid() -> void: 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() return 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 columns = new_cols rows = new_rows _save_modules() _teardown_cells() _build_cells() _restore_modules() _layout_cells() func _build_cells() -> void: _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 _style_cell(cell) add_child(cell) _cells[row].append(cell) func _style_cell(cell: PanelContainer) -> void: var style := StyleBoxFlat.new() style.bg_color = Color(0.14, 0.14, 0.18, 1.0) style.border_width_left = 1 style.border_width_top = 1 style.border_width_right = 1 style.border_width_bottom = 1 style.border_color = Color(0.25, 0.25, 0.35, 1.0) style.corner_radius_top_left = 6 style.corner_radius_top_right = 6 style.corner_radius_bottom_right = 6 style.corner_radius_bottom_left = 6 cell.add_theme_stylebox_override("panel", style) cell.add_theme_stylebox_override("panel_focused", style) func _teardown_cells() -> void: for child in get_children(): remove_child(child) if is_instance_valid(child): child.queue_free() _cells.clear() func _layout_cells() -> void: if rows == 0 or columns == 0: return 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 for row in range(rows): for col in range(columns): var cell: PanelContainer = _cells[row][col] if not is_instance_valid(cell): continue var x := margin + cell_spacing + col * (cell_w + cell_spacing) var y := margin + cell_spacing + row * (cell_h + cell_spacing) cell.set_position(Vector2(x, y)) cell.set_size(Vector2(cell_w, cell_h)) func _save_modules() -> void: _module_positions.clear() for row in range(_cells.size()): for col in range(_cells[row].size()): var cell: PanelContainer = _cells[row][col] if cell.get_child_count() > 0: var mod := cell.get_child(0) cell.remove_child(mod) _module_positions["%d,%d" % [col, row]] = mod func _restore_modules() -> void: for key in _module_positions: var parts: PackedStringArray = key.split(",") var col: int = int(parts[0]) var row := int(parts[1]) var module: Control = _module_positions[key] if _is_valid_cell(col, row): _set_cell_child(_cells[row][col], module) 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) else: module.queue_free() _module_positions.clear()