Jovian Popup System
A lightweight, low-allocation popup and tooltip system for Unity with category-based isolation, a generic element cache, type-safe element identifiers, and extensible animations.
Requirements
- Unity 2022.3 or later
- TextMeshPro 3.0.6+
- Input System 1.18.0+
- Newtonsoft JSON 3.2.1+
Install via the Unity Package Manager by adding the package from its local path or from a git URL.
Quick Start
1. Create a PopupSettings asset
In the Unity Editor, go to Assets > Create > Jovian > Popup System > Popup Settings. Configure element prefabs by adding entries to the Element Prefabs list, mapping PopupElementType values to your prefabs.
2. Build a PopupReference prefab
See the Prefab Setup section below.
3. Create and use PopupSystem
using Jovian.PopupSystem;
using Jovian.PopupSystem.UI;
// Create the system. canvasRoot auto-scans all PopupTrigger components.
var popup = new PopupSystem(settings, viewPrefab, canvasRoot);
popup.RegisterCategory(PopupCategory.Item);
popup.RegisterCategory(PopupCategory.Character);
// Set content on auto-scanned triggers by name.
var handler = popup.GetTriggerHandler("ItemSlot");
handler?.SetContent(builder => {
builder
.AddText("Health Potion", PopupElementType.Header)
.AddSeparator(PopupElementType.Separator)
.AddText("Restores health.", PopupElementType.Text)
.AddNameValue("Heal Amount", 50, PopupElementType.LabelValueText);
});
// Or show a popup directly from code.
popup.Show(PopupCategory.Item, builder => {
builder
.AddText("Iron Sword", PopupElementType.Header)
.AddNameValue("Damage", 12, PopupElementType.LabelValueText);
}, anchorRect, AnchorSide.Right);
// Tick each frame.
popup.Tick(Time.deltaTime);
// Clean up when the game state exits.
popup.Dispose();
PopupElementType
PopupElementType is a type-safe struct identifying element prefabs in the registry. The Inspector shows a dropdown with built-in types plus a custom text field for game-specific elements.
Built-in types
| Static Field | ID | Usage |
|---|---|---|
PopupElementType.Header |
"header" |
Bold header text |
PopupElementType.Text |
"text" |
Body text |
PopupElementType.LabelValueText |
"label_value_text" |
Label + value row (two TMP_Text children) |
PopupElementType.Image |
"image" |
Image/icon element |
PopupElementType.Separator |
"separator" |
Horizontal divider line |
Custom types
Define game-specific element types as static fields:
public static class MyPopupElements {
public static readonly PopupElementType Badge = new("badge");
public static readonly PopupElementType ProgressBar = new("progress_bar");
}
Variants
Create variants of built-in types using Variant():
// "header_gold" — a gold-styled header
PopupElementType.Header.Variant("gold")
// "separator_thick" — a thicker separator
PopupElementType.Separator.Variant("thick")
Register variant prefabs in PopupSettings with the variant ID (e.g. "header_gold").
PopupCategory
PopupCategory is a readonly struct that acts as a channel for isolating popups. Each category gets its own view instance.
Built-in categories
PopupCategory.Character // Character tooltips
PopupCategory.Item // Item tooltips
PopupCategory.Skill // Skill tooltips
PopupCategory.General // General-purpose tooltips
Custom categories
var lootCategory = new PopupCategory("Loot");
popup.RegisterCategory(lootCategory, priority: 5);
PopupSettings
ScriptableObject holding all configuration. Create via Assets > Create > Jovian > Popup System > Popup Settings.
| Field | Type | Default | Description |
|---|---|---|---|
popupDelay |
float | 0.4 | Seconds before popup appears. |
fadeDuration |
float | 0.2 | Fade animation duration. |
defaultAnchorSide |
AnchorSide | Below | Default anchor side. |
screenEdgePadding |
float | 10 | Minimum pixels from screen edge. |
maxPopupWidth |
float | 400 | Maximum popup width in pixels. |
sortingOrder |
int | 100 | Canvas sorting order. |
followMouseOffset |
Vector2 | (15, -15) | Cursor offset in follow mode. |
touchHoldDuration |
float | 0.6 | Touch hold duration. |
gamepadFocusTrigger |
bool | true | Trigger on gamepad focus. |
elementPrefabs |
List | empty | Element prefab registry (PopupElementType -> prefab). |
categoryPriorities |
List | empty | Per-category priority overrides. |
categoryDelayOverrides |
List | empty | Per-category delay overrides. |
PopupContentBuilder
Fluent API struct for composing popup content. All methods take a PopupElementType to identify which prefab to use.
Methods
// Generic — returns raw GameObject for custom elements
builder.Add(PopupElementType.Header);
builder.Add(new PopupElementType("custom_widget"));
// Text — sets TMP_Text on the element
builder.AddText("Fireball", PopupElementType.Header);
builder.AddText("Body text here.", PopupElementType.Text);
builder.AddText("Colored text", "#FFD700", PopupElementType.Text);
// Name/Value — sets two TMP_Text children (label + value)
builder.AddNameValue("Damage", 120, PopupElementType.LabelValueText);
builder.AddNameValue("Range", "15m", PopupElementType.LabelValueText);
// Image — sets sprite and height
builder.AddImage(iconSprite, PopupElementType.Image, 64f);
// Separator
builder.AddSeparator(PopupElementType.Separator);
Full example
popup.Show(PopupCategory.Skill, builder => {
builder
.AddText("Fireball", PopupElementType.Header)
.AddText("Hurls a ball of fire.", PopupElementType.Text)
.AddSeparator(PopupElementType.Separator)
.AddNameValue("Damage", 120, PopupElementType.LabelValueText)
.AddNameValue("Mana Cost", 35, PopupElementType.LabelValueText)
.AddSeparator(PopupElementType.Separator)
.AddText("Requires: Level 5", "FF6666", PopupElementType.Text);
}, targetRect);
PopupTrigger
MonoBehaviour attached to UI elements for hover-based popups. Forwards pointer events to a PopupTriggerView behavior handler.
Inspector fields
| Field | Type | Description |
|---|---|---|
category |
PopupCategory | Which category channel to use. |
anchorSide |
AnchorSide | Which side to anchor the popup. |
positionMode |
PopupPositionMode | AnchorToElement or FollowMouse. |
Approach 1: Auto-scanned triggers (recommended for static UI)
var popup = new PopupSystem(settings, viewPrefab, canvasRoot);
popup.RegisterCategory(PopupCategory.Character);
// GetTriggerHandler returns PopupTriggerView (the behavior handler)
var handler = popup.GetTriggerHandler("HeroPortrait");
handler?.SetContent(builder => {
builder
.AddText("Kael", PopupElementType.Header)
.AddNameValue("Health", 55, PopupElementType.LabelValueText);
});
// Or all handlers for a category
foreach(var h in popup.GetTriggerHandlers(PopupCategory.Item)) {
h.SetContent(builder => builder.AddText("Item", PopupElementType.Header));
}
Approach 2: Dynamic triggers (for runtime-created UI)
popup.InitializeTriggersInChildren(slotsContainer, (trigger, view) => {
var slotData = trigger.GetComponentInParent<SlotComponent>();
view.SetContent(builder => {
builder.AddText(slotData.itemName, PopupElementType.Header);
});
});
Approach 3: Code-only (no PopupTrigger needed)
// Anchored
popupSystem.Show(PopupCategory.Item, builder => { ... }, targetRect, AnchorSide.Right);
// Follow mouse
popupSystem.Show(PopupCategory.General, builder => { ... });
// Fixed screen position
popupSystem.ShowAtPosition(PopupCategory.General, builder => { ... }, screenPos);
// Hide
popupSystem.Hide(PopupCategory.Item);
popupSystem.HideAll();
Priority System
Higher priority categories dismiss lower ones when shown:
popup.RegisterCategory(PopupCategory.Character, priority: 10);
popup.RegisterCategory(PopupCategory.Item, priority: 5);
Settings-based priorities take precedence over registration arguments.
Lifecycle Integration
Created per game state, not as a singleton:
var guiCanvas = guiReferences.GetComponentInParent<Canvas>().transform;
var popupSystem = new PopupSystem(popupSettings, popupViewPrefab, guiCanvas);
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
// Tick each frame
popupSystem.Tick(Time.deltaTime);
// Dispose on state exit
popupSystem.Dispose();
IPopupAnimator
Extensible show/hide animation interface. Default: FadePopupAnimator (alpha lerp). Pass a factory to the constructor:
var popup = new PopupSystem(settings, viewPrefab, canvasRoot, () => new ScalePopupAnimator());
Prefab Setup
PopupReferencePrefab Canvas, CanvasGroup, CanvasScaler, PopupReference
Background Image, ContentSizeFitter (V=Preferred)
Content VerticalLayoutGroup (Control Child Width, Force Expand Width)
- Root: Canvas (Screen Space Overlay), CanvasScaler (Scale With Screen Size), CanvasGroup, PopupReference. Point anchors, no ContentSizeFitter.
- Background: Image, ContentSizeFitter (Vertical = Preferred).
- Content: Stretched to Background, VerticalLayoutGroup (Control Child Size Width, Child Force Expand Width), no ContentSizeFitter.
- Element prefabs: Create separate prefabs, register in PopupSettings under Element Prefabs with their
PopupElementType. No ContentSizeFitter on element prefabs. - Wire PopupReference: Assign Content, CanvasGroup, and Background fields.
Optimization Notes
PopupCategoryandPopupElementTypeare value-type structs — zero heap allocationPopupContentBuilderis a readonly struct operating directly on cached elements- Content elements use a grow-only cache keyed by
PopupElementType— activate/deactivate, never destroy after warmup - Delay timers and animation driven by float fields in
Tick()— no coroutines - Screen rect uses static
Vector3[4]buffer — no per-frame allocation - Layout rebuilds only on content change (dirty flag)
- Each category gets its own
IPopupAnimator— no concurrent animation corruption - MonoBehaviour (
PopupReference,PopupTrigger) holds references only; all behavior in plain C# classes (PopupView,PopupTriggerView)
Samples
The Samples~ folder contains:
- Prefabs: PopupReferencePrefab and all element prefabs
- Settings: Pre-configured PopupSettings asset
- Scripts: Three example scripts (auto-scanned triggers, dynamic triggers, code-only with variants)
Import via Unity Package Manager: select the package, expand Samples, click Import.
API Reference
Core Types
| Type | Namespace | Description |
|---|---|---|
IPopupSystem |
Jovian.PopupSystem |
Main interface for showing/hiding popups. |
PopupSystem |
Jovian.PopupSystem |
Implementation. Constructor: (PopupSettings, PopupReference, Transform, Func<IPopupAnimator>). |
PopupView |
Jovian.PopupSystem |
Behavior class: generic element cache, positioning, visibility. |
PopupSettings |
Jovian.PopupSystem |
ScriptableObject configuration + element prefab registry. |
PopupCategory |
Jovian.PopupSystem |
Struct identifying a popup channel. |
PopupElementType |
Jovian.PopupSystem |
Struct identifying an element prefab type. Built-ins + Variant(). |
PopupContentBuilder |
Jovian.PopupSystem |
Fluent readonly struct for building content. |
UI Types
| Type | Namespace | Description |
|---|---|---|
PopupReference |
Jovian.PopupSystem.UI |
MonoBehaviour reference holder (content, canvasGroup, background). |
PopupTrigger |
Jovian.PopupSystem.UI |
MonoBehaviour reference holder for hover triggers. |
PopupTriggerView |
Jovian.PopupSystem |
Behavior handler for triggers (SetContent, pointer forwarding). |
Animation Types
| Type | Namespace | Description |
|---|---|---|
IPopupAnimator |
Jovian.PopupSystem |
Interface for custom show/hide animations. |
FadePopupAnimator |
Jovian.PopupSystem |
Default fade animation via CanvasGroup alpha. |
Enums
| Type | Values |
|---|---|
AnchorSide |
Below, Above, Left, Right |
PopupPositionMode |
AnchorToElement, FollowMouse |