using System; using System.Numerics; using AcDream.App.UI.Layout; namespace AcDream.App.UI; /// /// Generic dat-widget button — the production replacement for any dat element of /// Type 1 (UIElement_Button, registered via RegisterElementClass(1, UIElement_Button::Create) /// @ acclient_2013_pseudo_c.txt:125828). /// /// /// Draws per-state sprite media exactly like (same /// ActiveState defaulting, same ActiveMedia() fallback chain, same tiled /// DrawSprite call with UV-repeat so chrome edges tile correctly) plus an /// optional centered text label. The click behavior mirrors /// one-for-one so the chat Send and Max/Min buttons that previously bound through /// UiDatElement.OnClick continue to work without behavioral change. /// /// /// /// State selection: picks if set, then /// "Normal" if the element has a Normal state sprite, then falls back to the unnamed /// DirectState ("" key) — identical to . /// /// /// /// Built by for Type-1 elements (chat Send 0x10000019, /// Max/Min 0x1000046F). NOT the same as , which is an /// earlier dev-scaffold widget with no dat sprites. /// /// public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful { private readonly ElementInfo _info; private readonly Func _resolve; private readonly HashSet _availableStates = new(); private bool _pressed; private bool _pointerOver; private bool _selected; private bool _hotClicking; private bool _suppressNextClick; private double _nextHotClickTime = double.NaN; /// Optional click handler. Wired by the controller (e.g. chat Submit, ToggleMaximize). public Action? OnClick { get; set; } /// The dat element id from . public uint ElementId => _info.Id; /// Optional centered text label drawn over the sprite (e.g. "Send" on a blank gold frame). public string? Label { get; set; } /// Dat font for . Required for the label to draw. public UiDatFont? LabelFont { get; set; } /// Label color (default white). public Vector4 LabelColor { get; set; } = Vector4.One; /// Horizontal alignment of . Center (default) for normal buttons; /// Left for the paperdoll "Slots" caption that sits at the left edge, before the slots. public LabelAlignment LabelAlign { get; set; } = LabelAlignment.Center; /// Label horizontal alignment options. public enum LabelAlignment { Center, Left } public bool ToggleBehavior { get; } public bool RolloverEnabled { get; } public bool HotClickEnabled { get; } public float HotClickInitialDelay { get; } public float HotClickRepeatInterval { get; } public bool Selected { get => _selected; set { if (_selected == value) return; _selected = value; UpdateVisualState(); } } /// /// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized). /// Matches . /// public string ActiveState { get; set; } = ""; public uint ActiveRetailStateId { get { if (string.IsNullOrEmpty(ActiveState)) return UiStateInfo.DirectStateId; foreach (var (id, state) in _info.States) if (string.Equals(state.Name, ActiveState, StringComparison.Ordinal)) return id; return UiButtonStateMachine.TryStateId(ActiveState, out uint standard) ? standard : RetailUiStateIds.TryStateId(ActiveState, out uint custom) ? custom : 0u; } } public override string ActiveCursorStateName => ActiveState; public bool TrySetRetailState(uint stateId) { if (ToggleBehavior && stateId is UiButtonStateMachine.Normal or UiButtonStateMachine.Highlight) { Selected = stateId == UiButtonStateMachine.Highlight; return true; } if (stateId == UiButtonStateMachine.Ghosted) { Enabled = false; return true; } if (!Enabled && stateId != UiButtonStateMachine.Ghosted) Enabled = true; if (stateId == UiStateInfo.DirectStateId) { if (!_info.States.ContainsKey(stateId) && !_info.StateMedia.ContainsKey("")) return false; ActiveState = ""; return true; } if (_info.States.TryGetValue(stateId, out var state)) { ActiveState = state.Name; return true; } string stateName = UiButtonStateMachine.StateName(stateId); if (string.IsNullOrEmpty(stateName)) stateName = RetailUiStateIds.StateName(stateId); if (!string.IsNullOrEmpty(stateName) && _info.StateMedia.ContainsKey(stateName)) { ActiveState = stateName; return true; } return false; } /// Merged for this element. /// Dat file-id → (GL texture handle, native px width, native px height). /// Returns (0,0,0) when the texture is not yet uploaded. public UiButton(ElementInfo info, Func resolve) { _info = info; _resolve = resolve; ClickThrough = false; // buttons are interactive — opt OUT of click-through foreach (uint stateId in info.States.Keys) if (stateId != UiStateInfo.DirectStateId) _availableStates.Add(stateId); foreach (string stateName in info.StateMedia.Keys) if (UiButtonStateMachine.TryStateId(stateName, out uint stateId)) _availableStates.Add(stateId); ToggleBehavior = info.TryGetEffectiveBool(0x0Bu, out bool toggle) && toggle; RolloverEnabled = info.TryGetEffectiveBool(0x13u, out bool rollover) && rollover; HotClickEnabled = info.TryGetEffectiveBool(0x0Fu, out bool hotClick) && hotClick; HotClickInitialDelay = info.TryGetEffectiveFloat(0x10u, out float initialDelay) ? initialDelay : 0f; HotClickRepeatInterval = info.TryGetEffectiveFloat(0x11u, out float repeatInterval) ? repeatInterval : 0f; _selected = info.TryGetEffectiveBool(0x0Eu, out bool selected) && selected; bool disabled = info.TryGetEffectiveBool(0x0Du, out bool ghosted) && ghosted; // State defaulting matches UiDatElement exactly: // DefaultStateName wins; else "Normal" if that state has a sprite; else DirectState (""). if (!string.IsNullOrEmpty(info.DefaultStateName)) ActiveState = info.DefaultStateName; else if (info.StateMedia.ContainsKey("Normal")) ActiveState = "Normal"; // else ActiveState stays "" (DirectState) Enabled = !disabled; UpdateVisualState(); } /// The button draws its own face + label; any dat label child is reproduced /// procedurally, so the importer must not build the button's children as widgets. public override bool ConsumesDatChildren => true; /// A button is interactive — it must receive its Click even inside a whole-window-Draggable /// frame (e.g. the paperdoll "Slots" toggle in the inventory window), so it opts out of the /// IA-12 whole-window-drag that would otherwise swallow the press. public override bool HandlesClick => true; /// /// Returns the File id for the current , falling back to /// the DirectState ("" key) if the named state is absent. /// Returns 0 if neither exists. /// Mirrors . /// private uint ActiveFile() => _info.StateMedia.TryGetValue(ActiveState, out var m) ? m.File : _info.StateMedia.TryGetValue("", out var d) ? d.File : 0u; protected override void OnDraw(UiRenderContext ctx) { uint file = ActiveFile(); if (file != 0) { var (tex, tw, th) = _resolve(file); if (tex != 0 && tw != 0 && th != 0) { // Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped // UI texture). Matches ImgTex::TileCSI; no Stretch mode exists. ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } } if (Label is { Length: > 0 } label && LabelFont is { } lf) { float tx = LabelAlign == LabelAlignment.Left ? 3f // small left pad (room for a future checkbox box) : (Width - lf.MeasureWidth(label)) * 0.5f; // centered (default) float ty = (Height - lf.LineHeight) * 0.5f; ctx.DrawStringDat(lf, label, tx, ty, LabelColor); } } public override bool OnEvent(in UiEvent e) { switch (e.Type) { case UiEventType.HoverEnter: _pointerOver = true; UpdateVisualState(); return true; case UiEventType.HoverLeave: _pointerOver = false; UpdateVisualState(); return true; case UiEventType.MouseDown: _pointerOver = ContainsLocal(e.Data1, e.Data2); _pressed = true; UpdateVisualState(); if (HotClickEnabled && Enabled) { OnClick?.Invoke(); _hotClicking = true; _nextHotClickTime = double.NaN; } return true; case UiEventType.MouseMove: if (_pressed) { _pointerOver = ContainsLocal(e.Data1, e.Data2); UpdateVisualState(); return true; } return false; case UiEventType.MouseUp: _pointerOver = ContainsLocal(e.Data1, e.Data2); _suppressNextClick = _hotClicking && _pointerOver; _hotClicking = false; _nextHotClickTime = double.NaN; if (_pressed && _pointerOver && Enabled && ToggleBehavior) _selected = !_selected; _pressed = false; UpdateVisualState(); return true; case UiEventType.Click: if (!Enabled) return true; if (_suppressNextClick) { _suppressNextClick = false; return true; } OnClick?.Invoke(); return OnClick is not null; default: return false; } } protected override void OnEnabledChanged() { if (!Enabled) { _pressed = false; _hotClicking = false; _nextHotClickTime = double.NaN; } UpdateVisualState(); } public void OnGlobalUiTime(double nowSeconds) { if (!_hotClicking || !HotClickEnabled) return; if (double.IsNaN(_nextHotClickTime)) _nextHotClickTime = nowSeconds + HotClickInitialDelay; if (!_pointerOver && nowSeconds >= _nextHotClickTime) { _nextHotClickTime = nowSeconds; return; } if (_pointerOver && nowSeconds >= _nextHotClickTime) { OnClick?.Invoke(); _nextHotClickTime += HotClickRepeatInterval; } } private bool ContainsLocal(int x, int y) => x >= 0 && y >= 0 && x < Width && y < Height; private void UpdateVisualState() { uint requested = UiButtonStateMachine.RequestedState(new UiButtonVisualInput( Disabled: !Enabled, Selected: _selected, RolloverEnabled: RolloverEnabled, Pressed: _pressed, PointerOver: _pointerOver)); if (_availableStates.Contains(requested)) ActiveState = UiButtonStateMachine.StateName(requested); } }