acdream/src/AcDream.App/UI/UiPanel.cs
Erik 7230c1590f docs+feat(ui): retail UI deep-dive research + C# port scaffold
Deep investigation of the retail AC client's GUI subsystem, driven by 6
parallel Opus research agents, plus the first cut of a retail-faithful
retained-mode widget toolkit that scaffolds Phase D.

Research (docs/research/retail-ui/):
- 00-master-synthesis.md        — cross-slice synthesis + port plan
- 01-architecture-and-init.md   — WinMain, CreateMainWindow, frame loop,
                                  Keystone bring-up (7 globals mapped)
- 02-class-hierarchy.md         — key finding: UI lives in keystone.dll,
                                  not acclient.exe; CUIManager + CUIListener
                                  MI pattern, CFont + CSurface + CString
- 03-rendering.md               — 24-byte XYZRHW+UV verts, per-font
                                  256x256 atlas baked from RenderSurface,
                                  TEXTUREFACTOR coloring, DrawPrimitiveUP
- 04-input-events.md            — Win32 WndProc → Device (DAT_00837ff4)
                                  → widget OnEvent(+0x128); full event-type
                                  table (0x01 click, 0x07 tooltip ~1000ms,
                                  0x15 drag-begin, 0x21 enter, 0x3E drop)
- 05-panels.md                  — chat, attributes, skills, spells, paperdoll
                                  (25-slot layout), inventory, fellowship,
                                  allegiance — with wire-message bindings
- 06-hud-and-assets.md          — vital orbs (scissor fill), radar
                                  (0x06001388/0x06004CC1, 1.18× shrink),
                                  compass strip, dat asset catalog

Key insight: keystone.dll owns the actual widget toolkit — we cannot
port a class hierarchy from the decompile because it's not there.
Instead we implement our own retained-mode toolkit with retail-faithful
behavior (event codes, focus/modal/capture, drag-drop state machine)
and will consume the same portal.dat fonts + sprites so the visual
identity is preserved.

C# scaffold (src/AcDream.App/UI/):
- UiEvent          — 24-byte event struct + retail event-type constants
                     (0x01 click, 0x15 drag-begin, 0x201 WM_LBUTTONDOWN,
                     etc.) matching retail decompile switches
- UiElement        — base widget: children, ZOrder, focus/capture flags,
                     virtual OnDraw/OnEvent/OnHitTest/OnTick; children-
                     first hit test + back-to-front composite
- UiPanel          — panel, label, button primitives
- UiRenderContext  — 2D draw context with translate stack
- UiRoot           — top-of-tree + Device responsibilities (mouse/
                     keyboard state, focus, modal, capture, drag-drop,
                     tooltip timer); WorldMouseFallThrough/
                     WorldKeyFallThrough preserves existing camera
                     controls when no widget consumes
- UiHost           — packages UiRoot + TextRenderer + input wiring
                     helpers for one-line integration into GameWindow
- README.md        — orientation for future agents

Roadmap (docs/plans/2026-04-11-roadmap.md):
- D.1 marked shipped (debug overlay from 2026-04-17)
- D.2 expanded to include the retail UI framework landed here
- D.3-D.7 added: AcFont, dat sprites, core panels, HUD, CursorManager
- D.8 remains sound

All existing 470 tests pass. 0 warnings, 0 errors.
2026-04-17 19:13:02 +02:00

93 lines
3.1 KiB
C#

using System.Numerics;
namespace AcDream.App.UI;
/// <summary>
/// Rectangular container with an optional translucent background and
/// border. Used as the base of every retail panel (attributes, chat,
/// inventory, login, etc.).
///
/// Retail has panel background art stored as 9-slice sprite assets in
/// the <c>0x06xxxxxx</c> RenderSurface range, and composed via
/// <c>LayoutDesc</c> (<c>0x21xxxxxx</c>) trees. Until our
/// <c>AcFont</c>/<c>UiSpriteBatch</c> consumes those directly, we draw a
/// simple translucent rectangle so panels are visible during development.
/// </summary>
public class UiPanel : UiElement
{
/// <summary>Background fill color. Set <see cref="Vector4.Zero"/> to skip.</summary>
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f);
/// <summary>Border color. Set <see cref="Vector4.Zero"/> to skip.</summary>
public Vector4 BorderColor { get; set; } = new(0.15f, 0.15f, 0.2f, 0.8f);
public float BorderThickness { get; set; } = 1f;
protected override void OnDraw(UiRenderContext ctx)
{
if (BackgroundColor.W > 0f)
ctx.DrawRect(0, 0, Width, Height, BackgroundColor);
if (BorderColor.W > 0f && BorderThickness > 0f)
ctx.DrawRectOutline(0, 0, Width, Height, BorderColor, BorderThickness);
}
}
/// <summary>
/// Static text label. Draws a single line of text using the context's
/// default font (or an override). Does not consume input.
///
/// Equivalent retail primitive: wide-string appended to a CString via
/// <c>FUN_0040b8f0</c> then drawn by the widget's draw method through
/// <c>FUN_00698330</c>.
/// </summary>
public class UiLabel : UiElement
{
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
public UiLabel() { ClickThrough = true; }
protected override void OnDraw(UiRenderContext ctx)
=> ctx.DrawString(Text, 0, 0, TextColor);
}
/// <summary>
/// Simple clickable button: panel background + centered label + click
/// callback. Retail equivalent is Keystone's button widget, driven by
/// a <c>StateDesc</c> per <c>UIStateId</c> (normal / hot / pressed /
/// disabled) from the panel layout.
/// </summary>
public class UiButton : UiPanel
{
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
public event System.Action? Click;
public UiButton()
{
BackgroundColor = new Vector4(0.1f, 0.1f, 0.15f, 0.8f);
BorderColor = new Vector4(0.45f, 0.45f, 0.55f, 1f);
}
public override bool OnEvent(in UiEvent e)
{
if (e.Type == UiEventType.Click && Enabled)
{
Click?.Invoke();
return true;
}
return false;
}
protected override void OnDraw(UiRenderContext ctx)
{
base.OnDraw(ctx);
if (Text.Length == 0 || ctx.DefaultFont is null) return;
float textW = ctx.DefaultFont.MeasureWidth(Text);
float tx = (Width - textW) * 0.5f;
float ty = (Height - ctx.DefaultFont.LineHeight) * 0.5f;
ctx.DrawString(Text, tx, ty, TextColor);
}
}