From 5aabc1f7ef2e5c286e93ce7eedde15aabc81df23 Mon Sep 17 00:00:00 2001 From: Eric Smith <5d@fifthdread.com> Date: Wed, 20 May 2026 12:54:09 -0400 Subject: [PATCH] add dashboard framework: responsive grid layout with modular cell system and fullscreen entry --- AGENTS.md | 5 +- project.godot | 27 ++++--- scenes/dashboard.gd | 8 ++ scenes/dashboard.tscn | 19 +++++ scripts/dashboard_grid.gd | 157 ++++++++++++++++++++++++++++++++++++++ scripts/module_base.gd | 32 ++++++++ 6 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 scenes/dashboard.gd create mode 100644 scenes/dashboard.tscn create mode 100644 scripts/dashboard_grid.gd create mode 100644 scripts/module_base.gd diff --git a/AGENTS.md b/AGENTS.md index 5d760d8..32f543b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,15 +27,16 @@ res:// │ ├── icons/ │ └── textures/ ├── autoload/ # Singleton/autoload scripts -├── panels/ # Individual status panels +├── panels/ # Individual status panels (modules) │ ├── cpu/ │ ├── memory/ │ ├── network/ │ └── disk/ +├── scenes/ # Root scenes (dashboard, etc.) ├── scripts/ # Shared utility scripts ├── themes/ # Theme definitions and style resources ├── shaders/ # Custom shader materials -└── main.tscn # Root scene +└── main.tscn # Entry point (loads dashboard) ``` ### Git Workflow diff --git a/project.godot b/project.godot index 267d5a9..588d898 100644 --- a/project.godot +++ b/project.godot @@ -1,30 +1,33 @@ -; V Panel — Godot Engine project configuration -; https://godotengine.org +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters config_version=5 [application] + config/name="V Panel" config/description="A fancy real-time status monitor built with Godot Engine." config/version="0.1.0" +run/main_scene="res://scenes/dashboard.tscn" config/features=PackedStringArray("4.6", "Forward Plus") -run/main_scene="res://main.tscn" config/icon="res://assets/icons/icon.svg" +[autoload] + +ConfigManager="*res://autoload/config_manager.gd" + [display] + window/size/viewport_width=1280 window/size/viewport_height=800 -window/size/mode=0 window/size/always_on_top=true -window/dpi/allow_hidpi=true window/stretch/mode="viewport" -window/stretch/aspect="keep" [rendering] -renderer/rendering_method="forward_plus" + renderer/rendering_method.mobile="forward_plus" - -[input] - -[autoload] -ConfigManager="*res://autoload/config_manager.gd" diff --git a/scenes/dashboard.gd b/scenes/dashboard.gd new file mode 100644 index 0000000..9688bc0 --- /dev/null +++ b/scenes/dashboard.gd @@ -0,0 +1,8 @@ +extends Control + + +@onready var grid: DashboardGrid = %DashboardGrid + + +func _ready() -> void: + get_window().mode = Window.MODE_FULLSCREEN diff --git a/scenes/dashboard.tscn b/scenes/dashboard.tscn new file mode 100644 index 0000000..efec138 --- /dev/null +++ b/scenes/dashboard.tscn @@ -0,0 +1,19 @@ +[gd_scene format=3 uid="uid://c3gq43o1aqy0b"] + +[ext_resource type="Script" path="res://scenes/dashboard.gd" id="1"] +[ext_resource type="Script" path="res://scripts/dashboard_grid.gd" id="2"] +[ext_resource type="Theme" path="res://themes/default_theme.tres" id="3"] + +[node name="Dashboard" type="Control"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +theme = ExtResource("3") +script = ExtResource("1") + +[node name="DashboardGrid" type="Control" parent="."] +unique_name_in_owner = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("2") diff --git a/scripts/dashboard_grid.gd b/scripts/dashboard_grid.gd new file mode 100644 index 0000000..d6487c0 --- /dev/null +++ b/scripts/dashboard_grid.gd @@ -0,0 +1,157 @@ +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 + add_child(cell) + _cells[row].append(cell) + + +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 := key.split(",") + var col := 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 := _cells[last_row].size() - 1 + _set_cell_child(_cells[last_row][last_col], module) + else: + module.queue_free() + + _module_positions.clear() diff --git a/scripts/module_base.gd b/scripts/module_base.gd new file mode 100644 index 0000000..dad4b76 --- /dev/null +++ b/scripts/module_base.gd @@ -0,0 +1,32 @@ +extends PanelBase +class_name ModuleBase + + +signal module_ready + +@export var module_title: String = "Module" + +var _initialized: bool = false + + +func _ready() -> void: + if not _initialized: + _initialize() + + +func initialize() -> void: + pass + + +func refresh(data: Dictionary) -> void: + pass + + +func get_module_title() -> String: + return module_title + + +func _initialize() -> void: + _initialized = true + initialize() + module_ready.emit()