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,
BackLeft = Hex((string?)el.Attribute("backleft")),
BackTile = Hex((string?)el.Attribute("backtile")),
BackRight = Hex((string?)el.Attribute("backright")),
FrontLeft = Hex((string?)el.Attribute("frontleft")),
FrontTile = Hex((string?)el.Attribute("fronttile")),
FrontRight = Hex((string?)el.Attribute("frontright")),
});
break;
case "label":
// Text may be a literal or a {Binding}. Bound labels re-read
// their property every frame through the Func, so a plugin
// updates its status line by assigning a property rather
// than by touching UI objects from its own thread.
var label = new UiLabel
{
Left = F(el, "x"),
Top = F(el, "y"),
TextSource = BindString((string?)el.Attribute("text"), binding),
};
if (el.Attribute("color") is not null)
label.TextColor = Color((string?)el.Attribute("color"));
panel.AddChild(label);
break;
case "button":
// onclick binds to an Action property on the binding
// object. Resolved once at build time: a button whose
// handler silently failed to bind is a bug worth failing
// loudly for, and MarkupDocument.Build is already inside
// the host's try/catch that reports panel load failures.
string? clickName = (string?)el.Attribute("onclick");
Action? onClick = BindAction(clickName, binding);
if (clickName is not null && onClick is null)
{
throw new FormatException(
$"