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