merge(vt): resizable plugin panels (resizable/minw/minh) and anchor markup (owner: larger default, resizable)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
2e63391cc4
5 changed files with 605 additions and 27 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")),
|
||||
|
|
@ -1156,6 +1181,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);
|
||||
|
|
@ -1306,19 +1343,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}\">";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue