using System; using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI; /// /// Base class for every UI widget in the retained-mode tree. /// /// Design notes: /// - Retail AC delegates widget semantics to the external /// keystone.dll library (see /// docs/research/retail-ui/02-class-hierarchy.md — there is no /// widget hierarchy inside acclient.exe itself). We implement /// our own retained-mode toolkit here, matching the behavior /// described in the decompile without trying to byte-match Keystone's /// internal class layout. /// - Events use the retail-faithful struct and /// the constants so that hand-ported panel /// code can use the same magic numbers the decompiled C uses /// (e.g. if (e.Type == 0x15) ... for drag-begin). /// - Hit-testing is children-first (topmost wins) with Z-order tie /// breaking; drawing is back-to-front so later children appear on top. /// - Coordinates are in screen pixels, origin top-left. /// is in the parent's local coordinate space. /// public abstract class UiElement { // ── Identity ───────────────────────────────────────────────────────── /// /// Unique 32-bit event ID. Retail uses the range 0x10000000+ /// for custom app events (see /// docs/research/retail-ui/04-input-events.md §3). Assigned /// by when the element is added to the tree. /// public uint EventId { get; internal set; } /// Human-readable name for debugging / FindByName. public string? Name { get; init; } // ── Geometry ──────────────────────────────────────────────────────── /// X in the parent's local pixel space. public float Left { get; set; } public float Top { get; set; } public float Width { get; set; } public float Height { get; set; } /// Absolute (screen-space) top-left, computed by walking Parent. public Vector2 ScreenPosition { get { var p = new Vector2(Left, Top); var parent = Parent; while (parent is not null) { p += new Vector2(parent.Left, parent.Top); parent = parent.Parent; } return p; } } // ── State flags ───────────────────────────────────────────────────── public bool Visible { get; set; } = true; public bool Enabled { get; set; } = true; /// /// If true, skips this element — the event /// passes through to whatever is behind. Used by decoration widgets /// (portrait frames, ornamental dividers). /// public bool ClickThrough { get; set; } /// /// If true, will set focus here on click, /// routing WM_KEYDOWN / WM_CHAR to as /// / . /// public bool AcceptsFocus { get; set; } /// /// True if this is a text-entry (edit box); used by focus routing /// to suppress global hotkeys while typing. /// public bool IsEditControl { get; set; } /// Painter's-algorithm z-order within siblings. Higher = on top. public int ZOrder { get; set; } // ── Tree structure ────────────────────────────────────────────────── public UiElement? Parent { get; private set; } private readonly List _children = new(); public IReadOnlyList Children => _children; public virtual void AddChild(UiElement child) { if (child.Parent is not null) child.Parent.RemoveChild(child); child.Parent = this; _children.Add(child); } public virtual bool RemoveChild(UiElement child) { if (!_children.Remove(child)) return false; child.Parent = null; return true; } // ── Virtual overrides ─────────────────────────────────────────────── /// /// Draw THIS element (not its children). Children are composited by /// after this returns. /// protected virtual void OnDraw(UiRenderContext ctx) { } /// Per-frame tick (animations, timers, caret blink). protected virtual void OnTick(double deltaSeconds) { } /// /// Custom hit-test override. Default is a rectangle containment /// check on (, ). /// protected virtual bool OnHitTest(float localX, float localY) => localX >= 0f && localX < Width && localY >= 0f && localY < Height; /// /// Event handler. Return true to consume the event (the /// will stop propagation). Return false /// to let ancestors / fall-through handle it. /// public virtual bool OnEvent(in UiEvent e) => false; /// /// Tooltip text for this widget. Retail fires event 0x07 after /// ~1000ms hover, then queries the widget's virtual "GetString" /// (vtable +0x88) to render the tooltip body. /// public virtual string? GetTooltipText() => null; // ── Framework entry points (internal, called by UiRoot) ───────────── internal void DrawSelfAndChildren(UiRenderContext ctx) { if (!Visible) return; // Translate into our local space. ctx.PushTransform(Left, Top); try { OnDraw(ctx); // Children painted back-to-front (lowest ZOrder first). if (_children.Count > 0) { // Avoid LINQ allocation by copying to a temp array and sorting. var ordered = _children.ToArray(); Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder)); for (int i = 0; i < ordered.Length; i++) ordered[i].DrawSelfAndChildren(ctx); } } finally { ctx.PopTransform(); } } internal void TickSelfAndChildren(double dt) { if (!Visible) return; OnTick(dt); for (int i = 0; i < _children.Count; i++) _children[i].TickSelfAndChildren(dt); } /// /// Top-down, children-first hit-test. / /// are in THIS element's local space. /// Returns the topmost descendant (or this) at the point, or null. /// internal UiElement? HitTest(float localX, float localY) { if (!Visible || !Enabled || ClickThrough) return null; // Children first, in reverse Z-order (topmost first). if (_children.Count > 0) { var ordered = _children.ToArray(); Array.Sort(ordered, static (a, b) => b.ZOrder.CompareTo(a.ZOrder)); for (int i = 0; i < ordered.Length; i++) { var c = ordered[i]; var childHit = c.HitTest(localX - c.Left, localY - c.Top); if (childHit is not null) return childHit; } } return OnHitTest(localX, localY) ? this : null; } }