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