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:
Erik 2026-08-16 20:03:24 +02:00
parent d3755eb231
commit a377b9bff7
16 changed files with 1386 additions and 9 deletions

View file

@ -70,6 +70,60 @@ public abstract class UiElement
/// </summary>
public bool AuthoredInvisible { get; internal set; }
/// <summary>
/// #409 (client-wide retail tooltip system): mirrors
/// <c>ElementInfo.TooltipEnabled</c> (dat property <c>0x4B</c>) — a
/// PURE DATA PASSTHROUGH set by <c>LayoutImporter.BuildWidget</c>.
/// Gates whether <see cref="RetailTooltipPresenter"/> may show a
/// tooltip for this widget at all (retail's per-element
/// <c>UIRegion::SetTooltipOn</c> bit, checked by
/// <c>UIElement::MouseHover @0x00462520</c> alongside the global
/// enable preference). See <see cref="AcDream.App.UI.Layout.ElementInfo.TooltipEnabled"/>.
/// </summary>
public bool AuthoredTooltipEnabled { get; internal set; }
/// <summary>
/// #409: mirrors <c>ElementInfo.TooltipText</c> (dat property
/// <c>0x49</c>), already resolved to a display string through
/// <c>DatStringResolver</c> and escape-normalized at import time —
/// the same treatment every other authored <c>StringInfo</c> caption
/// gets. Null when the element authors no tooltip text.
/// </summary>
public string? AuthoredTooltipText { get; internal set; }
/// <summary>
/// #409: mirrors <c>ElementInfo.TooltipRootElementId</c> (dat property
/// <c>0x47</c>) — the element-desc id WITHIN
/// <see cref="AuthoredTooltipLayoutDid"/>'s LayoutDesc to instantiate
/// as the tooltip popup's root. Zero when absent.
/// </summary>
public uint AuthoredTooltipRootElementId { get; internal set; }
/// <summary>
/// #409: mirrors <c>ElementInfo.TooltipLayoutDid</c> (dat property
/// <c>0x48</c>) — the tooltip popup LayoutDesc DID (retail authors
/// <c>0x21000041</c> on every tooltip-bearing element). Zero when
/// absent.
/// </summary>
public uint AuthoredTooltipLayoutDid { get; internal set; }
/// <summary>
/// #409: mirrors <c>ElementInfo.TooltipTextChildElementId</c> (dat
/// property <c>0x4A</c>). Meaningful only when read off a tooltip
/// POPUP's own instantiated ROOT widget — see the
/// <c>ElementInfo</c> field's own doc for why retail reads this off
/// the popup, not the hovering trigger element.
/// </summary>
public uint AuthoredTooltipTextChildElementId { get; internal set; }
/// <summary>
/// #409: mirrors <c>ElementInfo.TooltipDelaySeconds</c> (dat property
/// <c>0x50</c>) — a per-element hover-dwell override in seconds, used
/// by <see cref="UiRoot"/>'s tooltip timer in place of the global
/// <see cref="UiRoot.TooltipDelayMs"/> when present. Null = no override.
/// </summary>
public float? AuthoredTooltipDelaySeconds { get; internal set; }
private readonly Dictionary<string, UiCursorMedia> _stateCursors = new();
/// <summary>Retail MediaDescCursor entries keyed by UIStateId.ToString(), or "" for DirectState.</summary>