Capture the direction change from M3-b: UI screens are authored editor-first as .tscn node trees (previewable/arrangeable in the Godot editor), scripts do on-load work only — never build the tree in _ready(). Add ADR 0001, a "UI scene conventions" how-to in client/docs, a §16 mandate in the charter, and mark M3-a/M3-b done in the roadmap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
4.5 KiB
Markdown
87 lines
4.5 KiB
Markdown
# 0001 — UI screens are editor-authored scenes, not code-built
|
||
|
||
- **Status:** accepted
|
||
- **Date:** 2026-07-11 (M3-b, Main Window shell)
|
||
|
||
## Context
|
||
|
||
The M3-b Main Window shell was first built by constructing every Control node in
|
||
`_ready()` (a pattern borrowed from `theme_showcase.gd`, where it was fine because
|
||
the showcase is a throwaway proof). For a real screen this was wrong:
|
||
|
||
- **It does not preview in the Godot editor.** Opening the `.tscn` showed a bare
|
||
root Control — every widget only existed at runtime — so the human could not see
|
||
or arrange the layout in the editor. Tuning meant editing pixel offsets in code,
|
||
running, eyeballing, repeating.
|
||
- It buried layout, styling, and content in GDScript, where it is verbose and
|
||
hard to reason about, and it drifted from charter §16's own words: *"each screen
|
||
is recreated as a Godot 4.7 Control-node scene."*
|
||
|
||
The human asked to be able to arrange the scene in the editor "as much as
|
||
possible," with code only for genuine on-load work.
|
||
|
||
## Decision
|
||
|
||
**UI screens (and their sub-scenes) are authored editor-first as `.tscn` node
|
||
trees.** The split:
|
||
|
||
- **In the `.tscn` (editor):** the whole node tree — containers, panels, labels,
|
||
buttons, art-slot placeholders — with the shared `Theme` assigned on the scene
|
||
root and every `theme_type_variation` set on its node. Authored placeholder
|
||
text/values so the screen previews with representative content. Sub-screens are
|
||
**instanced** (`ExtResource` PackedScene), not rebuilt.
|
||
- **In the script (on-load only):** `@onready` references to named nodes; wiring
|
||
(signals, the DM loop, injected services); animations (e.g. the dock slide);
|
||
and **binding state into the authored nodes** (setting label text/colours from
|
||
the model). Never construct the tree in `_ready()`.
|
||
- **Data-driven repeats** (turn-order tokens, consumable slots, later inventory
|
||
grids) are a **fixed set of authored nodes** bound from state at runtime —
|
||
spare slots hide. This keeps them previewable; combat/inventory become the
|
||
writers later. (Author enough slots for the common case; if a screen ever needs
|
||
a genuinely unbounded list, revisit with a code-instanced container then.)
|
||
|
||
Supporting conventions established alongside this decision:
|
||
|
||
- **Scene root sizing.** Give the `.tscn` root a fixed size (e.g. 1920×1080) via
|
||
offsets so it previews, but **no full-rect anchors on the root** — a root
|
||
Control run via F6 is not sized by full-rect anchors and collapses to (0,0).
|
||
The script keeps `size = get_viewport_rect().size` on `_ready` +
|
||
`size_changed` (the M3-a lesson).
|
||
- **Button styling uses Button-base theme variations.** Never apply a
|
||
PanelContainer-base variation (`DarkPanel`, `ParchmentCard`) to a `Button` — a
|
||
Button looks up `normal/hover/pressed`, which a `panel`-only variation lacks, so
|
||
it renders unskinned. M3-b added Button-base `DockButton` and `ParchmentButton`
|
||
to the theme for exactly this.
|
||
- **RichTextLabel prose reads its style from the theme.** `RichTextLabel` does
|
||
**not** inherit `default_font`/`default_color`, so without a theme entry its
|
||
text renders near-white (unreadable on parchment). The theme now sets a base
|
||
`RichTextLabel` style (dark `INK_BODY` serif + italic face). All colours still
|
||
come from `Palette` via the theme builder — no hex in scripts or `.tscn`.
|
||
|
||
## Consequences
|
||
|
||
**Easier:**
|
||
|
||
- The human arranges any screen directly in the Godot editor; layout persists in
|
||
the `.tscn`. Fine-tuning is drag-and-drop, not code-run-eyeball.
|
||
- Screens preview with themed, representative content on open.
|
||
- Scripts shrink to logic; less code, easier to review, focused files.
|
||
- The look stays one system (theme assigned per scene root).
|
||
|
||
**Caveats / harder:**
|
||
|
||
- **Shader surfaces render only at runtime.** `DarkBay`/`VignetteOverlay` set
|
||
their `ShaderMaterial` in `_ready()` (M3-a accepted deviation), so their
|
||
texture appears only on an F6 run — the editor shows layout but a plain panel
|
||
behind it. Layout still previews fine.
|
||
- Tests reference nodes by their authored `@onready` paths; renaming/moving a
|
||
node in the editor means updating the path (and any test that reads it).
|
||
- The visual gate stays a **human F6 eyeball** — headless tests prove structure
|
||
(nodes mount, sizes are non-zero, bindings run) but not rendering.
|
||
|
||
## Reference implementation
|
||
|
||
M3-b shell scenes: `client/scenes/shell/{MainWindowShell,SystemDock,NarrationBook}.tscn`
|
||
with their `client/scripts/ui/shell/*.gd` scripts. Practical how-to lives in
|
||
[`client/docs/README.md`](../../client/docs/README.md) → "UI scene conventions."
|