test(vtank): slice 7 fix round B item 1 — retire the red plain-menu-scroll pin

Menu_Scroll_DrawsAScrollbarWhenTheMarkupItemCountOverflowsTheVisibleRows was
RED at HEAD: <menu> now defaults to the plain style (RetailButtonArt=false,
the "BIG gold/yellow buttons" owner fix), so a markup menu with no style
attribute draws DrawScrollablePopupPlain's flat DrawFill thumb, never the
retail sprite path (DrawPopupScrollbar) the test asserted on.

Fix: give the existing test style="retail" so it keeps proving the retail
sprite path draws ScrollThumbSprite. Add a plain sibling
(Menu_Scroll_DrawsAPlainFlatThumbFillWhenTheMarkupItemCountOverflowsTheVisibleRows)
that proves the DEFAULT (no style attribute) path draws the flat thumb fill:
an untextured (texture=0) quad sized ScrollbarWidth-2 wide, tinted
PlainBorderColor. Untextured DrawFill calls all batch into one texture=0
render segment, so the assertion scans per-quad (6 verts x 8 floats) inside
each segment rather than treating a whole segment as one quad.

Mutation check: commenting out DrawPopupScrollbarPlain's thumb DrawFill call
turned the new test red ("expected a plain flat thumb fill... among the
drawn segments"); restoring the call turns it green again.

tests/AcDream.Plugins.MossTank.Tests: 660/660 (unchanged).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276 passed / 3
skipped / 279 total (was 274 passed / 1 failed / 3 skipped / 278 total).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 11:24:12 +02:00
parent dbdde0783d
commit 040d5f3d81

View file

@ -460,6 +460,13 @@ public class MarkupDocumentTests
Assert.NotEqual(0u, menu.ScrollDownSprite);
}
// Campaign VT slice 7 fix round B item 1: <menu> now defaults to the
// PLAIN style (RetailButtonArt=false — see the "BIG gold/yellow buttons"
// owner fix), so a markup menu with no style attribute never reaches the
// retail sprite-scrollbar path (DrawPopupScrollbar) any more — it draws
// DrawPopupScrollbarPlain's flat DrawFill thumb instead. This test now
// opts INTO style="retail" to keep exercising the retail sprite path;
// the plain default gets its own sibling below.
[Fact]
public void Menu_Scroll_DrawsAScrollbarWhenTheMarkupItemCountOverflowsTheVisibleRows()
{
@ -467,7 +474,7 @@ public class MarkupDocumentTests
<panel x="0" y="0" w="160" h="40">
<menu x="4" y="4" w="120" h="18" items="{ManyChoices}"
selected="{Selected}" onchange="{SelectChoice}"
rows="6" rowheight="18" scroll="true" />
rows="6" rowheight="18" scroll="true" style="retail" />
</panel>
""";
var binding = new ManyChoicesBinding();
@ -475,6 +482,7 @@ public class MarkupDocumentTests
UiNineSlicePanel panel = MarkupDocument.Build(
xml, binding, id => (id, 16, 16));
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
Assert.True(menu.RetailButtonArt);
var device = new RecordingGpuDevice();
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
@ -495,6 +503,72 @@ public class MarkupDocumentTests
s => s.Texture == menu.ScrollThumbSprite);
}
// Plain sibling (default style, no style attribute): the flat thumb fill
// is an untextured DrawFill quad (UiTextureTableHandle.None == 0) sized
// ScrollbarWidth-2 wide and tinted PlainBorderColor — see
// UiMenu.DrawPopupScrollbarPlain. This is what actually renders today for
// any markup menu that doesn't opt into style="retail".
[Fact]
public void Menu_Scroll_DrawsAPlainFlatThumbFillWhenTheMarkupItemCountOverflowsTheVisibleRows()
{
const string xml = """
<panel x="0" y="0" w="160" h="40">
<menu x="4" y="4" w="120" h="18" items="{ManyChoices}"
selected="{Selected}" onchange="{SelectChoice}"
rows="6" rowheight="18" scroll="true" />
</panel>
""";
var binding = new ManyChoicesBinding();
UiNineSlicePanel panel = MarkupDocument.Build(
xml, binding, id => (id, 16, 16));
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
Assert.False(menu.RetailButtonArt);
var device = new RecordingGpuDevice();
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
renderer.Begin(new Vector2(800f, 600f));
var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f));
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5));
menu.OnEvent(new UiEvent(0, menu, UiEventType.Scroll, Data0: 0));
Assert.True(menu.PopupScroll.HasOverflow);
menu.DrawOverlays(ctx);
// An untextured segment batches every DrawFill call in submission order
// together (same texture=0 for background/border/row-fills/thumb), so
// scan per-QUAD (6 verts x 8 floats = 48 floats) inside each segment
// rather than treating a whole segment as one quad.
const int floatsPerQuad = 6 * 8;
float expectedThumbWidth = menu.ScrollbarWidth - 2f;
bool foundThumb = renderer.DebugSpriteSegmentVerts.Any(s =>
{
if (s.Texture != 0u) return false;
for (int q = 0; q + floatsPerQuad <= s.Verts.Count; q += floatsPerQuad)
{
float xMin = float.MaxValue, xMax = float.MinValue;
for (int v = 0; v < 6; v++)
{
float x = s.Verts[q + v * 8];
if (x < xMin) xMin = x;
if (x > xMax) xMax = x;
}
float width = xMax - xMin;
if (MathF.Abs(width - expectedThumbWidth) > 0.5f) continue;
float r = s.Verts[q + 4], g = s.Verts[q + 5], b = s.Verts[q + 6], a = s.Verts[q + 7];
if (MathF.Abs(r - menu.PlainBorderColor.X) < 0.01f
&& MathF.Abs(g - menu.PlainBorderColor.Y) < 0.01f
&& MathF.Abs(b - menu.PlainBorderColor.Z) < 0.01f
&& MathF.Abs(a - menu.PlainBorderColor.W) < 0.01f)
return true;
}
return false;
});
Assert.True(foundThumb, "expected a plain flat thumb fill (untextured quad, "
+ $"width~{expectedThumbWidth}, tinted PlainBorderColor) among the drawn segments");
}
private sealed class ManyChoicesBinding
{
public string Selected { get; private set; } = "Item 0";