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 ButtonGap = 4f;
|
||||||
private const float DefaultTop = 116f;
|
private const float DefaultTop = 116f;
|
||||||
|
|
||||||
/// <summary>Height of the top drag-grip band. Also the collapsed shelf's
|
/// <summary>
|
||||||
/// total height (only the grip remains when collapsed).</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;
|
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
|
/// <summary>Width of the collapse-toggle button, anchored to the grip's
|
||||||
/// right end.</summary>
|
/// right end.</summary>
|
||||||
private const float ToggleWidth = 16f;
|
private const float ToggleWidth = 16f;
|
||||||
|
|
@ -179,7 +206,7 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
|
||||||
_font = font;
|
_font = font;
|
||||||
|
|
||||||
Width = ButtonExtent + OuterPadding * 2f;
|
Width = ButtonExtent + OuterPadding * 2f;
|
||||||
Height = GripHeight + OuterPadding * 2f;
|
Height = ExpandedGripBandHeight + OuterPadding * 2f;
|
||||||
Top = DefaultTop;
|
Top = DefaultTop;
|
||||||
Anchors = AnchorEdges.None;
|
Anchors = AnchorEdges.None;
|
||||||
// See the class doc for why this stays false: FindDragHandleWindow does
|
// 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.
|
// same as before Slice A.
|
||||||
float availableHeight = MathF.Max(
|
float availableHeight = MathF.Max(
|
||||||
ButtonExtent + OuterPadding * 2f,
|
ButtonExtent + OuterPadding * 2f,
|
||||||
parent.Height - Top - GripHeight - OuterPadding);
|
parent.Height - Top - ExpandedGripBandHeight - OuterPadding);
|
||||||
if (MathF.Abs(availableHeight - _lastLayoutHeight) > 0.5f)
|
if (MathF.Abs(availableHeight - _lastLayoutHeight) > 0.5f)
|
||||||
{
|
{
|
||||||
_lastLayoutHeight = availableHeight;
|
_lastLayoutHeight = availableHeight;
|
||||||
|
|
@ -580,18 +607,39 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
|
||||||
/// <summary>Repositions the grip/toggle children to span the current
|
/// <summary>Repositions the grip/toggle children to span the current
|
||||||
/// Width — called whenever <see cref="Reflow"/> changes it (entries added/
|
/// Width — called whenever <see cref="Reflow"/> changes it (entries added/
|
||||||
/// removed, collapse toggled, row-wrap) so the toggle always sits flush
|
/// 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()
|
private void LayoutChrome()
|
||||||
{
|
{
|
||||||
|
float bandHeight = _collapsed ? ButtonExtent : ExpandedGripBandHeight;
|
||||||
_grip.Left = 0f;
|
_grip.Left = 0f;
|
||||||
_grip.Top = 0f;
|
_grip.Top = 0f;
|
||||||
_grip.Width = MathF.Max(0f, Width - ToggleWidth);
|
_grip.Width = MathF.Max(0f, Width - ToggleWidth);
|
||||||
_grip.Height = GripHeight;
|
_grip.Height = bandHeight;
|
||||||
|
|
||||||
_toggle.Left = Width - ToggleWidth;
|
_toggle.Left = Width - ToggleWidth;
|
||||||
_toggle.Top = 0f;
|
_toggle.Top = 0f;
|
||||||
_toggle.Width = ToggleWidth;
|
_toggle.Width = ToggleWidth;
|
||||||
_toggle.Height = GripHeight;
|
_toggle.Height = bandHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -636,7 +684,7 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
|
||||||
int row = index % maximumRows;
|
int row = index % maximumRows;
|
||||||
entry.Button.Left = OuterPadding
|
entry.Button.Left = OuterPadding
|
||||||
+ column * (ButtonExtent + ButtonGap);
|
+ column * (ButtonExtent + ButtonGap);
|
||||||
entry.Button.Top = GripHeight + OuterPadding
|
entry.Button.Top = ExpandedGripBandHeight + OuterPadding
|
||||||
+ row * (ButtonExtent + ButtonGap);
|
+ row * (ButtonExtent + ButtonGap);
|
||||||
entry.Button.Visible = !_collapsed;
|
entry.Button.Visible = !_collapsed;
|
||||||
index++;
|
index++;
|
||||||
|
|
@ -647,15 +695,18 @@ public sealed class PluginSidePanel : UiPanel, IDisposable, IRetainedWindowState
|
||||||
|
|
||||||
if (_collapsed)
|
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;
|
Width = CollapsedWidth;
|
||||||
Height = GripHeight;
|
Height = ButtonExtent;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Width = OuterPadding * 2f
|
Width = OuterPadding * 2f
|
||||||
+ columns * ButtonExtent
|
+ columns * ButtonExtent
|
||||||
+ Math.Max(0, columns - 1) * ButtonGap;
|
+ Math.Max(0, columns - 1) * ButtonGap;
|
||||||
Height = GripHeight
|
Height = ExpandedGripBandHeight
|
||||||
+ OuterPadding * 2f
|
+ OuterPadding * 2f
|
||||||
+ rows * ButtonExtent
|
+ rows * ButtonExtent
|
||||||
+ Math.Max(0, rows - 1) * ButtonGap;
|
+ 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// WindowMoveHandle subtree can start a move (see the class doc). Proven
|
||||||
// to fail against the PRE-4fada238e shelf: temporarily flipping the
|
// 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
|
root.Tick(0.016d, 16L); // establishes the initial right-edge dock
|
||||||
|
|
||||||
int pressX = (int)shelf.Left + 2; // left of button.Left (4)
|
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 leftBefore = shelf.Left;
|
||||||
float topBefore = shelf.Top;
|
float topBefore = shelf.Top;
|
||||||
|
|
||||||
|
|
@ -370,6 +375,46 @@ public sealed class PluginSidePanelTests
|
||||||
Assert.False(shelf.CaptureWindowState().Collapsed);
|
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]
|
[Fact]
|
||||||
public void ToggleVisibility_ShowHideShow_AndStaysHiddenWhenANewWindowRegisters()
|
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