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
- Leave
enableTooltips on in your GameplayKitConfig asset (in a Resources folder, named
GameplayKitConfig — same convention every other module uses).
- 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.
- 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.
- 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. |
| 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. |
| 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. |
| 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. |
| 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. |