4-task TDD plan for Sub-phase A: UiRoot named-window registry (Show/Hide/Toggle/BringToFront) + raise-on-click + F12 wiring of the existing ToggleInventoryPanel action to a throwaway placeholder inventory window that Sub-phase B replaces. Divergence handled by extending IA-12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
472 lines
19 KiB
Markdown
472 lines
19 KiB
Markdown
# D.2b Window Manager Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Add open/close/raise for top-level retail-UI windows and toggle a (placeholder) inventory window with F12.
|
||
|
||
**Architecture:** A named-window registry on `UiRoot` (`RegisterWindow` / `ShowWindow` / `HideWindow` / `ToggleWindow` / `BringToFront`) — `UiRoot` already owns the top-level children, `ZOrder`, hit-test, and focus, so window show/hide/raise is its job. `Show`/`Hide` flip `UiElement.Visible` (which already gates Draw/Tick/HitTest); `BringToFront` bumps `ZOrder` above peers. The existing F12-bound `InputAction.ToggleInventoryPanel` is wired through `OnInputAction` to toggle a throwaway placeholder window that Sub-phase B replaces with the real `gmInventoryUI`.
|
||
|
||
**Tech Stack:** C# .NET 10, xUnit, the in-tree `AcDream.App.UI` retained-mode toolkit (`UiRoot`/`UiElement`/`UiHost`/`UiNineSlicePanel`), the `InputDispatcher` input pipeline.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-06-20-d2b-window-manager-design.md`
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
- **Create** `src/AcDream.App/UI/WindowNames.cs` — canonical registry-name constants (one literal shared by mount, registry, toggle).
|
||
- **Modify** `src/AcDream.App/UI/UiRoot.cs` — add the window registry + `BringToFront`; add raise-on-click in `OnMouseDown`.
|
||
- **Modify** `src/AcDream.App/UI/UiHost.cs` — two forwarders (`RegisterWindow`, `ToggleWindow`) delegating to `Root`.
|
||
- **Modify** `src/AcDream.App/Rendering/GameWindow.cs` — mount the placeholder inventory window (default-hidden) in the `_options.RetailUi` block; add the `ToggleInventoryPanel` case in `OnInputAction`.
|
||
- **Modify** `tests/AcDream.App.Tests/UI/UiRootInputTests.cs` — registry + z-order + raise-on-click unit tests.
|
||
- **Modify** `docs/architecture/retail-divergence-register.md` — extend IA-12 "Where" to cite the window manager.
|
||
|
||
---
|
||
|
||
## Task 1: `WindowNames` + `UiRoot` window registry (visibility)
|
||
|
||
**Files:**
|
||
- Create: `src/AcDream.App/UI/WindowNames.cs`
|
||
- Modify: `src/AcDream.App/UI/UiRoot.cs` (insert after `ReleaseCapture`, ~line 475)
|
||
- Test: `tests/AcDream.App.Tests/UI/UiRootInputTests.cs`
|
||
|
||
- [ ] **Step 1: Create the `WindowNames` constant holder**
|
||
|
||
Create `src/AcDream.App/UI/WindowNames.cs`:
|
||
|
||
```csharp
|
||
namespace AcDream.App.UI;
|
||
|
||
/// <summary>Canonical registry names for top-level retail-UI windows, so the
|
||
/// mount, the window registry, and the toggle keybind all agree on one literal.</summary>
|
||
public static class WindowNames
|
||
{
|
||
public const string Inventory = "inventory";
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Write the failing tests**
|
||
|
||
Append to `tests/AcDream.App.Tests/UI/UiRootInputTests.cs`, inside the class (before the final closing `}`):
|
||
|
||
```csharp
|
||
[Fact]
|
||
public void ToggleWindow_FlipsVisible_AndReturnsNewState()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var win = new UiPanel { Width = 100, Height = 100, Visible = false };
|
||
root.AddChild(win);
|
||
root.RegisterWindow("inventory", win);
|
||
|
||
Assert.True(root.ToggleWindow("inventory")); // hidden -> shown
|
||
Assert.True(win.Visible);
|
||
Assert.False(root.ToggleWindow("inventory")); // shown -> hidden
|
||
Assert.False(win.Visible);
|
||
}
|
||
|
||
[Fact]
|
||
public void ShowHideWindow_SetVisibility_UnknownNameIsNoOp()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var win = new UiPanel { Width = 100, Height = 100, Visible = false };
|
||
root.AddChild(win);
|
||
root.RegisterWindow("inventory", win);
|
||
|
||
Assert.True(root.ShowWindow("inventory"));
|
||
Assert.True(win.Visible);
|
||
Assert.True(root.HideWindow("inventory"));
|
||
Assert.False(win.Visible);
|
||
|
||
Assert.False(root.ShowWindow("nope"));
|
||
Assert.False(root.HideWindow("nope"));
|
||
Assert.False(root.ToggleWindow("nope"));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Run the tests to verify they fail**
|
||
|
||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiRootInputTests"`
|
||
Expected: FAIL — compile error, `'UiRoot' does not contain a definition for 'RegisterWindow'`.
|
||
|
||
- [ ] **Step 4: Implement the registry**
|
||
|
||
In `src/AcDream.App/UI/UiRoot.cs`, find:
|
||
|
||
```csharp
|
||
public void SetCapture(UiElement e) => Captured = e;
|
||
public void ReleaseCapture() => Captured = null;
|
||
```
|
||
|
||
Insert immediately after it:
|
||
|
||
```csharp
|
||
|
||
// ── Window manager (named top-level windows: Show / Hide / Toggle) ───
|
||
|
||
private readonly Dictionary<string, UiElement> _windows = new();
|
||
|
||
/// <summary>Register a top-level window under a name for Show/Hide/Toggle.
|
||
/// Does NOT add it to the tree — the caller mounts via AddChild and controls
|
||
/// initial Visible. Idempotent (re-register replaces). The dict is the source
|
||
/// of truth — independent of UiElement.Name (init-only, not set here).</summary>
|
||
public void RegisterWindow(string name, UiElement window) => _windows[name] = window;
|
||
|
||
/// <summary>Make the named window visible. No-op (returns false) if unknown.</summary>
|
||
public bool ShowWindow(string name)
|
||
{
|
||
if (!_windows.TryGetValue(name, out var w)) return false;
|
||
w.Visible = true;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Hide the named window. No-op (returns false) if unknown.</summary>
|
||
public bool HideWindow(string name)
|
||
{
|
||
if (!_windows.TryGetValue(name, out var w)) return false;
|
||
w.Visible = false;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Flip the named window's visibility (Show if hidden, Hide if shown).
|
||
/// Returns the new IsVisible state (false for an unknown name).</summary>
|
||
public bool ToggleWindow(string name)
|
||
{
|
||
if (!_windows.TryGetValue(name, out var w)) return false;
|
||
if (w.Visible) { HideWindow(name); return false; }
|
||
ShowWindow(name);
|
||
return true;
|
||
}
|
||
```
|
||
|
||
(`using System.Collections.Generic;` is already present at the top of `UiRoot.cs`.)
|
||
|
||
- [ ] **Step 5: Run the tests to verify they pass**
|
||
|
||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiRootInputTests"`
|
||
Expected: PASS (all `UiRootInputTests`, including the two new ones).
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.App/UI/WindowNames.cs src/AcDream.App/UI/UiRoot.cs tests/AcDream.App.Tests/UI/UiRootInputTests.cs
|
||
git commit -m "feat(ui): D.2b-A — UiRoot named-window registry (Show/Hide/Toggle)
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: `BringToFront` + raise-on-show + raise-on-click
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.App/UI/UiRoot.cs` (add `BringToFront`; edit `ShowWindow` + `OnMouseDown`)
|
||
- Test: `tests/AcDream.App.Tests/UI/UiRootInputTests.cs`
|
||
|
||
- [ ] **Step 1: Write the failing tests**
|
||
|
||
Append to `tests/AcDream.App.Tests/UI/UiRootInputTests.cs`, inside the class:
|
||
|
||
```csharp
|
||
[Fact]
|
||
public void ShowWindow_RaisesAbovePeers()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var a = new UiPanel { Width = 100, Height = 100 };
|
||
var b = new UiPanel { Width = 100, Height = 100 };
|
||
var win = new UiPanel { Width = 100, Height = 100, Visible = false };
|
||
root.AddChild(a); root.AddChild(b); root.AddChild(win);
|
||
root.RegisterWindow("inventory", win);
|
||
|
||
root.ShowWindow("inventory");
|
||
Assert.True(win.ZOrder > a.ZOrder);
|
||
Assert.True(win.ZOrder > b.ZOrder);
|
||
}
|
||
|
||
[Fact]
|
||
public void BringToFront_SetsStrictlyGreatestZOrder()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
var a = new UiPanel { Width = 100, Height = 100, ZOrder = 5 };
|
||
var b = new UiPanel { Width = 100, Height = 100, ZOrder = 9 };
|
||
root.AddChild(a); root.AddChild(b);
|
||
|
||
root.BringToFront(a);
|
||
Assert.True(a.ZOrder > b.ZOrder); // 10 > 9
|
||
}
|
||
|
||
[Fact]
|
||
public void MouseDown_OnWindow_RaisesItAbovePeers()
|
||
{
|
||
var root = new UiRoot { Width = 800, Height = 600 };
|
||
// 'peer' has a higher ZOrder but does NOT cover (50,50); 'clicked' does.
|
||
var peer = new UiPanel { Left = 300, Top = 0, Width = 100, Height = 100, ZOrder = 9 };
|
||
var clicked = new UiPanel { Left = 0, Top = 0, Width = 100, Height = 100, ZOrder = 0, Draggable = true };
|
||
root.AddChild(peer); root.AddChild(clicked);
|
||
|
||
root.OnMouseDown(UiMouseButton.Left, 50, 50); // only 'clicked' occupies (50,50)
|
||
Assert.True(clicked.ZOrder > peer.ZOrder);
|
||
root.OnMouseUp(UiMouseButton.Left, 50, 50);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the tests to verify they fail**
|
||
|
||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiRootInputTests"`
|
||
Expected: FAIL — compile error, `'UiRoot' does not contain a definition for 'BringToFront'`.
|
||
|
||
- [ ] **Step 3: Add `BringToFront`**
|
||
|
||
In `src/AcDream.App/UI/UiRoot.cs`, find the end of `ToggleWindow` (added in Task 1):
|
||
|
||
```csharp
|
||
if (w.Visible) { HideWindow(name); return false; }
|
||
ShowWindow(name);
|
||
return true;
|
||
}
|
||
```
|
||
|
||
Insert immediately after it:
|
||
|
||
```csharp
|
||
|
||
/// <summary>Raise a top-level window above its siblings by setting its ZOrder
|
||
/// one past the current max among the OTHER top-level children. Used on Show
|
||
/// and on click. Leaves ZOrder unchanged if it is the only / already-topmost child.</summary>
|
||
public void BringToFront(UiElement window)
|
||
{
|
||
int top = window.ZOrder;
|
||
foreach (var c in Children)
|
||
if (!ReferenceEquals(c, window))
|
||
top = System.Math.Max(top, c.ZOrder + 1);
|
||
window.ZOrder = top;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Make `ShowWindow` raise**
|
||
|
||
In `src/AcDream.App/UI/UiRoot.cs`, find (inside `ShowWindow`):
|
||
|
||
```csharp
|
||
if (!_windows.TryGetValue(name, out var w)) return false;
|
||
w.Visible = true;
|
||
return true;
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```csharp
|
||
if (!_windows.TryGetValue(name, out var w)) return false;
|
||
w.Visible = true;
|
||
BringToFront(w);
|
||
return true;
|
||
```
|
||
|
||
- [ ] **Step 5: Add raise-on-click in `OnMouseDown`**
|
||
|
||
In `src/AcDream.App/UI/UiRoot.cs`, find (in `OnMouseDown`):
|
||
|
||
```csharp
|
||
var window = FindWindow(target);
|
||
if (btn == UiMouseButton.Left && window is not null)
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```csharp
|
||
var window = FindWindow(target);
|
||
// Retail-faithful: pressing on a window raises it above its peers.
|
||
if (window is not null) BringToFront(window);
|
||
if (btn == UiMouseButton.Left && window is not null)
|
||
```
|
||
|
||
- [ ] **Step 6: Run the tests to verify they pass**
|
||
|
||
Run: `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter "FullyQualifiedName~UiRootInputTests"`
|
||
Expected: PASS — all `UiRootInputTests` (the new three plus the existing window-drag/resize tests, which still pass: raise-on-click only changes `ZOrder`, not geometry).
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.App/UI/UiRoot.cs tests/AcDream.App.Tests/UI/UiRootInputTests.cs
|
||
git commit -m "feat(ui): D.2b-A — BringToFront + raise on show/click
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: `UiHost` forwarders + `GameWindow` wiring (placeholder + F12)
|
||
|
||
No unit tests — these sites are GL-coupled (`UiHost` needs a `GL`) and only run with `ACDREAM_RETAIL_UI=1`; they are verified by build + the visual check in Task 4.
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.App/UI/UiHost.cs` (add forwarders before `Dispose`)
|
||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (placeholder mount in the `_options.RetailUi` block; `OnInputAction` case)
|
||
|
||
- [ ] **Step 1: Add `UiHost` forwarders**
|
||
|
||
In `src/AcDream.App/UI/UiHost.cs`, find:
|
||
|
||
```csharp
|
||
public void Dispose()
|
||
{
|
||
TextRenderer.Dispose();
|
||
}
|
||
```
|
||
|
||
Insert immediately before it:
|
||
|
||
```csharp
|
||
// ── Window manager forwarders (delegate to UiRoot) ─────────────────
|
||
|
||
/// <summary>Register a top-level window for Show/Hide/Toggle. See <see cref="UiRoot.RegisterWindow"/>.</summary>
|
||
public void RegisterWindow(string name, UiElement window) => Root.RegisterWindow(name, window);
|
||
|
||
/// <summary>Toggle a registered window's visibility; returns the new IsVisible.</summary>
|
||
public bool ToggleWindow(string name) => Root.ToggleWindow(name);
|
||
|
||
```
|
||
|
||
- [ ] **Step 2: Mount the placeholder inventory window**
|
||
|
||
In `src/AcDream.App/Rendering/GameWindow.cs`, find the END of the plugin-panel drain, which is the last block inside `if (_options.RetailUi)`:
|
||
|
||
```csharp
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[D.2b] plugin UI panel '{p.MarkupPath}' failed to load: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Insert the placeholder mount between the inner `}` (closing `if (_uiRegistry is not null)`) and the outer `}` (closing `if (_options.RetailUi)`) — i.e. make it the last statement inside the `_options.RetailUi` block:
|
||
|
||
```csharp
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[D.2b] plugin UI panel '{p.MarkupPath}' failed to load: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Phase D.2b-A — placeholder inventory window. Starts HIDDEN; F12
|
||
// (InputAction.ToggleInventoryPanel) reveals it via the UiRoot window
|
||
// manager. Throwaway scaffolding: Sub-phase B replaces the body with the
|
||
// real gmInventoryUI (0x21000023) nested layout, keeping the same name.
|
||
var inventoryWindow = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
|
||
{
|
||
Left = 220, Top = 120, Width = 320, Height = 400,
|
||
Visible = false,
|
||
};
|
||
_uiHost.Root.AddChild(inventoryWindow);
|
||
_uiHost.RegisterWindow(AcDream.App.UI.WindowNames.Inventory, inventoryWindow);
|
||
Console.WriteLine("[D.2b-A] placeholder inventory window registered (F12 toggles).");
|
||
}
|
||
```
|
||
|
||
(`ResolveChrome` and `_uiHost` are both in scope here — `ResolveChrome` is the local function defined earlier in the same block; `_uiHost` is the field assigned at the top of the block.)
|
||
|
||
- [ ] **Step 3: Wire F12 → toggle in `OnInputAction`**
|
||
|
||
In `src/AcDream.App/Rendering/GameWindow.cs`, find the first arm of the `switch (action)` in `OnInputAction`:
|
||
|
||
```csharp
|
||
case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleDebugPanel:
|
||
```
|
||
|
||
Insert this case immediately before it:
|
||
|
||
```csharp
|
||
case AcDream.UI.Abstractions.Input.InputAction.ToggleInventoryPanel:
|
||
// Retail F12 (rebindable). Gated upstream by WantsKeyboard, so it
|
||
// does not fire while the chat input holds focus. Null _uiHost =
|
||
// retail UI off (ACDREAM_RETAIL_UI unset) → no-op.
|
||
_uiHost?.ToggleWindow(AcDream.App.UI.WindowNames.Inventory);
|
||
break;
|
||
|
||
```
|
||
|
||
- [ ] **Step 4: Build to verify it compiles**
|
||
|
||
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||
Expected: Build succeeded, 0 errors.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.App/UI/UiHost.cs src/AcDream.App/Rendering/GameWindow.cs
|
||
git commit -m "feat(ui): D.2b-A — F12 toggles placeholder inventory window
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Divergence register + full verification
|
||
|
||
**Files:**
|
||
- Modify: `docs/architecture/retail-divergence-register.md` (extend IA-12 "Where")
|
||
|
||
- [ ] **Step 1: Extend IA-12 to cite the window manager**
|
||
|
||
In `docs/architecture/retail-divergence-register.md`, find the IA-12 row's "Where" cell:
|
||
|
||
```
|
||
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms) | `src/AcDream.App/UI/README.md:3` | keystone.dll has no PDB/decomp; semantics reconstructed from the six `docs/research/retail-ui/` deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly | Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against | keystone.dll Device DAT_00837ff4; docs/research/retail-ui/04-input-events.md |
|
||
```
|
||
|
||
Replace the "Where" cell `` `src/AcDream.App/UI/README.md:3` `` with:
|
||
|
||
```
|
||
`src/AcDream.App/UI/README.md:3` (window mgr: `UiRoot.cs` RegisterWindow/Show/Hide/Toggle/BringToFront — F12 inventory toggle IS retail-faithful)
|
||
```
|
||
|
||
So the full row becomes:
|
||
|
||
```
|
||
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms) | `src/AcDream.App/UI/README.md:3` (window mgr: `UiRoot.cs` RegisterWindow/Show/Hide/Toggle/BringToFront — F12 inventory toggle IS retail-faithful) | keystone.dll has no PDB/decomp; semantics reconstructed from the six `docs/research/retail-ui/` deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly | Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against | keystone.dll Device DAT_00837ff4; docs/research/retail-ui/04-input-events.md |
|
||
```
|
||
|
||
- [ ] **Step 2: Run the full test suite**
|
||
|
||
Run: `dotnet test`
|
||
Expected: PASS — full suite green (2752+ tests; the window-manager facts add ~5).
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add docs/architecture/retail-divergence-register.md
|
||
git commit -m "docs(register): D.2b-A — extend IA-12 to cite the window manager
|
||
|
||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||
```
|
||
|
||
- [ ] **Step 4: Hand off for visual verification**
|
||
|
||
Stop and ask the user to launch the client (`ACDREAM_RETAIL_UI=1`, the standard launch in CLAUDE.md) and confirm:
|
||
- F12 shows the placeholder inventory window above vitals/chat/toolbar; F12 again hides it.
|
||
- Clicking any window (vitals/chat/toolbar/inventory) raises it above the others.
|
||
- F12 does nothing while the chat input is focused (write mode).
|
||
|
||
Do NOT mark Sub-phase A done until the user confirms the visual behavior.
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
**Spec coverage:**
|
||
- Registry on `UiRoot` (§4.1) → Task 1. ✓
|
||
- `BringToFront` + raise-on-click (§4.2) → Task 2. ✓
|
||
- `UiHost` forwarders (§4.3) → Task 3 Step 1. ✓
|
||
- F12 wiring in `OnInputAction` (§4.4) → Task 3 Step 3. ✓
|
||
- Placeholder inventory window (§4.5) → Task 3 Step 2. ✓
|
||
- In-memory persistence (§5) → no code needed (Visible + hardcoded positions); covered by construction. ✓
|
||
- Tests (§6) → Tasks 1–2. ✓
|
||
- Divergence register (§7) → Task 4 Step 1 (extend IA-12, dedup convention). ✓
|
||
- Acceptance (§8): build+tests green → Tasks 1–4; visual → Task 4 Step 4. ✓
|
||
|
||
**Placeholder scan:** No TBD/TODO/"handle edge cases"/"similar to" — every code step shows full code. ✓
|
||
|
||
**Type consistency:** `RegisterWindow(string, UiElement)`, `ShowWindow/HideWindow/ToggleWindow(string)→bool`, `BringToFront(UiElement)`, `WindowNames.Inventory`, `InputAction.ToggleInventoryPanel`, `_uiHost.ToggleWindow` — names identical across Tasks 1–3 and the spec. ✓
|