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>
This commit is contained in:
Erik 2026-09-07 08:23:29 +02:00
commit 045cd0a195
17 changed files with 385 additions and 1 deletions

View file

@ -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.4S7.6 follow.

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View file

@ -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>&lt;menu
/// style="..."&gt;</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>&lt;icon&gt;</c> element
/// uses: dispatch by <c>iconkind</c> (default <c>"did"</c>) to the

View file

@ -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>&lt;menu&gt;</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.

View file

@ -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);
}
}

View 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>&lt;menu&gt;</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));
}
}