Two independent Opus reviews of 01b98ca30 (Slice A - movable, collapsible
plugin shelf) found blocking design and behavior issues plus several
should-fix gaps. All addressed in this commit:
1. Grip/toggle are now REAL children instead of a drawn band + a
cursor-position-dependent HandlesClick override + an OnEvent toggle
hack: ShelfGripPanel (WindowMoveHandle=true) spans the top band minus
the toggle width; a UiSimpleButton toggle sits beside it (HandlesClick
already wins at UiRoot.OnMouseDown before the Draggable-window
fallback). The shelf's own Draggable is now FALSE - verified against
UiRoot.FindDragHandleWindow, which never reads a window's own
Draggable flag at all (it walks for a WindowMoveHandle ancestor-or-self
then climbs to the nearest child of UiRoot), so Draggable=true was
never required for the grip to work and only armed the whole-window-
drag fallback for clicks on the shelf's own padding - exactly the
behavior the review asked NOT to have. The two Assert.Single(shelf.
Children) test sites now filter by the (now internal) PluginShelfButton
type instead of asserting child count.
2. Deleted the per-tick "always highest ZOrder" raise in OnTick. It fought
RetailDialogFactory.Tick's own per-frame dialog re-raise, so a dialog
opened while the shelf was visible could never end up on top of it.
Registration's ordinary press-to-raise (a grip press calls
BringToFront before the drag starts) remains.
3. Collapse and hide/show intent are now persisted through their own
channel. RetainedWindowState gained a nullable RequestedVisible;
RetailWindowHandle gained an internal StateChanged event that
RetailWindowLayoutPersistence subscribes to (alongside Moved/Resized/
Shown/Hidden) and that the shelf raises after a collapse toggle or
Show/Hide. Capture() now persists state.RequestedVisible (the panel's
own intent) instead of the derived IsVisible, so an availability hide
(last plugin window unregistered) is never mistaken for a user hide.
WindowNames.PluginShelf is now one of RetailUiRuntime's state-managed
visibility windows, so Apply() restores the intent through
RestoreWindowState directly rather than via Show()/Hide().
4. RetailWindowLayoutPersistence now subscribes to
RetailWindowManager.WindowRegistered/WindowUnregistered so a window
(a plugin window loaded after startup, or the shelf on any path that
constructs persistence first) attaches even when it registers after
persistence already exists.
5. Reflow()'s default height argument is now nullable and falls back to
the last height OnTick actually measured (or unbounded if none yet),
instead of always forcing a single-column layout - every call site
OTHER than OnTick's own row-wrap (Add, unregister, Show/Hide, the
collapse toggle, RestoreWindowState) used to collapse a wrapped
multi-column layout to one column for a frame.
6. _userPositioned is now flipped only when the handle's position differs
from the recorded dock placement, not on every RetailWindowHandle.
Moved (which fires unconditionally on any completed window-drag
release, including a zero-movement grip click, and on any
ClampAllToScreen reachable-clamp).
7. New tests cover: a press+drag starting on an entry button does not
move the shelf; the removed per-tick raise (a sibling with higher
ZOrder keeps it after a tick); TogglePluginManager's hidden-and-
collapsed -> shown-and-expanded / visible -> hidden transitions at the
shelf API (no RetailUiRuntime construction harness exists in this test
suite to exercise the action-routing switch itself - the "no plugin
windows registered" message branch is therefore not covered here).
8. The hide branch of TogglePluginManager now displays "Plugin shelf
hidden. Press Shift+Ctrl+F1 to show it again."; the show branch stays
silent.
9. The collapse toggle now draws ASCII '<'/'>' instead of the DAT-font-
dependent '«'/'»' glyphs (the only use of those code points in the App
UI, silently dropped by UiDatFont when absent), and gets the same
DatFont + bitmap fallback the shelf's entry buttons already have
through UiSimpleButton. A new installed-DAT test pins that the default
font actually carries both ASCII glyphs.
10. ResizeX/ResizeY are false on the shelf so a restored layout's saved
dimensions can never stomp the derived Width/Height via ResizeTo.
11. WindowNames.PluginShelf replaces the "plugin-shelf" literal at every
site (RetailUiRuntime, docs comments, tests).
12. The grip dims to half opacity while RetailWindowManager.IsLocked, the
same visual cue every other retail window gets (the shelf's grip has
no DatElementId, so RetailWindowLockPresentationController's authored-
chrome dimming does not reach it on its own).
Every new test was verified to fail against the pre-fix source: reverting
src/AcDream.App/UI/PluginSidePanel.cs, IRetainedWindowStateController.cs,
RetailWindowHandle.cs, WindowNames.cs, and RetailUiRuntime.cs to their
01b98ca30 state makes the whole PluginSidePanelTests.cs file fail to even
compile (missing WindowNames.PluginShelf, the now-internal
PluginShelfButton type, and RetainedWindowState.RequestedVisible);
reverting RetailWindowLayoutPersistence.cs alone (fixed source elsewhere)
makes WindowRegisteredAfterConstruction_StillRoundTrips fail at runtime
with a null saved layout, confirming finding 4 in isolation.
Verified: dotnet build src/AcDream.App (Release) green; dotnet test
tests/AcDream.App.Tests (Release) targeted filter
(PluginSidePanel|RetailWindowLayout|Markup|UiRootInput) 105/105 green;
full suite 7303 passed / 97 skipped / 36 failed (identical failure set to
the pre-fix-round baseline - installed-DAT live-mount probes, Linux-only
pacing/credential tests, and alpha-flush COUNT-only conformance
divergences, none touching plugin UI; +9 tests, all passing, over the
prior 7294/97/36 baseline).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a
second (background) glyph atlas per font, dilated 2px on every side,
plus two border-pixel scalars (Font.NumHorizontalBorderPixels/
NumVerticalBorderPixels) that acdream's font reader never read — so
even the pre-existing outline parameter drew almost nothing once
enabled. Landed together (either half alone is a no-op or a
regression):
- UiDatFont carries BorderX/BorderY from the DAT font resource.
- UiRenderContext.DrawStringDat inflates the background blit's source
and destination rect by that margin and restructures into retail's
exact two-pass whole-string outline-then-fill model
(UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for
fonts with no background atlas. Corrects the stale "property 0xd"
comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor).
- LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/
OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/
ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every
authored-outline element across the DAT set is correct at once.
- SpewBox: RetailFontId corrected from a round-3 heuristic
(0x40000025) to the actually-authored 0x40000001 (18px bold serif),
Outline=true set on the controller's UiText. Fill colour stays the
user-gate-round-1-pinned yellow — font atlases are alpha-only
(PFID_A8), so there is no baked shading that could explain the
screenshot's gold as anything other than the outline itself.
- Chat transcript: default fill now seeds from its authored
ARGB(255,204,204,204) instead of an unrelated color-table slot
(ChatTranscriptRenderer.BuildLines takes the transcript's own
DefaultColor as a parameter); the 34-entry LogTextType table is
untouched, and every existing CH1 conformance test stays green
unmodified.
Regenerated the committed chat_2100006f.json fixture from the real
installed DAT, confirming end to end (not by missing-field default)
that the transcript carries no outline.
Tests: font-reader border fields + inflation math pinned against the
real DAT font, two-pass draw ordering/tint/inflation via a new
TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22
import at both the ElementReader.Merge and StateDesc-property layers,
SpewBox font/outline, and the chat default-shade seed with the color
table proven untouched.
Full Release suite: 12,610 passed / 4 skipped / 0 failed
(AcDream.slnx, complete solution).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>