# 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](#prefab-setup) section below. ### 3. Create and use PopupSystem ```csharp 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: ```csharp 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()`: ```csharp // "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 ```csharp PopupCategory.Character // Character tooltips PopupCategory.Item // Item tooltips PopupCategory.Skill // Skill tooltips PopupCategory.General // General-purpose tooltips ``` ### Custom categories ```csharp 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 ```csharp // 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 ```csharp 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) ```csharp 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) ```csharp popup.InitializeTriggersInChildren(slotsContainer, (trigger, view) => { var slotData = trigger.GetComponentInParent(); view.SetContent(builder => { builder.AddText(slotData.itemName, PopupElementType.Header); }); }); ``` ### Approach 3: Code-only (no PopupTrigger needed) ```csharp // 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: ```csharp 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: ```csharp var guiCanvas = guiReferences.GetComponentInParent().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: ```csharp 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) ``` 1. **Root**: Canvas (Screen Space Overlay), CanvasScaler (Scale With Screen Size), CanvasGroup, PopupReference. Point anchors, no ContentSizeFitter. 2. **Background**: Image, ContentSizeFitter (Vertical = Preferred). 3. **Content**: Stretched to Background, VerticalLayoutGroup (Control Child Size Width, Child Force Expand Width), no ContentSizeFitter. 4. **Element prefabs**: Create separate prefabs, register in PopupSettings under Element Prefabs with their `PopupElementType`. No ContentSizeFitter on element prefabs. 5. **Wire PopupReference**: Assign Content, CanvasGroup, and Background fields. ## Optimization Notes - `PopupCategory` and `PopupElementType` are value-type structs — zero heap allocation - `PopupContentBuilder` is 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)`. | | `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` |