using System;
using System.Numerics;
namespace AcDream.App.UI;
///
/// 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 0x06xxxxxx RenderSurface range, and composed via
/// LayoutDesc (0x21xxxxxx) trees. Until our
/// AcFont/UiSpriteBatch consumes those directly, we draw a
/// simple translucent rectangle so panels are visible during development.
///
public class UiPanel : UiElement
{
/// Background fill color. Set to skip.
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f);
/// Border color. Set to skip.
public Vector4 BorderColor { get; set; } = new(0.15f, 0.15f, 0.2f, 0.8f);
public float BorderThickness { get; set; } = 1f;
/// Optional dat RenderSurface id for the panel background sprite, drawn
/// in place of (or alongside) . 0 = none.
/// CT5 fix round (SHOULD-FIX 4): drawn at NATIVE SIZE — retail's
/// copy-or-tile blit, never scaled (see UiDatElement.OnDraw's own
/// doc comment, ~lines 273-360, for the full ground truth). The UV
/// rectangle below (Width / tw, Height / th) is deliberately
/// UV-REPEAT, not a fixed 0..1 a stretch would use — GL_REPEAT-wrapped
/// UI textures tile past their native pixel size rather than scaling.
/// Retail's generic sprite blit (Graphic::Draw 0x00693b20 /
/// Graphic::PutImage 0x00693a30) has exactly two behaviors, copy
/// or tile, and can never scale a source image up to fill a larger
/// destination.
/// Used by the character-panel attribute/skill row's Highlight-state
/// swap (sprite 0x06000F93, template 0x10000248's Highlight-state
/// media) and CT5's Normal-state row background (0x06004CC2). The two
/// sprites behave differently under this same tile formula: 0x06000F93
/// is authored at exactly the row's own 282x20 native size (decoded
/// against the installed DAT, 2026-08-25: PFID_R8G8B8, 282x20), so it
/// draws as a plain COPY with no visible seam; 0x06004CC2 is a 48x48
/// uniform-color tile (PFID_A8R8G8B8, single color (0,0,0,175)) that
/// visibly TILES across the wider row — both are the same code path,
/// just different source-vs-destination ratios.
/// Retail's own UIRegion::SetImageByDID (@0x0069F960)
/// confirms its third parameter is a BlitMode COLOR-BLEND
/// selector, not a resize flag — the decompiled body switches purely on
/// that value (param_2 == 2 -> Blit_3Alpha,
/// == 3 -> Blit_4Alpha, else Blit_Normal) and
/// never touches width/height at all. This answers CT1's open "icon
/// draw mode 3" question (ground-truth doc §2): mode 3 selects
/// Blit_4Alpha, an alpha-blend variant, not a resize.
///
public uint BackgroundSprite { get; set; }
/// Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
/// Required when is non-zero.
public Func? SpriteResolve { get; set; }
protected override void OnDraw(UiRenderContext ctx)
{
if (BackgroundSprite != 0 && SpriteResolve is { } sr)
{
var (tex, tw, th) = sr(BackgroundSprite);
if (tex != 0 && tw != 0 && th != 0)
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
}
else if (BackgroundColor.W > 0f)
{
// Panel fills are backgrounds. Draw them through the sprite/fill
// bucket so children that render as sprites/dat-font glyphs stay on
// top in painter order; DrawRect flushes after sprites and would
// cover text/icons.
ctx.DrawFill(0, 0, Width, Height, BackgroundColor);
}
if (BorderColor.W > 0f && BorderThickness > 0f)
ctx.DrawRectOutline(0, 0, Width, Height, BorderColor, BorderThickness);
}
}
///
/// 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
/// FUN_0040b8f0 then drawn by the widget's draw method through
/// FUN_00698330.
///
public class UiLabel : UiElement
{
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
///
/// Optional live text reader, evaluated each draw and preferred over
/// when set. Markup {Binding} labels use this so
/// the displayed value tracks the binding object instead of freezing at
/// whatever it held when the panel was built — which is what a plugin
/// status line needs.
///
public Func? TextSource { get; set; }
///
/// Retail dat font. When set the label renders through the same glyph path
/// every authored panel uses, so plugin text matches the rest of the
/// interface instead of falling back to the development bitmap font.
///
public UiDatFont? DatFont { get; set; }
/// Two-plane glyph outline, as retail draws interface text.
public bool Outline { get; set; } = true;
public UiLabel() { ClickThrough = true; }
///
/// CT7 gate regression (2026-08-25, MossTank plugin text vanished): a
/// markup <label> authors position only, so this element's
/// box is degenerate (0x0) — under CT-GF1's completed
/// UIRegion::DrawHere @0x0069FA30 port the self-clip cropped the
/// glyphs to nothing. A label must not self-clip: it is
/// pure text whose real clip is
/// its ancestors (the plugin panel/window, which ARE properly sized) —
/// the same effective containment retail gives a text region whose box
/// hugs its glyphs. below also keeps the box
/// truthful by measuring the current text, so layout/hit consumers see
/// real extents (converges the frame after a bound text change).
///
protected override bool ClipsChildren => false;
protected override void OnDraw(UiRenderContext ctx)
{
string text = TextSource?.Invoke() ?? Text;
float w = DatFont is { } df
? df.MeasureWidth(text)
: (ctx.DefaultFont?.MeasureWidth(text) ?? text.Length * 7f);
float h = DatFont?.LineHeight
?? ctx.DefaultFont?.LineHeight ?? 14f;
if (w != Width) Width = w;
if (h != Height) Height = h;
if (DatFont is { } dat)
ctx.DrawStringDat(dat, text, 0, 0, TextColor, Outline);
else
ctx.DrawString(text, 0, 0, TextColor);
}
}
///
/// Simple clickable button: panel background + centered label + click
/// callback. Retail equivalent is Keystone's button widget, driven by
/// a StateDesc per UIStateId (normal / hot / pressed /
/// disabled) from the panel layout.
/// Note: the dat-widget button (Type 1 / UIElement_Button) is
/// in UiButton.cs — that is the production widget used by D.2b panels.
/// This class is the earlier dev-scaffold button (plain rect + text; no dat sprites).
///
public class UiSimpleButton : UiPanel
{
public string Text { get; set; } = string.Empty;
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
///
/// Optional live caption reader, preferred over when
/// set, so a markup-bound button can change its own label (Buff / Stop)
/// without the binding object touching UI objects.
///
public Func? TextSource { get; set; }
/// Retail dat font for the caption; see .
public UiDatFont? DatFont { get; set; }
/// Two-plane glyph outline, as retail draws interface text.
public bool Outline { get; set; } = true;
public event System.Action? Click;
///
/// Without this the button is unclickable inside any draggable window.
/// UiRoot's press handling asks the pressed widget whether it owns the
/// pointer; a widget that does not claim the press falls through to
/// "move the ancestor window", which swallows the release and never emits
/// a Click. and
/// already declare it — this one did not, which is why a markup plugin
/// panel's button did nothing while its hit-test was perfectly fine.
///
public override bool HandlesClick => true;
public UiSimpleButton()
{
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);
string caption = TextSource?.Invoke() ?? Text;
if (caption.Length == 0) return;
if (DatFont is { } dat)
{
float datW = dat.MeasureWidth(caption);
ctx.DrawStringDat(
dat, caption,
(Width - datW) * 0.5f, (Height - dat.LineHeight) * 0.5f,
TextColor, Outline);
return;
}
if (ctx.DefaultFont is null) return;
float textW = ctx.DefaultFont.MeasureWidth(caption);
float tx = (Width - textW) * 0.5f;
float ty = (Height - ctx.DefaultFont.LineHeight) * 0.5f;
ctx.DrawString(caption, tx, ty, TextColor);
}
}
///
/// A that fires an callback when the user
/// left-clicks it. Used for the attribute-list rows in the Character window — each row
/// is a transparent container that needs to respond to pointer hits while its children
/// (icon, name, value) are ClickThrough decorations.
///
/// Retail analog: the AttributeInfoRegion row widget in gmAttributeUI
/// catches UIEvent_LeftClick (0x01) and calls SetSelectedAttribute on the
/// parent window. In acdream we wire the equivalent via this action callback instead of
/// the retail message bus.
///
/// Campaign CT slice CT5 (2026-08-25): the selected-row highlight draws through the
/// inherited full-panel
/// draw (native-size copy-or-tile — see that property's own doc comment for the CT5 fix
/// round's correction of the mechanism; NOT a stretch) — no override here. This class
/// previously had its own "selection bars" draw mode (a thin top/bottom-bar rendering
/// tuned to look like sprite 0x06001397's dark bars); CT1's ground-truth research found
/// that sprite belongs to a DIFFERENT retail mechanism entirely (the spellbook row's
/// overlay child), and the row's actual retail Highlight state (InfoRegion::SetState,
/// media 0x06000F93) is a plain full-row background SWAP — exactly what the inherited
/// already draws (0x06000F93 is authored at exactly the row's
/// own 282x20 native size, so the swap needs no scaling to look right). The bars mode was
/// therefore retired rather than reconfigured to a wrong sprite's geometry; no consumer
/// outside CharacterStatController ever set it.
///
public class UiClickablePanel : UiPanel
{
/// Called when the user releases the left mouse button over this panel.
public Action? OnClick { get; set; }
/// Settable tooltip, surfaced through the shared
/// hover pipeline (same pattern as
/// / ). TS-85's
/// character-panel gap: retail's AttributeInfoRegion /
/// Attribute2ndInfoRegion / SkillInfoRegion row constructors
/// (UIElement::SetTooltip at 0x004f1617 / 0x004f1777 / 0x004f222f) stamp
/// this once per row at construction — retail never updates it afterward, so a
/// plain settable string (not a live provider) matches.
public string? TooltipText { get; set; }
///
public override string? GetTooltipText() =>
string.IsNullOrWhiteSpace(TooltipText) ? null : TooltipText;
public UiClickablePanel()
{
// Rows must receive pointer events — override the UiPanel default (ClickThrough=false,
// which is the UiElement base default). Explicit for clarity.
ClickThrough = false;
}
/// HandlesClick = true ensures this row receives its own Click even when
/// it is nested inside a Draggable ancestor window frame (e.g. the future character
/// window with a whole-window drag handle). Without this, the press would be consumed
/// by the ancestor's drag logic and the Click would never fire.
public override bool HandlesClick => true;
public override bool OnEvent(in UiEvent e)
{
if (e.Type == UiEventType.Click && Enabled)
{
OnClick?.Invoke();
return true;
}
return false;
}
}