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; // 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; } }