# Design — D.2b Sub-phase B-Grid (inventory sub-window mount + UiItemList grid mode) **Date:** 2026-06-20 **Phase:** D.2b retail-UI arc, Sub-phase **B** (inventory window), step **B-Grid** — the first of four (B-Grid → B-Controller → B-Wire → B-Drag). Decomposition approved 2026-06-20. **Status:** approved design shape, dat-grounded, pre-implementation. **Predecessor:** Sub-phase A (window manager) shipped; F12 toggles a hidden placeholder window registered as `WindowNames.Inventory`. B-Grid makes that window show the real nested frame. --- ## 1. Context & goal B-Grid makes `LayoutImporter.Import(0x21000023)` (gmInventoryUI) produce the **full nested inventory frame** and gives `UiItemList` an **N-cell grid** so item cells tile. After B-Grid, F12 shows the real bordered inventory frame — outer chrome + a backpack strip + a 3D-items area + an (empty) paperdoll panel — replacing the placeholder. Cells are empty until B-Controller populates them; paperdoll *content* is Sub-phase C. **Goal is structural, not interactive:** the frame renders with the correct nested geometry and a working grid widget. No wire, no controller, no drag (those are B-Wire / B-Controller / B-Drag). --- ## 2. The dat-grounded finding (why this design) Dumping `LayoutDesc 0x21000023` (via `AcDream.Cli dump-vitals-layout`) overturned the research agent's "game-class Type + recursive Import" model. The actual structure: - Root `0x100001CC`, Type `0x10000023` (gmInventoryUI), 300×362. Children: - `0x100001CD` paperdoll panel — **Type 0**, `BaseElement 0x100001D4`, **`BaseLayoutId 0x21000024`**, childless, **no own media**, 224×214 @ (0,23) - `0x100001CE` backpack panel — **Type 0**, `BaseElement 0x100001C8`, **`BaseLayoutId 0x21000022`**, childless, **no own media**, 61×339 @ (239,23) - `0x100001CF` 3D-items panel — **Type 0**, `BaseElement 0x100001C4`, **`BaseLayoutId 0x21000021`**, childless, **no own media**, 234×120 @ (0,237) - `0x100001D2` close button — Type 0, inherits, **has own media** (Normal/Normal_pressed) - `0x100001D3` title — Type 0, inherits, **has own media** (DirectState) - `0x100001D0` backdrop, `0x100001D1` bottom rule — Type 3, `BaseElement 0`, own media So the three panels are **Type-0 pure-container leaves that nest via the existing `BaseElement`+`BaseLayoutId` inheritance path** — *not* game-class Types. Their content lives in the separate layouts `0x21000024/22/21`. `LayoutImporter.Resolve` already loads `BaseLayoutId`, finds `BaseElement`, recurses it, and `Merge`s — **but** `ElementReader.Merge` (`ElementReader.cs:162`) sets `Children = new List(derived.Children)`, dropping the base's children. So today the panels import as **empty containers**. The fix is a surgical attach of the base's resolved children to the panel — not a new Type-dispatch mechanism. --- ## 3. Scope **In scope (B-Grid):** - **Sub-window mount** — `LayoutImporter.Resolve` attaches a base element's resolved children to a childless, media-less inheriting element (the panels), so `Import(0x21000023)` yields the full nested tree. - **`UiItemList` grid mode** — column count + cell pitch so `AddItem`/`OnDraw` tile cells in a grid; single-cell (toolbar) behavior preserved. - Unit tests; a regression guard that vitals/chat/toolbar import unchanged. **Out of scope (deferred):** - Populating cells from `ClientObjectTable`, the burden meter, find-by-id binding → **B-Controller**. - Inventory wire gaps (`DropItem`/`ViewContents`/…) → **B-Wire**. - Inventory cell as a drag source → **B-Drag**. - Paperdoll *content* rendering (the `UiViewport` doll) → **Sub-phase C**. (B-Grid's mount will pull in the paperdoll panel's equip-slot frames as ordinary elements, but the 3D doll viewport is C.) - `0x10000032` (UiItemSlot) factory registration — **not needed**: `UiItemList.ConsumesDatChildren` is true, so the importer skips cell templates; cells are built procedurally. - Cell pitch *values* + exact column counts per panel — read by **B-Controller** from the nested layouts at bind time. B-Grid supplies the mechanism, not the numbers. --- ## 4. Component 1 — sub-window mount (`LayoutImporter.Resolve`) Restructure `Resolve` to capture the base's children and attach them when the derived element is a pure container that inherits from a base with content: ```csharp private static ElementInfo Resolve( DatCollection dats, ElementDesc d, HashSet<(uint, uint)> baseChain) { var self = ToInfo(d); var result = self; List? baseChildren = null; if (d.BaseElement != 0 && d.BaseLayoutId != 0 && baseChain.Add((d.BaseLayoutId, d.BaseElement))) { var baseLd = dats.Get(d.BaseLayoutId); var baseDesc = baseLd is null ? null : FindDesc(baseLd, d.BaseElement); if (baseDesc is not null) { var baseInfo = Resolve(dats, baseDesc, baseChain); result = ElementReader.Merge(baseInfo, self); baseChildren = baseInfo.Children; // capture the base's resolved subtree } } // Derived's own children (authoritative when present). foreach (var kv in d.Children) result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>())); // Sub-window mount: a PURE-CONTAINER leaf (no own children AND no own media) that inherits // from a base WITH content attaches the base's subtree. This targets the gmInventoryUI // panels (0x100001CD/CE/CF — Type-0, media-less, childless, BaseLayoutId → a gm*UI window) // and is inert for: media-bearing inheritors (close button/title keep their own media, // so self.StateMedia is non-empty → skipped), normal elements with their own children // (result.Children already populated → skipped), and childless style-prototype inheritors // (vitals/chat/toolbar text — the base prototype has no children → baseChildren empty). if (LayoutImporter.ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren?.Count ?? 0)) result.Children.AddRange(baseChildren!); return result; } /// True when a pure-container leaf should inherit its base's subtree (sub-window mount): /// the derived element has no own children, no own state media, and the base resolved to ≥1 child. internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount) => derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0; ``` The predicate is extracted as `ShouldMountBaseChildren` so it is **unit-testable without dats**. **Coordinate correctness:** the panel inherits the base window-root's fields via `Merge` (so it gets that window's size) and the base's children via the mount. The children's X/Y are relative to the panel; `ScreenPosition` composes parent offsets, so a child at panel-local (x,y) with the panel at (0,23) lands at the right screen pixel. **Cycle safety:** `baseChain` already guards the inheritance recursion; the nested layouts are distinct ids, so no cycle. --- ## 5. Component 2 — `UiItemList` grid mode Add a column count + cell pitch; keep single-cell as the default. ```csharp public int Columns { get; set; } = 1; // grid columns; 1 = single column public float CellWidth { get; set; } // 0 = "fill the list" (single-cell legacy) public float CellHeight { get; set; } ``` Layout rule (applied in `AddItem` and re-applied in `OnDraw` so a list resize reflows): - **Fill mode** (`CellWidth <= 0`): the single cell fills the list — `cell[0]` at `(0, 0, Width, Height)`. Unchanged toolbar behavior. - **Grid mode** (`CellWidth > 0`): `cell[i]` at `(col·CellWidth, row·CellHeight, CellWidth, CellHeight)` where `col = i % Columns`, `row = i / Columns`. The toolbar constructor keeps adding its one cell with `CellWidth = 0` (defaults), so it stays in fill mode. B-Controller sets `Columns` + `CellWidth/CellHeight` (read from the nested layout's ItemList element + cell template) and `Flush()` + `AddItem()`s the grid cells. The grid math is a pure helper for testing: ```csharp internal static (float x, float y) CellOffset(int index, int columns, float cellW, float cellH) { int col = index % columns, row = index / columns; return (col * cellW, row * cellH); } ``` --- ## 6. Testing **Grid mode (pure, `tests/AcDream.App.Tests/UI/`):** - `CellOffset(4, 3, 36, 36)` ⇒ `(36, 36)` (index 4 → col 1, row 1). - Build a `UiItemList { Columns = 3, CellWidth = 36, CellHeight = 36 }`, `AddItem` 7 cells, assert `GetItem(4)` is at `Left == 36 && Top == 36 && Width == 36`. - Single-cell legacy: a default `UiItemList` (CellWidth 0) keeps its cell at the list size after `OnDraw` — assert the existing toolbar behavior is unchanged. **Mount predicate (pure):** - `ShouldMountBaseChildren(0, 0, 5)` ⇒ true (panel: childless, media-less, base has 5 kids). - `ShouldMountBaseChildren(0, 1, 5)` ⇒ false (close button: has own media). - `ShouldMountBaseChildren(2, 0, 5)` ⇒ false (has own children). - `ShouldMountBaseChildren(0, 0, 0)` ⇒ false (style prototype: base childless — vitals text). **Regression guard (hard acceptance):** vitals (`0x2100006C`), chat (`0x21000006`), and toolbar (`0x21000016`) must import + render unchanged. Their inherited bases are childless prototypes, so the mount is inert for them — but this is verified, not assumed (the existing importer tests + the visual check). --- ## 7. Divergence register **No new row.** The mount makes `BaseElement`/`BaseLayoutId` inheritance carry the base's content subtree, which is what retail must do for the gmInventoryUI panels to render at all (they are childless in the dat — the content is only reachable through the base reference). This is *completing* faithful inheritance, not diverging from it. IA-12 (toolkit-defined UI) already covers the importer's reconstruction of keystone semantics. If a future retail-decomp pass shows retail's child-inheritance differs in the children-present merge case (which no inventory element exercises), that is a separate, later concern. --- ## 8. Acceptance criteria - `dotnet build` green; `dotnet test` green (grid + mount-predicate tests added). - `Import(0x21000023)` yields a tree where the three panels (`0x100001CD/CE/CF`) each have children (the nested layout content), confirmed by a dat-backed assertion or the dump. - Vitals/chat/toolbar import + render unchanged (regression guard). - With `ACDREAM_RETAIL_UI=1`, F12 shows the real nested inventory frame (chrome + backpack strip + 3D-items area + empty paperdoll panel) instead of the blank placeholder. - Visual verification by the user. --- ## 9. Open / confirmed-at-build items - **Cell pitch + column counts** — B-Controller reads them from the nested layouts (`0x21000022` backpack, `0x21000021` 3D-items) at bind time. Geometry from the outer dump suggests the backpack strip is ~1 column and the 3D-items panel ~6 columns of 36×36, but the exact values are B-Controller's to read, not B-Grid's to hardcode. - The placeholder mount in `GameWindow` (Sub-phase A) is replaced here: instead of a bare `UiNineSlicePanel`, the inventory window becomes `LayoutImporter.Import(0x21000023)`'s root, registered under the same `WindowNames.Inventory`. (Mechanically a few lines in the RetailUi block; the F12 wiring is untouched.) --- ## 10. References - Dat dump: `AcDream.Cli dump-vitals-layout "" 0x21000023` (the structure in §2). - Format: `docs/research/2026-06-15-layoutdesc-format.md` (§8 Type table, §10/§12 inheritance). - Code: `src/AcDream.App/UI/Layout/LayoutImporter.cs` (`Resolve`/`Merge` path), `src/AcDream.App/UI/Layout/ElementReader.cs:162` (the children-drop), `UiItemList.cs`, `UiItemSlot.cs` (the shipped drag spine — reused in B-Drag, untouched here). - Handoff/decomposition: `docs/research/2026-06-20-window-manager-inventory-handoff.md` §4; the Sub-phase B decomposition (B-Grid → B-Controller → B-Wire → B-Drag).