feat(ui): add resizable/minw/minh + anchor grammar to plugin markup
Owner direction (2026-09-07): plugin panels need a bigger default size and real resizability. Plugin markup panels were fixed-size with no way to opt in to drag-resize, and only <meter> exposed an anchor attribute (comma-separated, silently dropping unknown tokens) — no other element could stretch or reposition when its window resized. <panel resizable="true" minw= minh=> is now the explicit opt-in (default false — a panel with none of these attributes gets Resizable=false, ResizeX=false, ResizeY=false, matching every plugin panel shipped today, e.g. mosstank.xml's resize="none"). resizable="true" arms both axes and defaults the min size to the authored w/h so a resizable panel never shrinks below the layout its author tested; the pre-existing resize= attribute still narrows to one axis on top of that. anchor="left top right bottom" (space-separated, case-insensitive) now applies uniformly via ApplyCommon to every element (<group>, <list>, <menu>, <field>, <label>, <button>, <icon>, plus <meter>/<tab>/<toggle>/ <slider> for free) instead of just <meter>'s own comma-separated, non-throwing parse. An unknown token now throws FormatException naming the element, matching this file's "malformed markup throws at Build" convention everywhere else. No new plumbing is needed for live re-layout or group-relative child anchoring — UiElement.ApplyAnchor/AnchorEdges already measure a child's margins against its own direct Parent's Width/Height every draw, and RetailWindowManager.ResizeTo/UiRoot's existing edge-drag resize already respect Resizable/ResizeX/ResizeY/ MinWidth/MinHeight generically for any registered window. Mutation proof: reverted MarkupDocument.cs to its pre-change state and reran the 25 new MarkupResizableAnchorTests — 17 failed (the anchor grammar, resizable/minw/minh parsing, live re-layout, and golden-draw tests), 8 passed trivially (cases asserting the unchanged no-attribute default). Restoring the implementation turned all 25 green with no regression in the existing 227 Markup/PluginSidePanel/RetailWindow/ Anchor-filtered tests (252/253, 1 pre-existing unrelated skip). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b36b036475
commit
3a3b40fab5
2 changed files with 395 additions and 8 deletions
|
|
@ -56,7 +56,31 @@ public static class MarkupDocument
|
|||
Height = F(root, "h"),
|
||||
};
|
||||
|
||||
// 2026-09-07 (docs/plans — owner direction "the size of the entire
|
||||
// window needs to be enlarged for default and should also be
|
||||
// resizeable"): a plugin panel is FIXED-SIZE by default —
|
||||
// resizable="true" is the opt-in that arms real user drag-resize
|
||||
// (both axes; UiRoot's generic edge/grip-drag mechanism already
|
||||
// exists for every UiElement with Resizable=true — see
|
||||
// UiElement.Resizable/ResizeX/ResizeY and RetailWindowManager.ResizeTo).
|
||||
// minw/minh set the floor UiRoot's live drag and
|
||||
// RetailWindowLayoutPersistence's restore clamp both already honor
|
||||
// (UiElement.MinWidth/MinHeight); they default to the AUTHORED w/h so
|
||||
// a resizable panel never shrinks below the layout its author tested.
|
||||
bool resizable = B(root, "resizable", false);
|
||||
panel.Resizable = resizable;
|
||||
panel.MinWidth = FOr(root, "minw", panel.Width);
|
||||
panel.MinHeight = FOr(root, "minh", panel.Height);
|
||||
panel.ResizeX = resizable;
|
||||
panel.ResizeY = resizable;
|
||||
|
||||
// Optional per-window resize-axis lock: resize="x" | "y" | "both" | "none".
|
||||
// Only meaningful once resizable="true" already armed the master
|
||||
// switch above — Resizable=false (the default) blocks any drag-resize
|
||||
// regardless of these axis flags, so this attribute alone can no
|
||||
// longer make a panel resizable the way it silently could before
|
||||
// resizable="true" existed (UiNineSlicePanel's own Resizable=true
|
||||
// constructor default used to make the master switch a no-op).
|
||||
string? resize = (string?)root.Attribute("resize");
|
||||
if (resize is not null)
|
||||
{
|
||||
|
|
@ -141,7 +165,8 @@ public static class MarkupDocument
|
|||
BarColor = Color((string?)el.Attribute("color")),
|
||||
Fill = BindFloat((string?)el.Attribute("fill"), binding),
|
||||
Label = () => (cur(), max()) is (uint c, uint m) ? $"{c}/{m}" : null,
|
||||
Anchors = Anchor((string?)el.Attribute("anchor")),
|
||||
// anchor= is applied uniformly for every element by
|
||||
// ApplyCommon below; no per-element handling needed here.
|
||||
SpriteResolve = resolve,
|
||||
BackLeft = Hex((string?)el.Attribute("backleft")),
|
||||
BackTile = Hex((string?)el.Attribute("backtile")),
|
||||
|
|
@ -1137,6 +1162,18 @@ public static class MarkupDocument
|
|||
{
|
||||
element.Name = (string?)source.Attribute("name")
|
||||
?? (string?)source.Attribute("id");
|
||||
|
||||
// 2026-09-07: anchor="left top right bottom" (space-separated; any
|
||||
// subset; default "left top" — today's fixed placement) on ANY
|
||||
// markup element. Semantics are identical to UiElement.Anchors/
|
||||
// AnchorEdges/ApplyAnchor: "left right" stretches width with the
|
||||
// parent, "top bottom" stretches height, "right" alone pins to the
|
||||
// right edge at fixed width. A <group>'s own children resolve their
|
||||
// anchor relative to the GROUP (their direct Parent), not the panel,
|
||||
// because UiElement.ApplyAnchor always measures against Parent.Width/
|
||||
// Height — no extra propagation code is needed for that.
|
||||
element.Anchors = ParseAnchor((string?)source.Attribute("anchor"), source);
|
||||
|
||||
BindBool((string?)source.Attribute("visible"), binding,
|
||||
value => element.Visible = value,
|
||||
sourceReader => element.VisibleSource = sourceReader);
|
||||
|
|
@ -1287,19 +1324,48 @@ public static class MarkupDocument
|
|||
System.Globalization.CultureInfo.InvariantCulture, out var v) ? v : 0u;
|
||||
}
|
||||
|
||||
private static AnchorEdges Anchor(string? csv)
|
||||
/// <summary>
|
||||
/// Parses <c>anchor="left top right bottom"</c> (space-separated, any
|
||||
/// subset of the four tokens, case-insensitive) into <see cref="AnchorEdges"/>.
|
||||
/// Absent/blank defaults to <c>Left | Top</c> — today's fixed top-left
|
||||
/// placement, unchanged. An unrecognized token is a Build-time author
|
||||
/// error, same "malformed markup throws" rule every other attribute in
|
||||
/// this grammar follows (see e.g. <see cref="ValidateIconKind"/>) — the
|
||||
/// message names the offending element via <see cref="ElementIdentity"/>
|
||||
/// so a plugin author with several anchored siblings can find which one
|
||||
/// is wrong.
|
||||
/// </summary>
|
||||
private static AnchorEdges ParseAnchor(string? tokens, XElement source)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csv)) return AnchorEdges.Left | AnchorEdges.Top;
|
||||
var a = AnchorEdges.None;
|
||||
foreach (var part in csv.Split(',', System.StringSplitOptions.TrimEntries | System.StringSplitOptions.RemoveEmptyEntries))
|
||||
a |= part.ToLowerInvariant() switch
|
||||
if (string.IsNullOrWhiteSpace(tokens))
|
||||
return AnchorEdges.Left | AnchorEdges.Top;
|
||||
|
||||
var edges = AnchorEdges.None;
|
||||
foreach (string token in tokens.Split(
|
||||
(char[]?)null, System.StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
edges |= token.ToLowerInvariant() switch
|
||||
{
|
||||
"left" => AnchorEdges.Left,
|
||||
"top" => AnchorEdges.Top,
|
||||
"right" => AnchorEdges.Right,
|
||||
"bottom" => AnchorEdges.Bottom,
|
||||
_ => AnchorEdges.None,
|
||||
_ => throw new FormatException(
|
||||
$"{ElementIdentity(source)} anchor=\"{tokens}\" has unknown token "
|
||||
+ $"\"{token}\" (expected left, top, right, bottom)"),
|
||||
};
|
||||
return a == AnchorEdges.None ? AnchorEdges.Left | AnchorEdges.Top : a;
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
/// <summary>Identifies a markup element for a Build-time error message:
|
||||
/// <c><button name="Foo"></c> when it carries a <c>name</c>/<c>id</c>,
|
||||
/// else just <c><button></c>.</summary>
|
||||
private static string ElementIdentity(XElement source)
|
||||
{
|
||||
string? name = (string?)source.Attribute("name") ?? (string?)source.Attribute("id");
|
||||
return name is null
|
||||
? $"<{source.Name.LocalName}>"
|
||||
: $"<{source.Name.LocalName} name=\"{name}\">";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
321
tests/AcDream.App.Tests/UI/MarkupResizableAnchorTests.cs
Normal file
321
tests/AcDream.App.Tests/UI/MarkupResizableAnchorTests.cs
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 2026-09-07 (owner direction: "The size of the entire window needs to be
|
||||
/// enlarged for default and should also be resizeable"): plugin markup's
|
||||
/// new <c><panel resizable="true" minw= minh=></c> grammar and the
|
||||
/// <c>anchor="left top right bottom"</c> attribute on <c><group></c>,
|
||||
/// <c><list></c>, <c><menu></c>, <c><field></c>,
|
||||
/// <c><label></c>, <c><button></c>, <c><icon></c>.
|
||||
/// Semantics are the existing <see cref="UiElement.Anchors"/>/
|
||||
/// <see cref="AnchorEdges"/>/<see cref="UiElement.ApplyAnchor"/> machinery —
|
||||
/// these tests prove MarkupDocument wires the two new attribute grammars
|
||||
/// into that machinery correctly, not the machinery itself (already covered
|
||||
/// by other UiElement anchor/resize tests).
|
||||
/// </summary>
|
||||
public sealed class MarkupResizableAnchorTests
|
||||
{
|
||||
private sealed class ListBinding
|
||||
{
|
||||
public IReadOnlyList<string> Items => ["A", "B", "C"];
|
||||
public int Selected { get; set; } = -1;
|
||||
public Action<int> OnSelect => value => Selected = value;
|
||||
}
|
||||
|
||||
// ── resizable / minw / minh parse tests ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Build_PanelWithoutResizableAttribute_IsFixedSizeByDefault()
|
||||
{
|
||||
// The golden default: a panel that predates this feature (no
|
||||
// resizable/minw/minh anywhere) must end up with the master switch
|
||||
// OFF and both axes locked — this is the "exactly as today" contract
|
||||
// item 2 of the plan requires.
|
||||
const string xml = "<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\"></panel>";
|
||||
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
|
||||
Assert.False(panel.Resizable);
|
||||
Assert.False(panel.ResizeX);
|
||||
Assert.False(panel.ResizeY);
|
||||
Assert.Equal(300f, panel.MinWidth);
|
||||
Assert.Equal(200f, panel.MinHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_PanelResizableTrue_ArmsBothAxesAndDefaultsMinToAuthoredSize()
|
||||
{
|
||||
const string xml = "<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\" resizable=\"true\"></panel>";
|
||||
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
|
||||
Assert.True(panel.Resizable);
|
||||
Assert.True(panel.ResizeX);
|
||||
Assert.True(panel.ResizeY);
|
||||
Assert.Equal(300f, panel.MinWidth);
|
||||
Assert.Equal(200f, panel.MinHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_PanelResizableTrueWithMinwMinh_OverridesTheAuthoredSizeFloor()
|
||||
{
|
||||
const string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\" resizable=\"true\" minw=\"150\" minh=\"90\"></panel>";
|
||||
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
|
||||
Assert.Equal(150f, panel.MinWidth);
|
||||
Assert.Equal(90f, panel.MinHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_PanelResizableTrueWithResizeAxisLock_NarrowsToOneAxis()
|
||||
{
|
||||
// The pre-existing resize="x"|"y"|"both"|"none" attribute still
|
||||
// layers on top of resizable="true" to narrow which axis actually
|
||||
// drags — it just can no longer be the SOLE switch (resizable is).
|
||||
const string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\" resizable=\"true\" resize=\"x\"></panel>";
|
||||
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
|
||||
Assert.True(panel.Resizable);
|
||||
Assert.True(panel.ResizeX);
|
||||
Assert.False(panel.ResizeY);
|
||||
}
|
||||
|
||||
// ── anchor grammar parse tests ───────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("group")]
|
||||
[InlineData("list")]
|
||||
[InlineData("menu")]
|
||||
[InlineData("field")]
|
||||
[InlineData("label")]
|
||||
[InlineData("button")]
|
||||
[InlineData("icon")]
|
||||
public void Build_ElementWithoutAnchorAttribute_DefaultsToLeftTop(string tag)
|
||||
{
|
||||
string xml = WrapSingle(tag, anchor: null);
|
||||
var panel = MarkupDocument.Build(xml, new ListBinding(), _ => (1u, 32, 32));
|
||||
|
||||
UiElement element = panel.Children[0];
|
||||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Top, element.Anchors);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("group")]
|
||||
[InlineData("list")]
|
||||
[InlineData("menu")]
|
||||
[InlineData("field")]
|
||||
[InlineData("label")]
|
||||
[InlineData("button")]
|
||||
[InlineData("icon")]
|
||||
public void Build_ElementAnchorLeftRight_SetsBothHorizontalEdges(string tag)
|
||||
{
|
||||
string xml = WrapSingle(tag, anchor: "left right");
|
||||
var panel = MarkupDocument.Build(xml, new ListBinding(), _ => (1u, 32, 32));
|
||||
|
||||
UiElement element = panel.Children[0];
|
||||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Right, element.Anchors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_AnchorAllFourTokens_SetsEveryEdge()
|
||||
{
|
||||
string xml = WrapSingle("group", anchor: "left top right bottom");
|
||||
var panel = MarkupDocument.Build(xml, new ListBinding(), _ => (1u, 32, 32));
|
||||
|
||||
Assert.Equal(
|
||||
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom,
|
||||
panel.Children[0].Anchors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_AnchorIsCaseInsensitiveAndOrderIndependent()
|
||||
{
|
||||
string xml = WrapSingle("button", anchor: "BOTTOM Right");
|
||||
var panel = MarkupDocument.Build(xml, new ListBinding(), _ => (1u, 32, 32));
|
||||
|
||||
Assert.Equal(AnchorEdges.Bottom | AnchorEdges.Right, panel.Children[0].Anchors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UnknownAnchorToken_ThrowsNamingTheElement()
|
||||
{
|
||||
const string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"100\" h=\"60\">" +
|
||||
"<button name=\"Fire1\" x=\"0\" y=\"0\" w=\"40\" h=\"20\" text=\"Go\" anchor=\"left frotz\"/>" +
|
||||
"</panel>";
|
||||
|
||||
FormatException ex = Assert.Throws<FormatException>(
|
||||
() => MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32)));
|
||||
|
||||
Assert.Contains("Fire1", ex.Message);
|
||||
Assert.Contains("frotz", ex.Message);
|
||||
}
|
||||
|
||||
private static string WrapSingle(string tag, string? anchor)
|
||||
{
|
||||
string anchorAttr = anchor is null ? string.Empty : $" anchor=\"{anchor}\"";
|
||||
string inner = tag switch
|
||||
{
|
||||
"group" => $"<group x=\"10\" y=\"10\" w=\"100\" h=\"60\"{anchorAttr}></group>",
|
||||
"list" => $"<list x=\"10\" y=\"10\" w=\"100\" h=\"60\" items=\"{{Items}}\" " +
|
||||
$"selected=\"{{Selected}}\" onchange=\"{{OnSelect}}\"{anchorAttr}/>",
|
||||
"menu" => $"<menu x=\"10\" y=\"10\" w=\"100\" h=\"20\" items=\"{{Items}}\"{anchorAttr}/>",
|
||||
"field" => $"<field x=\"10\" y=\"10\" w=\"100\" h=\"20\"{anchorAttr}/>",
|
||||
"label" => $"<label x=\"10\" y=\"10\" text=\"Hi\"{anchorAttr}/>",
|
||||
"button" => $"<button x=\"10\" y=\"10\" w=\"40\" h=\"20\" text=\"Go\"{anchorAttr}/>",
|
||||
"icon" => $"<icon x=\"10\" y=\"10\" w=\"32\" h=\"32\" did=\"0x06000001\"{anchorAttr}/>",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(tag)),
|
||||
};
|
||||
return "<panel x=\"0\" y=\"0\" w=\"200\" h=\"150\">" + inner + "</panel>";
|
||||
}
|
||||
|
||||
// ── dynamic anchor re-layout tests (recording renderer) ─────────────────
|
||||
|
||||
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
|
||||
{
|
||||
public IGpuFrame? CurrentFrame => null;
|
||||
}
|
||||
|
||||
private static UiRenderContext MakeContext(float w, float h)
|
||||
{
|
||||
var device = new RecordingGpuDevice();
|
||||
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
|
||||
renderer.Begin(new Vector2(w, h));
|
||||
return new UiRenderContext(renderer, new Vector2(w, h));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizingPanel_LeftRightList_WidensWithThePanel()
|
||||
{
|
||||
const string xml = """
|
||||
<panel x="0" y="0" w="300" h="200" resizable="true" minw="200" minh="150">
|
||||
<list anchor="left right" x="10" y="10" w="280" h="150"
|
||||
items="{Items}" selected="{Selected}" onchange="{OnSelect}"/>
|
||||
</panel>
|
||||
""";
|
||||
var panel = MarkupDocument.Build(xml, new ListBinding(), _ => (1u, 32, 32));
|
||||
var list = Assert.IsType<UiMarkupList>(panel.Children[0]);
|
||||
|
||||
UiRenderContext ctx = MakeContext(600f, 400f);
|
||||
// First draw at the authored width captures the list's 10px margin to
|
||||
// each side (10 = 300 - (10 + 280)) as its anchor baseline.
|
||||
panel.DrawSelfAndChildren(ctx);
|
||||
Assert.Equal(280f, list.Width);
|
||||
|
||||
// A live drag-resize (RetailWindowManager.ResizeTo) mutates Width
|
||||
// directly; the next draw re-applies the captured 10px margins
|
||||
// against the NEW panel width.
|
||||
panel.Width = 500f;
|
||||
panel.DrawSelfAndChildren(ctx);
|
||||
|
||||
Assert.Equal(480f, list.Width); // 500 - 10 - 10
|
||||
Assert.Equal(10f, list.Left); // left margin preserved
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizingPanel_RightAnchoredButton_MovesWithTheRightEdge()
|
||||
{
|
||||
const string xml = """
|
||||
<panel x="0" y="0" w="300" h="200" resizable="true" minw="200" minh="150">
|
||||
<button anchor="right" x="250" y="10" w="40" h="20" text="X"/>
|
||||
</panel>
|
||||
""";
|
||||
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
var button = Assert.IsType<UiSimpleButton>(panel.Children[0]);
|
||||
|
||||
UiRenderContext ctx = MakeContext(600f, 400f);
|
||||
panel.DrawSelfAndChildren(ctx);
|
||||
Assert.Equal(250f, button.Left); // unchanged: 10px margin to the right edge
|
||||
Assert.Equal(40f, button.Width); // fixed width — right-only anchor never stretches
|
||||
|
||||
panel.Width = 500f;
|
||||
panel.DrawSelfAndChildren(ctx);
|
||||
|
||||
Assert.Equal(450f, button.Left); // 500 - 10 - 40: follows the right edge
|
||||
Assert.Equal(40f, button.Width); // still fixed width
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizingPanel_GroupStretchesBothAxesAndNestedListFollowsGroupWidth()
|
||||
{
|
||||
// Group anchors to every edge (generalizing "top bottom stretches" to
|
||||
// both axes so this one test can drive BOTH panel dimensions), and
|
||||
// its nested list anchors left+right RELATIVE TO THE GROUP — proving
|
||||
// "groups propagate to children" (item 1): the list's own margins are
|
||||
// captured against the group's Width, not the panel's.
|
||||
const string xml = """
|
||||
<panel x="0" y="0" w="300" h="200" resizable="true" minw="200" minh="150">
|
||||
<group anchor="left top right bottom" x="10" y="10" w="280" h="180">
|
||||
<list anchor="left right" x="5" y="5" w="270" h="170"
|
||||
items="{Items}" selected="{Selected}" onchange="{OnSelect}"/>
|
||||
</group>
|
||||
</panel>
|
||||
""";
|
||||
var panel = MarkupDocument.Build(xml, new ListBinding(), _ => (1u, 32, 32));
|
||||
var group = Assert.IsType<UiPanel>(panel.Children[0]);
|
||||
var list = Assert.IsType<UiMarkupList>(group.Children[0]);
|
||||
|
||||
UiRenderContext ctx = MakeContext(600f, 400f);
|
||||
panel.DrawSelfAndChildren(ctx);
|
||||
Assert.Equal(280f, group.Width);
|
||||
Assert.Equal(180f, group.Height);
|
||||
Assert.Equal(270f, list.Width);
|
||||
|
||||
panel.Width = 500f;
|
||||
panel.Height = 300f;
|
||||
panel.DrawSelfAndChildren(ctx);
|
||||
|
||||
// Group stretches on both axes (10px margin preserved on every side).
|
||||
Assert.Equal(480f, group.Width); // 500 - 10 - 10
|
||||
Assert.Equal(280f, group.Height); // 300 - 10 - 10
|
||||
// The nested list follows the GROUP's new width (5px margin to each
|
||||
// side of the group, not the panel).
|
||||
Assert.Equal(470f, list.Width); // 480 - 5 - 5
|
||||
}
|
||||
|
||||
// ── golden: a plain panel with none of the new attributes is unaffected ──
|
||||
|
||||
[Fact]
|
||||
public void Build_PlainPanel_DrawsIdenticallyAcrossRepeatedBuilds()
|
||||
{
|
||||
// No resizable/minw/minh/anchor anywhere — the pre-existing markup
|
||||
// shape every current plugin panel uses today. Two independent
|
||||
// builds + draws of the identical markup must produce byte-identical
|
||||
// recorded GPU call sequences and geometry, proving the new
|
||||
// ApplyCommon anchor-parsing code path is a no-op on the default
|
||||
// path this feature must not disturb.
|
||||
const string xml = """
|
||||
<panel x="0" y="0" w="200" h="120" title="V">
|
||||
<group x="4" y="4" w="180" h="40" background="#FF102030">
|
||||
<button x="4" y="4" w="60" h="20" text="Go"/>
|
||||
<label x="4" y="28" text="Hi"/>
|
||||
</group>
|
||||
<list x="4" y="48" w="180" h="60" items="{Items}" selected="{Selected}" onchange="{OnSelect}"/>
|
||||
</panel>
|
||||
""";
|
||||
|
||||
var panelA = MarkupDocument.Build(xml, new ListBinding(), _ => (7u, 32, 32));
|
||||
var panelB = MarkupDocument.Build(xml, new ListBinding(), _ => (7u, 32, 32));
|
||||
|
||||
var deviceA = new RecordingGpuDevice();
|
||||
var rendererA = new TextRenderer(deviceA, new NullGpuFrameSource(), "unused");
|
||||
rendererA.Begin(new Vector2(400f, 300f));
|
||||
panelA.DrawSelfAndChildren(new UiRenderContext(rendererA, new Vector2(400f, 300f)));
|
||||
|
||||
var deviceB = new RecordingGpuDevice();
|
||||
var rendererB = new TextRenderer(deviceB, new NullGpuFrameSource(), "unused");
|
||||
rendererB.Begin(new Vector2(400f, 300f));
|
||||
panelB.DrawSelfAndChildren(new UiRenderContext(rendererB, new Vector2(400f, 300f)));
|
||||
|
||||
Assert.Equal(deviceA.Calls, deviceB.Calls);
|
||||
Assert.False(panelA.Resizable);
|
||||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Top, panelA.Children[0].Anchors);
|
||||
Assert.Equal(AnchorEdges.Left | AnchorEdges.Top, panelB.Children[0].Anchors);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue