feat(D.2b): MarkupDocument (XML -> UiElement tree); vitals panel from vitals.xml
Implements Task 8 of the D.2b retail-UI plan. MarkupDocument.Build() parses
KSML-style panel markup into a live UiNineSlicePanel subtree, resolving
{Binding} attribute expressions against a supplied object via reflection.
Color format is #AARRGGBB (alpha-first, matching controls.ini). Handles
<panel> root (geometry + optional title label) and <meter> children (fill,
label, bar color). Future element kinds (label, button, image) extend the
switch without touching existing code.
vitals.xml encodes the just-approved vitals panel layout (health red #FFC70D0D,
stamina gold #FFD49E1F, mana blue #FF1F33D9); ships next to the binary via
PreserveNewest csproj rule. GameWindow.cs drops the 35-line hand-built panel
block in favour of a 4-line File.ReadAllText + MarkupDocument.Build call —
identical tree, identical render, now data-driven.
2 new tests (Build_CreatesPanelWithMeterFillLabelAndGeometry,
Build_NullBindingValuesYieldNullFillAndLabel) + 11 total targeted green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
97bd1d2f09
commit
07bf6cbf60
5 changed files with 182 additions and 35 deletions
118
src/AcDream.App/UI/MarkupDocument.cs
Normal file
118
src/AcDream.App/UI/MarkupDocument.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Parses our KSML-style panel markup (mirrors retail's ElementDesc fields)
|
||||
/// into a live <see cref="UiElement"/> subtree. <c>{Binding}</c> attribute
|
||||
/// values resolve against a supplied object by property name (reflection).
|
||||
/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7.
|
||||
/// </summary>
|
||||
public static class MarkupDocument
|
||||
{
|
||||
/// <param name="xml">Raw XML markup for a single panel.</param>
|
||||
/// <param name="binding">Object whose public properties are bound to <c>{PropName}</c> attributes.</param>
|
||||
/// <param name="resolve">Surface id → (GL handle, width, height) for chrome sprites.</param>
|
||||
/// <param name="style">Optional controls.ini stylesheet for the title color.</param>
|
||||
public static UiNineSlicePanel Build(
|
||||
string xml, object binding, Func<uint, (uint, int, int)> resolve,
|
||||
ControlsIni? style = null)
|
||||
{
|
||||
var root = XDocument.Parse(xml).Root ?? throw new FormatException("empty markup");
|
||||
if (root.Name.LocalName != "panel")
|
||||
throw new FormatException($"root must be <panel>, got <{root.Name.LocalName}>");
|
||||
|
||||
var panel = new UiNineSlicePanel(resolve)
|
||||
{
|
||||
Left = F(root, "x"),
|
||||
Top = F(root, "y"),
|
||||
Width = F(root, "w"),
|
||||
Height = F(root, "h"),
|
||||
};
|
||||
|
||||
string? title = (string?)root.Attribute("title");
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
Vector4 tc = style is not null && style.TryColor("title", "color", out var c) ? c : Vector4.One;
|
||||
panel.AddChild(new UiLabel { Text = title, Left = 8, Top = 4, TextColor = tc });
|
||||
}
|
||||
|
||||
foreach (var el in root.Elements())
|
||||
{
|
||||
switch (el.Name.LocalName)
|
||||
{
|
||||
case "meter":
|
||||
var cur = BindUint((string?)el.Attribute("cur"), binding);
|
||||
var max = BindUint((string?)el.Attribute("max"), binding);
|
||||
panel.AddChild(new UiMeter
|
||||
{
|
||||
Left = F(el, "x"),
|
||||
Top = F(el, "y"),
|
||||
Width = F(el, "w"),
|
||||
Height = F(el, "h"),
|
||||
BarColor = Color((string?)el.Attribute("color")),
|
||||
Fill = BindFloat((string?)el.Attribute("fill"), binding),
|
||||
Label = () => (cur(), max()) is (uint c, uint m) ? $"{c}/{m}" : null,
|
||||
});
|
||||
break;
|
||||
// future element kinds (label, button, image) added here
|
||||
}
|
||||
}
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static float F(XElement e, string attr)
|
||||
=> float.TryParse((string?)e.Attribute(attr), NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture, out var v) ? v : 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Parses <c>#AARRGGBB</c> → RGBA <see cref="Vector4"/> (alpha first, matching
|
||||
/// controls.ini convention). Falls back to opaque white on bad input.
|
||||
/// </summary>
|
||||
private static Vector4 Color(string? hex)
|
||||
{
|
||||
if (hex is { Length: 9 } && hex[0] == '#'
|
||||
&& uint.TryParse(hex.AsSpan(1), NumberStyles.HexNumber,
|
||||
CultureInfo.InvariantCulture, out uint argb))
|
||||
return new Vector4(
|
||||
((argb >> 16) & 0xFF) / 255f,
|
||||
((argb >> 8) & 0xFF) / 255f,
|
||||
(argb & 0xFF) / 255f,
|
||||
((argb >> 24) & 0xFF) / 255f);
|
||||
return Vector4.One;
|
||||
}
|
||||
|
||||
private static Func<float?> BindFloat(string? expr, object binding)
|
||||
{
|
||||
var pi = Prop(expr, binding);
|
||||
if (pi is null) return () => 0f;
|
||||
return () => pi.GetValue(binding) switch
|
||||
{
|
||||
float f => f,
|
||||
null => (float?)null,
|
||||
var v => Convert.ToSingle(v, CultureInfo.InvariantCulture),
|
||||
};
|
||||
}
|
||||
|
||||
private static Func<uint?> BindUint(string? expr, object binding)
|
||||
{
|
||||
var pi = Prop(expr, binding);
|
||||
if (pi is null) return () => null;
|
||||
return () => pi.GetValue(binding) switch
|
||||
{
|
||||
uint u => u,
|
||||
null => (uint?)null,
|
||||
var v => Convert.ToUInt32(v, CultureInfo.InvariantCulture),
|
||||
};
|
||||
}
|
||||
|
||||
private static PropertyInfo? Prop(string? expr, object binding)
|
||||
{
|
||||
if (expr is null || expr.Length < 3 || expr[0] != '{' || expr[^1] != '}') return null;
|
||||
return binding.GetType().GetProperty(expr[1..^1]);
|
||||
}
|
||||
}
|
||||
5
src/AcDream.App/UI/assets/vitals.xml
Normal file
5
src/AcDream.App/UI/assets/vitals.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<panel id="acdream.vitals" x="10" y="30" w="220" h="96" title="Vitals">
|
||||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue