feat(ui): port retained widget foundations
This commit is contained in:
parent
44f9ec13d9
commit
d825572e31
44 changed files with 84813 additions and 292 deletions
|
|
@ -30,10 +30,17 @@ namespace AcDream.App.UI;
|
|||
/// earlier dev-scaffold widget with no dat sprites.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiButton : UiElement
|
||||
public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
||||
{
|
||||
private readonly ElementInfo _info;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
private readonly HashSet<uint> _availableStates = new();
|
||||
private bool _pressed;
|
||||
private bool _pointerOver;
|
||||
private bool _selected;
|
||||
private bool _hotClicking;
|
||||
private bool _suppressNextClick;
|
||||
private double _nextHotClickTime = double.NaN;
|
||||
|
||||
/// <summary>Optional click handler. Wired by the controller (e.g. chat Submit, ToggleMaximize).</summary>
|
||||
public Action? OnClick { get; set; }
|
||||
|
|
@ -57,14 +64,84 @@ public sealed class UiButton : UiElement
|
|||
/// <summary>Label horizontal alignment options.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized).
|
||||
/// Matches <see cref="UiDatElement.ActiveState"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <param name="info">Merged <see cref="ElementInfo"/> for this element.</param>
|
||||
/// <param name="resolve">Dat file-id → (GL texture handle, native px width, native px height).
|
||||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||||
|
|
@ -74,6 +151,25 @@ public sealed class UiButton : UiElement
|
|||
_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))
|
||||
|
|
@ -81,6 +177,9 @@ public sealed class UiButton : UiElement
|
|||
else if (info.StateMedia.ContainsKey("Normal"))
|
||||
ActiveState = "Normal";
|
||||
// else ActiveState stays "" (DirectState)
|
||||
|
||||
Enabled = !disabled;
|
||||
UpdateVisualState();
|
||||
}
|
||||
|
||||
/// <summary>The button draws its own face + label; any dat label child is reproduced
|
||||
|
|
@ -128,7 +227,103 @@ public sealed class UiButton : UiElement
|
|||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (e.Type == UiEventType.Click && OnClick is not null) { OnClick(); return true; }
|
||||
return false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue