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

@ -528,6 +528,7 @@ public sealed class RetailUiRuntime : IDisposable
MountIndicators();
MountJumpPowerbar();
MountDialogFactory();
MountTooltipPresenter();
MountSocialPanel();
MountCharacter();
MountPlugins();
@ -630,6 +631,9 @@ public sealed class RetailUiRuntime : IDisposable
public UiNineSlicePanel? InventoryFrame { get; private set; }
public InventoryController? InventoryPanelController { get; private set; }
public RetailDialogFactory? DialogFactory { get; private set; }
/// <summary>#409: client-wide retail hover-tooltip presenter. Constructed
/// alongside <see cref="DialogFactory"/> — see <c>ConfigureDialogFactory</c>.</summary>
public RetailTooltipPresenter? TooltipPresenter { get; private set; }
public ExternalContainerController? ExternalContainerController { get; private set; }
public VendorUiController? VendorController { get; private set; }
public OptionsPanelController? OptionsPanelController { get; private set; }
@ -813,6 +817,11 @@ public sealed class RetailUiRuntime : IDisposable
CharacterCreationController?.Tick();
DialogFactory?.Tick();
Host.Tick(deltaSeconds);
// #409: MUST tick after both DialogFactory and Host so the tooltip's
// own BringToFront re-raise always lands on top of whatever z-order
// those two just asserted this frame — see RetailTooltipPresenter.Tick's
// own doc comment and the register row filed alongside this class.
TooltipPresenter?.Tick();
_automation?.Tick(deltaSeconds);
}
@ -979,6 +988,7 @@ public sealed class RetailUiRuntime : IDisposable
{
CharacterManagementController?.ResetSession();
DialogFactory?.Reset();
TooltipPresenter?.HideCurrent();
}
finally
{
@ -3314,6 +3324,47 @@ public sealed class RetailUiRuntime : IDisposable
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
}
/// <summary>
/// #409: mounts the client-wide retail tooltip presenter. Unlike
/// <see cref="MountDialogFactory"/>'s catalog resolve, the tooltip popup
/// layout/root are entirely data-driven off each hovered widget's OWN
/// authored <c>0x47</c>/<c>0x48</c> properties (<see cref="RetailTooltipPresenter"/>'s
/// own class doc) — this method only wires the generic layout-by-DID
/// resolver and seeds the client-local <c>Misc.TooltipEnable</c>/
/// <c>Misc.TooltipDelay</c> preferences from the shared
/// <see cref="SettingsStore"/> (the same instance
/// <c>_bindings.Chat.Store</c> already threads to every other client-local
/// preference reader in this file).
/// </summary>
private void MountTooltipPresenter()
{
if (TooltipPresenter is not null)
return;
ImportedLayout? CreateTooltipLayout(uint layoutDid, uint rootElementId)
{
lock (_bindings.Assets.DatLock)
{
return LayoutImporter.Import(
_bindings.Assets.Dats,
layoutDid,
rootElementId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont);
}
}
TooltipPresenter = new RetailTooltipPresenter(Host.Root, CreateTooltipLayout);
if (_bindings.Chat.Store is { } store)
{
MiscSettings misc = store.LoadMisc();
TooltipPresenter.Enabled = misc.TooltipEnable;
Host.Root.TooltipDelayMs = (int)(misc.TooltipDelaySeconds * 1000f);
}
}
private UiShortcutDigitGraphics LoadShortcutDigitGraphics()
{
if (_shortcutDigitGraphics is not null)
@ -4194,7 +4245,11 @@ public sealed class RetailUiRuntime : IDisposable
_characterCreationMount?.Dispose();
_gameplayConfirmationController?.Dispose();
},
() => DialogFactory?.Dispose(),
() =>
{
DialogFactory?.Dispose();
TooltipPresenter?.Dispose();
},
_panelUi.Dispose,
Host.Dispose);
_shutdown.CompleteOrThrow();