# 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<SlotComponent>();
    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<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:

```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<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` |
