feat(ui): #409 — client-wide retail tooltip system
Full re-derivation from named-retail decomp: UIElement::StartTooltipAtMouse @0x00460D70 -> UIElementManager::StartTooltip @0x0045DE90/@0x00459700, UIElement::MouseHover @0x00462520 (P0x4B TooltipOn gate + global m_tooltipEnable), UIElementManager::CheckTooltip @0x0045B6E0 (dwell/ auto-hide timer, default 0.25s/10s), SwitchMouseOver/DeletingElement (dismissal). Corrects the earlier GF-16 investigation: P0x47 is the element-desc id WITHIN the popup LayoutDesc (P0x48), not a "behavior enum"; P0x4A is read off the popup's own instantiated root, not the trigger element. - ElementInfo/UiElement gain six tooltip data fields (P0x47/48/49/4A/4B/50), read generically by ElementReader and copied through LayoutImporter, mirroring the existing AuthoredInvisible passthrough pattern. - UiRoot's existing CheckTooltip-derived hover timer gains TooltipShow/ TooltipHide events, a per-element P0x50 delay override, and dismissal wiring at every retail-confirmed teardown site. - RetailTooltipPresenter (owned by RetailUiRuntime, mounted alongside RetailDialogFactory) builds the popup via the existing LayoutImporter dat-lock seam, auto-resizes by the measured-vs-authored text delta (word-wrapped via the existing UiText.WrapWords primitive), positions at the mouse clamped to the display, and stays topmost over dialogs via its own later per-tick BringToFront (register AD-106). - Misc.TooltipEnable/Misc.TooltipDelay are client-local UserPreferences (retail's own 2013 Config tab authors no visible row for either) — SettingsStore gains a MiscSettings section, no new options-panel row. - Live-DAT sweep: 434 elements author >=1 trigger property (243 with literal text this port shows; 191 rely on retail's dynamic InqProperty(0x49) override, deferred as register TS-85 alongside the unmodeled P0x3D wrap-width override). Gates: Release build 0 errors; App suite (live-DAT env) 5410/5407 passed/ 3 skipped (was 5379/3); Runtime 1735/0 unchanged; UI.Abstractions 926/0; full solution 14,617/14,548 passed/69 skipped/0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d3755eb231
commit
a377b9bff7
16 changed files with 1386 additions and 9 deletions
|
|
@ -330,11 +330,51 @@ public sealed class UiRoot : UiElement
|
|||
// Hover / tooltip tracking.
|
||||
private UiElement? _hoverWidget;
|
||||
private long _hoverStartedMs;
|
||||
/// <summary>Global hover-dwell delay in ms before a tooltip fires — retail
|
||||
/// <c>UIElementManager::m_tooltipDelay</c>, default 0.25 s
|
||||
/// (<c>UIElementManager::UIElementManager @0x0045F5D0</c>), the
|
||||
/// <c>Misc.TooltipDelay</c> preference. A hovered widget's own
|
||||
/// <see cref="UiElement.AuthoredTooltipDelaySeconds"/> (dat property
|
||||
/// <c>0x50</c>) overrides this per <c>UIElementManager::CheckTooltip
|
||||
/// @0x0045B6E0</c> — see <see cref="EffectiveTooltipDelayMs"/>.</summary>
|
||||
public int TooltipDelayMs { get; set; } = 250;
|
||||
/// <summary>How long a shown tooltip stays up before auto-hiding — retail
|
||||
/// <c>m_tooltipDuration</c>, a fixed 10 s
|
||||
/// (<c>UIElementManager::CheckTooltip @0x0045B6E0</c>'s own auto-hide
|
||||
/// check); not a user preference.</summary>
|
||||
public int TooltipDurationMs { get; set; } = 10_000;
|
||||
private bool _tooltipFired;
|
||||
private long _tooltipShownMs;
|
||||
|
||||
/// <summary>
|
||||
/// #409: fired once, synchronously, when a hovered widget's dwell delay
|
||||
/// elapses (retail's <c>UIElementManager::StartHover</c> ->
|
||||
/// <c>UIElement::MouseHover</c> edge). <see cref="RetailTooltipPresenter"/>
|
||||
/// is the production consumer — it decides whether the widget actually
|
||||
/// authors a tooltip and, if so, builds/positions the popup.
|
||||
/// </summary>
|
||||
public event Action<UiElement>? TooltipShow;
|
||||
|
||||
/// <summary>
|
||||
/// #409: fired when a previously-shown tooltip must go away — hover
|
||||
/// left the widget (<see cref="UpdateHover"/>), the widget's subtree is
|
||||
/// being removed (<see cref="ClearSubtreeOwnership"/>), or the shown
|
||||
/// duration elapsed (<see cref="Tick"/>'s auto-hide branch). Only fires
|
||||
/// if <see cref="TooltipShow"/> actually fired for this widget first
|
||||
/// (mirrors retail's own <c>m_pTooltipElement != null</c> guard at every
|
||||
/// one of those three call sites).
|
||||
/// </summary>
|
||||
public event Action<UiElement>? TooltipHide;
|
||||
|
||||
/// <summary>Retail <c>UIElementManager::CheckTooltip @0x0045B6E0</c>: a
|
||||
/// per-element FLOAT delay override (dat property <c>0x50</c>) replaces
|
||||
/// the global <see cref="TooltipDelayMs"/> when the hovered element
|
||||
/// authors one.</summary>
|
||||
private int EffectiveTooltipDelayMs(UiElement widget)
|
||||
=> widget.AuthoredTooltipDelaySeconds is { } seconds
|
||||
? (int)(seconds * 1000f)
|
||||
: TooltipDelayMs;
|
||||
|
||||
private long _nowMs;
|
||||
|
||||
/// <summary>Raised when an event was not consumed by any widget.</summary>
|
||||
|
|
@ -440,6 +480,12 @@ public sealed class UiRoot : UiElement
|
|||
{
|
||||
var leave = new UiEvent(_hoverWidget!.EventId, _hoverWidget, UiEventType.HoverLeave);
|
||||
_hoverWidget.OnEvent(in leave);
|
||||
// #409: retail UIElementManager::DeletingElement @0x0045E520 tears
|
||||
// down the active tooltip when its owner element is removed
|
||||
// (m_pTooltipOwner == ebp). Only fire if a tooltip actually
|
||||
// showed for this widget (mirrors that null-guarded check).
|
||||
if (_tooltipFired)
|
||||
TooltipHide?.Invoke(_hoverWidget);
|
||||
_hoverWidget = null;
|
||||
_tooltipFired = false;
|
||||
}
|
||||
|
|
@ -475,21 +521,23 @@ public sealed class UiRoot : UiElement
|
|||
{
|
||||
_nowMs = nowMs;
|
||||
|
||||
// Tooltip timer: once mouse has hovered over the same widget for
|
||||
// TooltipDelayMs, fire a Tooltip event on it exactly once.
|
||||
// Tooltip timer: once mouse has hovered over the same widget for its
|
||||
// effective dwell delay, fire a Tooltip event on it exactly once.
|
||||
if (_hoverWidget is not null && !_tooltipFired
|
||||
&& _nowMs - _hoverStartedMs >= TooltipDelayMs)
|
||||
&& _nowMs - _hoverStartedMs >= EffectiveTooltipDelayMs(_hoverWidget))
|
||||
{
|
||||
var e = new UiEvent(_hoverWidget.EventId, _hoverWidget, UiEventType.Tooltip);
|
||||
_hoverWidget.OnEvent(in e);
|
||||
_tooltipFired = true;
|
||||
_tooltipShownMs = _nowMs;
|
||||
TooltipShow?.Invoke(_hoverWidget);
|
||||
}
|
||||
else if (_hoverWidget is not null && _tooltipFired
|
||||
&& _nowMs - _tooltipShownMs >= TooltipDurationMs)
|
||||
{
|
||||
var leave = new UiEvent(_hoverWidget.EventId, _hoverWidget, UiEventType.HoverLeave);
|
||||
_hoverWidget.OnEvent(in leave);
|
||||
TooltipHide?.Invoke(_hoverWidget);
|
||||
_hoverWidget = null;
|
||||
_tooltipFired = false;
|
||||
}
|
||||
|
|
@ -1241,6 +1289,16 @@ public sealed class UiRoot : UiElement
|
|||
{
|
||||
var leave = new UiEvent(_hoverWidget.EventId, _hoverWidget, UiEventType.HoverLeave);
|
||||
_hoverWidget.OnEvent(in leave);
|
||||
// #409: retail UIElementManager::SwitchMouseOver @0x0045B560 calls
|
||||
// StopHover (which tears down m_pTooltipElement) the instant the
|
||||
// hovered element changes — the ONLY confirmed dismissal trigger
|
||||
// besides duration-timeout and element-removal. Notably, retail's
|
||||
// MouseDownEvent @0x0045DB60 calls SwitchMouseOver with the SAME
|
||||
// hit-tested element, so clicking the tooltip's own owner does
|
||||
// NOT dismiss it (SwitchMouseOver no-ops when the target hasn't
|
||||
// changed) — no click-dismissal is ported here for that reason.
|
||||
if (_tooltipFired)
|
||||
TooltipHide?.Invoke(_hoverWidget);
|
||||
}
|
||||
_hoverWidget = w;
|
||||
_hoverStartedMs = _nowMs;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue