forked from Shardstone/trail-into-darkness
Added a popup system
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Jovian.PopupSystem.Editor",
|
||||
"rootNamespace": "Jovian.PopupSystem.Editor",
|
||||
"references": [
|
||||
"Jovian.PopupSystem"
|
||||
],
|
||||
"includePlatforms": ["Editor"],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem.Editor {
|
||||
public sealed class PopupSettingsProvider : SettingsProvider {
|
||||
private PopupSettings settings;
|
||||
|
||||
private PopupSettingsProvider(string path, SettingsScope scope)
|
||||
: base(path, scope) { }
|
||||
|
||||
private SerializedObject serializedSettings;
|
||||
|
||||
public override void OnActivate(string searchContext, UnityEngine.UIElements.VisualElement rootElement) {
|
||||
var guids = AssetDatabase.FindAssets("t:PopupSettings");
|
||||
if(guids.Length > 0) {
|
||||
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
settings = AssetDatabase.LoadAssetAtPath<PopupSettings>(path);
|
||||
}
|
||||
if(settings != null) {
|
||||
serializedSettings = new SerializedObject(settings);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnGUI(string searchContext) {
|
||||
if(settings == null) {
|
||||
EditorGUILayout.HelpBox("No PopupSettings asset found. Create one via Assets > Create > Jovian > Popup System > Popup Settings.", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
EditorGUILayout.LabelField("Popup System Settings", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
serializedSettings.Update();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("popupDelay"));
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("fadeDuration"));
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("defaultAnchorSide"));
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("screenEdgePadding"));
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("maxPopupWidth"));
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("sortingOrder"));
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Follow Mouse", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("followMouseOffset"));
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Input", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("touchHoldDuration"));
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("gamepadFocusTrigger"));
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Priority", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("categoryPriorities"), true);
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
EditorGUILayout.LabelField("Per-Category Overrides", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serializedSettings.FindProperty("categoryDelayOverrides"), true);
|
||||
|
||||
if(EditorGUI.EndChangeCheck()) {
|
||||
serializedSettings.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(settings);
|
||||
}
|
||||
}
|
||||
|
||||
[SettingsProvider]
|
||||
public static SettingsProvider CreateProvider() {
|
||||
var provider = new PopupSettingsProvider("Project/Jovian/Popup System", SettingsScope.Project) {
|
||||
keywords = new HashSet<string>(new[] { "popup", "tooltip", "hover", "delay" })
|
||||
};
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
417
Packages/com.jovian.popup-system/README.md
Normal file
417
Packages/com.jovian.popup-system/README.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# Jovian Popup System
|
||||
|
||||
A lightweight, low-allocation popup and tooltip system for Unity with category-based isolation, a fluent content builder, 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**. Place the asset somewhere accessible (e.g. `Assets/Settings/PopupSettings.asset`). You can also configure settings via **Project Settings > Jovian > Popup System**.
|
||||
|
||||
### 2. Build a PopupView prefab
|
||||
|
||||
See the [Prefab Setup](#prefab-setup) section below for step-by-step instructions.
|
||||
|
||||
### 3. Create and use PopupSystem
|
||||
|
||||
```csharp
|
||||
using Jovian.PopupSystem;
|
||||
using Jovian.PopupSystem.UI;
|
||||
|
||||
// Create the system. viewPrefab is a reference to your PopupView prefab.
|
||||
var popup = new PopupSystem(settings, viewPrefab);
|
||||
|
||||
// Register categories you intend to use.
|
||||
popup.RegisterCategory(PopupCategory.Item);
|
||||
popup.RegisterCategory(PopupCategory.Character);
|
||||
|
||||
// Show a popup anchored to a UI element.
|
||||
popup.Show(PopupCategory.Item, builder => {
|
||||
builder
|
||||
.AddHeader("Health Potion")
|
||||
.AddSeparator()
|
||||
.AddText("Restores a moderate amount of health.")
|
||||
.AddStat("Heal Amount", 50)
|
||||
.AddStat("Uses", "3 / 3");
|
||||
}, anchorRect, AnchorSide.Right);
|
||||
|
||||
// Tick each frame (typically in Update or a game-state loop).
|
||||
popup.Tick(Time.deltaTime);
|
||||
|
||||
// Clean up when the game state is torn down.
|
||||
popup.Dispose();
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
### Built-in categories
|
||||
|
||||
```csharp
|
||||
PopupCategory.Character // Character tooltips
|
||||
PopupCategory.Item // Item tooltips
|
||||
PopupCategory.Skill // Skill tooltips
|
||||
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**.
|
||||
|
||||
| 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. |
|
||||
|
||||
## 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`.
|
||||
|
||||
### Available 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
|
||||
```
|
||||
|
||||
### Full example
|
||||
|
||||
```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");
|
||||
}, targetRect);
|
||||
```
|
||||
|
||||
All elements are drawn from a grow-only pool inside `PopupView`. 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`.
|
||||
|
||||
### Inspector fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `category` | PopupCategory | Which category channel to use. |
|
||||
| `anchorSide` | AnchorSide | Which side of this element to anchor the popup. |
|
||||
| `positionMode` | PopupPositionMode | `AnchorToElement` or `FollowMouse`. |
|
||||
|
||||
### Initialization
|
||||
|
||||
`PopupTrigger` must be initialized from code before it will respond to pointer events:
|
||||
|
||||
```csharp
|
||||
var trigger = button.GetComponent<PopupTrigger>();
|
||||
trigger.Initialize(popupSystem, builder => {
|
||||
builder
|
||||
.AddHeader("Attack")
|
||||
.AddStat("Damage", 25);
|
||||
});
|
||||
```
|
||||
|
||||
You can also override the category at initialization time:
|
||||
|
||||
```csharp
|
||||
trigger.Initialize(popupSystem, PopupCategory.Skill, builder => {
|
||||
builder.AddHeader("Fireball");
|
||||
});
|
||||
```
|
||||
|
||||
To change content without re-initializing:
|
||||
|
||||
```csharp
|
||||
trigger.UpdateContent(builder => {
|
||||
builder
|
||||
.AddHeader("Attack")
|
||||
.AddStat("Damage", newDamageValue);
|
||||
});
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```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`.
|
||||
|
||||
## 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:
|
||||
|
||||
```csharp
|
||||
// In your game state constructor or initialization:
|
||||
var popupSystem = new PopupSystem(popupSettings, popupViewPrefab);
|
||||
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
|
||||
popupSystem.RegisterCategory(PopupCategory.Item, priority: 5);
|
||||
|
||||
// Pass to views/play modes via constructor DI
|
||||
|
||||
// In your game state's Tick/Update:
|
||||
popupSystem.Tick(Time.deltaTime);
|
||||
|
||||
// When the game state exits:
|
||||
popupSystem.Dispose(); // destroys all popup view GameObjects
|
||||
```
|
||||
|
||||
Each category lazily creates its own `PopupView` instance on first `Show` call. On `Dispose`, all views are destroyed. This ensures no leaked GameObjects when transitioning between game states.
|
||||
|
||||
## IPopupAnimator
|
||||
|
||||
The popup system uses `IPopupAnimator` for show/hide transitions. The default implementation is `FadePopupAnimator`, which lerps the `CanvasGroup.alpha` over the configured `fadeDuration`.
|
||||
|
||||
### Interface
|
||||
|
||||
```csharp
|
||||
public interface IPopupAnimator {
|
||||
void Show(CanvasGroup canvasGroup, float duration, Action onComplete);
|
||||
void Hide(CanvasGroup canvasGroup, float duration, Action onComplete);
|
||||
void Tick(float deltaTime);
|
||||
bool IsAnimating { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### Custom animator example
|
||||
|
||||
```csharp
|
||||
public sealed class ScalePopupAnimator : IPopupAnimator {
|
||||
private CanvasGroup target;
|
||||
private float duration;
|
||||
private float elapsed;
|
||||
private float startScale;
|
||||
private float endScale;
|
||||
private Action onComplete;
|
||||
|
||||
public bool IsAnimating => target != null;
|
||||
|
||||
public void Show(CanvasGroup canvasGroup, float duration, Action onComplete) {
|
||||
target = canvasGroup;
|
||||
this.duration = Mathf.Max(duration, 0.001f);
|
||||
elapsed = 0f;
|
||||
startScale = 0f;
|
||||
endScale = 1f;
|
||||
this.onComplete = onComplete;
|
||||
canvasGroup.transform.localScale = Vector3.zero;
|
||||
canvasGroup.alpha = 1f;
|
||||
}
|
||||
|
||||
public void Hide(CanvasGroup canvasGroup, float duration, Action onComplete) {
|
||||
target = canvasGroup;
|
||||
this.duration = Mathf.Max(duration, 0.001f);
|
||||
elapsed = 0f;
|
||||
startScale = 1f;
|
||||
endScale = 0f;
|
||||
this.onComplete = onComplete;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime) {
|
||||
if(target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
elapsed += deltaTime;
|
||||
var t = Mathf.Clamp01(elapsed / duration);
|
||||
var scale = Mathf.Lerp(startScale, endScale, t);
|
||||
target.transform.localScale = Vector3.one * scale;
|
||||
|
||||
if(t >= 1f) {
|
||||
var callback = onComplete;
|
||||
target = null;
|
||||
onComplete = null;
|
||||
callback?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pass a factory function to the constructor so each category gets its own animator instance:
|
||||
|
||||
```csharp
|
||||
var popup = new PopupSystem(settings, viewPrefab, () => new ScalePopupAnimator());
|
||||
```
|
||||
|
||||
## Prefab Setup
|
||||
|
||||
Build the `PopupView` prefab with the following hierarchy:
|
||||
|
||||
### Step 1: Root GameObject
|
||||
|
||||
1. Create a new GameObject named `PopupView`.
|
||||
2. Add a `Canvas` component. Set **Render Mode** to **Screen Space - Overlay**. Set **Sort Order** to match `PopupSettings.sortingOrder` (default 100).
|
||||
3. Add a `CanvasGroup` component.
|
||||
4. Add a `PopupView` component (from `Jovian.PopupSystem.UI`).
|
||||
|
||||
### Step 2: Background
|
||||
|
||||
1. Create a child GameObject named `Background`.
|
||||
2. Add a `RectTransform` and an `Image` component. Set the image color/sprite to your desired popup background.
|
||||
3. Add a `ContentSizeFitter` with **Vertical Fit** set to **Preferred Size**.
|
||||
|
||||
### Step 3: Content container
|
||||
|
||||
1. Create a child of `Background` named `Content`.
|
||||
2. Add a `RectTransform`. Stretch it to fill the background with appropriate padding.
|
||||
3. Add a `VerticalLayoutGroup`. Configure padding and spacing to taste.
|
||||
4. Add a `ContentSizeFitter` with **Vertical Fit** set to **Preferred Size**.
|
||||
|
||||
### Step 4: Element prefabs
|
||||
|
||||
Create these as child prefabs (or separate prefabs). Each must be a prefab reference, not an in-hierarchy instance:
|
||||
|
||||
| Prefab | Component | Notes |
|
||||
|---|---|---|
|
||||
| Header | `TMP_Text` | Bold, larger font size |
|
||||
| Text | `TMP_Text` | Regular body text |
|
||||
| Stat | `RectTransform` with `HorizontalLayoutGroup` | Must have exactly two `TMP_Text` children: label and value |
|
||||
| Image | `Image` | For icons or artwork |
|
||||
| Separator | `Image` | Thin horizontal line |
|
||||
|
||||
### Step 5: Wire references
|
||||
|
||||
On the `PopupView` component, assign:
|
||||
|
||||
- **Content** - the Content RectTransform
|
||||
- **Canvas Group** - the root CanvasGroup
|
||||
- **Background** - the Background RectTransform
|
||||
- **Header Prefab** - your header TMP_Text prefab
|
||||
- **Text Prefab** - your body text TMP_Text prefab
|
||||
- **Stat Prefab** - your stat row RectTransform prefab
|
||||
- **Image Prefab** - your image prefab
|
||||
- **Separator Prefab** - your separator Image prefab
|
||||
|
||||
Save as a prefab. The `maxPopupWidth` from `PopupSettings` is applied at runtime to constrain the popup's horizontal size.
|
||||
|
||||
## Optimization Notes
|
||||
|
||||
The popup system is designed for minimal runtime allocation:
|
||||
|
||||
- `PopupCategory` is a readonly struct with value semantics -- zero heap allocation on pass or comparison
|
||||
- `PopupContentBuilder` is a struct that operates directly on cached UI elements -- no intermediate data structures
|
||||
- Content elements use a **grow-only cache**: elements are activated/deactivated, never instantiated or destroyed after warmup. `GetStat` caches TMP_Text references in a struct to avoid `GetComponentsInChildren` on reuse
|
||||
- Delay timers and animation are driven by `float` fields in `Tick()` -- no coroutines, no `WaitForSeconds`
|
||||
- Screen rect calculation uses a static `Vector3[4]` buffer -- no per-frame array allocation
|
||||
- Layout rebuilds only occur when content changes (dirty flag), not on every position update
|
||||
- Each registered category gets its own `IPopupAnimator` instance, preventing state corruption during concurrent show/hide animations
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Types
|
||||
|
||||
| Type | Namespace | Description |
|
||||
|---|---|---|
|
||||
| `IPopupSystem` | `Jovian.PopupSystem` | Main interface for showing/hiding popups. |
|
||||
| `PopupSystem` | `Jovian.PopupSystem` | Concrete implementation. Constructor: `(PopupSettings, PopupView, Func<IPopupAnimator>)`. |
|
||||
| `PopupSettings` | `Jovian.PopupSystem` | ScriptableObject with all configuration fields. |
|
||||
| `PopupCategory` | `Jovian.PopupSystem` | Readonly struct identifying a popup channel. |
|
||||
| `PopupContentBuilder` | `Jovian.PopupSystem` | Fluent struct for building popup content in callbacks. |
|
||||
|
||||
### UI Types
|
||||
|
||||
| Type | Namespace | Description |
|
||||
|---|---|---|
|
||||
| `PopupView` | `Jovian.PopupSystem.UI` | MonoBehaviour managing popup layout, pooling, and positioning. |
|
||||
| `PopupTrigger` | `Jovian.PopupSystem.UI` | MonoBehaviour for hover-based popup triggers on UI elements. |
|
||||
|
||||
### 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` |
|
||||
|
||||
## License
|
||||
|
||||
See the LICENSE file in the package root.
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
public sealed class FadePopupAnimator : IPopupAnimator {
|
||||
private CanvasGroup target;
|
||||
private float duration;
|
||||
private float elapsed;
|
||||
private float startAlpha;
|
||||
private float endAlpha;
|
||||
private Action onComplete;
|
||||
|
||||
public bool IsAnimating => target != null;
|
||||
|
||||
public void Show(CanvasGroup canvasGroup, float duration, Action onComplete) {
|
||||
target = canvasGroup;
|
||||
this.duration = Mathf.Max(duration, 0.001f);
|
||||
elapsed = 0f;
|
||||
startAlpha = 0f;
|
||||
endAlpha = 1f;
|
||||
this.onComplete = onComplete;
|
||||
canvasGroup.alpha = 0f;
|
||||
}
|
||||
|
||||
public void Hide(CanvasGroup canvasGroup, float duration, Action onComplete) {
|
||||
target = canvasGroup;
|
||||
this.duration = Mathf.Max(duration, 0.001f);
|
||||
elapsed = 0f;
|
||||
startAlpha = canvasGroup.alpha;
|
||||
endAlpha = 0f;
|
||||
this.onComplete = onComplete;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime) {
|
||||
if(target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
elapsed += deltaTime;
|
||||
var t = Mathf.Clamp01(elapsed / duration);
|
||||
target.alpha = Mathf.Lerp(startAlpha, endAlpha, t);
|
||||
|
||||
if(t >= 1f) {
|
||||
var callback = onComplete;
|
||||
target = null;
|
||||
onComplete = null;
|
||||
callback?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs
Normal file
11
Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
public interface IPopupAnimator {
|
||||
void Show(CanvasGroup canvasGroup, float duration, Action onComplete);
|
||||
void Hide(CanvasGroup canvasGroup, float duration, Action onComplete);
|
||||
void Tick(float deltaTime);
|
||||
bool IsAnimating { get; }
|
||||
}
|
||||
}
|
||||
16
Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs
Normal file
16
Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
public interface IPopupSystem {
|
||||
void RegisterCategory(PopupCategory category, int priority = 0);
|
||||
void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
|
||||
RectTransform anchor = null, AnchorSide? anchorSide = null);
|
||||
void ShowAtPosition(PopupCategory category, Action<PopupContentBuilder> buildContent,
|
||||
Vector2 screenPosition);
|
||||
void Hide(PopupCategory category);
|
||||
void HideAll();
|
||||
void Tick(float deltaTime);
|
||||
void Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Jovian.PopupSystem",
|
||||
"rootNamespace": "Jovian.PopupSystem",
|
||||
"references": [
|
||||
"Unity.TextMeshPro",
|
||||
"Unity.InputSystem"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"Newtonsoft.Json.dll"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
44
Packages/com.jovian.popup-system/Runtime/PopupCategory.cs
Normal file
44
Packages/com.jovian.popup-system/Runtime/PopupCategory.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
[Serializable]
|
||||
public readonly struct PopupCategory : IEquatable<PopupCategory> {
|
||||
[SerializeField] readonly string id;
|
||||
|
||||
public string Id => id;
|
||||
|
||||
public PopupCategory(string id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public static readonly PopupCategory Character = new("Character");
|
||||
public static readonly PopupCategory Item = new("Item");
|
||||
public static readonly PopupCategory Skill = new("Skill");
|
||||
public static readonly PopupCategory General = new("General");
|
||||
|
||||
public bool Equals(PopupCategory other) {
|
||||
return string.Equals(id, other.id, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
return obj is PopupCategory other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return id != null ? id.GetHashCode() : 0;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return id ?? string.Empty;
|
||||
}
|
||||
|
||||
public static bool operator ==(PopupCategory left, PopupCategory right) {
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(PopupCategory left, PopupCategory right) {
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
public sealed class PopupCategoryJsonConverter : JsonConverter<PopupCategory> {
|
||||
public override void WriteJson(JsonWriter writer, PopupCategory value, JsonSerializer serializer) {
|
||||
writer.WriteValue(value.Id);
|
||||
}
|
||||
|
||||
public override PopupCategory ReadJson(JsonReader reader, Type objectType, PopupCategory existingValue, bool hasExistingValue, JsonSerializer serializer) {
|
||||
var id = reader.Value as string;
|
||||
return new PopupCategory(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Jovian.PopupSystem.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
public struct PopupContentBuilder {
|
||||
readonly PopupView view;
|
||||
|
||||
public PopupContentBuilder(PopupView view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddHeader(string text) {
|
||||
var header = view.GetHeader();
|
||||
header.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddText(string text) {
|
||||
var label = view.GetText();
|
||||
label.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddText(string text, string hexColor) {
|
||||
var label = view.GetText();
|
||||
var prefix = hexColor.Length > 0 && hexColor[0] == '#' ? "" : "#";
|
||||
label.text = $"<color={prefix}{hexColor}>{text}</color>";
|
||||
return this;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddStat(string label, int value) {
|
||||
var (labelText, valueText) = view.GetStat();
|
||||
labelText.text = label;
|
||||
valueText.text = value.ToString();
|
||||
return this;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddStat(string label, string value) {
|
||||
var (labelText, valueText) = view.GetStat();
|
||||
labelText.text = label;
|
||||
valueText.text = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddImage(Sprite sprite, float height = 64f) {
|
||||
var image = view.GetImage();
|
||||
image.sprite = sprite;
|
||||
var rt = (RectTransform)image.transform;
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PopupContentBuilder AddSeparator() {
|
||||
view.GetSeparator();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Packages/com.jovian.popup-system/Runtime/PopupEnums.cs
Normal file
13
Packages/com.jovian.popup-system/Runtime/PopupEnums.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Jovian.PopupSystem {
|
||||
public enum AnchorSide {
|
||||
Below,
|
||||
Above,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
public enum PopupPositionMode {
|
||||
AnchorToElement,
|
||||
FollowMouse
|
||||
}
|
||||
}
|
||||
59
Packages/com.jovian.popup-system/Runtime/PopupSettings.cs
Normal file
59
Packages/com.jovian.popup-system/Runtime/PopupSettings.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
[CreateAssetMenu(fileName = "PopupSettings", menuName = "Jovian/Popup System/Popup Settings")]
|
||||
public class PopupSettings : ScriptableObject {
|
||||
[Header("General")]
|
||||
public float popupDelay = 0.4f;
|
||||
public float fadeDuration = 0.2f;
|
||||
public AnchorSide defaultAnchorSide = AnchorSide.Below;
|
||||
public float screenEdgePadding = 10f;
|
||||
public float maxPopupWidth = 400f;
|
||||
public int sortingOrder = 100;
|
||||
|
||||
[Header("Follow Mouse")]
|
||||
public Vector2 followMouseOffset = new(15f, -15f);
|
||||
|
||||
[Header("Input")]
|
||||
public float touchHoldDuration = 0.6f;
|
||||
public bool gamepadFocusTrigger = true;
|
||||
|
||||
[Header("Priority")]
|
||||
public List<CategoryPriority> categoryPriorities = new();
|
||||
|
||||
[Header("Per-Category Overrides")]
|
||||
public List<CategoryDelay> categoryDelayOverrides = new();
|
||||
|
||||
public int GetPriority(PopupCategory category) {
|
||||
foreach(var cp in categoryPriorities) {
|
||||
if(cp.category == category) {
|
||||
return cp.priority;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public float GetDelay(PopupCategory category) {
|
||||
foreach(var cd in categoryDelayOverrides) {
|
||||
if(cd.category == category) {
|
||||
return cd.delay;
|
||||
}
|
||||
}
|
||||
return popupDelay;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class CategoryPriority {
|
||||
public PopupCategory category;
|
||||
public int priority;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class CategoryDelay {
|
||||
public PopupCategory category;
|
||||
public float delay;
|
||||
}
|
||||
}
|
||||
187
Packages/com.jovian.popup-system/Runtime/PopupSystem.cs
Normal file
187
Packages/com.jovian.popup-system/Runtime/PopupSystem.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jovian.PopupSystem.UI;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Jovian.PopupSystem {
|
||||
public sealed class PopupSystem : IPopupSystem {
|
||||
readonly PopupSettings settings;
|
||||
readonly PopupView viewPrefab;
|
||||
readonly Func<IPopupAnimator> animatorFactory;
|
||||
readonly Dictionary<PopupCategory, ViewState> categories = new();
|
||||
|
||||
public PopupSystem(PopupSettings settings, PopupView viewPrefab, Func<IPopupAnimator> animatorFactory = null) {
|
||||
this.settings = settings;
|
||||
this.viewPrefab = viewPrefab;
|
||||
this.animatorFactory = animatorFactory ?? (() => new FadePopupAnimator());
|
||||
}
|
||||
|
||||
public void RegisterCategory(PopupCategory category, int priority = 0) {
|
||||
if(categories.ContainsKey(category)) {
|
||||
return;
|
||||
}
|
||||
var effectivePriority = settings.GetPriority(category);
|
||||
if(effectivePriority == 0) {
|
||||
effectivePriority = priority;
|
||||
}
|
||||
categories[category] = new ViewState {
|
||||
priority = effectivePriority,
|
||||
delay = settings.GetDelay(category),
|
||||
animator = animatorFactory()
|
||||
};
|
||||
}
|
||||
|
||||
public void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
|
||||
RectTransform anchor = null, AnchorSide? anchorSide = null) {
|
||||
if(!categories.TryGetValue(category, out var state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DismissLowerPriority(state.priority);
|
||||
|
||||
state.pendingBuild = buildContent;
|
||||
state.pendingAnchor = anchor;
|
||||
state.pendingAnchorSide = anchorSide ?? settings.defaultAnchorSide;
|
||||
state.pendingScreenPos = null;
|
||||
state.delayTimer = state.delay;
|
||||
state.isPending = true;
|
||||
}
|
||||
|
||||
public void ShowAtPosition(PopupCategory category, Action<PopupContentBuilder> buildContent,
|
||||
Vector2 screenPosition) {
|
||||
if(!categories.TryGetValue(category, out var state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DismissLowerPriority(state.priority);
|
||||
|
||||
state.pendingBuild = buildContent;
|
||||
state.pendingAnchor = null;
|
||||
state.pendingScreenPos = screenPosition;
|
||||
state.delayTimer = state.delay;
|
||||
state.isPending = true;
|
||||
}
|
||||
|
||||
public void Hide(PopupCategory category) {
|
||||
if(!categories.TryGetValue(category, out var state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.isPending = false;
|
||||
if(state.view != null && state.view.IsVisible) {
|
||||
state.animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => {
|
||||
state.view.SetVisible(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void HideAll() {
|
||||
foreach(var kvp in categories) {
|
||||
Hide(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime) {
|
||||
foreach(var kvp in categories) {
|
||||
kvp.Value.animator.Tick(deltaTime);
|
||||
}
|
||||
|
||||
foreach(var kvp in categories) {
|
||||
var state = kvp.Value;
|
||||
if(!state.isPending) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.delayTimer -= deltaTime;
|
||||
if(state.delayTimer > 0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.isPending = false;
|
||||
ShowImmediate(state);
|
||||
}
|
||||
|
||||
foreach(var kvp in categories) {
|
||||
var state = kvp.Value;
|
||||
if(state.view != null && state.view.IsVisible && state.isFollowMouse) {
|
||||
state.view.UpdatePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach(var kvp in categories) {
|
||||
if(kvp.Value.view != null) {
|
||||
Object.Destroy(kvp.Value.view.gameObject);
|
||||
}
|
||||
}
|
||||
categories.Clear();
|
||||
}
|
||||
|
||||
private void ShowImmediate(ViewState state) {
|
||||
EnsureView(state);
|
||||
state.view.ClearContent();
|
||||
|
||||
var builder = new PopupContentBuilder(state.view);
|
||||
state.pendingBuild?.Invoke(builder);
|
||||
|
||||
if(state.pendingScreenPos.HasValue) {
|
||||
state.view.SetFixedPosition(state.pendingScreenPos.Value, settings.screenEdgePadding);
|
||||
state.isFollowMouse = false;
|
||||
}
|
||||
else if(state.pendingAnchor != null) {
|
||||
state.view.SetAnchorMode(state.pendingAnchor, state.pendingAnchorSide, settings.screenEdgePadding);
|
||||
state.isFollowMouse = false;
|
||||
}
|
||||
else {
|
||||
state.view.SetFollowMouseMode(settings.followMouseOffset, settings.screenEdgePadding);
|
||||
state.isFollowMouse = true;
|
||||
}
|
||||
|
||||
state.view.SetVisible(true);
|
||||
state.view.UpdatePosition();
|
||||
state.animator.Show(state.view.CanvasGroup, settings.fadeDuration, null);
|
||||
}
|
||||
|
||||
private void EnsureView(ViewState state) {
|
||||
if(state.view != null) {
|
||||
return;
|
||||
}
|
||||
state.view = Object.Instantiate(viewPrefab);
|
||||
state.view.SetVisible(false);
|
||||
state.view.SetMaxWidth(settings.maxPopupWidth);
|
||||
|
||||
var canvas = state.view.GetComponent<Canvas>();
|
||||
if(canvas != null) {
|
||||
canvas.sortingOrder = settings.sortingOrder;
|
||||
}
|
||||
}
|
||||
|
||||
private void DismissLowerPriority(int showingPriority) {
|
||||
foreach(var kvp in categories) {
|
||||
var state = kvp.Value;
|
||||
if(state.priority < showingPriority && state.view != null && state.view.IsVisible) {
|
||||
state.isPending = false;
|
||||
state.animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => {
|
||||
state.view.SetVisible(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ViewState {
|
||||
public PopupView view;
|
||||
public IPopupAnimator animator;
|
||||
public int priority;
|
||||
public float delay;
|
||||
public float delayTimer;
|
||||
public bool isPending;
|
||||
public bool isFollowMouse;
|
||||
public Action<PopupContentBuilder> pendingBuild;
|
||||
public RectTransform pendingAnchor;
|
||||
public AnchorSide pendingAnchorSide;
|
||||
public Vector2? pendingScreenPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs
Normal file
54
Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace Jovian.PopupSystem.UI {
|
||||
public class PopupTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler {
|
||||
[SerializeField] PopupCategory category;
|
||||
[SerializeField] AnchorSide anchorSide = AnchorSide.Below;
|
||||
[SerializeField] PopupPositionMode positionMode = PopupPositionMode.AnchorToElement;
|
||||
|
||||
IPopupSystem popupSystem;
|
||||
Action<PopupContentBuilder> contentCallback;
|
||||
bool initialized;
|
||||
|
||||
public PopupCategory Category => category;
|
||||
|
||||
public void Initialize(IPopupSystem popupSystem, Action<PopupContentBuilder> contentCallback) {
|
||||
this.popupSystem = popupSystem;
|
||||
this.contentCallback = contentCallback;
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public void Initialize(IPopupSystem popupSystem, PopupCategory category, Action<PopupContentBuilder> contentCallback) {
|
||||
this.popupSystem = popupSystem;
|
||||
this.category = category;
|
||||
this.contentCallback = contentCallback;
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public void UpdateContent(Action<PopupContentBuilder> contentCallback) {
|
||||
this.contentCallback = contentCallback;
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData) {
|
||||
if(!initialized || popupSystem == null || contentCallback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(positionMode == PopupPositionMode.AnchorToElement) {
|
||||
popupSystem.Show(category, contentCallback, (RectTransform)transform, anchorSide);
|
||||
}
|
||||
else {
|
||||
popupSystem.Show(category, contentCallback);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData) {
|
||||
if(!initialized || popupSystem == null) {
|
||||
return;
|
||||
}
|
||||
popupSystem.Hide(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
214
Packages/com.jovian.popup-system/Runtime/UI/PopupView.cs
Normal file
214
Packages/com.jovian.popup-system/Runtime/UI/PopupView.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Jovian.PopupSystem.UI {
|
||||
public class PopupView : MonoBehaviour {
|
||||
[SerializeField] RectTransform content;
|
||||
[SerializeField] CanvasGroup canvasGroup;
|
||||
[SerializeField] RectTransform background;
|
||||
|
||||
[Header("Element Prefabs")]
|
||||
[SerializeField] TMP_Text headerPrefab;
|
||||
[SerializeField] TMP_Text textPrefab;
|
||||
[SerializeField] RectTransform statPrefab; // horizontal layout with label + value TMP_Text children
|
||||
[SerializeField] Image imagePrefab;
|
||||
[SerializeField] Image separatorPrefab;
|
||||
|
||||
// Element caches (grow-only)
|
||||
readonly List<TMP_Text> headerCache = new();
|
||||
readonly List<TMP_Text> textCache = new();
|
||||
readonly List<StatEntry> statCache = new();
|
||||
readonly List<Image> imageCache = new();
|
||||
readonly List<Image> separatorCache = new();
|
||||
|
||||
struct StatEntry {
|
||||
public RectTransform root;
|
||||
public TMP_Text label;
|
||||
public TMP_Text value;
|
||||
}
|
||||
|
||||
int headerIndex;
|
||||
int textIndex;
|
||||
int statIndex;
|
||||
int imageIndex;
|
||||
int separatorIndex;
|
||||
|
||||
// Positioning state
|
||||
PopupPositionMode positionMode;
|
||||
RectTransform anchorTarget;
|
||||
AnchorSide anchorSide;
|
||||
Vector2 followOffset;
|
||||
float screenEdgePadding;
|
||||
float maxWidth;
|
||||
bool isVisible;
|
||||
bool layoutDirty;
|
||||
|
||||
public CanvasGroup CanvasGroup => canvasGroup;
|
||||
public RectTransform Content => content;
|
||||
public bool IsVisible => isVisible;
|
||||
|
||||
public void SetMaxWidth(float maxPopupWidth) {
|
||||
maxWidth = maxPopupWidth;
|
||||
if(maxWidth > 0f) {
|
||||
var rt = (RectTransform)transform;
|
||||
var size = rt.sizeDelta;
|
||||
size.x = Mathf.Min(size.x, maxWidth);
|
||||
rt.sizeDelta = size;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible) {
|
||||
isVisible = visible;
|
||||
gameObject.SetActive(visible);
|
||||
if(!visible) {
|
||||
canvasGroup.alpha = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearContent() {
|
||||
DeactivateRange(headerCache, ref headerIndex);
|
||||
DeactivateRange(textCache, ref textIndex);
|
||||
for(int i = 0; i < statIndex; i++) {
|
||||
statCache[i].root.gameObject.SetActive(false);
|
||||
}
|
||||
statIndex = 0;
|
||||
DeactivateRange(imageCache, ref imageIndex);
|
||||
DeactivateRange(separatorCache, ref separatorIndex);
|
||||
layoutDirty = true;
|
||||
}
|
||||
|
||||
private static void DeactivateRange<T>(List<T> cache, ref int index) where T : Component {
|
||||
for(int i = 0; i < index; i++) {
|
||||
cache[i].gameObject.SetActive(false);
|
||||
}
|
||||
index = 0;
|
||||
}
|
||||
|
||||
// --- Element access (grow-only) ---
|
||||
|
||||
public TMP_Text GetHeader() {
|
||||
return GetOrCreate(headerCache, headerPrefab, ref headerIndex);
|
||||
}
|
||||
|
||||
public TMP_Text GetText() {
|
||||
return GetOrCreate(textCache, textPrefab, ref textIndex);
|
||||
}
|
||||
|
||||
public (TMP_Text label, TMP_Text value) GetStat() {
|
||||
if(statIndex < statCache.Count) {
|
||||
var existing = statCache[statIndex];
|
||||
existing.root.gameObject.SetActive(true);
|
||||
existing.root.SetAsLastSibling();
|
||||
statIndex++;
|
||||
return (existing.label, existing.value);
|
||||
}
|
||||
|
||||
var created = Instantiate(statPrefab, content);
|
||||
created.gameObject.SetActive(true);
|
||||
created.SetAsLastSibling();
|
||||
var children = created.GetComponentsInChildren<TMP_Text>(true);
|
||||
var entry = new StatEntry { root = created, label = children[0], value = children[1] };
|
||||
statCache.Add(entry);
|
||||
statIndex++;
|
||||
return (entry.label, entry.value);
|
||||
}
|
||||
|
||||
public Image GetImage() {
|
||||
return GetOrCreate(imageCache, imagePrefab, ref imageIndex);
|
||||
}
|
||||
|
||||
public Image GetSeparator() {
|
||||
return GetOrCreate(separatorCache, separatorPrefab, ref separatorIndex);
|
||||
}
|
||||
|
||||
private T GetOrCreate<T>(List<T> cache, T prefab, ref int index) where T : Component {
|
||||
if(index < cache.Count) {
|
||||
var existing = cache[index];
|
||||
existing.gameObject.SetActive(true);
|
||||
existing.transform.SetAsLastSibling();
|
||||
index++;
|
||||
return existing;
|
||||
}
|
||||
|
||||
var created = Instantiate(prefab, content);
|
||||
created.gameObject.SetActive(true);
|
||||
created.transform.SetAsLastSibling();
|
||||
cache.Add(created);
|
||||
index++;
|
||||
return created;
|
||||
}
|
||||
|
||||
// --- Positioning ---
|
||||
|
||||
public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) {
|
||||
positionMode = PopupPositionMode.AnchorToElement;
|
||||
anchorTarget = target;
|
||||
anchorSide = side;
|
||||
screenEdgePadding = edgePadding;
|
||||
}
|
||||
|
||||
public void SetFollowMouseMode(Vector2 offset, float edgePadding) {
|
||||
positionMode = PopupPositionMode.FollowMouse;
|
||||
followOffset = offset;
|
||||
screenEdgePadding = edgePadding;
|
||||
}
|
||||
|
||||
public void SetFixedPosition(Vector2 screenPos, float edgePadding) {
|
||||
positionMode = PopupPositionMode.AnchorToElement;
|
||||
anchorTarget = null;
|
||||
screenEdgePadding = edgePadding;
|
||||
PositionAtScreenPoint(screenPos);
|
||||
}
|
||||
|
||||
public void UpdatePosition() {
|
||||
if(positionMode == PopupPositionMode.FollowMouse) {
|
||||
PositionAtScreenPoint(Mouse.current.position.ReadValue() + followOffset);
|
||||
}
|
||||
else if(anchorTarget != null) {
|
||||
PositionAnchoredTo(anchorTarget, anchorSide);
|
||||
}
|
||||
}
|
||||
|
||||
private void PositionAnchoredTo(RectTransform target, AnchorSide side) {
|
||||
var targetRect = GetScreenRect(target);
|
||||
var popupSize = GetScreenRect((RectTransform)transform).size;
|
||||
|
||||
var pos = side switch {
|
||||
AnchorSide.Below => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMin - popupSize.y),
|
||||
AnchorSide.Above => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMax),
|
||||
AnchorSide.Left => new Vector2(targetRect.xMin - popupSize.x, targetRect.center.y - popupSize.y * 0.5f),
|
||||
AnchorSide.Right => new Vector2(targetRect.xMax, targetRect.center.y - popupSize.y * 0.5f),
|
||||
_ => targetRect.center
|
||||
};
|
||||
|
||||
PositionAtScreenPoint(pos);
|
||||
}
|
||||
|
||||
private void PositionAtScreenPoint(Vector2 screenPos) {
|
||||
var rt = (RectTransform)transform;
|
||||
if(layoutDirty) {
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(content);
|
||||
layoutDirty = false;
|
||||
}
|
||||
var popupSize = rt.rect.size;
|
||||
|
||||
// Clamp to screen
|
||||
screenPos.x = Mathf.Clamp(screenPos.x, screenEdgePadding, Screen.width - popupSize.x - screenEdgePadding);
|
||||
screenPos.y = Mathf.Clamp(screenPos.y, screenEdgePadding, Screen.height - popupSize.y - screenEdgePadding);
|
||||
|
||||
rt.position = screenPos;
|
||||
}
|
||||
|
||||
private static readonly Vector3[] cornersBuffer = new Vector3[4];
|
||||
|
||||
private static Rect GetScreenRect(RectTransform rt) {
|
||||
rt.GetWorldCorners(cornersBuffer);
|
||||
var min = new Vector2(cornersBuffer[0].x, cornersBuffer[0].y);
|
||||
var max = new Vector2(cornersBuffer[2].x, cornersBuffer[2].y);
|
||||
return new Rect(min, max - min);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Packages/com.jovian.popup-system/Samples~/README.md
Normal file
3
Packages/com.jovian.popup-system/Samples~/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Samples
|
||||
|
||||
This folder is reserved for sample scenes and scripts demonstrating the Popup System.
|
||||
20
Packages/com.jovian.popup-system/package.json
Normal file
20
Packages/com.jovian.popup-system/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "com.jovian.popup-system",
|
||||
"version": "0.1.0",
|
||||
"displayName": "Jovian Popup System",
|
||||
"description": "A lightweight, low-allocation popup and tooltip system with category-based isolation, fluent content builder, and extensible animations.",
|
||||
"unity": "2022.3",
|
||||
"dependencies": {
|
||||
"com.unity.textmeshpro": "3.0.6",
|
||||
"com.unity.inputsystem": "1.18.0",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1"
|
||||
},
|
||||
"keywords": [
|
||||
"popup",
|
||||
"tooltip",
|
||||
"ui"
|
||||
],
|
||||
"author": {
|
||||
"name": "Jovian"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user