using System;
using System.Globalization;
using System.Numerics;
using System.Reflection;
using System.Xml.Linq;
namespace AcDream.App.UI;
///
/// Parses our KSML-style panel markup (mirrors retail's ElementDesc fields)
/// into a live subtree. {Binding} 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.
///
public static class MarkupDocument
{
/// Raw XML markup for a single panel.
/// Object whose public properties are bound to {PropName} attributes.
/// Surface id → (GL handle, width, height) for chrome sprites.
/// Optional controls.ini stylesheet for the title color.
public static UiNineSlicePanel Build(
string xml, object binding, Func 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 , 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"),
};
// Optional per-window resize-axis lock: resize="x" | "y" | "both" | "none".
string? resize = (string?)root.Attribute("resize");
if (resize is not null)
{
panel.ResizeX = resize is "x" or "both";
panel.ResizeY = resize is "y" or "both";
}
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,
Anchors = Anchor((string?)el.Attribute("anchor")),
SpriteResolve = resolve,
BackSpriteId = Hex((string?)el.Attribute("back")),
FrontSpriteId = Hex((string?)el.Attribute("front")),
});
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;
///
/// Parses #AARRGGBB → RGBA (alpha first, matching
/// controls.ini convention). Falls back to opaque white on bad input.
///
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 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 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]);
}
private static uint Hex(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return 0;
var t = s.Trim();
if (t.StartsWith("0x", System.StringComparison.OrdinalIgnoreCase)) t = t[2..];
return uint.TryParse(t, System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out var v) ? v : 0u;
}
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;
}
}