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:
parent
8217a349e0
commit
2ebcc01640
3 changed files with 377 additions and 12 deletions
|
|
@ -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 '<'/'>': 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 < 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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 < after I minimize
|
||||
/// the window" — after collapsing the plugin shelf with the <c>></c>
|
||||
/// toggle, the <c><</c> (expand) glyph was not visible.
|
||||
///
|
||||
/// <para>
|
||||
/// Reproduction findings (a real-DAT draw probe against font 0x40000000, the
|
||||
/// shelf's default font): '<' and '>' 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue