feat(D.2b): anchor layout — vital bars stretch with window; drop Vitals heading
Add AnchorEdges [Flags] enum and Anchors property (default Left|Top, so all existing elements are unchanged) to UiElement. ApplyAnchor() captures the design-time margins on first call then recomputes Left/Top/Width/Height each frame; DrawSelfAndChildren drives it for every child before painting. ComputeAnchoredRect is public + static so it can be unit-tested without a running frame loop. MarkupDocument.Build gains a private Anchor() CSV parser and threads it into the <meter> initializer via the anchor= attribute. vitals.xml: remove title="Vitals" (retail vitals has no heading) and add anchor="left,top,right" to all three meter bars so they stretch when the panel is dragged wider. Two new xUnit tests in UiRootInputTests: Left+Right stretches width; Left+Top only keeps fixed size. All 19 App.Tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
af91b8432a
commit
f911b5f0af
4 changed files with 102 additions and 4 deletions
|
|
@ -65,6 +65,7 @@ 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")),
|
||||
});
|
||||
break;
|
||||
// future element kinds (label, button, image) added here
|
||||
|
|
@ -123,4 +124,20 @@ public static class MarkupDocument
|
|||
if (expr is null || expr.Length < 3 || expr[0] != '{' || expr[^1] != '}') return null;
|
||||
return binding.GetType().GetProperty(expr[1..^1]);
|
||||
}
|
||||
|
||||
private static AnchorEdges Anchor(string? csv)
|
||||
{
|
||||
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
|
||||
{
|
||||
"left" => AnchorEdges.Left,
|
||||
"top" => AnchorEdges.Top,
|
||||
"right" => AnchorEdges.Right,
|
||||
"bottom" => AnchorEdges.Bottom,
|
||||
_ => AnchorEdges.None,
|
||||
};
|
||||
return a == AnchorEdges.None ? AnchorEdges.Left | AnchorEdges.Top : a;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ using System.Numerics;
|
|||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Which parent edges a child keeps a fixed margin to on resize.
|
||||
/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches.</summary>
|
||||
[System.Flags]
|
||||
public enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 }
|
||||
|
||||
/// <summary>
|
||||
/// Base class for every UI widget in the retained-mode tree.
|
||||
///
|
||||
|
|
@ -106,6 +111,10 @@ public abstract class UiElement
|
|||
/// <summary>Allow vertical (height) resize. Ignored unless <see cref="Resizable"/>.</summary>
|
||||
public bool ResizeY { get; set; } = true;
|
||||
|
||||
/// <summary>Edges this element anchors to in its parent. Default Left|Top
|
||||
/// (pinned top-left, fixed size — no reflow). Left|Right stretches width.</summary>
|
||||
public AnchorEdges Anchors { get; set; } = AnchorEdges.Left | AnchorEdges.Top;
|
||||
|
||||
// ── Tree structure ──────────────────────────────────────────────────
|
||||
public UiElement? Parent { get; private set; }
|
||||
|
||||
|
|
@ -170,6 +179,10 @@ public abstract class UiElement
|
|||
{
|
||||
OnDraw(ctx);
|
||||
|
||||
// Anchor layout: reflow children to this element's current size.
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
_children[i].ApplyAnchor(Width, Height);
|
||||
|
||||
// Children painted back-to-front (lowest ZOrder first).
|
||||
if (_children.Count > 0)
|
||||
{
|
||||
|
|
@ -218,4 +231,51 @@ public abstract class UiElement
|
|||
|
||||
return OnHitTest(localX, localY) ? this : null;
|
||||
}
|
||||
|
||||
// ── Anchor layout ────────────────────────────────────────────────────
|
||||
|
||||
private bool _anchorCaptured;
|
||||
private float _amL, _amT, _amR, _amB, _aw0, _ah0;
|
||||
|
||||
/// <summary>Reposition/resize this element per <see cref="Anchors"/>, keeping
|
||||
/// the margins captured (at first layout / design size) to each anchored edge.
|
||||
/// Called by the parent each frame before drawing children.</summary>
|
||||
internal void ApplyAnchor(float parentW, float parentH)
|
||||
{
|
||||
if (Anchors == AnchorEdges.None) return;
|
||||
if (!_anchorCaptured)
|
||||
{
|
||||
_amL = Left; _amT = Top;
|
||||
_amR = parentW - (Left + Width);
|
||||
_amB = parentH - (Top + Height);
|
||||
_aw0 = Width; _ah0 = Height;
|
||||
_anchorCaptured = true;
|
||||
}
|
||||
var (x, y, w, h) = ComputeAnchoredRect(Anchors, _amL, _amT, _amR, _amB, _aw0, _ah0, parentW, parentH);
|
||||
Left = x; Top = y; Width = w; Height = h;
|
||||
}
|
||||
|
||||
/// <summary>Compute an anchored child rect. Left&Right ⇒ stretch width
|
||||
/// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise
|
||||
/// pin left at fixed width. Same logic vertically.</summary>
|
||||
public static (float x, float y, float w, float h) ComputeAnchoredRect(
|
||||
AnchorEdges a, float mL, float mT, float mR, float mB,
|
||||
float w0, float h0, float parentW, float parentH)
|
||||
{
|
||||
bool l = (a & AnchorEdges.Left) != 0, r = (a & AnchorEdges.Right) != 0;
|
||||
float x, w;
|
||||
if (l && r) { x = mL; w = parentW - mR - mL; }
|
||||
else if (r) { w = w0; x = parentW - mR - w0; }
|
||||
else { x = mL; w = w0; }
|
||||
|
||||
bool t = (a & AnchorEdges.Top) != 0, b = (a & AnchorEdges.Bottom) != 0;
|
||||
float y, h;
|
||||
if (t && b) { y = mT; h = parentH - mB - mT; }
|
||||
else if (b) { h = h0; y = parentH - mB - h0; }
|
||||
else { y = mT; h = h0; }
|
||||
|
||||
if (w < 0) w = 0;
|
||||
if (h < 0) h = 0;
|
||||
return (x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<panel id="acdream.vitals" x="10" y="30" w="220" h="96" title="Vitals" resize="x">
|
||||
<meter id="health" x="8" y="24" w="200" h="14" fill="{HealthPercent}" cur="{HealthCurrent}" max="{HealthMax}" color="#FFC70D0D"/>
|
||||
<meter id="stamina" x="8" y="44" w="200" h="14" fill="{StaminaPercent}" cur="{StaminaCurrent}" max="{StaminaMax}" color="#FFD49E1F"/>
|
||||
<meter id="mana" x="8" y="64" w="200" h="14" fill="{ManaPercent}" cur="{ManaCurrent}" max="{ManaMax}" color="#FF1F33D9"/>
|
||||
<panel id="acdream.vitals" x="10" y="30" w="220" h="96" resize="x">
|
||||
<meter id="health" x="8" y="24" w="200" h="14" fill="{HealthPercent}" cur="{HealthCurrent}" max="{HealthMax}" color="#FFC70D0D" anchor="left,top,right"/>
|
||||
<meter id="stamina" x="8" y="44" w="200" h="14" fill="{StaminaPercent}" cur="{StaminaCurrent}" max="{StaminaMax}" color="#FFD49E1F" anchor="left,top,right"/>
|
||||
<meter id="mana" x="8" y="64" w="200" h="14" fill="{ManaPercent}" cur="{ManaCurrent}" max="{ManaMax}" color="#FF1F33D9" anchor="left,top,right"/>
|
||||
</panel>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue