Ports retail UIElement_ItemList (class 0x10000031) as a behavioral-leaf
container that owns its UiItemSlot children procedurally. Single-cell
default covers every toolbar slot; N-cell grid is deferred to the inventory
phase. OnDraw syncs the cell rect to the list's Width/Height each frame so
the cell is sized and hit-testable from the first rendered frame, even
though the factory sets rect AFTER construction.
Factory: adds `0x10000031u => new UiItemList(resolve)` arm before the
fallback, so all 18 toolbar itemlist slots route to UiItemList instead of
UiDatElement.
Tests: 4 new (IsLeafWidget, StartsWithOneCell, Cell_returnsFirstSlot,
Create_buildsUiItemList_forItemListClassId). All 4 pass; full suite green
(415 pass / 2 skip in App.Tests; 0 fail total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vitals cur/max numbers now render through the generic UiText widget — retail
gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController
attaches a centered, non-interactive UiText child to each meter and stops the meter
drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line
centered H+V with the SAME formula UiMeter's overlay used, so the numbers are
pixel-identical — user-confirmed in the live client.
This completes the D.2b widget-generalization pass: every chat + vitals widget is now
built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/
Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays
UiDatElement for chrome.)
Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite:
404 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 6 registered Type 3 -> UiField globally, which broke acdream's Type-3 dat
elements: in these layouts Type 3 is sprite-bearing CHROME (the 8-piece bevel
corners, e.g. vitals 0x10000633 -> sprite 0x060074C3) and the transcript/input
CONTAINER panels — NOT editable fields. UiField draws no dat sprite, so the
vitals bevel corners would render empty; the regression was masked by weakening
VitalsTree_ChromeCornerHasExpectedSprite (UiDatElement+sprite -> UiField+exists).
Retail Type 3 IS UIElement_Field, but retail draws those chrome elements as inert
media-bearing Fields, which our UiDatElement reproduces pixel-for-pixel without a
spurious focus/edit affordance. The one true editable field — the chat input
0x10000016 — resolves to Type 12 and is controller-placed as a UiField (Variant B,
kept). So Type 3 stays on the generic fallback; register it as UiField only when a
window carries a factory-built editable Type-3 field (and UiField grows a
background-media draw + an opt-in editable flag then).
Restored the chrome-corner conformance test (asserts UiDatElement + sprite, an
early warning if Type 3 is ever wrongly routed to UiField). Kept the good Task-6
work: UiField rename + the Variant-B input wiring (stray Type-12 placeholder
removed). Full suite: 404 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rename UiChatInput → UiField (UIElement_Field, RegisterElementClass(3) @ :126190);
update doc to cite retail's CatchDroppedItem/MouseOverTop drag-drop hooks for
future item windows. BackgroundColor default → transparent (controller sets
the translucent 0.35α value explicitly, matching UiText pattern).
- Register Type 3 in DatWidgetFactory.Create: `3 => new UiField()`.
- ChatWindowController.Bind (Variant B): factory now builds 0x10000016 as an
invisible UiText placeholder (Type 12); Bind removes that placeholder via
FindElement(InputId).Parent.RemoveChild and places a UiField at the same rect.
Result: exactly ONE input widget in the input bar, no stray UiText duplicate.
- Input property type changed from UiChatInput to UiField; GameWindow.cs:1861
UiField.Keyboard assignment compiles unchanged (field exists).
- Tests: UiChatInputTests → UiFieldTests (class + all ctor refs renamed);
DatWidgetFactoryTests: new Type3_Field_MakesUiField test; ChatWindowControllerTests:
updated stale "skipped by factory" comments; LayoutConformanceTests: updated
VitalsTree_ChromeCornerHasExpectedSprite — Type-3 chrome-corner elements are
now UiField (sprite rendering for Type-3 dat image elements is a known
limitation, tracked for post-Task-8 UiField.BackgroundSprite follow-up).
- Full suite: 404 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename UiChatView -> UiText (the retail UIElement_Text class,
RegisterElementClass(0xc) @ acclient_2013_pseudo_c.txt:115655).
Factory changes (DatWidgetFactory.cs):
- Remove the Type-12 skip (was: no-media -> null, with-media -> UiDatElement).
- Add Type 12 -> BuildText() -> UiText in the switch.
- BuildText extracts the element's Direct/Normal sprite as BackgroundSprite
so any dat-media the element carried keeps rendering under the text.
UiText changes (renamed from UiChatView.cs):
- BackgroundColor default: (0,0,0,0.35) -> (0,0,0,0) (transparent).
An unbound UiText draws nothing; the controller opts in to the translucent bg.
- New BackgroundSprite + SpriteResolve: optional dat state-sprite background
drawn UNDER DrawFill+text (faithful UIElement_Text media support).
ChatWindowController.cs (Task 5 Step 8):
- Transcript property: UiChatView -> UiText.
- Bind() now uses layout.FindElement(TranscriptId) as UiText (factory-built)
instead of manually constructing + AddChild-ing a new UiChatView.
- Sets BackgroundColor = (0,0,0,0.35) on the found widget (retail translucent bg).
- Removes the tInfo null-check from the early guard (transcript is factory-built;
iInfo lookup kept for the input widget which is still manually constructed).
- BuildLines: UiChatView.Line -> UiText.Line throughout.
Vitals frozen: the Type-12 vitals number elements are meter children and are
never recursed by BuildWidget (the `if (w is not UiMeter)` gate), so they are
not built as widgets and keep rendering via UiMeter.Label. Vitals fixture
vitals_2100006C.json unchanged; LayoutConformanceTests + VitalsBindingTests green.
Tests:
- UiChatViewTests.cs -> UiTextTests.cs (class: UiTextTests, all UiChatView.* -> UiText.*)
- UiChatViewDatFontTests.cs -> UiTextDatFontTests.cs (same)
- DatWidgetFactoryTests: delete Type12_StylePrototype_ReturnsNull +
DatWidgetFactory_Type12WithMedia_Renders; add Type12_Text_MakesUiText +
DatWidgetFactory_Type12_AlwaysMakesUiText.
- LayoutImporterTests: BuildFromInfos_Type12Child_IsSkipped_Type3Present updated
to assert IsType<UiText> (element is now in tree, transparent, not skipped).
Divergence register: AP-37 amended -- removed the "standalone Type-0 text
elements skipped / dat-text widget is Plan 2" clause (now shipped as UiText);
kept the meter-collapse clause and the vitals-numbers-via-UiMeter.Label clause.
AP-38/AP-39/AD-28 file references updated UiChatView.cs -> UiText.cs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UiChannelMenu → UiMenu: removed ChatChannelKind, the 14-item array, the
button-text map, and the availability default. Generic surface: MenuItem
(label + object? Payload), Selected (object?), OnSelect, EnabledProvider,
ButtonLabelProvider, RowsPerColumn/RowHeight/ColumnWidth (all settable).
All draw/event mechanics unchanged — same popup geometry, same click
coordinates, same 8-piece bevel, same 3-slice button face.
ChatWindowController gains ChannelItems[], ChannelButtonLabel(), and
ChannelAvailable() (verbatim from old widget), and populates the
factory-built Type-6 UiMenu via find-by-id rather than constructing a
replacement widget. The Menu property type is now UiMenu. OnChannelChanged
wrap replaced with the generic OnSelect wrap for the ReflowInputRow hook.
DatWidgetFactory registers Type 6 → new UiMenu().
Tests: UiChannelMenuTests → UiMenuTests (10 tests, all green); factory
Type6 test added; ChatWindowControllerTests updated to use OnSelect.
Divergence register: AP-42 added (flat item model vs retail nested-submenu
MakePopup @0x46d310 — latent, unreachable through the chat menu).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces UiButton: a dedicated dat-widget button that ports UIElement_Button
(RegisterElementClass(1,...) @ acclient_2013_pseudo_c.txt:125828). State selection,
tiled DrawSprite, and label rendering mirror UiDatElement exactly so the chat Send
and Max/Min buttons have zero behavioral change.
DatWidgetFactory now maps Type 1 → UiButton (beside Type 7 → UiMeter, Type 11 →
UiScrollbar). ChatWindowController's Send and Max/Min bind blocks updated from
UiDatElement casts to UiButton casts; ClickThrough=false lines dropped (UiButton
is interactive by construction).
The old UiPanel.cs UiButton (a plain dev-scaffold rect+text button with no dat
sprites) is renamed UiSimpleButton to free the name — no production code
instantiated it.
Full suite: 402 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- git mv UiChatScrollbar.cs → UiScrollbar.cs; rename class + update doc summary to
"Generic scrollbar. Ports retail UIElement_Scrollbar (RegisterElementClass(0xb) @
acclient_2013_pseudo_c.txt:124137); thumb size = trackLen * ThumbRatio (min 8px); step ±1 line."
- git mv UiChatScrollbarTests.cs → UiScrollbarTests.cs; rename test class + replace
every UiChatScrollbar reference with UiScrollbar (bodies unchanged).
- DatWidgetFactory: register Type 11 → new UiScrollbar() before the _ fallback case.
- ChatWindowController: change Scrollbar property type to UiScrollbar; replace the old
"construct-remove-add" block with a "find factory-built UiScrollbar and bind in place"
block (no RemoveChild/AddChild); keep `var track` assignment in scope so the Max/Min
block's track.Left/track.Width reads still compile against UiElement?.
- AP-41 divergence register: update file:line to UiScrollbar.cs:35; narrow wording to
"fallback only — single-tile drawn only when cap ids are unset; the chat controller
passes all three cap ids so the 3-slice path is the active code path."
- Update inline UiChatScrollbar doc-comment references in UiScrollable.cs + UiChatView.cs.
- Full suite: 399 passed, 2 skipped (dat/tower fixture skips), 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add ChatLayoutFixtureGenerator.cs (Skip-by-default) to regenerate
chat_21000006.json from the live portal.dat via LayoutImporter.ImportInfos
- Commit generated fixture chat_21000006.json (13 KB, 400 lines) — dat-free,
auto-copied to test output via existing *.json csproj glob
- Refactor FixtureLoader: extract shared LoadInfos(fileName) helper; add
LoadChat() + LoadChatInfos() mirroring the vitals pattern; LoadVitalsInfos()
now delegates to the shared loader (behavior unchanged, vitals tests green)
- Add ChatLayoutConformanceTests: ResolvesKnownElements + ResolvedTypes_MatchRetailRegistry
Confirmed resolved Types from live dat:
0x10000011 (transcript) → Type 12 (style-prototype, skipped by factory)
0x10000016 (input) → Type 12 (style-prototype, skipped by factory)
0x10000014 (menu) → Type 6
0x10000012 (scrollbar) → Type 11
0x10000019 (send) → Type 1
0x1000046F (max/min) → Type 1
Also fix pre-existing build break: UiChatInput.MoveCaret(int delta) was made
private in ce848c1 but UiChatInputTests.Backspace_DeletesBeforeCaret called it
as public. Expose a public MoveCaret(int) overload (no-shift) alongside the
private MoveCaret(int,bool) — restores the intended test surface.
Full suite: 398 passed, 2 skipped (generator + pre-existing), 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live session + its LiveCommandBus are created after the retail-UI block in
OnLoad, so binding the bus by value captured NullCommandBus and silently dropped
outbound chat. Pass a Func<ICommandBus> resolved at submit time (mirrors how the
ImGui ChatPanel re-reads the bus each frame).
AP-41: scrollbar thumb drawn as single stretched tile (0x06004C63) instead of
retail's 3-slice top-cap/middle/bottom-cap — acknowledged in UiChatScrollbar.cs:37,
registered per the divergence-register rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements Task G2: binds the imported chat LayoutDesc (0x21000006) to live
behavior, the acdream analogue of retail ChatInterface + gmMainChatUI::PostInit.
- UiDatElement: add OnClick hook + OnEvent override so Send/max-min buttons
can be wired by a controller without needing a dedicated widget type.
- ChatWindowController.Bind: reads transcript (0x10000011) and input
(0x10000016) rects from the raw ElementInfo tree (factory skips them as
Type-12/no-media), places UiChatView under the transcript panel and
UiChatInput under the input bar; replaces the imported scrollbar track
(0x10000012) with UiChatScrollbar driving UiChatView.Scroll; replaces
the channel menu placeholder (0x10000014) with UiChannelMenu; wires
Send button and max/min toggle via the new OnClick hook.
ChatCommandRouter.Submit routes all input through the existing pipeline.
- 6 smoke tests: Bind returns non-null, Transcript is child of panel,
Input is child of bar, Input.OnSubmit publishes SendChatCmd, channel
change updates submit channel, returns null when panels missing.
Build: 0 errors. Test suite: 392 passed / 1 skipped / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task G1: two gaps blocked chat window static sprite elements from rendering.
Change 1 — DatWidgetFactory: only skip Type-12 elements that have no own
state media (pure style prototypes). A Type-12 element that carries sprites
(e.g. a chat Send button whose derived Type-0 element inherited Type 12 from
its base prototype) now renders as a UiDatElement.
Change 2 — ElementInfo: add DefaultStateName field (string, default "").
Change 3 — LayoutImporter.ToInfo: read ElementDesc.DefaultState.ToString()
into DefaultStateName; normalize Undef/Undefined/0 sentinels to "".
Change 4 — ElementReader.Merge: inherit DefaultStateName (derived wins if
non-empty, else base).
Change 5 — UiDatElement ctor: initialize ActiveState to DefaultStateName
when set; else "Normal" when a Normal-state sprite is present (retail's
implicit default for buttons/tabs); else "" (DirectState). This makes the
Send button, max/min button, and numbered tabs render their default sprite
without requiring explicit state assignment at runtime.
Vitals neutrality: all vitals chrome/grip elements carry DirectState-only
sprites with no "Normal" named state and DefaultStateName="" (Undef in dat),
so their ActiveState stays "" and their existing conformance tests are
unaffected. Vitals text labels (Type 0→12 via Merge, no StateMedia) are
still skipped by the refined Type-12 guard (StateMedia.Count==0).
Tests: 4 new tests (2 in DatWidgetFactoryTests, 3 in UiDatElementTests).
All 386 pass; 387 total (1 pre-existing skip).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToAnchors was inverted vs retail UIElement::UpdateForParentSizeChange @0x00462640:
stretch is RightEdge==1 (not ==2/==4), LeftEdge==2 = track-right. Verified against
all 19 vitals fixture pieces. Enables Resizable/ResizeX on the importer vitals root
(the prior 'dat is fixed-size' conclusion was wrong). At-rest render unchanged
(anchors only fire on resize). Added a 160->200 resize conformance test.
Also fixed DatWidgetFactoryTests.RectAndAnchors_SetFromElementInfo which encoded
the old inverted model (Right=2 expecting Right anchor; corrected to Right=1).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Process/quality items from the LayoutDesc-importer final review — no runtime
behavior change.
I1a — amend IA-15: the 8-piece chrome edge/corner→position mapping is no longer
a guess. The LayoutImporter (ACDREAM_RETAIL_UI_IMPORTER) reads real LayoutDesc
dat data and resolves positions + sprite ids directly; locked by the conformance
fixture vitals_2100006C.json. Residual risk trimmed to anchor resolution at
non-800×600 + controls.ini cascade. Pointers added to LayoutImporter.cs and the
format-doc.
I1b — add AP-32: the importer collapses the dat's nested meter structure
(Type-7 → two Type-3 containers → three image-slice grandchildren each) into
UiMeter's programmatic 3-slice fields instead of building those nodes generically
and porting UIElement_Meter::DrawChildren. Standalone Type-0 text elements are
also skipped (Plan 2). Retail oracles: UIElement_Meter::DrawChildren @0x46fbd0,
UIElement_Text::DrawSelf @0x467aa0.
I1c — AP section header 31 → 32.
N1 — ElementReader.cs: comment at the Type-merge line explaining that a derived
Type 0 (text element) inherits the base's Type 12 (style prototype), which
DatWidgetFactory skips; safe for Plan 1 because vitals numbers render via
UiMeter.Label. Format-doc §10: correct the "render as UiDatElement" sentence to
"skipped entirely" (Type-0 → inherits Type-12 via Merge → factory returns null).
N4 — new conformance test VitalsTree_TextLabel_InheritsFontDidFromBaseLayout:
walks the raw ElementInfo tree from the fixture and asserts at least one element
carries FontDid==0x40000000, proving Resolve()'s inheritance merge fired against
real dat data. FixtureLoader gains LoadVitalsInfos() that returns the raw tree
without calling Build.
Tests: 36 pass (was 35), 0 errors, 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: replace 3 copy-paste meter blocks in VitalsTree_MetersHaveExpectedSliceIds
with a single table-driven loop — a 4th meter is now a one-liner and failures
name the failing meter id directly.
Fix 2: FixtureLoader now reads the fixture as bytes and strips the UTF-8 BOM
(EF BB BF) before passing the span to JsonSerializer, so a BOM-bearing fixture
file never causes a spurious JsonReaderException.
Fix 3: add [Trait("Category", "Conformance")] at the class level so conformance
tests are selectable by category filter.
Fix 4: add missing <param name="layoutId"> doc tag to LayoutImporter.ImportInfos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Job 1: extract LayoutImporter.ImportInfos() (public dat-shell half that returns the
resolved ElementInfo tree without building widgets) so fixture generation and
conformance tests can call it directly. Import() now delegates to ImportInfos() +
Build() — existing 32 Layout tests stay green.
Job 2: generate tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json
from the real portal.dat via a throwaway [Fact] generator (deleted, not committed).
System.Text.Json with IncludeFields=true — ValueTuple serializes as Item1/Item2.
Pre-write validation confirmed health meter BackLeft=0x0600747E FrontRight=0x06007483
rect (5,5,150,16). Round-trip deserialization re-validated before writing.
Job 3: FixtureLoader.LoadVitals() deserializes the fixture from the test output
directory (CopyToOutputDirectory item in csproj) and returns ImportedLayout via
LayoutImporter.Build(root, _ => (0,0,0), null) — no dats, no GL.
Job 4: LayoutConformanceTests — 3 golden tests (35 asserts total):
- VitalsTree_HasThreeMetersAtExpectedRects: 3 meters at x=5, w=150, h=16, y=5/21/37
- VitalsTree_MetersHaveExpectedSliceIds: all 18 back+front slice ids health/stamina/mana
- VitalsTree_ChromeCornerHasExpectedSprite: TL corner 0x10000633 → sprite 0x060074C3
Full App suite: 326 pass / 1 skip (pre-existing) / 0 fail. Build: 0 errors, 0 warnings.
Throwaway generator not committed (confirmed via git status).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: Added a <para> to the VitalsController class summary citing
docs/research/2026-06-15-layoutdesc-format.md §11 as the source of the
three dat element ids, giving a paper trail back to the evidence per
the project's cite-in-comments rule.
Fix 2: Changed FakeLayout in VitalsBindingTests to accept (uint id,
UiElement e) tuples instead of (string idHex, UiElement e), and updated
all three call sites to pass VitalsController.Health/.Stamina/.Mana.
Tests now follow the constants automatically if they ever change rather
than silently passing with stale hex literals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors retail gmVitalsUI::PostInit: grab Health/Stamina/Mana meters from
the imported layout by their dat element ids (0x100000E6 / EC / EE) and
wire Func<float> fill + Func<string> label providers. Missing ids are
silently skipped (no throw). Slice sprites + dat font already set by the
factory — this is pure data wiring, not graphics.
3 TDD tests: single-meter fill+label, all-three distinct providers, missing-id no-throw.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 5 of the LayoutDesc Importer (Plan 1 — vitals conformance).
Pure layer (BuildFromInfos / Build):
- ImportedLayout result type: UiElement root + O(1) FindElement(uint id) lookup
- BuildWidget dispatches via DatWidgetFactory.Create; skips Type-12 prototypes (null)
- Meters consume their children (DatWidgetFactory already extracted slice ids —
adding the dat children as UiElement nodes would duplicate geometry)
- All other element types recurse children generically via AddChild
Dat shell (Import):
- Loads LayoutDesc from dats; null-safe if layout is absent
- Resolves each top-level ElementDesc to ElementInfo via Resolve():
BaseElement/BaseLayoutId chain with (layoutId,elementId) cycle guard
- ToInfo(): reads ElementDesc scalar fields (uint → float cast) + DirectState +
named States (UIStateId.ToString() as key)
- ReadState(): extracts first MediaDescImage (File + DrawMode) per state +
font DID from Properties[0x1A] → ArrayBaseProperty → DataIdBaseProperty.Value
- Each sibling element gets a fresh base-chain set (siblings don't share guards)
DRW API: all members confirmed from VitalsLayoutDump.cs usings — no
adjustments needed: LayoutDesc in DBObjs; ElementDesc/StateDesc/MediaDescImage/
ArrayBaseProperty/DataIdBaseProperty in Types; DrawModeType/UIStateId in Enums.
Tests (3/3 green):
- BuildFromInfos_HealthMeter_IsUiMeterAtRect — Type-7 child → UiMeter, Left=5, Width=150
- BuildFromInfos_Type12Child_IsSkipped_Type3Present — prototype absent, container present
- BuildFromInfos_MeterWithChildren_MeterPresent_ChildrenNotInTree — meter findable,
both dat-children absent, UiMeter.Children empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: SliceIds now projects the File id during Select rather than calling
TryGetValue twice (once in Where, once in the local File() helper). Added a
comment noting that OrderBy is stable so X-tie order follows insertion order.
Fix 2: BuildMeter emits a [D.2b] Console.WriteLine when the Type-3 container
count is not exactly 2, surfacing malformed or non-vitals meter elements during
Task 8 conformance testing without disturbing the existing solid-color fallback.
Fix 3: Test 5 adds two explicit NotEqual assertions confirming the
ShowDetail-only overlay sprite (OverlayFile = 0x06007490) did not leak into
FrontRight or FrontTile.
5/5 tests pass, 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hybrid factory mapping ElementInfo.Type to a behavioral widget or the
UiDatElement generic fallback. Type 7 (UIElement_Meter) → UiMeter with
back/front 3-slice ids populated from grandchild image elements; Type 12
(style prototypes / BaseElement stores) → null so the importer skips
them; all other types → UiDatElement. Rect + anchors are set on every
returned widget via ElementReader.ToAnchors.
BuildMeter walks two levels of the element tree: the two Type-3 slice
containers ordered by ReadOrder (back behind, front on top), then within
each container the image children that carry a DirectState ("" key)
ordered by X for left-cap/center-tile/right-cap. The expand-detail
overlay (present in the front container with only named ShowDetail/
HideDetail states and no "" entry) is excluded by the TryGetValue("")
filter automatically — no name-matching needed.
Fill/Label providers are intentionally NOT set here; Task 6
(VitalsController) binds them to live stat data.
5 TDD tests: Type7→UiMeter, UnknownType→UiDatElement, Type12→null,
rect+anchors propagation, and meter slice extraction with overlay exclusion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generic fallback widget for every LayoutDesc element type without a
dedicated behavioral widget (chrome corners/edges, drag bars, resize grips).
Holds an ElementInfo + active-state name; draws that state's media by tiling
(UV-repeat on both S+T axes, matching ImgTex::TileCSI). DrawMode constants
documented per format spec §6 (Undefined=0, Normal=1, Overlay=2,
Alphablend=3 — no Stretch mode). Plan 1: all modes render as the same
alpha-blended tiled quad; per-mode branches deferred to Plan 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Merge: defensive copy `new List<ElementInfo>(derived.Children)` so a
later mutation of the merged result or the input can't corrupt the other
- Merge: add comment on Width/Height 0-sentinel (Plan-1 safe; Plan-2
limitation and float?-upgrade path documented inline)
- Test: replace mid-sentence "Wait —" authoring trace in
EdgeFlagsToAnchors_ValueThree_FallsBackToTopLeft with a clean
conclusion-first summary of the value-3 mapping rule
9/9 ElementReaderTests pass; 0 build errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 2 of the LayoutDesc Importer (Plan 1 — vitals conformance).
- ElementInfo POCO: GL-free/dat-free snapshot of a resolved layout element.
Shape matches the plan spec exactly (Id, Type as uint, X/Y/Width/Height as
float, raw Left/Top/Right/Bottom uint edge flags, ReadOrder, FontDid, StateMedia
dict, Children list). Tasks 3–6 depend on this shape.
- ElementReader.ToAnchors(uint,uint,uint,uint): maps dat edge-flag values
(0=none, 1=near-pin, 2=far-pin, 3=floating-center, 4=stretch) to AnchorEdges
bit flags. Corrects the plan's stale assumption that value 4 was the only anchor
trigger; the verified format doc §4 shows 1→Left/Top, 2→Right/Bottom, 4→both.
All-zero falls back to Left|Top (default pin top-left).
- ElementReader.Merge(base_, derived): inheritance merge mirroring BaseElement/
BaseLayoutId. Derived scalars win when non-zero; position/edge-flags/ReadOrder
always from derived; StateMedia merged (base defaults, derived overrides);
Children from derived only.
TDD: tests written first (9 tests covering ToAnchors near-pin/far-pin/stretch/
zero/value-3, Merge scalar override/font inheritance/StateMedia merge/children).
All 9 pass; dotnet build 0 errors 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>