diff --git a/Assets/Code/GameState/UI/PartyGuiView.cs b/Assets/Code/GameState/UI/PartyGuiView.cs
index 05bcaa2..905e843 100644
--- a/Assets/Code/GameState/UI/PartyGuiView.cs
+++ b/Assets/Code/GameState/UI/PartyGuiView.cs
@@ -103,10 +103,10 @@ namespace Nox.Game.UI {
private void BuildCharacterPopup(PopupContentBuilder builder, CharacterDefinition member) {
// Header
builder
- .AddHeader(member.Name)
- .AddText($"{member.Race} {member.Class}", "#CCCCCC")
- .AddText($"Role: {member.Role}")
- .AddSeparator();
+ .AddText(member.Name, PopupElementType.Header)
+ .AddText($"{member.Race} {member.Class}", "#CCCCCC", PopupElementType.Text)
+ .AddText($"Role: {member.Role}", PopupElementType.Text)
+ .AddSeparator(PopupElementType.Separator);
// Stats
if(member.Stats?.stats != null) {
@@ -115,12 +115,12 @@ namespace Nox.Game.UI {
var health = member.Stats.GetValue(StatType.Health);
var mana = member.Stats.GetValue(StatType.Mana);
builder
- .AddStat("Level", level)
- .AddStat("XP", xp)
- .AddSeparator()
- .AddStat("Health", health)
- .AddStat("Mana", mana)
- .AddSeparator();
+ .AddNameValue("Level", level, PopupElementType.LabelValueText)
+ .AddNameValue("XP", xp, PopupElementType.LabelValueText)
+ .AddSeparator(PopupElementType.Separator)
+ .AddNameValue("Health", health, PopupElementType.LabelValueText)
+ .AddNameValue("Mana", mana, PopupElementType.LabelValueText)
+ .AddSeparator(PopupElementType.Separator);
}
// Attributes
@@ -129,26 +129,26 @@ namespace Nox.Game.UI {
if(attr.attribute == AttributeType.None) {
continue;
}
- builder.AddStat(attr.attribute.ToString(), attr.value);
+ builder.AddNameValue(attr.attribute.ToString(), attr.value, PopupElementType.LabelValueText);
}
}
- builder.AddSeparator();
+ builder.AddSeparator(PopupElementType.Separator);
// Perks
- if(member.Perks?.perks != null && member.Perks.perks.Count > 0) {
- builder.AddText("Perks", "#FFD700");
+ if(member.Perks?.perks is { Count: > 0 }) {
+ builder.AddText("Perks", "#FFD700", PopupElementType.Text);
foreach(var perk in member.Perks.perks) {
- builder.AddText($" {perk.Name}");
+ builder.AddText($" {perk.Name}", PopupElementType.Text);
}
- builder.AddSeparator();
+ builder.AddSeparator(PopupElementType.Separator);
}
// Modifiers
if(member.Modifiers?.modifiers is { Count: > 0 }) {
- builder.AddText("Modifiers", "#87CEEB");
+ builder.AddText("Modifiers", "#87CEEB", PopupElementType.Text);
foreach(var mod in member.Modifiers.modifiers) {
var target = mod.Target != null ? mod.Target.ToString() : "";
- builder.AddText($" {mod.Name} ({mod.Operation} {mod.Value} {target})");
+ builder.AddText($" {mod.Name} ({mod.Operation} {mod.Value} {target})", PopupElementType.Text);
}
}
}
diff --git a/Assets/Database/UI/PopupSettings.asset b/Assets/Database/UI/PopupSettings.asset
index 300fcc1..e8a3267 100644
--- a/Assets/Database/UI/PopupSettings.asset
+++ b/Assets/Database/UI/PopupSettings.asset
@@ -21,6 +21,22 @@ MonoBehaviour:
followMouseOffset: {x: 15, y: -15}
touchHoldDuration: 0.6
gamepadFocusTrigger: 1
+ elementPrefabs:
+ - type:
+ id: header
+ prefab: {fileID: 7034836061828108288, guid: dfc1bc0bd5b4905409615c3e770a5b77, type: 3}
+ - type:
+ id: image
+ prefab: {fileID: 5887814251614319338, guid: 5e715f4b614d02b4fa0b4d3fcfe3c053, type: 3}
+ - type:
+ id: label_value_text
+ prefab: {fileID: 6246834368258800846, guid: 5882db210c62d8647858933649f64c29, type: 3}
+ - type:
+ id: separator
+ prefab: {fileID: 6770634903822758885, guid: 7ccdfa1a2079db044be4b1684303ec7f, type: 3}
+ - type:
+ id: text
+ prefab: {fileID: 3157287847714375358, guid: bfa97c92d1878cc448ddc7dc456f4b17, type: 3}
categoryPriorities:
- category:
id: Character
diff --git a/Assets/Prefabs/UI/PopupSystem/PopupReferencePrefab.prefab b/Assets/Prefabs/UI/PopupSystem/PopupReferencePrefab.prefab
index 15f696e..3524044 100644
--- a/Assets/Prefabs/UI/PopupSystem/PopupReferencePrefab.prefab
+++ b/Assets/Prefabs/UI/PopupSystem/PopupReferencePrefab.prefab
@@ -68,11 +68,6 @@ MonoBehaviour:
content: {fileID: 176628901263125209}
canvasGroup: {fileID: 1835601435911948781}
background: {fileID: 8899521584296352500}
- headerPrefab: {fileID: 6612787789151041457, guid: dfc1bc0bd5b4905409615c3e770a5b77, type: 3}
- textPrefab: {fileID: 2506259255305457008, guid: bfa97c92d1878cc448ddc7dc456f4b17, type: 3}
- statPrefab: {fileID: 1843470073663794312, guid: 5882db210c62d8647858933649f64c29, type: 3}
- imagePrefab: {fileID: 7093821785826926595, guid: 5e715f4b614d02b4fa0b4d3fcfe3c053, type: 3}
- separatorPrefab: {fileID: 4190588985333916705, guid: 7ccdfa1a2079db044be4b1684303ec7f, type: 3}
--- !u!223 &3081303906751693297
Canvas:
m_ObjectHideFlags: 0
@@ -118,7 +113,7 @@ MonoBehaviour:
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
- m_PresetInfoIsWorld: 0
+ m_PresetInfoIsWorld: 1
--- !u!114 &7236468329886607693
MonoBehaviour:
m_ObjectHideFlags: 0
diff --git a/Packages/com.jovian.popup-system/Editor/PopupElementTypeDrawer.cs b/Packages/com.jovian.popup-system/Editor/PopupElementTypeDrawer.cs
new file mode 100644
index 0000000..dd12062
--- /dev/null
+++ b/Packages/com.jovian.popup-system/Editor/PopupElementTypeDrawer.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+
+namespace Jovian.PopupSystem.Editor {
+ ///
+ /// Custom PropertyDrawer for . Shows a dropdown with
+ /// built-in element types plus a "Custom..." option for free-text entry.
+ ///
+ [CustomPropertyDrawer(typeof(PopupElementType))]
+ public sealed class PopupElementTypeDrawer : PropertyDrawer {
+ private static readonly string[] builtInIds = {
+ "header",
+ "text",
+ "label_value_text",
+ "image",
+ "separator"
+ };
+
+ private const string customLabel = "Custom...";
+
+ public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
+ var idProp = FindIdProperty(property);
+ if(idProp == null) {
+ return EditorGUIUtility.singleLineHeight;
+ }
+
+ var currentValue = idProp.stringValue ?? "";
+ var isCustom = Array.IndexOf(builtInIds, currentValue) < 0 && !string.IsNullOrEmpty(currentValue);
+
+ return isCustom
+ ? EditorGUIUtility.singleLineHeight * 2 + 2
+ : EditorGUIUtility.singleLineHeight;
+ }
+
+ public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
+ EditorGUI.BeginProperty(position, label, property);
+
+ var idProp = FindIdProperty(property);
+ if(idProp == null) {
+ EditorGUI.LabelField(position, label.text, "Cannot resolve PopupElementType id field");
+ EditorGUI.EndProperty();
+ return;
+ }
+
+ var currentValue = idProp.stringValue ?? "";
+ var builtInIndex = Array.IndexOf(builtInIds, currentValue);
+ var isCustom = builtInIndex < 0 && !string.IsNullOrEmpty(currentValue);
+
+ var options = new List(builtInIds);
+ options.Add(customLabel);
+
+ int selectedIndex;
+ if(builtInIndex >= 0) {
+ selectedIndex = builtInIndex;
+ }
+ else {
+ selectedIndex = options.Count - 1;
+ }
+
+ var dropdownRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
+ var newIndex = EditorGUI.Popup(dropdownRect, label.text, selectedIndex, options.ToArray());
+
+ if(newIndex != selectedIndex) {
+ if(newIndex < builtInIds.Length) {
+ idProp.stringValue = builtInIds[newIndex];
+ }
+ else {
+ if(!isCustom) {
+ idProp.stringValue = "custom_element";
+ }
+ }
+ }
+
+ var finalIsCustom = newIndex >= builtInIds.Length || (newIndex == selectedIndex && isCustom);
+ if(finalIsCustom) {
+ var textRect = new Rect(
+ position.x + EditorGUIUtility.labelWidth + 2,
+ position.y + EditorGUIUtility.singleLineHeight + 2,
+ position.width - EditorGUIUtility.labelWidth - 2,
+ EditorGUIUtility.singleLineHeight);
+ var newValue = EditorGUI.TextField(textRect, idProp.stringValue);
+ if(newValue != idProp.stringValue) {
+ idProp.stringValue = newValue;
+ }
+ }
+
+ EditorGUI.EndProperty();
+ }
+
+ private static SerializedProperty FindIdProperty(SerializedProperty property) {
+ var prop = property.FindPropertyRelative("id");
+ if(prop != null) {
+ return prop;
+ }
+ return property.FindPropertyRelative("k__BackingField");
+ }
+ }
+}
diff --git a/Packages/com.jovian.popup-system/Editor/PopupElementTypeDrawer.cs.meta b/Packages/com.jovian.popup-system/Editor/PopupElementTypeDrawer.cs.meta
new file mode 100644
index 0000000..562f232
--- /dev/null
+++ b/Packages/com.jovian.popup-system/Editor/PopupElementTypeDrawer.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9ea57be8755c60e4bbf156ac2b32c98c
\ No newline at end of file
diff --git a/Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs b/Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs
index 91b1b60..8753416 100644
--- a/Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs
+++ b/Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs
@@ -51,6 +51,10 @@ namespace Jovian.PopupSystem.Editor {
EditorGUILayout.PropertyField(serializedSettings.FindProperty("touchHoldDuration"));
EditorGUILayout.PropertyField(serializedSettings.FindProperty("gamepadFocusTrigger"));
+ EditorGUILayout.Space(5);
+ EditorGUILayout.LabelField("Element Prefabs", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedSettings.FindProperty("elementPrefabs"), true);
+
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Priority", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedSettings.FindProperty("categoryPriorities"), true);
diff --git a/Packages/com.jovian.popup-system/README.md b/Packages/com.jovian.popup-system/README.md
index d48f602..68a01c1 100644
--- a/Packages/com.jovian.popup-system/README.md
+++ b/Packages/com.jovian.popup-system/README.md
@@ -1,6 +1,6 @@
# Jovian Popup System
-A lightweight, low-allocation popup and tooltip system for Unity with category-based isolation, a fluent content builder, and extensible animations.
+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
@@ -15,11 +15,11 @@ Install via the Unity Package Manager by adding the package from its local path
### 1. Create a PopupSettings asset
-In the Unity Editor, go to **Assets > Create > Jovian > Popup System > Popup Settings**. Place the asset somewhere accessible (e.g. `Assets/Settings/PopupSettings.asset`). You can also configure settings via **Project Settings > Jovian > Popup System**.
+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 for step-by-step instructions.
+See the [Prefab Setup](#prefab-setup) section below.
### 3. Create and use PopupSystem
@@ -27,38 +27,78 @@ See the [Prefab Setup](#prefab-setup) section below for step-by-step instruction
using Jovian.PopupSystem;
using Jovian.PopupSystem.UI;
-// Create the system. Pass the scene Canvas as canvasRoot to auto-scan all PopupTrigger components.
+// Create the system. canvasRoot auto-scans all PopupTrigger components.
var popup = new PopupSystem(settings, viewPrefab, canvasRoot);
-// Register categories you intend to use.
popup.RegisterCategory(PopupCategory.Item);
popup.RegisterCategory(PopupCategory.Character);
-// Option A: Set content on auto-scanned triggers by name.
-var trigger = popup.GetTrigger("ItemSlot");
-trigger?.SetContent(builder => {
+// Set content on auto-scanned triggers by name.
+var handler = popup.GetTriggerHandler("ItemSlot");
+handler?.SetContent(builder => {
builder
- .AddHeader("Health Potion")
- .AddSeparator()
- .AddText("Restores a moderate amount of health.")
- .AddStat("Heal Amount", 50);
+ .AddText("Health Potion", PopupElementType.Header)
+ .AddSeparator(PopupElementType.Separator)
+ .AddText("Restores health.", PopupElementType.Text)
+ .AddNameValue("Heal Amount", 50, PopupElementType.LabelValueText);
});
-// Option B: Show a popup directly from code.
+// Or show a popup directly from code.
popup.Show(PopupCategory.Item, builder => {
- builder.AddHeader("Iron Sword").AddStat("Damage", 12);
+ builder
+ .AddText("Iron Sword", PopupElementType.Header)
+ .AddNameValue("Damage", 12, PopupElementType.LabelValueText);
}, anchorRect, AnchorSide.Right);
-// Tick each frame (typically in Update or a game-state loop).
+// Tick each frame.
popup.Tick(Time.deltaTime);
-// Clean up when the game state is torn down.
+// 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 and can have independent priority and delay settings.
+`PopupCategory` is a readonly struct that acts as a channel for isolating popups. Each category gets its own view instance.
### Built-in categories
@@ -71,47 +111,55 @@ PopupCategory.General // General-purpose tooltips
### Custom categories
-Create any number of additional categories:
-
```csharp
var lootCategory = new PopupCategory("Loot");
popup.RegisterCategory(lootCategory, priority: 5);
```
-Categories are compared by their string ID using ordinal comparison.
-
## PopupSettings
-`PopupSettings` is a ScriptableObject that holds all configuration. Create one via **Assets > Create > Jovian > Popup System > Popup Settings**.
+ScriptableObject holding all configuration. Create via **Assets > Create > Jovian > Popup System > Popup Settings**.
| Field | Type | Default | Description |
|---|---|---|---|
-| `popupDelay` | float | 0.4 | Seconds before the popup appears after a show request. |
-| `fadeDuration` | float | 0.2 | Duration of the fade-in and fade-out animation. |
-| `defaultAnchorSide` | AnchorSide | Below | Default side to anchor the popup relative to the target element. |
-| `screenEdgePadding` | float | 10 | Minimum pixel distance from screen edges. |
-| `maxPopupWidth` | float | 400 | Maximum width of the popup in pixels. |
-| `sortingOrder` | int | 100 | Sorting order applied to the popup Canvas. |
-| `followMouseOffset` | Vector2 | (15, -15) | Pixel offset from the cursor in follow-mouse mode. |
-| `touchHoldDuration` | float | 0.6 | Seconds a touch must be held before triggering a popup. |
-| `gamepadFocusTrigger` | bool | true | Whether gamepad focus triggers popups. |
-| `categoryPriorities` | List | empty | Per-category priority overrides. Higher priority popups dismiss lower ones. |
-| `categoryDelayOverrides` | List | empty | Per-category delay overrides. Overrides `popupDelay` for specific categories. |
+| `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
-`PopupContentBuilder` is a struct with a fluent API for composing popup content. You receive it in the build callback passed to `Show` or `ShowAtPosition`.
+Fluent API struct for composing popup content. All methods take a `PopupElementType` to identify which prefab to use.
-### Available methods
+### Methods
```csharp
-builder.AddHeader("Fireball"); // Bold header text
-builder.AddText("Deals fire damage to all enemies."); // Body text
-builder.AddText("Rare", "FFD700"); // Colored text (hex, with or without #)
-builder.AddStat("Damage", 120); // Label + integer value row
-builder.AddStat("Range", "15m"); // Label + string value row
-builder.AddImage(iconSprite, 64f); // Sprite with optional height
-builder.AddSeparator(); // Horizontal divider line
+// 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
@@ -119,331 +167,143 @@ builder.AddSeparator(); // Horizontal divider lin
```csharp
popup.Show(PopupCategory.Skill, builder => {
builder
- .AddHeader("Fireball")
- .AddText("Hurls a ball of fire that explodes on impact.")
- .AddSeparator()
- .AddStat("Damage", 120)
- .AddStat("Mana Cost", 35)
- .AddStat("Range", "15m")
- .AddSeparator()
- .AddText("Requires: Level 5", "FF6666");
+ .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);
```
-All elements are drawn from a grow-only pool inside `PopupReference`. No allocations occur once the pool is warmed.
-
## PopupTrigger
-`PopupTrigger` is a MonoBehaviour you attach to UI elements to get hover-based popup behavior automatically. It implements `IPointerEnterHandler` and `IPointerExitHandler`.
+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 of this element to anchor the popup. |
+| `anchorSide` | AnchorSide | Which side to anchor the popup. |
| `positionMode` | PopupPositionMode | `AnchorToElement` or `FollowMouse`. |
### Approach 1: Auto-scanned triggers (recommended for static UI)
-When you pass a `canvasRoot` to the `PopupSystem` constructor, it auto-scans all `PopupTrigger` components and binds them. You only need to set content:
-
```csharp
var popup = new PopupSystem(settings, viewPrefab, canvasRoot);
popup.RegisterCategory(PopupCategory.Character);
-// Find a trigger by its GameObject name and set content
-var trigger = popup.GetTrigger("HeroPortrait");
-trigger?.SetContent(builder => {
- builder.AddHeader("Kael").AddStat("Health", 55);
+// 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 set content on all triggers of a category
-foreach(var t in popup.GetTriggers(PopupCategory.Item)) {
- t.SetContent(builder => builder.AddHeader("Item"));
+// 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)
-For UI elements instantiated at runtime (inventory slots, party portraits), use `InitializeTriggersInChildren` after instantiation:
-
```csharp
-// After instantiating slot prefabs under a container:
-popup.InitializeTriggersInChildren(slotsContainer, trigger => {
+popup.InitializeTriggersInChildren(slotsContainer, (trigger, view) => {
var slotData = trigger.GetComponentInParent();
- trigger.SetContent(builder => {
- builder.AddHeader(slotData.itemName);
+ view.SetContent(builder => {
+ builder.AddText(slotData.itemName, PopupElementType.Header);
});
});
```
-This scans, binds, and registers all triggers under the parent, then calls the callback on each.
-
-### Approach 3: Full manual initialization
-
-For complete control, bypass auto-scan and initialize triggers directly:
+### Approach 3: Code-only (no PopupTrigger needed)
```csharp
-var trigger = button.GetComponent();
-trigger.Initialize(popupSystem, builder => {
- builder.AddHeader("Attack").AddStat("Damage", 25);
-});
+// Anchored
+popupSystem.Show(PopupCategory.Item, builder => { ... }, targetRect, AnchorSide.Right);
-// Or with a category override:
-trigger.Initialize(popupSystem, PopupCategory.Skill, builder => {
- builder.AddHeader("Fireball");
-});
-```
+// Follow mouse
+popupSystem.Show(PopupCategory.General, builder => { ... });
-### Updating content
+// Fixed screen position
+popupSystem.ShowAtPosition(PopupCategory.General, builder => { ... }, screenPos);
-To change content on an already-initialized trigger without re-binding:
-
-```csharp
-trigger.SetContent(builder => {
- builder.AddHeader("Attack").AddStat("Damage", newDamageValue);
-});
-
-// UpdateContent is an alias for SetContent
-trigger.UpdateContent(builder => { ... });
-```
-
-## Code-Only Triggers
-
-You do not need `PopupTrigger` to show popups. Call `IPopupSystem` methods directly:
-
-### Anchor to a RectTransform
-
-```csharp
-popupSystem.Show(PopupCategory.Item, builder => {
- builder.AddHeader("Iron Sword");
-}, inventorySlotRect, AnchorSide.Right);
-```
-
-### Follow the mouse cursor
-
-Pass no anchor argument:
-
-```csharp
-popupSystem.Show(PopupCategory.General, builder => {
- builder.AddText("Click to interact");
-});
-```
-
-### Show at a fixed screen position
-
-```csharp
-popupSystem.ShowAtPosition(PopupCategory.General, builder => {
- builder.AddText("Tutorial tip");
-}, new Vector2(Screen.width * 0.5f, Screen.height * 0.5f));
-```
-
-### Hide
-
-```csharp
-popupSystem.Hide(PopupCategory.Item); // Hide a specific category
-popupSystem.HideAll(); // Hide all categories
+// Hide
+popupSystem.Hide(PopupCategory.Item);
+popupSystem.HideAll();
```
## Priority System
-When a popup is shown, any visible popup from a lower-priority category is automatically dismissed. Priority is configured per category via `PopupSettings.categoryPriorities` or at registration time:
+Higher priority categories dismiss lower ones when shown:
```csharp
popup.RegisterCategory(PopupCategory.Character, priority: 10);
popup.RegisterCategory(PopupCategory.Item, priority: 5);
-popup.RegisterCategory(PopupCategory.General, priority: 1);
```
-If the player hovers a character portrait (priority 10) while an item tooltip (priority 5) is visible, the item tooltip is dismissed. Popups of equal or higher priority are not affected.
-
-Settings-based priorities (from `PopupSettings.categoryPriorities`) take precedence over the `priority` argument in `RegisterCategory`.
+Settings-based priorities take precedence over registration arguments.
## Lifecycle Integration
-The popup system is designed to be created per game state, not as a global singleton. Each game state owns its own `IPopupSystem` instance:
+Created per game state, not as a singleton:
```csharp
-// In your game state initialization:
var guiCanvas = guiReferences.GetComponentInParent