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. /// When set, the sprite is stretched to fill the panel rect. /// Used by the attribute-list selected-row highlight (sprite 0x06001397 = Button state 6). 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) { ctx.DrawRect(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); public UiLabel() { ClickThrough = true; } protected override void OnDraw(UiRenderContext ctx) => 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); public event System.Action? Click; 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); 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); } } /// /// 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. /// /// When is true and /// is non-zero, draws the sprite as a thin full-width bar at the TOP and BOTTOM edges of /// the row (not stretched to fill). This matches retail's selection highlight which shows /// a horizontal dark bar on both the top and bottom edge of the selected attribute row, /// with NO left/right end-caps. Bar height is pixels /// (default 3px). /// public class UiClickablePanel : UiPanel { /// Called when the user releases the left mouse button over this panel. public Action? OnClick { get; set; } /// When true and is non-zero, draws /// the sprite as a thin horizontal bar at the top AND bottom edges of the panel, /// NOT as a full-height stretched fill. Matches retail's selected-row highlight /// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill). /// Default false (preserves legacy full-stretch behavior). public bool UseSelectionBars { get; set; } /// Height in pixels of each selection bar (top and bottom). Default 3px. /// Ignored when is false. public float SelectionBarHeight { get; set; } = 3f; 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; } protected override void OnDraw(UiRenderContext ctx) { if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr) { // Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row. // The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at // native height (SelectionBarHeight), stretched to full panel width (UV tile // horizontally). No left/right end-caps: u0=0, u1=Width/nativeW (UV repeat). var (tex, tw, th) = sr(BackgroundSprite); if (tex != 0 && tw > 0 && th > 0) { float barH = SelectionBarHeight; float uTile = tw > 0 ? Width / tw : 1f; // Top bar: shows the top barH px of the sprite (v = 0 → barH/th). float vBot = th > 0 ? barH / th : 1f; ctx.DrawSprite(tex, 0f, 0f, Width, barH, 0f, 0f, uTile, vBot, Vector4.One); // Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1). float vTop2 = th > 0 ? 1f - barH / th : 0f; ctx.DrawSprite(tex, 0f, Height - barH, Width, barH, 0f, vTop2, uTile, 1f, Vector4.One); } // Selection-bar mode draws no border (rows have BorderColor=Zero by design). } else { // Default UiPanel draw: handles BackgroundSprite, BackgroundColor, AND border. base.OnDraw(ctx); } } }