Skip to content

Tooltip (UI)

A headless tooltip service plus a dumb uGUI skin — the first occupant of the Kit's UI assembly. Doc 07 catalog §35 frames this as "data-bound tooltip service + uGUI widget," and flags the risk that matters most here explicitly: keep the tooltip service headless so a UITK (or any other) skin can exist later without touching a single line of content-building logic. That's this module's whole design brief, and its differentiator from a typical "drop a Text component and call it done" tooltip: logic never gets born inside UI.

What it is

  • TooltipContent / TooltipSection (pure C#) — the headless data model: an ordered, immutable list of sections (Kind — Title/Body/Tags/Value/Custom — plus already-resolved display Text and an optional Accent color). Zero UnityEngine references, including no UnityEngine.Color — see "Why a plain tuple, not UnityEngine.Color" below.
  • TooltipContentBuilder — fluent assembly helper (AddTitle/AddBody/AddTags/AddValue/ AddCustom) that silently skips any section whose text is null/blank. Every ITooltipSource in this module is built with it.
  • ITooltipContext / LocalizationTooltipContext — the minimal seam an ITooltipSource needs to turn a LocString into display text at build time. The stock implementation resolves through ILocalizationService.Format when one is available, otherwise falls back to the LocString's raw key.
  • ITooltipSource — anything that can produce TooltipContent on demand (BuildTooltip(ITooltipContext)). The Kit's binding seam: a future module (or a game's own code) implements this against its own data without this assembly needing to know about it.
  • ItemTooltipSource — the built-in binding for ItemDefinition: title (display name, tinted by rarity if any), body (description), a tags line, a value line — skipping any section whose underlying data is missing or misconfigured. Both a static builder (Build(item, context)) and an ITooltipSource instance wrapper over the same logic.
  • Service layer — ITooltipService / TooltipService: a headless state machine tracking at most one Current TooltipContent at a time, with plain C# events (ContentChanged/Hidden). No pooled GameEvents — see "Why no pooled events" below.
  • TooltipViewComponent (MonoBehaviour) — the dumb uGUI skin: subscribes to the service's events, fills Text fields, shows/hides a root panel, optionally follows the cursor. Contains no content-building logic whatsoever.
  • TooltipTriggerComponent (MonoBehaviour) — convenience hover trigger: IPointerEnterHandler/IPointerExitHandler that shows/hides a tooltip for a serialized ItemDefinition, or for a sibling ITooltipSource component if one is present. This is what makes the module demoable in one drag.

Quickstart

  1. Leave enableTooltips on in your GameplayKitConfig asset (in a Resources folder, named GameplayKitConfig — same convention every other module uses).
  2. That's it for the service — registration is automatic. The module ships UIServiceRegistrar, discovered by the engine's assembly scan, constructed parameterlessly, and run after all engine services are up. It resolves an optional ILocalizationService, constructs TooltipService, calls Initialize() itself, and registers ITooltipService.
  3. For the drag-and-drop view: add TooltipViewComponent to a Canvas panel (wire its root/title/ body/tags/value fields in the Inspector; leave it inactive by default — it activates itself), then add TooltipTriggerComponent (plus a Graphic with Raycast Target on) to anything hoverable and assign its ItemDefinition field.
  4. Or drive it entirely from code:
var tooltips = ServiceLocator.Instance.Get<ITooltipService>();

tooltips.Show(new ItemTooltipSource(someItemDefinition));
// ...
tooltips.Hide();

// Or assemble content by hand for anything that isn't an ItemDefinition:
var content = new TooltipContentBuilder()
    .AddTitle("Hand-Built Tooltip")
    .AddBody("Not backed by an ItemDefinition at all.")
    .Build();
tooltips.Show(content);

See Samples/Scripts/TooltipSample.cs for the full pattern, including listening to ContentChanged/Hidden from a non-TooltipViewComponent consumer.

API overview

TooltipContent / TooltipSection (headless data model)

Member Notes
TooltipContent.Sections Ordered, read-only.
TooltipContent.HasContent true when at least one section is present.
TooltipContent.Empty The degrade-gracefully result for a null/missing source — never null itself.
TooltipSection.Kind Title/Body/Tags/Value/Custom.
TooltipSection.Text Already-resolved display text. Never null.
TooltipSection.Accent Optional TooltipAccentColor? — null means "no accent, use the view's default styling."

TooltipContentBuilder

Member Notes
AddTitle/AddBody/AddTags/AddValue(text, accent?) Skips silently when text is null/blank. Title and Custom accept an optional accent.
AddCustom(text, accent?) Escape hatch for a project-specific section kind this assembly doesn't render (a subclassed/replacement view can handle it).
Build() Produces the final TooltipContent.

ItemTooltipSource

Member Notes
Build(item, context) (static) Null-safe: a null item returns TooltipContent.Empty. A misconfigured item (blank display-name/description loc key — see ItemDefinition.Validate()) skips just that section rather than throwing.
new ItemTooltipSource(item).BuildTooltip(context) Instance form, for dropping straight into ITooltipService.Show(ITooltipSource) or TooltipTriggerComponent.

ITooltipService / TooltipService

Member Notes
Current The TooltipContent currently shown, or null when hidden.
IsVisible Current != null.
Show(ITooltipSource) Builds through the service's ITooltipContext and shows the result. A throwing source degrades to TooltipContent.Empty rather than propagating. Null hides instead.
Show(TooltipContent) Shows already-built content directly. Null hides instead.
Hide() No-op (raises nothing) when already hidden.
event Action<TooltipContent> ContentChanged Fires on every Show call that sets content — including re-showing the same source, since a fresh BuildTooltip may produce different text.
event Action Hidden Fires only on an actual visible→hidden transition.

TooltipViewComponent

Member Notes
root, titleText, bodyText, tagsText, valueText Serialized references. Leave a text field empty to skip that Kind entirely (an unmapped Custom section is also simply not rendered by this stock skin).
screenFollowCursor When true, repositions root to the cursor every frame while visible, clamped to the screen (assumes a Screen Space - Overlay canvas). When false, root stays wherever it was placed.
cursorOffset, screenEdgePadding Tuning for the follow-cursor placement.

TooltipTriggerComponent

Member Notes
item Serialized ItemDefinition, used via ItemTooltipSource when no sibling ITooltipSource component is present.
IPointerEnterHandler.OnPointerEnter / IPointerExitHandler.OnPointerExit Show/hide. Purely event-driven (UnityEngine.EventSystems) — no Update, no input polling.

Key design decisions

  • Headless service, dumb view. All content-building logic lives in ITooltipSource implementations and the ITooltipContext they resolve through. TooltipService only tracks "what is currently shown" and notifies listeners — it never touches uGUI. TooltipViewComponent only ever reads a TooltipContent it's handed — it never decides what that content is. This is the same separation WorldFlagStore/WorldFlagService and InteractionResolver/ InteractionDetectorComponent already draw between headless core and engine-facing glue, extended one layer further into UI: the view is now also just glue, not the place logic is born. A UITK skin, or a second uGUI skin with a different layout, can be written against ITooltipService/TooltipContent without this module's binding or service code changing at all.
  • Why a plain tuple, not UnityEngine.Color. TooltipContent/TooltipSection are the Kit's headless tooltip data model and deliberately hold zero UnityEngine references, mirroring WorldFlagStore/CalendarModel's headless-core discipline. TooltipAccentColor (a plain readonly struct of four floats) carries an optional section accent instead of UnityEngine.Color — and instead of a RarityDefinition reference, which would tie the generic tooltip data model to one specific binding's content type (ItemDefinition's rarity concept), even though nothing about a TooltipSection's accent is Items-specific. The engine coupling belongs in the binding layer that already has it: ItemTooltipSource converts RarityDefinition.Tint (a Color) into a TooltipAccentColor at the point of production, and TooltipViewComponent converts back to Color at the point of consumption. TooltipContent itself never sees either type.
  • Why no pooled events. Every other Tier-1 module with runtime state dispatches a pooled GameEvent for changes (WorldFlagChangedEvent, Interactable*Event) because those changes are genuinely cross-system: quest logic, save systems, and UI all care when a flag changes or something gets focused. Tooltip visibility has exactly one intended consumer — TooltipViewComponent (or an equivalent alternative skin) — and is inherently local to whatever triggered it (a hover, an inspection action). There is no cross-system "the tooltip changed, and other unrelated systems need to know" use case at this scope, so ITooltipService uses plain C# events only. Revisit at the Kit's usual rule of two, if a second unrelated consumer shows up that actually needs pool/lifecycle semantics.
  • ItemTooltipSource never throws, even though ItemDefinition.DisplayName/Description can. ItemDefinition's LocString properties assemble a LocString on every read and throw ArgumentException when the underlying loc key is blank — a misconfiguration ItemDefinition.Validate() already flags as a content issue. ItemTooltipSource guards each read and degrades to skipping that section instead of propagating, honoring ITooltipSource's "must never throw" contract. TooltipService.Show(ITooltipSource) additionally wraps the whole BuildTooltip call in a guard, so a third-party source that doesn't honor the contract as carefully still can't take down the service for every other consumer — belt and suspenders, not a substitute for the binding's own discipline.
  • Cursor-follow and the "no input polling" guidance. TooltipViewComponent's optional screenFollowCursor reads Input.mousePosition once per frame while visible. This is the one per-frame input read in this module's Runtime code, and it's a deliberate, narrow judgment call: it samples cursor position purely to reposition an already-visible panel — it never decides whether anything happens, unlike the action-triggering polling the Kit's guidance is aimed at (compare InteractionDetectorComponent, which is explicitly input-agnostic because the module's entire point is deciding whether an interaction happens — a decision that must come from the consumer's own input code, not from this module reading Input itself). Nothing about "should a tooltip be visible" is decided by this read; that's entirely ITooltipService's job, driven by TooltipTriggerComponent's fully event-driven IPointerEnterHandler/IPointerExitHandler. Additionally, the read is compiled under #if ENABLE_LEGACY_INPUT_MANAGER — on a consumer project whose Active Input Handling is "Input System (New)" only, UnityEngine.Input would throw every frame, so there cursor-follow simply no-ops (the panel stays where it's placed; use an Input-System-aware view subclass to follow the cursor). If a project's policy doesn't accept the philosophical distinction either, disable screenFollowCursor and position the panel some other way.
  • Text vs TMP_Text. LoLEngine-GameplayKit-UI's asmdef references UnityEngine.UI only — not Unity.TextMeshPro — so TooltipViewComponent uses legacy UnityEngine.UI.Text fields. This matches the engine's own legacy-Text uGUI samples (SaveLoadUI.cs, LocalizationExample.cs under LoL-Engine/Assets/LoLEngine/Samples) rather than the TMP-based ones (11_NowPlayingSample.cs) — the engine itself is inconsistent between samples, and without a Unity.TextMeshPro reference already scaffolded onto this assembly, Text is the only choice that compiles as shipped. A project with the TextMeshPro package available can swap these fields for TMP_Text by adding that assembly reference to LoLEngine-GameplayKit-UI.asmdef and changing the four field types — nothing else in this module (service, sources, content model) knows or cares which text component a view uses.

Events contract

None — see "Why no pooled events" above. ITooltipService.ContentChanged/Hidden are plain C# events only.

Save behavior

None. TooltipContent/ITooltipService.Current are transient presentation state — nothing here persists across a save, matching Items' "static content, not runtime state" reasoning for having no save domain, just for a different underlying reason (this module's state isn't content at all; it's "what's currently on screen," which is meaningless to restore from a save).

Deliberately deferred (per doc 07)

  • Rich comparison tooltips (side-by-side item comparison) — T2.
  • Controller focus routing (showing a tooltip for whatever a gamepad cursor/selection is on, as opposed to mouse hover) — T2. TooltipTriggerComponent is mouse/pointer-hover-shaped only; a controller-driven trigger is a different component this module doesn't ship.
  • A theming system beyond per-section Accent — T2.
  • A UI Toolkit (UITK) skin — deferred, not blocked: ITooltipService/TooltipContent were built headless specifically so this can exist later without touching the service or binding layer, per doc 07's risk note for this module.

Seam ledger

Per the Kit's portability discipline (doc 07, "Portability discipline"), every module's docs page lists exactly which engine surface it consumes. TooltipContent/TooltipSection/ TooltipContentBuilder/TooltipAccentColor (the headless data model) consume none — zero UnityEngine/LoLEngine.Core references. ITooltipSource/ITooltipContext/TooltipService consume:

Engine surface Namespace Used for
ILoLEngineService LoLEngine.Core.ServiceManagement.Interfaces Service lifecycle (Initialize/Shutdown) on ITooltipService.
LocString LoLEngine.Core.Localization.LocString ITooltipContext.Resolve's parameter type.
ILocalizationService LoLEngine.Core.Localization.Interfaces LocalizationTooltipContext's optional resolution path.

ItemTooltipSource (the Items binding) additionally consumes LoLEngine.GameplayKit.Items (ItemDefinition, RarityDefinition) and, implicitly through RarityDefinition.Tint, UnityEngine.Color (converted to TooltipAccentColor, never stored as Color).

TooltipViewComponent/TooltipTriggerComponent/UIServiceRegistrar (the engine-facing layer) additionally consume:

Engine surface Namespace Used for
MonoBehaviour, RectTransform, [SerializeField], [Tooltip], [Header], [AddComponentMenu], Input, Screen, Mathf, Vector2 UnityEngine Component shape, cursor-follow positioning.
UnityEngine.UI.Text UnityEngine.UI The stock skin's text fields — see "Text vs TMP_Text" above.
IPointerEnterHandler, IPointerExitHandler, PointerEventData UnityEngine.EventSystems TooltipTriggerComponent's hover detection.
ServiceLocator LoLEngine.Core.ServiceManagement.Service Resolving ITooltipService in OnEnable/Start().
Resources, UnityEngine.Debug UnityEngine UIServiceRegistrar loading GameplayKitConfig, fail-loud logging.
UnityEngine.Scripting.Preserve UnityEngine.Scripting IL2CPP-safe reflection construction of UIServiceRegistrar.