merge(vt): plain <menu> style from latest-main-sync into slice7 panel work
Brings cfa703065's owner-driven fix (plugin <menu> renders VTank's plain flat combo by default; style="retail" opts back into the gold pushbutton art) into the S7.1-S7.3 window/tabs work so the fix round below can build on both. Resolved conflicts: the ledger keeps both worktrees' entries in chronological order (07:55/08:05 owner-read entries appended after this worktree's own S7.1/S7.2/S7.3 entries); plugin-ui-markup.md's <menu> row lists both new attributes (scroll, style); MarkupDocumentTests.cs keeps both new test blocks (slider min/max + menu scroll from this worktree, menu style from latest-main-sync). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
|
@ -126,3 +126,6 @@ re-review, merge to the campaign branch, then the owner's visual gate.
|
|||
this sub-slice's scope. Screenshots NOT captured (same ACE-down
|
||||
reason as S7.1/S7.2). Owner's visual gate against a running VTank is
|
||||
still owed for both S7.1/S7.2 and S7.3.
|
||||
- 2026-09-07 07:55 live screenshots of `89a49836c` captured (gate worktree, ACE up — the earlier "ACE down" was a TCP probe against a UDP server) → `docs/research/2026-09-07-slice7-screenshots/` (cropped to the panel, 864×360). Lead's read for the review + fix round: (1) Vitals reads like VTank; (2) Options has the right arrangement but VTank's literal `L,T,W,H` do not fit our DAT font — captions overlap the next row ("Follow/Nav Min Distance" over "Enable Navigation"): scale VTank's geometry to our font (row pitch from the DAT font's line height instead of VVS's 16 px; widths likewise), keep the proportions, never pixel-match; (3) Profiles still shows legacy controls (three stacked "New" buttons, the Loot engine row, the macro-profile path string drawn twice) — delete them; (4) the window is 856×350 with ~180 px of dead space below the content: bring the main panel back to VTank's 210-tall proportion (scaled) and make Advanced Options / Loot Editor their own plugin panels (separate windows, like VTank's popups) instead of in-panel groups; (5) the Route, Meta and Loot-editor pages show blank trailing button slots.
|
||||
- 2026-09-07 08:05 owner looked at the live gate build: (a) "The Options tab labels overlap, fix that"; (b) "Those BIG gold/yellow buttons HAS to go. That is not how vtank looks." — the plugin `<menu>` renders retail's gold pushbutton art; VTank's combos are plain (flat dark box, thin border, value text, small arrow at right — the same look as its lists). Fix round: plugin-markup `<menu>` draws the plain combo by default (a `style="retail"` opt-in keeps the gold art for anyone who wants it), plus the geometry scaling, the Profiles leftovers, the 210-tall main window and the two popups as separate panels.
|
||||
- 2026-09-07 09:10 S7.3 Monsters landed on the panel worktree (`57ced0aff`, `c3b4f7862`; MossTank suite 645 → 651) — same commits as the entry above; recorded again here because the plain-menu-style branch (`cfa703065`) merged into the panel worktree afterward to pick up fix (b) before fix round A started. Fix round A (grid scaling, Profiles leftovers, 260-tall window, Advanced Options / Loot Editor as their own panels, blank trailing slots, fresh screenshots) dispatched on the same worktree after merging the plain-menu style in. S7.4–S7.6 follow.
|
||||
|
|
|
|||
|
|
@ -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 min max` |
|
||||
| `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 scroll` |
|
||||
| `menu` | Dropdown selector | `x y w h items selected onchange rows rowheight openupward scroll 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
|
||||
|
|
|
|||
BIN
docs/research/2026-09-07-slice7-screenshots/popup-advanced.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 228 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-buffs.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-consumables.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-items.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-meta.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-monsters.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-options.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-profiles.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-route.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
docs/research/2026-09-07-slice7-screenshots/tab-vitals.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
|
|
@ -481,6 +481,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"),
|
||||
|
|
@ -515,6 +516,7 @@ public static class MarkupDocument
|
|||
PopupBgSprite = 0x0600124Cu,
|
||||
ItemNormalSprite = 0x0600124Eu,
|
||||
ItemHighlightSprite = 0x0600124Du,
|
||||
RetailButtonArt = menuRetailButtonArt,
|
||||
ButtonLabelProvider = () => menuSelected() ?? string.Empty,
|
||||
OnSelect = payload =>
|
||||
{
|
||||
|
|
@ -668,6 +670,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.
|
||||
|
|
|
|||
|
|
@ -507,4 +507,58 @@ public class MarkupDocumentTests
|
|||
{
|
||||
public IGpuFrame? CurrentFrame => null;
|
||||
}
|
||||
|
||||
// ── 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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||