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( $"