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