add plugin system with plugin/layout managers

- plugin_manager: scans res://plugins/*/plugin.cfg, loads tile definitions
- plugin_tile: new base class for plugin tiles, extends ModuleBase
- layout_manager: save/restore tile grid positions per named layout
- migrate existing cpu/memory/testing tiles into system_monitor plugin
- add module_resized signal to DashboardGrid for auto-save
- dashboard reads from PluginManager + LayoutManager instead of hardcoded paths
- project.godot registers PluginManager and LayoutManager autoloads
- AGENTS.md updated with new structure and conventions
This commit is contained in:
Eric Smith 2026-05-21 08:39:15 -04:00
parent 63af41ea61
commit f43676e46c
19 changed files with 528 additions and 39 deletions

View file

@ -61,12 +61,22 @@ V Panel is a Godot Engine project that builds a fancy real-time status monitor.
- All percentage labels get a 3px black outline (`outline_size` / `font_outline_color`) for legibility against the liquid shader background. - All percentage labels get a 3px black outline (`outline_size` / `font_outline_color`) for legibility against the liquid shader background.
- Title labels get a 2px outline. - Title labels get a 2px outline.
### Plugin System
- `autoload/plugin_manager.gd` — autoload singleton, scans `res://plugins/*/plugin.cfg` at startup.
- Each plugin folder contains a `plugin.cfg` INI file with `[plugin]` metadata and `[tile_N]` entries defining tiles (id, name, scene path, min/max grid spans).
- Tile IDs are scoped as `{plugin_id}/{tile_id}` to guarantee global uniqueness.
- `PluginManager.instantiate_tile(tile_id)` loads the tile scene, instantiates it, and tags the root node with `tile_id` and `tile_config` metadata.
- `scripts/plugin_tile.gd` — class_name `PluginTile`, extends `ModuleBase`. Plugin tiles extend this instead of ModuleBase directly. Stores `tile_id` and `tile_config` properties, also written to node metadata for grid/layout access.
### Layout System
- `autoload/layout_manager.gd` — autoload singleton for saving/restoring tile grid positions.
- Layout files stored as INI in `res://config/layouts/{name}.cfg`. Each entry maps a `tile_id` to `{col, row, w, h}`.
- `save_layout()` writes the current in-memory layout to disk. `load_layout(name)` reads it back.
- `switch_layout(name)` saves current layout, loads the named one, emits signals.
- Current layout name persisted in `ConfigManager` as `layout_current`.
- Layout auto-saves on every `module_placed` and `module_resized` signal from `DashboardGrid`.
### Config System ### Config System
- `autoload/config_manager.gd` — autoload singleton using Godot's built-in `ConfigFile` class for INI-style config files.
- Two files: `res://config/default.cfg` (shipped defaults, tracked) and `res://config/config.cfg` (user overrides, gitignored). User overrides are merged on top of defaults per-key.
- Keys are flattened as `{section}_{key}` for simple `get_setting(key, default)` lookups (e.g., `[background]` + `color` => `"background_color"`).
- `get_color(key, default_color)` parses `"r, g, b"` string values from config into `Color`.
- `set_setting(key, value)` diffs against defaults and writes only the diff to `config.cfg` automatically.
### Splash Screen ### Splash Screen
- `scenes/splash.tscn` + `scenes/splash.gd` — entry point (set in `project.godot` as `main_scene`), handles fullscreen, font-size zoom animation, crossfade to dashboard. - `scenes/splash.tscn` + `scenes/splash.gd` — entry point (set in `project.godot` as `main_scene`), handles fullscreen, font-size zoom animation, crossfade to dashboard.
@ -83,15 +93,19 @@ res://
│ ├── icons/ │ ├── icons/
│ └── textures/ # noise_100.png (tileable Perlin noise for shader distortion) │ └── textures/ # noise_100.png (tileable Perlin noise for shader distortion)
├── autoload/ # Singleton/autoload scripts ├── autoload/ # Singleton/autoload scripts
│ └── config_manager.gd # INI config via ConfigFile │ ├── config_manager.gd # INI config via ConfigFile
│ ├── plugin_manager.gd # Plugin scanning, tile instantiation
│ └── layout_manager.gd # Layout save/load/switch
├── config/ # Configuration files (INI format) ├── config/ # Configuration files (INI format)
│ ├── default.cfg # Shipped defaults, tracked in git │ ├── default.cfg # Shipped defaults, tracked in git
│ └── config.cfg # User overrides, gitignored (auto-generated) │ ├── config.cfg # User overrides, gitignored (auto-generated)
├── panels/ # Individual status panels (modules) │ └── layouts/ # Saved layout files (*.cfg)
├── plugins/ # Plugin folders
│ └── system_monitor/ # Built-in system monitoring plugin
│ ├── plugin.cfg
│ └── tiles/
│ ├── cpu/ │ ├── cpu/
│ ├── memory/ │ ├── memory/
│ ├── network/
│ ├── disk/
│ └── testing/ │ └── testing/
├── scenes/ # Root scenes ├── scenes/ # Root scenes
│ ├── splash.tscn # Animated splash → dashboard transition │ ├── splash.tscn # Animated splash → dashboard transition
@ -100,6 +114,7 @@ res://
│ ├── dashboard_grid.gd # Responsive grid with drag, resize, preset popup │ ├── dashboard_grid.gd # Responsive grid with drag, resize, preset popup
│ ├── module_base.gd # Base class for all modules (mouse_filter IGNORE) │ ├── module_base.gd # Base class for all modules (mouse_filter IGNORE)
│ ├── panel_base.gd # Base class for panels │ ├── panel_base.gd # Base class for panels
│ ├── plugin_tile.gd # Base class for plugin tiles (extends ModuleBase)
│ └── shader_presets.gd # Shader preset definitions and apply function │ └── shader_presets.gd # Shader preset definitions and apply function
├── themes/ # Theme definitions and style resources ├── themes/ # Theme definitions and style resources
├── shaders/ # Custom shader materials ├── shaders/ # Custom shader materials

150
autoload/layout_manager.gd Normal file
View file

@ -0,0 +1,150 @@
extends Node
class_name LayoutManager
## Manages saved dashboard layouts.
##
## Layouts map tile_id -> {col, row, w, h}. The current layout is loaded
## on startup and auto-saved when modules are placed or resized.
## Supports multiple named layouts with a simple file-per-layout scheme.
signal layout_changed(layout_name: String)
signal layout_saved(layout_name: String)
signal layout_loaded(layout_name: String)
const LAYOUT_DIR: String = "res://config/layouts/"
## Current layout name (e.g. "default").
var current_layout_name: String = "default"
## The active layout data: tile_id -> {col, row, w, h}
var _layout: Dictionary = {}
func _ready() -> void:
if not DirAccess.dir_exists_absolute(LAYOUT_DIR):
DirAccess.make_dir_recursive_absolute(LAYOUT_DIR)
var configured_name: String = _get_configured_layout()
if not configured_name.is_empty():
current_layout_name = configured_name
if not load_layout(current_layout_name):
current_layout_name = "default"
_layout.clear()
## Return the layout name from config, or empty.
func _get_configured_layout() -> String:
if ConfigManager.has_method("get_setting"):
var val: Variant = ConfigManager.get_setting("layout_current", "")
if val is String and not val.is_empty():
return val
return ""
## Set the current layout name (does not load or save).
func set_current_layout(name: String) -> void:
current_layout_name = name
if ConfigManager.has_method("set_setting"):
ConfigManager.set_setting("layout_current", name)
layout_changed.emit(name)
## Get tile_id -> {col, row, w, h} for the current layout.
func get_layout() -> Dictionary:
return _layout.duplicate(true)
## Set a single tile's position in the current layout.
func set_tile_position(tile_id: String, col: int, row: int, w: int, h: int) -> void:
_layout[tile_id] = {col = col, row = row, w = w, h = h}
## Remove a tile from the current layout.
func remove_tile(tile_id: String) -> void:
_layout.erase(tile_id)
## Save the current layout to config/layouts/{name}.cfg
func save_layout(name: String = "") -> void:
if name.is_empty():
name = current_layout_name
var cfg := ConfigFile.new()
cfg.set_value("layout", "name", name)
cfg.set_value("layout", "saved_at", Time.get_datetime_string_from_system())
var tile_ids: Array = _layout.keys()
cfg.set_value("tiles", "count", tile_ids.size())
for i in tile_ids.size():
var tid: String = tile_ids[i] as String
var entry: Dictionary = _layout[tid]
var section := "tile_%d" % i
cfg.set_value(section, "id", tid)
cfg.set_value(section, "col", entry.get("col", 0))
cfg.set_value(section, "row", entry.get("row", 0))
cfg.set_value(section, "w", entry.get("w", 1))
cfg.set_value(section, "h", entry.get("h", 1))
var path := LAYOUT_DIR.path_join(name + ".cfg")
var result := cfg.save(path)
if result == OK:
current_layout_name = name
layout_saved.emit(name)
## Load a saved layout, replacing the current in-memory layout.
## Returns true if the file existed and was parsed.
func load_layout(name: String) -> bool:
var path := LAYOUT_DIR.path_join(name + ".cfg")
if not FileAccess.file_exists(path):
return false
var cfg := ConfigFile.new()
if cfg.load(path) != OK:
return false
_layout.clear()
current_layout_name = name
var tile_count: int = cfg.get_value("tiles", "count", 0)
for i in range(tile_count):
var section := "tile_%d" % i
var tid: String = cfg.get_value(section, "id", "")
if tid.is_empty():
continue
_layout[tid] = {
col = cfg.get_value(section, "col", 0),
row = cfg.get_value(section, "row", 0),
w = cfg.get_value(section, "w", 1),
h = cfg.get_value(section, "h", 1),
}
layout_loaded.emit(name)
return true
## Get a list of all saved layout names.
func get_layout_names() -> PackedStringArray:
var dir := DirAccess.open(LAYOUT_DIR)
if dir == null:
return PackedStringArray()
var names: PackedStringArray = []
dir.list_dir_begin()
var entry: String = dir.get_next()
while entry != "":
if entry.ends_with(".cfg") and not entry.begins_with("."):
names.append(entry.trim_suffix(".cfg"))
entry = dir.get_next()
dir.list_dir_end()
return names
## Switch to a different layout: saves current, loads new, emits signal.
func switch_layout(name: String) -> void:
if name == current_layout_name:
return
save_layout(current_layout_name)
load_layout(name)
set_current_layout(name)

154
autoload/plugin_manager.gd Normal file
View file

@ -0,0 +1,154 @@
extends Node
class_name PluginManager
## Scans res://plugins/ for plugin manifests (plugin.cfg) and provides
## access to tile definitions. Instantiate tiles by tile_id.
signal plugin_loaded(plugin_id: String, plugin_name: String)
signal plugin_load_failed(plugin_id: String, reason: String)
## All loaded plugin data: plugin_id -> Dictionary
var _plugins: Dictionary = {}
## All tile definitions: tile_id -> Dictionary
var _tiles: Dictionary = {}
## Plugin-id -> Array[tile_id] for reverse lookup
var _plugin_tiles: Dictionary = {}
func _ready() -> void:
_scan_plugins()
## Scan res://plugins/*/plugin.cfg and load all manifests.
func _scan_plugins() -> void:
var dir := DirAccess.open("res://plugins")
if dir == null:
return
dir.list_dir_begin()
var entry: String = dir.get_next()
while entry != "":
if entry.begins_with("."):
entry = dir.get_next()
continue
if dir.current_is_dir():
var manifest_path := "res://plugins/%s/plugin.cfg" % entry
if FileAccess.file_exists(manifest_path):
_load_manifest(entry, manifest_path)
else:
plugin_load_failed.emit(entry, "Missing plugin.cfg")
entry = dir.get_next()
dir.list_dir_end()
func _load_manifest(plugin_id: String, path: String) -> void:
var cfg := ConfigFile.new()
if cfg.load(path) != OK:
plugin_load_failed.emit(plugin_id, "Failed to parse plugin.cfg")
return
var plugin_info := {
id = plugin_id,
name = cfg.get_value("plugin", "name", plugin_id),
version = cfg.get_value("plugin", "version", "0.1.0"),
description = cfg.get_value("plugin", "description", ""),
author = cfg.get_value("plugin", "author", ""),
base_path = path.get_base_dir(),
}
_plugins[plugin_id] = plugin_info
# Load tile definitions
var tile_count: int = cfg.get_value("tiles", "count", 0)
for i in range(tile_count):
var section := "tile_%d" % i
var tile_id: String = cfg.get_value(section, "id", "")
if tile_id.is_empty():
continue
# Scoped tile-id: "plugin_id/tile_id" ensures global uniqueness
var scoped_id := "%s/%s" % [plugin_id, tile_id]
if _tiles.has(scoped_id):
plugin_load_failed.emit(plugin_id, "Duplicate tile id: %s" % scoped_id)
continue
var tile_def := {
id = scoped_id,
plugin_id = plugin_id,
name = cfg.get_value(section, "name", tile_id),
scene = cfg.get_value(section, "scene", ""),
min_w = cfg.get_value(section, "min_w", 1),
min_h = cfg.get_value(section, "min_h", 1),
max_w = cfg.get_value(section, "max_w", 4),
max_h = cfg.get_value(section, "max_h", 4),
default_w = cfg.get_value(section, "default_w", 1),
default_h = cfg.get_value(section, "default_h", 1),
}
_tiles[scoped_id] = tile_def
if not _plugin_tiles.has(plugin_id):
_plugin_tiles[plugin_id] = []
_plugin_tiles[plugin_id].append(scoped_id)
plugin_loaded.emit(plugin_id, plugin_info.name)
## Returns a list of all loaded plugin IDs.
func get_plugin_ids() -> PackedStringArray:
var result: PackedStringArray = []
for pid in _plugins:
result.append(pid)
return result
## Returns plugin info Dictionary for the given plugin_id, or null.
func get_plugin(plugin_id: String) -> Dictionary:
return _plugins.get(plugin_id, {})
## Returns all tile definitions across all plugins.
func get_all_tile_defs() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for tid in _tiles:
result.append(_tiles[tid])
return result
## Returns a single tile definition by scoped tile_id, or null.
func get_tile_def(tile_id: String) -> Dictionary:
return _tiles.get(tile_id, {})
## Returns tile IDs belonging to a specific plugin.
func get_plugin_tile_ids(plugin_id: String) -> PackedStringArray:
var result: PackedStringArray = []
for tid in _plugin_tiles.get(plugin_id, []):
result.append(tid)
return result
## Instantiate a tile by its scoped tile_id.
## Returns the root Control node, or null on failure.
func instantiate_tile(tile_id: String) -> Control:
var def: Dictionary = _tiles.get(tile_id, {})
if def.is_empty():
return null
var scene_path: String = def.get("scene", "")
if scene_path.is_empty():
return null
var scene := load(scene_path) as PackedScene
if scene == null:
return null
var instance: Control = scene.instantiate() as Control
if instance == null:
return null
# Tag the instance so the layout manager can correlate it
if instance.has_method("set_tile_id"):
instance.set_tile_id(tile_id)
if instance.has_method("set_tile_config"):
instance.set_tile_config(def)
return instance

View file

@ -12,3 +12,6 @@ refresh_interval=1.0
[general] [general]
theme=default theme=default
[layout]
current="default"

0
plugins/.gitkeep Normal file
View file

View file

@ -0,0 +1,41 @@
[plugin]
name="System Monitor"
version="1.0.0"
description="Monitors system resources: CPU and memory usage"
author="Fifthdread"
[tiles]
count=3
[tile_0]
id="cpu"
name="CPU Monitor"
scene="res://plugins/system_monitor/tiles/cpu/cpu_tile.tscn"
min_w=1
min_h=1
max_w=4
max_h=4
default_w=1
default_h=1
[tile_1]
id="memory"
name="Memory Monitor"
scene="res://plugins/system_monitor/tiles/memory/memory_tile.tscn"
min_w=1
min_h=1
max_w=4
max_h=4
default_w=1
default_h=1
[tile_2]
id="testing"
name="Testing"
scene="res://plugins/system_monitor/tiles/testing/testing_tile.tscn"
min_w=1
min_h=1
max_w=4
max_h=4
default_w=1
default_h=1

View file

@ -1,5 +1,4 @@
extends ModuleBase extends PluginTile
class_name CpuModule
@onready var title_label: Label = %Title @onready var title_label: Label = %Title

View file

@ -1,9 +1,9 @@
[gd_scene format=3 uid="uid://bq3bs2hb4r7fb"] [gd_scene format=3 uid="uid://bq3bs2hb4r7fb"]
[ext_resource type="Script" path="res://panels/cpu/cpu_module.gd" id="1"] [ext_resource type="Script" path="res://plugins/system_monitor/tiles/cpu/cpu_tile.gd" id="1"]
[ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"] [ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"]
[node name="CpuModule" type="PanelContainer"] [node name="CpuTile" type="PanelContainer"]
anchors_preset = 0 anchors_preset = 0
script = ExtResource("1") script = ExtResource("1")

View file

@ -1,5 +1,4 @@
extends ModuleBase extends PluginTile
class_name MemoryModule
@onready var title_label: Label = %Title @onready var title_label: Label = %Title

View file

@ -1,9 +1,9 @@
[gd_scene format=3 uid="uid://d2d4uqrd2hh3d"] [gd_scene format=3 uid="uid://d2d4uqrd2hh3d"]
[ext_resource type="Script" path="res://panels/memory/memory_module.gd" id="1"] [ext_resource type="Script" path="res://plugins/system_monitor/tiles/memory/memory_tile.gd" id="1"]
[ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"] [ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"]
[node name="MemoryModule" type="PanelContainer"] [node name="MemoryTile" type="PanelContainer"]
anchors_preset = 0 anchors_preset = 0
script = ExtResource("1") script = ExtResource("1")

View file

@ -1,5 +1,4 @@
extends ModuleBase extends PluginTile
class_name TestingModule
@onready var title_label: Label = %Title @onready var title_label: Label = %Title

View file

@ -1,9 +1,9 @@
[gd_scene format=3] [gd_scene format=3]
[ext_resource type="Script" path="res://panels/testing/testing_module.gd" id="1"] [ext_resource type="Script" path="res://plugins/system_monitor/tiles/testing/testing_tile.gd" id="1"]
[ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"] [ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"]
[node name="TestingModule" type="PanelContainer"] [node name="TestingTile" type="PanelContainer"]
anchors_preset = 0 anchors_preset = 0
script = ExtResource("1") script = ExtResource("1")

View file

@ -20,6 +20,8 @@ config/icon="res://assets/icons/icon.svg"
[autoload] [autoload]
ConfigManager="*res://autoload/config_manager.gd" ConfigManager="*res://autoload/config_manager.gd"
PluginManager="*res://autoload/plugin_manager.gd"
LayoutManager="*res://autoload/layout_manager.gd"
[display] [display]

View file

@ -4,13 +4,13 @@ extends PanelContainer
@onready var grid: DashboardGrid = %DashboardGrid @onready var grid: DashboardGrid = %DashboardGrid
var _modules: Array = [] var _tile_instances: Array[Control] = []
func _ready() -> void: func _ready() -> void:
_set_background() _set_background()
if not Engine.is_editor_hint(): if not Engine.is_editor_hint():
_add_modules() _load_tiles_from_plugins()
func _set_background() -> void: func _set_background() -> void:
@ -24,28 +24,124 @@ func _set_background() -> void:
add_theme_stylebox_override("panel", bg) add_theme_stylebox_override("panel", bg)
func _add_modules() -> void: func _load_tiles_from_plugins() -> void:
var cpu := preload("res://panels/cpu/cpu_module.tscn").instantiate() if not PluginManager.has_method("get_all_tile_defs"):
grid.place_module(cpu, 0, 0) return
_modules.append(cpu)
var mem := preload("res://panels/memory/memory_module.tscn").instantiate() var all_tile_defs: Array[Dictionary] = PluginManager.get_all_tile_defs()
grid.place_module(mem, 1, 0) if all_tile_defs.is_empty():
_modules.append(mem) return
var test := preload("res://panels/testing/testing_module.tscn").instantiate() # Load saved layout (if any)
grid.place_module(test, 2, 0) var layout_data: Dictionary = {}
_modules.append(test) if LayoutManager.has_method("get_layout"):
layout_data = LayoutManager.get_layout()
# Track which tile_defs we've placed
var placed_ids: Array[String] = []
# First pass: place tiles that have a saved layout position
for tid in layout_data:
var pos: Dictionary = layout_data[tid]
var def := _find_tile_def(all_tile_defs, tid)
if def.is_empty():
continue
var instance := PluginManager.instantiate_tile(tid)
if instance == null:
continue
grid.place_module(instance, pos.col, pos.row, pos.w, pos.h)
_tile_instances.append(instance)
placed_ids.append(tid)
# Second pass: place any remaining tile_defs at default positions
# Find the next free grid column
var next_col: int = 0
var next_row: int = 0
if not placed_ids.is_empty():
var gs := grid.get_grid_size()
next_col = gs.x
next_row = 0
for def in all_tile_defs:
if def.id in placed_ids:
continue
var instance := PluginManager.instantiate_tile(def.id)
if instance == null:
continue
grid.place_module(instance, next_col, next_row, def.default_w, def.default_h)
_tile_instances.append(instance)
placed_ids.append(def.id)
# Bump default insertion point
next_col += def.default_w
var gs := grid.get_grid_size()
if next_col >= gs.x and gs.x > 0:
next_col = 0
next_row = gs.y
# Connect auto-save signals
grid.module_placed.connect(_save_layout)
if grid.has_signal("module_resized"):
grid.module_resized.connect(_save_layout)
# Start refresh timer
_start_refresh_timer()
func _start_refresh_timer() -> void:
var interval: float = 1.0
if ConfigManager.has_method("get_setting"):
var val: Variant = ConfigManager.get_setting("performance_refresh_interval", 1.0)
if typeof(val) == TYPE_FLOAT or typeof(val) == TYPE_INT:
interval = float(val)
var timer := Timer.new() var timer := Timer.new()
timer.timeout.connect(_refresh_modules) timer.timeout.connect(_refresh_modules)
timer.autostart = true timer.autostart = true
timer.wait_time = 1.0 timer.wait_time = interval
add_child(timer) add_child(timer)
func _refresh_modules() -> void: func _refresh_modules() -> void:
var data: Dictionary = {} var data: Dictionary = {}
for mod in _modules: for mod in _tile_instances:
if is_instance_valid(mod) and mod.has_method("refresh"): if is_instance_valid(mod) and mod.has_method("refresh"):
mod.refresh(data) mod.refresh(data)
func _save_layout(_mod: Control = null, _a: int = 0, _b: int = 0) -> void:
if not LayoutManager.has_method("set_tile_position"):
return
# Walk all modules in the grid and store their positions
var gs := grid.get_grid_size()
for r in range(gs.y):
for c in range(gs.x):
var mod := grid.get_module_at(c, r)
if mod == null:
continue
var tid: String = mod.get_meta("tile_id", "")
if tid.is_empty():
continue
var d := _get_module_grid_data(mod)
LayoutManager.set_tile_position(tid, d.col, d.row, d.w, d.h)
LayoutManager.save_layout()
## Mirror of DashboardGrid._get_module_grid_data for layout saving.
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),
}
## Find a tile def by scoped tile_id.
func _find_tile_def(defs: Array[Dictionary], tile_id: String) -> Dictionary:
for d in defs:
if d.get("id", "") == tile_id:
return d
return {}

View file

@ -5,6 +5,7 @@ class_name DashboardGrid
signal module_placed(module: Control, col: int, row: int) signal module_placed(module: Control, col: int, row: int)
signal module_removed(module: Control) signal module_removed(module: Control)
signal module_resized(module: Control, col: int, row: int, w: int, h: int)
@export var cell_min_size: Vector2 = Vector2(300, 240) @export var cell_min_size: Vector2 = Vector2(300, 240)
@export var cell_spacing: float = 8.0 @export var cell_spacing: float = 8.0
@ -161,6 +162,12 @@ func remove_module(module: Control) -> void:
_layout_cells() _layout_cells()
func get_module_at(col: int, row: int) -> Control:
if not _is_valid_cell(col, row):
return null
return _grid[row][col] as Control
func get_grid_size() -> Vector2i: func get_grid_size() -> Vector2i:
return Vector2i(columns, rows) return Vector2i(columns, rows)
@ -421,6 +428,7 @@ func _end_resize(_mouse_pos: Vector2) -> void:
_set_module_grid_data(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h) _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) _occupy_cells(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
_layout_cells() _layout_cells()
module_resized.emit(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
_is_resizing = false _is_resizing = false
_resize_module = null _resize_module = null

23
scripts/plugin_tile.gd Normal file
View file

@ -0,0 +1,23 @@
extends ModuleBase
class_name PluginTile
## Base class for all plugin tiles.
##
## Extends ModuleBase with plugin-aware metadata so the system can
## correlate tile instances with their definitions and layouts.
var tile_id: String = ""
var tile_config: Dictionary = {}
## Called by PluginManager.instantiate_tile() to tag this instance.
func set_tile_id(value: String) -> void:
tile_id = value
## Called by PluginManager.instantiate_tile() to pass the tile definition.
func set_tile_config(config: Dictionary) -> void:
tile_config = config
# Store in meta so grid/layout can read it on any Control
set_meta("tile_id", tile_id)
set_meta("tile_config", config)