feat(ui): port retained widget foundations
This commit is contained in:
parent
44f9ec13d9
commit
d825572e31
44 changed files with 84813 additions and 292 deletions
|
|
@ -9,24 +9,15 @@ namespace AcDream.App.UI;
|
|||
public enum ResizeEdges { None = 0, Left = 1, Right = 2, Top = 4, Bottom = 8 }
|
||||
|
||||
/// <summary>
|
||||
/// Top-level UI container. Implements the retail "Device" responsibilities
|
||||
/// Top-level UI container. Implements the retail <c>UIElementManager</c> responsibilities
|
||||
/// (mouse cursor tracking, keyboard focus, modal overlay, mouse capture,
|
||||
/// drag-drop state machine, tooltip timer). Routes Silk.NET input events
|
||||
/// into the widget tree with retail-faithful <see cref="UiEvent"/>
|
||||
/// semantics.
|
||||
///
|
||||
/// Retail analog: the <c>DAT_00837ff4</c> Device object (see
|
||||
/// <c>docs/research/retail-ui/04-input-events.md §2</c>). That object has
|
||||
/// a ~20-slot vtable; the methods we emulate here are:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>+0x18 / +0x1C : <see cref="MouseX"/> / <see cref="MouseY"/></item>
|
||||
/// <item>+0x34 : <see cref="RegisterTimerEvent"/> (tooltip delay)</item>
|
||||
/// <item>+0x38 : <see cref="FireEvent"/></item>
|
||||
/// <item>+0x44 : <see cref="KeyboardFocus"/></item>
|
||||
/// <item>+0x48 / +0x4C : <see cref="SetCapture"/> / <see cref="ReleaseCapture"/></item>
|
||||
/// <item>+0x74 / +0x78 : drag cursor set / reset</item>
|
||||
/// </list>
|
||||
/// Retail analog: <c>UIElementManager::UseTime @ 0x0045CFD0</c>. Tooltip
|
||||
/// deadlines are polled before global time message 3; there is no generic Device
|
||||
/// timer queue in the named client.
|
||||
///
|
||||
/// When no widget consumes an event, the <see cref="WorldMouseFallThrough"/>
|
||||
/// or <see cref="WorldKeyFallThrough"/> event fires so the game world
|
||||
|
|
@ -125,8 +116,10 @@ public sealed class UiRoot : UiElement
|
|||
// Hover / tooltip tracking.
|
||||
private UiElement? _hoverWidget;
|
||||
private long _hoverStartedMs;
|
||||
private const int TooltipDelayMs = 1000; // retail typical
|
||||
public int TooltipDelayMs { get; set; } = 250;
|
||||
public int TooltipDurationMs { get; set; } = 10_000;
|
||||
private bool _tooltipFired;
|
||||
private long _tooltipShownMs;
|
||||
|
||||
private long _nowMs;
|
||||
|
||||
|
|
@ -152,10 +145,92 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public override void AddChild(UiElement child)
|
||||
{
|
||||
if (child.EventId == 0) child.EventId = _nextEventId++;
|
||||
AssignEventIds(child);
|
||||
base.AddChild(child);
|
||||
}
|
||||
|
||||
private void AssignEventIds(UiElement element)
|
||||
{
|
||||
if (element.EventId == 0)
|
||||
element.EventId = _nextEventId++;
|
||||
foreach (var child in element.Children)
|
||||
AssignEventIds(child);
|
||||
}
|
||||
|
||||
private static void BroadcastGlobalUiTime(UiElement element, double nowSeconds)
|
||||
{
|
||||
if (element is IUiGlobalTimeListener listener)
|
||||
listener.OnGlobalUiTime(nowSeconds);
|
||||
|
||||
// A listener may synchronously close/remove a window. Snapshot the walk,
|
||||
// then skip children no longer owned by this parent so a deleted subtree
|
||||
// cannot receive a stale pulse and collection mutation cannot invalidate it.
|
||||
foreach (var child in element.Children.ToArray())
|
||||
if (ReferenceEquals(child.Parent, element))
|
||||
BroadcastGlobalUiTime(child, nowSeconds);
|
||||
}
|
||||
|
||||
internal void OnSubtreeRemoving(UiElement subtree)
|
||||
{
|
||||
if (IsWithinSubtree(KeyboardFocus, subtree))
|
||||
SetKeyboardFocus(null);
|
||||
if (IsWithinSubtree(Captured, subtree))
|
||||
{
|
||||
ReleaseCapture();
|
||||
_dragCandidate = false;
|
||||
}
|
||||
if (IsWithinSubtree(DefaultTextInput, subtree))
|
||||
DefaultTextInput = null;
|
||||
if (IsWithinSubtree(Modal, subtree))
|
||||
Modal = null;
|
||||
if (IsWithinSubtree(DragSource, subtree))
|
||||
{
|
||||
DragSource = null;
|
||||
DragPayload = null;
|
||||
_dragGhost = null;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
if (IsWithinSubtree(_hoverWidget, subtree))
|
||||
{
|
||||
var leave = new UiEvent(_hoverWidget!.EventId, _hoverWidget, UiEventType.HoverLeave);
|
||||
_hoverWidget.OnEvent(in leave);
|
||||
_hoverWidget = null;
|
||||
_tooltipFired = false;
|
||||
}
|
||||
if (IsWithinSubtree(_lastDragHoverTarget, subtree))
|
||||
_lastDragHoverTarget = null;
|
||||
if (IsWithinSubtree(_lastClickTarget, subtree))
|
||||
_lastClickTarget = null;
|
||||
if (IsWithinSubtree(_windowDragTarget, subtree))
|
||||
{
|
||||
_windowDragTarget = null;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
if (IsWithinSubtree(_resizeTarget, subtree))
|
||||
{
|
||||
_resizeTarget = null;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
|
||||
foreach (string name in _windows
|
||||
.Where(pair => IsWithinSubtree(pair.Value, subtree))
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
{
|
||||
_windows.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWithinSubtree(UiElement? element, UiElement subtree)
|
||||
{
|
||||
while (element is not null)
|
||||
{
|
||||
if (ReferenceEquals(element, subtree)) return true;
|
||||
element = element.Parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Per-frame pumping ────────────────────────────────────────────────
|
||||
|
||||
public void Tick(double dt, long nowMs)
|
||||
|
|
@ -170,8 +245,18 @@ public sealed class UiRoot : UiElement
|
|||
var e = new UiEvent(_hoverWidget.EventId, _hoverWidget, UiEventType.Tooltip);
|
||||
_hoverWidget.OnEvent(in e);
|
||||
_tooltipFired = true;
|
||||
_tooltipShownMs = _nowMs;
|
||||
}
|
||||
else if (_hoverWidget is not null && _tooltipFired
|
||||
&& _nowMs - _tooltipShownMs >= TooltipDurationMs)
|
||||
{
|
||||
var leave = new UiEvent(_hoverWidget.EventId, _hoverWidget, UiEventType.HoverLeave);
|
||||
_hoverWidget.OnEvent(in leave);
|
||||
_hoverWidget = null;
|
||||
_tooltipFired = false;
|
||||
}
|
||||
|
||||
BroadcastGlobalUiTime(this, nowMs / 1000d);
|
||||
TickSelfAndChildren(dt);
|
||||
}
|
||||
|
||||
|
|
@ -550,7 +635,13 @@ public sealed class UiRoot : UiElement
|
|||
}
|
||||
|
||||
public void SetCapture(UiElement e) => Captured = e;
|
||||
public void ReleaseCapture() => Captured = null;
|
||||
public void ReleaseCapture()
|
||||
{
|
||||
Captured = null;
|
||||
// Retail restarts the tooltip idle deadline when capture is released.
|
||||
_hoverStartedMs = _nowMs;
|
||||
_tooltipFired = false;
|
||||
}
|
||||
|
||||
// ── Window manager (named top-level windows: Show / Hide / Toggle) ───
|
||||
|
||||
|
|
@ -694,14 +785,6 @@ public sealed class UiRoot : UiElement
|
|||
target.OnEvent(in e);
|
||||
}
|
||||
|
||||
public void RegisterTimerEvent(int type, UiElement target, int delayMs,
|
||||
object? payload = null)
|
||||
{
|
||||
_timers.Add((_nowMs + delayMs, new UiEvent(target.EventId, target, type, Payload: payload)));
|
||||
}
|
||||
|
||||
private readonly List<(long fireAt, UiEvent e)> _timers = new();
|
||||
|
||||
private void UpdateButtonFlag(UiMouseButton b, bool down)
|
||||
{
|
||||
switch (b)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue