fix(plugin-ui): size the shelf grip from the DAT font and make the collapsed tab findable

Owner report: "I dont see the < after I minimize the window" — after
collapsing the plugin shelf with the > toggle, the < (expand) glyph was not
visible.

Root cause, established with a real-DAT draw probe (font 0x40000000) before
changing anything: '<' and '>' share IDENTICAL glyph metrics (OffsetY=4,
Width=5, Height=7, VerticalOffsetBefore=5), so there is no per-glyph
asymmetry to explain "I see one but not the other." Against the pre-fix 12px
grip band, the toggle's FILL glyph plane measured fully INSIDE the band in
both states (local y=[3,10] of [0,12]) — the "16px line box overhangs a 12px
band" theory alone does not erase the glyph, so a bare clip fix would not
have addressed the report. What IS true: the border-inflated OUTLINE
(background/shadow) plane, drawn first per retail's UIElement_Text::DrawSelf,
spans y=[-1,14] before clipping and was cropped by the band's self-clip
(UiElement.ClipsChildren) to exactly [0,12] — a real but minor defect. The
actual explanation for the report is discoverability: the collapsed shelf
shrank to a bare 24x12 near-black sliver at the screen edge, several times
smaller than any other clickable affordance in the UI — easy to overlook even
though its pixels were, in fact, being drawn.

Fix (src/AcDream.App/UI/PluginSidePanel.cs):
- ExpandedGripBandHeight (new internal property) derives the EXPANDED
  grip/toggle band from the real font metrics — max(12, font.LineHeight + 2)
  — so neither the fill nor the border-inflated outline plane can clip for
  any font; the 12px constant remains only as the bitmap-font fallback.
  Threaded through the ctor's initial Height, OnTick's row-wrap height calc,
  LayoutChrome, and Reflow's entry-Top/expanded-Height math.
- The COLLAPSED tab is now ButtonExtent (28px) tall instead of the 12px grip
  band — the same size as an ordinary entry button — with the toggle glyph
  filling and centering in the taller band. This is the actual fix for the
  report: the collapsed affordance is now button-sized and findable, not a
  bug-for-bug-identical-but-larger clip fix.

Tests:
- tests/AcDream.App.Tests/UI/PluginSidePanelToggleGlyphClipTests.cs (new,
  Lane=InstalledDat): loads the real DAT font, builds a live shelf, and
  proves via TextRenderer.DebugSpriteSegmentVerts that the toggle's ink (fill
  + outline) is fully contained in its own clip band in BOTH the expanded
  ('>') and real-click-collapsed ('<') states, plus that the collapsed tab is
  button-sized. Verified failing against the pre-fix code (git stash of just
  this file) with concrete numbers: computed unclipped span [-1,14] does not
  fit inside the 12px band; collapsed height measured 12 (not button-sized).
  Passes after the fix.
- tests/AcDream.App.Tests/UI/PluginSidePanelTests.cs: added
  CollapseThenExpand_WhileStillDocked_ReturnsToTheIdenticalLeftAndTop (the
  dock/anchor invariant survives the collapsed-height change), and derived
  Drag_StartingOnShelfPadding_DoesNotMoveTheShelf's press-below-the-grip-band
  Y coordinate from the new ExpandedGripBandHeight accessor instead of a
  re-hard-coded literal.

Verification: dotnet build (App + tests) green. Filtered run
(PluginSidePanel|UiDatFont|Markup|UiRootInput) 131/131 passed, 0 skipped —
the InstalledDat lane tests actually ran (DAT dir resolved). Full
AcDream.App.Tests suite: 7334 passed / 97 skipped / 36 failed (baseline was
7331/97/36) — the 3 new tests are the only delta; the 36 failed test names
are byte-identical to the pre-existing set (cathedral collision installed-dat
gates, alpha-flush conformance, layout live-mount probes, Linux frame-pacing,
credential resolver — all unrelated to this change).

No retail-divergence register change needed: IA-27 already covers the
plugin shelf's non-retail collapse-toggle glyphs/behavior in general; this
is a bug fix within that already-declared deviation, not a new one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 15:16:03 +02:00
parent 8217a349e0
commit 2ebcc01640
3 changed files with 377 additions and 12 deletions

View file

@ -75,10 +75,37 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
private const float ButtonGap = 4f;
private const float DefaultTop = 116f;
/// <summary>Height of the top drag-grip band. Also the collapsed shelf's
/// total height (only the grip remains when collapsed).</summary>
/// <summary>
/// Minimum/bitmap-fallback height of the top drag-grip band while
/// EXPANDED. Owner-reported defect (2026-09-06): with this literal
/// hard-coded regardless of font, the retail 16px-tall dat-font glyph
/// line box (<see cref="UiDatFont.LineHeight"/>) overhangs a 12px band —
/// measured against the real DAT (font 0x40000000): the outline
/// (background/shadow) plane's inflated quad is clipped by 1px at the
/// top and 2px at the bottom of a 12px band, even though the FILL glyph
/// itself happens to fit (measured '&lt;'/'&gt;': OffsetY=4 Height=7
/// VerticalOffsetBefore=5 — the fill quad lands at local y=[3,10],
/// inside [0,12]; the border-inflated outline quad would span
/// y=[-1,14], clipped to [0,12]). <see cref="ExpandedGripBandHeight"/>
/// now derives the real band from the font's own metrics so neither
/// plane clips for any font, and this constant remains only as the
/// bitmap-font (no <see cref="UiDatFont"/>) fallback.</summary>
private const float GripHeight = 12f;
/// <summary>
/// The actual EXPANDED grip/toggle band height for the current font: the
/// bitmap fallback (<see cref="GripHeight"/>) when there is no
/// <see cref="UiDatFont"/>, else <c>max(GripHeight, font.LineHeight + 2)</c>
/// so the whole-line-height glyph box (fill AND the border-inflated
/// outline plane) always fits with margin to spare. <c>internal</c> so
/// tests can derive expected coordinates from the real value instead of
/// re-hard-coding the constant this replaces (the shelf's own EXPANDED
/// band height is always this value, never <see cref="GripHeight"/>
/// directly, once a dat font is supplied).
/// </summary>
internal float ExpandedGripBandHeight =>
_font is { } f ? MathF.Max(GripHeight, f.LineHeight + 2f) : GripHeight;
/// <summary>Width of the collapse-toggle button, anchored to the grip's
/// right end.</summary>
private const float ToggleWidth = 16f;
@ -179,7 +206,7 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
_font = font;
Width = ButtonExtent + OuterPadding * 2f;
Height = GripHeight + OuterPadding * 2f;
Height = ExpandedGripBandHeight + OuterPadding * 2f;
Top = DefaultTop;
Anchors = AnchorEdges.None;
// See the class doc for why this stays false: FindDragHandleWindow does
@ -325,7 +352,7 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
// same as before Slice A.
float availableHeight = MathF.Max(
ButtonExtent + OuterPadding * 2f,
parent.Height - Top - GripHeight - OuterPadding);
parent.Height - Top - ExpandedGripBandHeight - OuterPadding);
if (MathF.Abs(availableHeight - _lastLayoutHeight) > 0.5f)
{
_lastLayoutHeight = availableHeight;
@ -580,18 +607,39 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
/// <summary>Repositions the grip/toggle children to span the current
/// Width — called whenever <see cref="Reflow"/> changes it (entries added/
/// removed, collapse toggled, row-wrap) so the toggle always sits flush
/// against the shelf's right edge and the grip fills the rest of the band.</summary>
/// against the shelf's right edge and the grip fills the rest of the band.
///
/// <para>
/// Owner-reported defect fix (2026-09-06, "I dont see the &lt; after I
/// minimize the window"): while COLLAPSED the band is <see cref="ButtonExtent"/>
/// tall (matching an ordinary entry button) instead of the 12px grip band —
/// direct evidence (a real-DAT draw probe, see
/// <c>PluginSidePanelToggleGlyphClipTests</c>) showed the toggle glyph's FILL
/// plane was never actually erased by the 12px band's self-clip (it measured
/// well inside it), so a bare clip fix would not have addressed the report;
/// the real problem is that the collapsed shelf shrinks to a barely-visible
/// 24x12 near-black sliver at the screen edge — several times smaller than
/// every other clickable affordance in the UI. Growing the collapsed tab to
/// button size makes it findable again, and the toggle glyph fills that
/// taller band (vertically centered by the unchanged <c>(Height -
/// dat.LineHeight) * 0.5f</c> formula in <see cref="UiSimpleButton.OnDraw"/>).
/// While EXPANDED the band uses <see cref="ExpandedGripBandHeight"/> (derived
/// from the font, not the bitmap-fallback constant) so neither the fill nor
/// the border-inflated outline glyph plane clips for any font metrics.
/// </para>
/// </summary>
private void LayoutChrome()
{
float bandHeight = _collapsed ? ButtonExtent : ExpandedGripBandHeight;
_grip.Left = 0f;
_grip.Top = 0f;
_grip.Width = MathF.Max(0f, Width - ToggleWidth);
_grip.Height = GripHeight;
_grip.Height = bandHeight;
_toggle.Left = Width - ToggleWidth;
_toggle.Top = 0f;
_toggle.Width = ToggleWidth;
_toggle.Height = GripHeight;
_toggle.Height = bandHeight;
}
/// <summary>
@ -636,7 +684,7 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
int row = index % maximumRows;
entry.Button.Left = OuterPadding
+ column * (ButtonExtent + ButtonGap);
entry.Button.Top = GripHeight + OuterPadding
entry.Button.Top = ExpandedGripBandHeight + OuterPadding
+ row * (ButtonExtent + ButtonGap);
entry.Button.Visible = !_collapsed;
index++;
@ -647,15 +695,18 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
if (_collapsed)
{
// Findability fix: the collapsed tab is exactly one entry
// button's height, not the thin grip band — see LayoutChrome's
// doc comment for the owner-reported symptom this addresses.
Width = CollapsedWidth;
Height = GripHeight;
Height = ButtonExtent;
}
else
{
Width = OuterPadding * 2f
+ columns * ButtonExtent
+ Math.Max(0, columns - 1) * ButtonGap;
Height = GripHeight
Height = ExpandedGripBandHeight
+ OuterPadding * 2f
+ rows * ButtonExtent
+ Math.Max(0, rows - 1) * ButtonGap;

View file

@ -249,7 +249,7 @@ public sealed class PluginSidePanelTests
// NEW-1 (residual round, docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md
// Slice A): pins the actual Draggable:true -> false change 4fada238e made.
// A press on the shelf's own PADDING — left of the first entry button's
// Left=4 (OuterPadding), below the 12px grip band — must never promote
// Left=4 (OuterPadding), below the grip band — must never promote
// to a whole-window drag now that Draggable is false; only the grip's
// WindowMoveHandle subtree can start a move (see the class doc). Proven
// to fail against the PRE-4fada238e shelf: temporarily flipping the
@ -274,7 +274,12 @@ public sealed class PluginSidePanelTests
root.Tick(0.016d, 16L); // establishes the initial right-edge dock
int pressX = (int)shelf.Left + 2; // left of button.Left (4)
int pressY = (int)shelf.Top + 20; // below the grip band (12)
// Below the grip band: derived from the shelf's own EXPANDED band
// height (owner-reported-defect fix, docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md)
// rather than a re-hard-coded literal — with font: null this is
// still the 12px bitmap-fallback band, so the numeric press point is
// unchanged, but the intent now survives a font change.
int pressY = (int)shelf.Top + (int)shelf.ExpandedGripBandHeight + 8;
float leftBefore = shelf.Left;
float topBefore = shelf.Top;
@ -370,6 +375,46 @@ public sealed class PluginSidePanelTests
Assert.False(shelf.CaptureWindowState().Collapsed);
}
[Fact]
public void CollapseThenExpand_WhileStillDocked_ReturnsToTheIdenticalLeftAndTop()
{
// Owner-reported defect fix (docs/plans/2026-09-06-plugin-shelf-and-dat-icons.md):
// the collapsed tab's HEIGHT changed (button-sized, not the thin grip
// band) — confirms that change alone does not disturb the docked
// anchor math: collapsing shifts Left to preserve the right edge (a
// width change), and expanding back must return to the EXACT same
// Left/Top, not merely an equal Width.
var root = new UiRoot { Width = 800f, Height = 600f };
using var shelf = new PluginSidePanel(
root.WindowManager, _ => (0u, 0, 0), font: null);
root.AddChild(shelf);
root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf);
var frame = new UiPanel { Width = 200f, Height = 100f };
root.AddChild(frame);
RetailWindowHandle pluginHandle = root.WindowManager.Register(
"plugin:acdream.test:main", frame);
shelf.Add(
new PluginUiOwner("acdream.test", "Test Plugin"),
new PluginPanelDescriptor("main", "Test Plugin"),
pluginHandle);
root.Tick(0.016d, 16L); // establishes the initial right-edge dock
float leftBefore = shelf.Left;
float topBefore = shelf.Top;
shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true));
// Width shrank while still docked, so Left shifted right to keep the
// right edge fixed; Top never moves on a collapse/expand.
Assert.NotEqual(leftBefore, shelf.Left);
Assert.Equal(topBefore, shelf.Top);
shelf.RestoreWindowState(new RetainedWindowState(Collapsed: false));
Assert.Equal(leftBefore, shelf.Left, precision: 3);
Assert.Equal(topBefore, shelf.Top, precision: 3);
}
[Fact]
public void ToggleVisibility_ShowHideShow_AndStaysHiddenWhenANewWindowRegisters()
{

View file

@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.App.UI;
using AcDream.Content;
using AcDream.Plugin.Abstractions;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using SysEnv = System.Environment;
namespace AcDream.App.Tests.UI;
/// <summary>
/// Owner-reported defect (2026-09-06): "I dont see the &lt; after I minimize
/// the window" — after collapsing the plugin shelf with the <c>&gt;</c>
/// toggle, the <c>&lt;</c> (expand) glyph was not visible.
///
/// <para>
/// Reproduction findings (a real-DAT draw probe against font 0x40000000, the
/// shelf's default font): '&lt;' and '&gt;' share IDENTICAL glyph metrics
/// (OffsetY=4, Width=5, Height=7, VerticalOffsetBefore=5 — symmetric
/// characters, as expected), so there is no per-glyph asymmetry to explain
/// "I see one but not the other." Against the PRE-FIX 12px grip band
/// (<c>GripHeight</c>), the FILL glyph plane measured fully INSIDE the band
/// (local y=[3,10] of [0,12]) in both collapsed and expanded states — the
/// text's "16px line box overhangs a 12px band" theory does not, on its own,
/// erase the glyph. What IS measurably true: the border-inflated OUTLINE
/// (background/shadow) plane, which retail draws first
/// (<c>UIElement_Text::DrawSelf</c>), spans y=[-1,14] before clipping and was
/// cropped by the 12px band's self-clip (<c>UiElement.ClipsChildren</c>) to
/// exactly [0,12] — a real, if minor (1px top / 2px bottom), defect. The
/// PRIMARY explanation for the report is discoverability, not erasure: the
/// collapsed shelf shrank to a bare 24x12 near-black sliver at the screen
/// edge, several times smaller than any other clickable affordance in the
/// UI, easy to overlook even though its pixels were, in fact, being drawn.
/// </para>
///
/// <para>
/// Fix: <see cref="PluginSidePanel.ExpandedGripBandHeight"/> derives the
/// EXPANDED band from the real font metrics
/// (<c>max(12, font.LineHeight + 2)</c>) so neither the fill nor the
/// border-inflated outline plane can ever clip, for any font. The COLLAPSED
/// tab is now <c>ButtonExtent</c> (28px) tall — the same size as an ordinary
/// entry button — instead of the 12px grip band, making it findable. This
/// file pins both: full containment of the measured ink (fill AND outline)
/// inside the toggle's own clip rect in BOTH states against the REAL
/// installed DAT font, and the collapsed tab's button-sized geometry.
/// </para>
/// </summary>
public sealed class PluginSidePanelToggleGlyphClipTests
{
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
public IGpuFrame? CurrentFrame => null;
}
private static string? ResolveDatDir()
{
string? fromEnv = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
string defaultDir = Path.Combine(
SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(defaultDir) ? defaultDir : null;
}
/// <summary>Decodes (x0,y0)/(x1,y1) of every quad (6 verts / 8 floats each)
/// in a recorded sprite segment — same 8-floats-per-vertex layout every
/// other draw-level test in this tree decodes
/// (<c>UiRenderContextDrawStringDatOutlineTests.DecodeQuads</c>).</summary>
private static IEnumerable<(float y0, float y1)> DecodeVerticalSpans(IReadOnlyList<float> verts)
{
const int floatsPerVertex = 8;
const int floatsPerQuad = floatsPerVertex * 6;
for (int i = 0; i + floatsPerQuad <= verts.Count; i += floatsPerQuad)
{
float y0 = verts[i + 1];
float y1 = verts[i + 8 + 1];
yield return (y0, y1);
}
}
/// <summary>
/// Independently computes the FULL, unclipped vertical ink span (fill
/// plane union the border-inflated outline plane) that
/// <c>UiRenderContext.DrawStringDatPass</c>/<c>DrawOutlineGlyph</c> would
/// produce for a single centered glyph drawn at
/// <c>(Height - font.LineHeight) * 0.5f</c> (exactly
/// <see cref="UiSimpleButton.OnDraw"/>'s formula) in a box of the given
/// <paramref name="bandHeight"/> — mirroring the production math
/// (<c>UiRenderContext.cs</c>'s <c>baseY</c>/<c>gy</c>/outline-inflation
/// lines) independently, so this test can tell a genuinely unclipped draw
/// apart from one whose recorded (already-clipped) geometry merely
/// happens to fill the band.
/// </summary>
private static (float top, float bottom) ComputeUnclippedInkSpan(
UiDatFont font, char glyph, float bandHeight)
{
Assert.True(font.TryGetGlyph(glyph, out FontCharDesc g), $"font is missing glyph '{glyph}'");
float y = (bandHeight - font.LineHeight) * 0.5f;
float baseY = MathF.Floor(y + 0.5f);
float gy = baseY + g.VerticalOffsetBefore;
float gh = g.Height;
float top = gy;
float bottom = gy + gh;
if (font.HasBackground)
{
float iy = gy - font.BorderY;
float ih = gh + 2f * font.BorderY;
top = MathF.Min(top, iy);
bottom = MathF.Max(bottom, iy + ih);
}
return (top, bottom);
}
[Fact]
[Trait("Lane", "InstalledDat")]
public void ToggleGlyph_InkFullyFitsInsideTheGripBand_ExpandedAndCollapsed()
{
string? datDir = ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var adapter = new DatCollectionAdapter(dats);
var device = new RecordingGpuDevice();
var cache = new TextureCache(device, adapter);
UiDatFont? font = UiDatFont.Load(adapter, cache);
Assert.NotNull(font);
var root = new UiRoot { Width = 800f, Height = 600f };
using var shelf = new PluginSidePanel(root.WindowManager, _ => (0u, 0, 0), font);
root.AddChild(shelf);
root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf);
var frame = new UiPanel { Width = 200f, Height = 100f };
root.AddChild(frame);
RetailWindowHandle pluginHandle = root.WindowManager.Register(
"plugin:acdream.test:main", frame);
shelf.Add(
new PluginUiOwner("acdream.test", "Test Plugin"),
new PluginPanelDescriptor("main", "Test Plugin"),
pluginHandle);
root.Tick(0.016d, 16L);
UiSimpleButton toggle = Assert.Single(
shelf.Children,
c => c is UiSimpleButton && c is not PluginSidePanel.PluginShelfButton) as UiSimpleButton
?? throw new InvalidOperationException("toggle button not found");
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f));
// ── Expanded state ('>') ────────────────────────────────────────
Assert.False(shelf.CaptureWindowState().Collapsed);
AssertGlyphInkFullyContained(font!, '>', toggle, renderer, ctx);
// ── Collapse via the REAL toggle click (production input path,
// not the state controller directly) ───────────────────────────
int toggleX = (int)shelf.Left + (int)shelf.Width - 8;
int toggleY = (int)shelf.Top + 4;
root.OnMouseDown(UiMouseButton.Left, toggleX, toggleY);
root.OnMouseUp(UiMouseButton.Left, toggleX, toggleY);
Assert.True(shelf.CaptureWindowState().Collapsed);
root.Tick(0.016d, 16L);
AssertGlyphInkFullyContained(font!, '<', toggle, renderer, ctx);
}
private static void AssertGlyphInkFullyContained(
UiDatFont font, char glyph, UiSimpleButton toggle, TextRenderer renderer, UiRenderContext ctx)
{
(float expectedTop, float expectedBottom) = ComputeUnclippedInkSpan(font, glyph, toggle.Height);
Assert.True(
expectedTop >= -0.01f && expectedBottom <= toggle.Height + 0.01f,
$"'{glyph}': computed unclipped ink span [{expectedTop},{expectedBottom}] " +
$"does not fit inside the {toggle.Height}px band — the band is too short for this font's metrics " +
$"(LineHeight={font.LineHeight}, BorderY={font.BorderY}).");
// Draw the toggle IN ISOLATION (its own DrawSelfAndChildren call, no
// ancestor transform) so the recorded quads are in the toggle's own
// local [0,Width]x[0,Height] space and reflect only its own self-clip
// (UiElement.ClipsChildren's [0,Width]x[0,Height] push).
renderer.Begin(new Vector2(800f, 600f));
toggle.DrawSelfAndChildren(ctx);
float observedMinY = float.MaxValue, observedMaxY = float.MinValue;
int quadCount = 0;
foreach (var seg in renderer.DebugSpriteSegmentVerts)
{
foreach ((float y0, float y1) in DecodeVerticalSpans(seg.Verts))
{
observedMinY = MathF.Min(observedMinY, MathF.Min(y0, y1));
observedMaxY = MathF.Max(observedMaxY, MathF.Max(y0, y1));
quadCount++;
}
}
Assert.True(quadCount > 0, $"toggle drew no glyph quads at all for '{glyph}'");
// The recorded (already self-clipped) geometry must match the
// INDEPENDENTLY computed unclipped span within a pixel — if the band
// were too short, the recorded span would be narrower than the
// computed one (clipped away), not equal to it.
Assert.Equal(expectedTop, observedMinY, 1);
Assert.Equal(expectedBottom, observedMaxY, 1);
// And, restated directly in terms of the toggle's own clip rect (the
// form the investigation asked for): every recorded quad lies fully
// inside [0, toggle.Height].
Assert.True(observedMinY >= -0.01f, $"'{glyph}': ink top ({observedMinY}) is above the band (0)");
Assert.True(
observedMaxY <= toggle.Height + 0.01f,
$"'{glyph}': ink bottom ({observedMaxY}) overflows the {toggle.Height}px band");
}
[Fact]
[Trait("Lane", "InstalledDat")]
public void CollapsedTab_IsButtonSized_ForFindability()
{
string? datDir = ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
using var adapter = new DatCollectionAdapter(dats);
var device = new RecordingGpuDevice();
var cache = new TextureCache(device, adapter);
UiDatFont? font = UiDatFont.Load(adapter, cache);
Assert.NotNull(font);
var root = new UiRoot { Width = 800f, Height = 600f };
using var shelf = new PluginSidePanel(root.WindowManager, _ => (0u, 0, 0), font);
root.AddChild(shelf);
root.WindowManager.Register(WindowNames.PluginShelf, shelf, shelf, controller: shelf);
var frame = new UiPanel { Width = 200f, Height = 100f };
root.AddChild(frame);
RetailWindowHandle pluginHandle = root.WindowManager.Register(
"plugin:acdream.test:main", frame);
shelf.Add(
new PluginUiOwner("acdream.test", "Test Plugin"),
new PluginPanelDescriptor("main", "Test Plugin"),
pluginHandle);
root.Tick(0.016d, 16L);
float expandedHeight = shelf.Height;
shelf.RestoreWindowState(new RetainedWindowState(Collapsed: true));
// The collapsed tab is button-sized, not the thin 12px grip band —
// this is the actual owner-reported fix (the tab is findable), not
// merely "the glyph is unclipped".
Assert.True(shelf.Height > 20f, $"collapsed shelf height ({shelf.Height}) is not button-sized");
Assert.NotEqual(expandedHeight, shelf.Height);
}
}