merge(vt): plain combo style for plugin <menu> (owner: the gold dropdowns go)
UiMenu.RetailButtonArt (default true; every retail user unchanged); MarkupDocument sets plain for plugin markup, style="retail" opts back. Lead-reviewed diff; 10 new tests; goldens for the retail path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
cfa7030659
5 changed files with 382 additions and 1 deletions
|
|
@ -108,9 +108,17 @@ vanishing from the built tree.
|
|||
| `toggle` | Lamp-style checkbox | `x y w h text checked onclick color` |
|
||||
| `slider` | Horizontal scalar | `x y w h value onchange` |
|
||||
| `field` | Single-line editable text | `x y w h text maxlength clearonsubmit onchange onsubmit color background` |
|
||||
| `menu` | Dropdown selector | `x y w h items selected onchange rows rowheight openupward` |
|
||||
| `menu` | Dropdown selector | `x y w h items selected onchange rows rowheight openupward style` |
|
||||
| `list` | Scrollable row list (+ Slice B icon column, + Campaign VT slice 1 multi-column) | `x y w h selected onchange rowheight` + either the single-column `items colors icons iconkind`, or one-to-many `<column>` children (see "Columns" below) — never both |
|
||||
|
||||
`menu style` is `plain` (the default) or `retail`: retail's gold pushbutton
|
||||
art read as an out-of-place "big yellow button" next to a plugin's own dark
|
||||
list boxes (owner live-client report, 2026-09-07), so a plugin `<menu>` now
|
||||
draws the flat VTank/Decal `HudCombo` box (list-matching fill/border, a
|
||||
left-aligned value, and a small ▾) by default; `style="retail"` opts back
|
||||
into the gold face for a panel that genuinely wants it. Any other value
|
||||
throws `FormatException` at `Build`.
|
||||
|
||||
Common to every element via `ApplyCommon`: `name`/`id` (a stable control
|
||||
name), `visible` (literal `true`/`false` or a bound `bool` property),
|
||||
`enabled` (same rule), and `tooltip` (a literal string or `{Binding}` shown
|
||||
|
|
|
|||
|
|
@ -461,6 +461,7 @@ public static class MarkupDocument
|
|||
Func<string?> menuSelected = BindString(
|
||||
(string?)el.Attribute("selected"),
|
||||
binding);
|
||||
bool menuRetailButtonArt = ValidateMenuStyle((string?)el.Attribute("style"));
|
||||
var menu = new UiMenu
|
||||
{
|
||||
Left = F(el, "x"),
|
||||
|
|
@ -480,6 +481,7 @@ public static class MarkupDocument
|
|||
PopupBgSprite = 0x0600124Cu,
|
||||
ItemNormalSprite = 0x0600124Eu,
|
||||
ItemHighlightSprite = 0x0600124Du,
|
||||
RetailButtonArt = menuRetailButtonArt,
|
||||
ButtonLabelProvider = () => menuSelected() ?? string.Empty,
|
||||
OnSelect = payload =>
|
||||
{
|
||||
|
|
@ -633,6 +635,25 @@ public static class MarkupDocument
|
|||
$"{context} must be did, spell, or item (got \"{other}\")"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Owner live-client report 2026-09-07 ("Those BIG gold/yellow buttons HAS
|
||||
/// to go. That is not how vtank looks."): validates <c><menu
|
||||
/// style="..."></c> and returns the <see cref="UiMenu.RetailButtonArt"/>
|
||||
/// value it selects. Default (attribute absent, or explicit
|
||||
/// <c>style="plain"</c>) is the flat VTank/Decal <c>HudCombo</c> box
|
||||
/// (<c>false</c>) — retail's gold pushbutton art is now an explicit
|
||||
/// <c>style="retail"</c> opt-in for a plugin panel that genuinely wants
|
||||
/// it. Any other value is a Build-time author error, same rule as
|
||||
/// <see cref="ValidateIconKind"/>.
|
||||
/// </summary>
|
||||
private static bool ValidateMenuStyle(string? style) => style switch
|
||||
{
|
||||
null or "plain" => false,
|
||||
"retail" => true,
|
||||
var other => throw new FormatException(
|
||||
$"<menu style=\"{other}\"> must be plain or retail"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Builds the zero-argument icon resolver the <c><icon></c> element
|
||||
/// uses: dispatch by <c>iconkind</c> (default <c>"did"</c>) to the
|
||||
|
|
|
|||
|
|
@ -304,6 +304,41 @@ public sealed class UiMenu : UiElement
|
|||
/// StateDesc (not a code symbol); ~0.5 neutral grey here pending a live cdb dump.</summary>
|
||||
public Vector4 TextColorGhosted { get; set; } = new(0.5f, 0.5f, 0.5f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Owner live-client report 2026-09-07 ("Those BIG gold/yellow buttons HAS
|
||||
/// to go. That is not how vtank looks."): whether the CLOSED-state button
|
||||
/// face draws retail's gold pushbutton art (<see cref="NormalSprite"/>/
|
||||
/// <see cref="PressedSprite"/> + the arrow-cap overlay) or the plain flat
|
||||
/// box below (<see cref="DrawPlainClosedState"/>) that reads as the same
|
||||
/// widget family as <see cref="UiMarkupList"/>'s dark list boxes — the
|
||||
/// VTank/Decal <c>HudCombo</c> shape (<c>docs/research/vtank-kb/08-ui-views.md</c>
|
||||
/// §2: a flat box, left-aligned current value, small down-arrow at the
|
||||
/// right edge). Default TRUE so every existing non-plugin <see cref="UiMenu"/>
|
||||
/// user (chat's channel menu, vendor's category dropdown, the Config
|
||||
/// option menus, the retail confirmation-dialog menu, and every generic
|
||||
/// dat Type-6 element built by <c>DatWidgetFactory</c>) keeps its exact
|
||||
/// retail look untouched; <see cref="MarkupDocument"/> flips this to
|
||||
/// <c>false</c> for plugin <c><menu></c> markup by default, with
|
||||
/// <c>style="retail"</c> as the opt-out back to this art.
|
||||
/// </summary>
|
||||
public bool RetailButtonArt { get; set; } = true;
|
||||
|
||||
// ── Plain closed-state chrome (RetailButtonArt = false). Colors mirror
|
||||
// UiMarkupList's own list chrome (BackgroundColor/BorderColor/TextColor)
|
||||
// so a plugin's dropdown reads as the same widget family as its lists.
|
||||
public Vector4 PlainBackgroundColor { get; set; } = new(0f, 0f, 0f, 0.92f);
|
||||
public Vector4 PlainBorderColor { get; set; } = new(0.46f, 0.37f, 0.16f, 1f);
|
||||
/// <summary>Border tint while the popup is open or the face is physically
|
||||
/// pressed — the only visual change those states make in plain mode (no
|
||||
/// gold art swap, ever).</summary>
|
||||
public Vector4 PlainOpenBorderColor { get; set; } = new(0.70f, 0.58f, 0.24f, 1f);
|
||||
public Vector4 PlainTextColor { get; set; } = new(0.91f, 0.87f, 0.76f, 1f);
|
||||
public Vector4 PlainTriangleColor { get; set; } = new(0.91f, 0.87f, 0.76f, 1f);
|
||||
/// <summary>Left-inset of the plain box's value text — matches
|
||||
/// <see cref="UiMarkupList.Padding"/>'s default so a combo's text lines up
|
||||
/// with the list rows beneath it.</summary>
|
||||
public const float PlainPadding = 3f;
|
||||
|
||||
private bool _open;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -457,6 +492,12 @@ public sealed class UiMenu : UiElement
|
|||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (!RetailButtonArt)
|
||||
{
|
||||
DrawPlainClosedState(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var resolve = SpriteResolve;
|
||||
|
||||
// Button face (3-sliced so it can widen to fit the label) + the active-target label.
|
||||
|
|
@ -486,6 +527,52 @@ public sealed class UiMenu : UiElement
|
|||
if (resolve is not null) DrawArrowCap(ctx, resolve);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The VTank/Decal <c>HudCombo</c> closed-state shape (see
|
||||
/// <see cref="RetailButtonArt"/>'s doc comment): flat fill, 1px border,
|
||||
/// left-aligned value text at <see cref="PlainPadding"/>, and a small ▾
|
||||
/// triangle right-aligned — no sprite or DAT quad at all, drawn entirely
|
||||
/// with <see cref="UiRenderContext.DrawFill"/>/<see cref="UiRenderContext.DrawRectOutline"/>
|
||||
/// (the same untextured sprite-bucket primitives <see cref="UiMarkupList"/>
|
||||
/// already uses for its own chrome). Open/pressed only tints the border —
|
||||
/// never a sprite swap.
|
||||
/// </summary>
|
||||
private void DrawPlainClosedState(UiRenderContext ctx)
|
||||
{
|
||||
ctx.DrawFill(0f, 0f, Width, Height, PlainBackgroundColor);
|
||||
Vector4 border = (_open || _facePressed) ? PlainOpenBorderColor : PlainBorderColor;
|
||||
ctx.DrawRectOutline(0f, 0f, Width, Height, border, 1f);
|
||||
|
||||
string caption = ButtonLabelProvider?.Invoke() ?? "";
|
||||
UiDatFont? captionFont = ButtonDatFont ?? DatFont;
|
||||
float captionLineH = captionFont?.LineHeight ?? Font?.LineHeight ?? 14f;
|
||||
float textY = (Height - captionLineH) * 0.5f;
|
||||
if (captionFont is { } cf)
|
||||
ctx.DrawStringDat(cf, caption, PlainPadding, textY, PlainTextColor, Outline, OutlineColor);
|
||||
else
|
||||
ctx.DrawString(caption, PlainPadding, textY, PlainTextColor, Font);
|
||||
|
||||
DrawPlainTriangle(ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 7px-wide, 4px-tall ▾ glyph built from four stacked
|
||||
/// <see cref="UiRenderContext.DrawFill"/> bands — the same "no DAT art,
|
||||
/// just fills" technique <see cref="UiCheckLamp.Draw"/> uses for its lamp
|
||||
/// glyph. Right-aligned with a small margin so it never crowds the box's
|
||||
/// own border.
|
||||
/// </summary>
|
||||
private void DrawPlainTriangle(UiRenderContext ctx)
|
||||
{
|
||||
const float w = 7f, rightMargin = 6f;
|
||||
float x = Width - rightMargin - w;
|
||||
float y = (Height - 4f) * 0.5f;
|
||||
ctx.DrawFill(x, y, w, 1f, PlainTriangleColor);
|
||||
ctx.DrawFill(x + 1f, y + 1f, w - 2f, 1f, PlainTriangleColor);
|
||||
ctx.DrawFill(x + 2f, y + 2f, w - 4f, 1f, PlainTriangleColor);
|
||||
ctx.DrawFill(x + 3f, y + 3f, w - 6f, 1f, PlainTriangleColor);
|
||||
}
|
||||
|
||||
// 3-slice caps for the 46px LED-arrow button face (0x06004D65): a LEFT cap holding the
|
||||
// round LED socket, a stretchable plain-gold MIDDLE, and a RIGHT cap holding the arrow
|
||||
// point. Slicing keeps the LED + arrow undistorted when the button widens to its label.
|
||||
|
|
|
|||
|
|
@ -314,4 +314,58 @@ public class MarkupDocumentTests
|
|||
Assert.Equal(0.78f, binding.AttackPower);
|
||||
Assert.Equal(0.78f, slider.ScalarPositionSource!());
|
||||
}
|
||||
|
||||
// ── S7 fix ("BIG gold/yellow buttons has to go" — owner live-client
|
||||
// report 2026-09-07): <menu style="..."> selects UiMenu.RetailButtonArt.
|
||||
|
||||
private sealed class MenuStyleBinding
|
||||
{
|
||||
public IReadOnlyList<string> Choices => ["First", "Second"];
|
||||
public string Selected { get; } = "First";
|
||||
}
|
||||
|
||||
private static string MenuXml(string? styleAttribute) =>
|
||||
"<panel x=\"0\" y=\"0\" w=\"240\" h=\"60\">" +
|
||||
$"<menu x=\"4\" y=\"4\" w=\"120\" h=\"20\" items=\"{{Choices}}\" selected=\"{{Selected}}\"{styleAttribute}/>" +
|
||||
"</panel>";
|
||||
|
||||
[Fact]
|
||||
public void Menu_NoStyleAttribute_DefaultsToPlain_RetailButtonArtFalse()
|
||||
{
|
||||
var panel = MarkupDocument.Build(MenuXml(styleAttribute: ""), new MenuStyleBinding(), _ => (1u, 32, 32));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
Assert.False(menu.RetailButtonArt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_StylePlain_Explicit_RetailButtonArtFalse()
|
||||
{
|
||||
var panel = MarkupDocument.Build(
|
||||
MenuXml(" style=\"plain\""), new MenuStyleBinding(), _ => (1u, 32, 32));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
Assert.False(menu.RetailButtonArt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_StyleRetail_OptsIntoTheGoldButtonArt()
|
||||
{
|
||||
var panel = MarkupDocument.Build(
|
||||
MenuXml(" style=\"retail\""), new MenuStyleBinding(), _ => (1u, 32, 32));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
Assert.True(menu.RetailButtonArt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_UnknownStyle_ThrowsFormatException_NamingTheElement()
|
||||
{
|
||||
var ex = Assert.Throws<FormatException>(
|
||||
() => MarkupDocument.Build(
|
||||
MenuXml(" style=\"chrome\""), new MenuStyleBinding(), _ => (1u, 32, 32)));
|
||||
|
||||
Assert.Contains("menu", ex.Message);
|
||||
Assert.Contains("chrome", ex.Message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
211
tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
Normal file
211
tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.App.UI;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
/// <summary>
|
||||
/// S7 fix ("BIG gold/yellow buttons has to go" — owner live-client report
|
||||
/// 2026-09-07, looking at the live client: retail's gold pushbutton art on
|
||||
/// the plugin-markup <c><menu></c> closed state does not match VTank's
|
||||
/// own <c>HudCombo</c> shape, a flat dark box with a thin border, left-
|
||||
/// aligned value text, and a small down-arrow — the same widget family as
|
||||
/// <see cref="UiMarkupList"/>'s own list boxes
|
||||
/// (<c>docs/research/vtank-kb/08-ui-views.md</c> §2).
|
||||
///
|
||||
/// <para>
|
||||
/// Draw-level pins against the same <see cref="RecordingGpuDevice"/>/
|
||||
/// <see cref="TextRenderer"/> apparatus <c>MarkupIconTests</c> uses: the
|
||||
/// plain closed state (<see cref="UiMenu.RetailButtonArt"/> = false) emits
|
||||
/// no textured button-face quad at all, only untextured fills (background +
|
||||
/// 1px border + the ▾ triangle) plus the DAT-font caption glyphs; the retail
|
||||
/// closed state (the class default, and every non-markup <see cref="UiMenu"/>
|
||||
/// caller) is untouched — it still resolves and draws its gold
|
||||
/// <see cref="UiMenu.NormalSprite"/>/<see cref="UiMenu.PressedSprite"/> face.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="TextRenderer.DebugSpriteSegmentVerts"/> merges CONTIGUOUS
|
||||
/// same-texture draws into one segment (<c>NextSpriteSeg</c>'s "extend the
|
||||
/// current same-texture run" rule) — a segment boundary appears only where
|
||||
/// the texture actually changes in submission order. So these tests count
|
||||
/// QUADS (48 floats = 6 verts × 8 floats each) summed across every segment
|
||||
/// of a given texture, rather than assuming one segment per draw call.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiMenuPlainStyleTests
|
||||
{
|
||||
private const int FloatsPerQuad = 48; // 6 vertices/quad × 8 floats/vertex (AppendQuad).
|
||||
|
||||
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
|
||||
{
|
||||
public IGpuFrame? CurrentFrame => null;
|
||||
}
|
||||
|
||||
private static (TextRenderer renderer, UiRenderContext ctx) MakeContext(float w, float h)
|
||||
{
|
||||
var device = new RecordingGpuDevice();
|
||||
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
|
||||
renderer.Begin(new Vector2(w, h));
|
||||
var ctx = new UiRenderContext(renderer, new Vector2(w, h));
|
||||
return (renderer, ctx);
|
||||
}
|
||||
|
||||
private static int QuadCount(
|
||||
System.Collections.Generic.IReadOnlyList<(uint Texture, System.Collections.Generic.IReadOnlyList<float> Verts)> segs,
|
||||
uint texture)
|
||||
=> segs.Where(s => s.Texture == texture).Sum(s => s.Verts.Count) / FloatsPerQuad;
|
||||
|
||||
// A distinctive, obviously-not-zero texture id for the retail gold face —
|
||||
// if the plain path ever regressed into calling SpriteResolve for its
|
||||
// face, this id would show up in the recorded segments.
|
||||
private const uint FaceTexture = 77u;
|
||||
private const uint FontTexture = 1u;
|
||||
|
||||
private static UiDatFont MakeFont()
|
||||
{
|
||||
var glyphs = new System.Collections.Generic.Dictionary<char, FontCharDesc>
|
||||
{
|
||||
['W'] = new FontCharDesc { Unicode = 'W', Width = 8, Height = 8 },
|
||||
};
|
||||
return new UiDatFont(
|
||||
fgTex: FontTexture, fgW: 32, fgH: 32,
|
||||
bgTex: 0, bgW: 0, bgH: 0,
|
||||
lineHeight: 16f, baselineOffset: 12f,
|
||||
glyphs);
|
||||
}
|
||||
|
||||
private static UiMenu MakeMenu(bool retailButtonArt) => new()
|
||||
{
|
||||
Width = 100f, Height = 20f,
|
||||
DatFont = MakeFont(),
|
||||
SpriteResolve = _ => (FaceTexture, 46, 17),
|
||||
RetailButtonArt = retailButtonArt,
|
||||
NormalSprite = 0x06004D65u,
|
||||
PressedSprite = 0x06004D66u,
|
||||
Items = new[] { new UiMenu.MenuItem("Warrior", (object?)"Warrior") },
|
||||
ButtonLabelProvider = () => "W",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Plain_ClosedState_DrawsNoTexturedFaceQuad()
|
||||
{
|
||||
var menu = MakeMenu(retailButtonArt: false);
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
menu.DrawSelfAndChildren(ctx);
|
||||
|
||||
Assert.Equal(0, QuadCount(renderer.DebugSpriteSegmentVerts, FaceTexture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plain_ClosedState_DrawsFillOutlineTextAndTriangle()
|
||||
{
|
||||
var menu = MakeMenu(retailButtonArt: false);
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
menu.DrawSelfAndChildren(ctx);
|
||||
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
// Exactly: background fill (1 quad) + 1px outline (4 sides) +
|
||||
// the ▾ triangle (4 stacked bands) = 9 untextured quads.
|
||||
Assert.Equal(9, QuadCount(segs, 0u));
|
||||
// The caption glyph drew exactly one quad through the DAT font texture.
|
||||
Assert.Equal(1, QuadCount(segs, FontTexture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plain_ClosedState_TriangleSitsRightAligned_TextSitsAtListPadding()
|
||||
{
|
||||
var menu = MakeMenu(retailButtonArt: false);
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
menu.DrawSelfAndChildren(ctx);
|
||||
|
||||
// The glyph's dest quad starts at UiMenu.PlainPadding (left-aligned,
|
||||
// no arrow-cap offset baked in the way the retail face indents it).
|
||||
var glyphSeg = Assert.Single(renderer.DebugSpriteSegmentVerts, s => s.Texture == FontTexture);
|
||||
Assert.Equal(UiMenu.PlainPadding, glyphSeg.Verts[0], 3);
|
||||
|
||||
// DrawPlainClosedState submits fill, then the 4 outline sides
|
||||
// (all texture 0, merged into one segment), THEN the caption glyph
|
||||
// (texture 1, its own segment), THEN the 4 triangle bands (texture 0
|
||||
// again — a NEW segment, since the glyph draw broke the run). That
|
||||
// last texture-0 segment is exactly the triangle: 4 quads, 192 floats.
|
||||
var untexturedSegs = renderer.DebugSpriteSegmentVerts.Where(s => s.Texture == 0u).ToList();
|
||||
Assert.Equal(2, untexturedSegs.Count); // [fill+outline], [triangle]
|
||||
var triangleSeg = untexturedSegs[^1];
|
||||
Assert.Equal(4 * FloatsPerQuad, triangleSeg.Verts.Count);
|
||||
|
||||
// Each band's right edge (Verts[8] within its own 48-float quad chunk
|
||||
// — the second AppendQuad vertex's X, same convention
|
||||
// MarkupIconTests.UiMarkupList_IconColumn_... uses) must sit within
|
||||
// the box's right 13px (7px glyph + 6px margin), never touching the
|
||||
// left-aligned text.
|
||||
for (int q = 0; q < 4; q++)
|
||||
{
|
||||
float rightEdgeX = triangleSeg.Verts[q * FloatsPerQuad + 8];
|
||||
Assert.True(rightEdgeX <= menu.Width - 6f + 0.01f);
|
||||
Assert.True(rightEdgeX >= menu.Width - 13f - 0.01f);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retail_ClosedState_StillEmitsItsGoldFaceSprite()
|
||||
{
|
||||
var menu = MakeMenu(retailButtonArt: true);
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
menu.DrawSelfAndChildren(ctx);
|
||||
|
||||
Assert.True(QuadCount(renderer.DebugSpriteSegmentVerts, FaceTexture) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailButtonArt_DefaultsTrue_SoNonMarkupCallersAreUnaffected()
|
||||
{
|
||||
// Every existing UiMenu construction site (chat, vendor, config
|
||||
// options, the retail confirmation dialog, DatWidgetFactory's generic
|
||||
// Type-6 element) never sets RetailButtonArt at all — the class
|
||||
// default must keep drawing retail's gold art.
|
||||
Assert.True(new UiMenu().RetailButtonArt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retail_ClosedState_DrawIsByteForByteUnchanged_RegressionGolden()
|
||||
{
|
||||
// A golden pin for a plain (non-markup) UiMenu built exactly the way
|
||||
// pre-S7 code built one: no RetailButtonArt set at all (class
|
||||
// default). Its drawn quad counts/textures must be identical to what
|
||||
// the retail branch always produced — the S7 style switch must not
|
||||
// have touched this path at all.
|
||||
UiMenu menu = new()
|
||||
{
|
||||
Width = 100f, Height = 20f,
|
||||
DatFont = MakeFont(),
|
||||
SpriteResolve = _ => (FaceTexture, 46, 17),
|
||||
NormalSprite = 0x06004D65u,
|
||||
PressedSprite = 0x06004D66u,
|
||||
Items = new[] { new UiMenu.MenuItem("Warrior", (object?)"Warrior") },
|
||||
ButtonLabelProvider = () => "W",
|
||||
};
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
menu.DrawSelfAndChildren(ctx);
|
||||
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
// 3-slice face (LED cap, stretched body, arrow cap) = 3 quads of
|
||||
// FaceTexture, plus exactly one caption glyph quad (FontTexture). No
|
||||
// arrow-cap overlay sprite (ids left at 0 -> DrawArrowCap no-ops) and
|
||||
// no untextured fill/outline/triangle quad at all — the plain path is
|
||||
// never reached for a menu that never sets RetailButtonArt.
|
||||
Assert.Equal(3, QuadCount(segs, FaceTexture));
|
||||
Assert.Equal(1, QuadCount(segs, FontTexture));
|
||||
Assert.Equal(0, QuadCount(segs, 0u));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue