forked from Shardstone/trail-into-darkness
Added a popup system
This commit is contained in:
306
docs/plans/2026-04-06-popup-system-design.md
Normal file
306
docs/plans/2026-04-06-popup-system-design.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Popup System Design
|
||||
|
||||
**Package:** `com.jovian.popup-system`
|
||||
**Date:** 2026-04-06
|
||||
|
||||
## Purpose
|
||||
|
||||
A lightweight, low-allocation popup/tooltip system for Unity UGUI. Supports hover tooltips, anchored popups, and follow-mouse modes with dynamic content built via a fluent API. Per-game-state instances with category-based isolation and priority dismissal.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Game Code / PopupTrigger MonoBehaviour
|
||||
│ popupSystem.Show(category, builder, anchor)
|
||||
│ popupSystem.Hide(category)
|
||||
▼
|
||||
IPopupSystem (per-game-state instance, injected via constructor DI)
|
||||
│ RegisterCategory(PopupCategory, priority)
|
||||
│ One PopupView per category (lazy-created on first Show)
|
||||
│ Priority: higher priority category dismisses lower on show
|
||||
│ Tick()-driven delay timers and animations (no coroutines)
|
||||
▼
|
||||
PopupView (MonoBehaviour — one instance per registered category)
|
||||
│ Canvas + CanvasGroup for transitions
|
||||
│ Content parent (VerticalLayoutGroup + ContentSizeFitter)
|
||||
│ Grow-only element cache (reuse, never destroy)
|
||||
│ Screen edge clamping
|
||||
│ Anchor-to-element or follow-mouse positioning
|
||||
▼
|
||||
IPopupAnimator (extensible interface for show/hide transitions)
|
||||
│ Default: FadePopupAnimator (CanvasGroup.alpha lerp)
|
||||
│ Custom implementations for scale, slide, etc.
|
||||
▼
|
||||
PopupContentBuilder (struct, fluent API)
|
||||
.AddHeader(text) .AddText(text) .AddStat(label, value)
|
||||
.AddImage(sprite) .AddSeparator()
|
||||
→ activates pre-existing child elements in PopupView
|
||||
```
|
||||
|
||||
## Core Types
|
||||
|
||||
### PopupCategory (readonly struct)
|
||||
|
||||
Same pattern as LogChannel. Value type, string-keyed, zero-alloc comparisons.
|
||||
|
||||
```csharp
|
||||
[Serializable]
|
||||
public readonly struct PopupCategory : IEquatable<PopupCategory> {
|
||||
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");
|
||||
}
|
||||
```
|
||||
|
||||
### PopupSettings (ScriptableObject)
|
||||
|
||||
Full configuration loaded via Addressables.
|
||||
|
||||
```
|
||||
General:
|
||||
popupDelay float 0.4s Time before popup appears after hover
|
||||
fadeDuration float 0.2s Default animation duration
|
||||
defaultAnchorSide enum Below Below/Above/Left/Right
|
||||
screenEdgePadding float 10px Minimum distance from screen edge
|
||||
maxPopupWidth float 400px Maximum content width
|
||||
sortingOrder int 100 Canvas sorting order
|
||||
|
||||
Follow Mouse:
|
||||
followMouseOffset Vector2 (15, -15) Offset from cursor in follow mode
|
||||
|
||||
Input:
|
||||
touchHoldDuration float 0.6s Press-and-hold duration for touch
|
||||
gamepadFocusTrigger bool true Trigger on gamepad focus events
|
||||
|
||||
Priority:
|
||||
categoryPriorities List<CategoryPriority>
|
||||
- category: PopupCategory
|
||||
- priority: int Higher value = higher priority
|
||||
|
||||
Per-Category Overrides:
|
||||
categoryDelayOverrides List<CategoryDelay>
|
||||
- category: PopupCategory
|
||||
- delay: float
|
||||
```
|
||||
|
||||
### IPopupSystem (interface)
|
||||
|
||||
```csharp
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
### PopupSystem (implementation)
|
||||
|
||||
- `Dictionary<PopupCategory, PopupViewState>` — O(1) lookup by category
|
||||
- `PopupViewState` holds: PopupView instance (null until first show), priority, delay timer, pending show data
|
||||
- `Tick(deltaTime)` drives delay countdown and animation lerp — no coroutines
|
||||
- On `Show()`:
|
||||
- If a higher-priority popup is already visible, queue or cancel
|
||||
- Start delay timer; on expiry, activate the view
|
||||
- PopupView clears content (deactivates cached elements), runs builder callback, positions, animates in
|
||||
- On `Hide()`:
|
||||
- Animate out, deactivate
|
||||
- On `Dispose()`:
|
||||
- Destroy all PopupView GameObjects
|
||||
|
||||
### PopupView (MonoBehaviour)
|
||||
|
||||
Single prefab, instantiated once per category:
|
||||
|
||||
```
|
||||
PopupView (Canvas, CanvasGroup, RectMask2D)
|
||||
├── Content (RectTransform, VerticalLayoutGroup, ContentSizeFitter)
|
||||
│ ├── [cached] HeaderElement (TMP_Text, deactivated)
|
||||
│ ├── [cached] TextElement x N (TMP_Text, deactivated)
|
||||
│ ├── [cached] StatElement x N (TMP_Text label + TMP_Text value, deactivated)
|
||||
│ ├── [cached] ImageElement x N (Image, deactivated)
|
||||
│ └── [cached] SeparatorElement x N (Image, deactivated)
|
||||
└── Background (Image, optional)
|
||||
```
|
||||
|
||||
**Grow-only element cache:**
|
||||
- Pre-creates a small set of each element type (e.g. 3 text, 2 stat, 1 image, 2 separator)
|
||||
- `GetOrCreateElement<T>(type)` returns next available deactivated element, or Instantiates if none free
|
||||
- New elements persist and are reused on next Show
|
||||
- `ClearContent()` deactivates all elements and resets the reuse index — no Destroy calls
|
||||
|
||||
**Positioning:**
|
||||
- `AnchorToElement(RectTransform target, AnchorSide side)` — positions relative to target element
|
||||
- `FollowMouse(Vector2 offset)` — updates position each Tick to track cursor
|
||||
- `ClampToScreen(float padding)` — adjusts position if popup overflows screen bounds, flips anchor side if needed
|
||||
|
||||
**Animation:**
|
||||
- Calls `IPopupAnimator.Show(canvasGroup, duration, onComplete)` and `Hide(...)`
|
||||
- Default `FadePopupAnimator` lerps `canvasGroup.alpha` 0→1 / 1→0 over duration
|
||||
- Driven by float timer in Tick, not coroutines
|
||||
|
||||
### PopupContentBuilder (struct)
|
||||
|
||||
```csharp
|
||||
public struct PopupContentBuilder {
|
||||
readonly PopupView view;
|
||||
|
||||
public PopupContentBuilder AddHeader(string text);
|
||||
public PopupContentBuilder AddText(string text);
|
||||
public PopupContentBuilder AddText(string text, string hexColor);
|
||||
public PopupContentBuilder AddStat(string label, int value);
|
||||
public PopupContentBuilder AddStat(string label, string value);
|
||||
public PopupContentBuilder AddImage(Sprite sprite, float height = 64f);
|
||||
public PopupContentBuilder AddSeparator();
|
||||
}
|
||||
```
|
||||
|
||||
Each method activates a cached element from PopupView, sets its data, calls `SetAsLastSibling()` for ordering. Returns `this` for chaining. No allocations.
|
||||
|
||||
### PopupTrigger (MonoBehaviour)
|
||||
|
||||
Attach to any UI element. Handles hover detection automatically.
|
||||
|
||||
```csharp
|
||||
public class PopupTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler {
|
||||
[SerializeField] PopupCategory category;
|
||||
[SerializeField] AnchorSide anchorSide;
|
||||
[SerializeField] PopupPositionMode positionMode; // AnchorToElement or FollowMouse
|
||||
|
||||
IPopupSystem popupSystem;
|
||||
Action<PopupContentBuilder> contentCallback;
|
||||
|
||||
public void Initialize(IPopupSystem popupSystem, Action<PopupContentBuilder> contentCallback);
|
||||
|
||||
// IPointerEnterHandler — calls popupSystem.Show(category, contentCallback, rectTransform)
|
||||
// IPointerExitHandler — calls popupSystem.Hide(category)
|
||||
}
|
||||
```
|
||||
|
||||
- Category, anchor side, position mode configurable in Inspector
|
||||
- `Initialize()` called from code to inject the popup system and content builder callback
|
||||
- No per-frame cost when not hovered
|
||||
- The `contentCallback` is set once and reused — no allocation per hover
|
||||
|
||||
### IPopupAnimator (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; }
|
||||
}
|
||||
```
|
||||
|
||||
Default implementation: `FadePopupAnimator` — lerps alpha. Custom implementations can do scale, slide, bounce, etc.
|
||||
|
||||
### PopupCategoryJsonConverter
|
||||
|
||||
Newtonsoft converter for serializing PopupCategory as string id (same pattern as LogChannelJsonConverter).
|
||||
|
||||
## Enums
|
||||
|
||||
```csharp
|
||||
public enum AnchorSide { Below, Above, Left, Right }
|
||||
public enum PopupPositionMode { AnchorToElement, FollowMouse }
|
||||
```
|
||||
|
||||
## Optimization Summary
|
||||
|
||||
| Concern | Approach |
|
||||
|---------|----------|
|
||||
| Hot path (Show/Hide) | Struct category, dict lookup, no alloc |
|
||||
| Content elements | Grow-only cache, activate/deactivate, never Destroy |
|
||||
| Timers | Float countdown in Tick(), no coroutines |
|
||||
| Builder | Struct, operates directly on cached elements |
|
||||
| Per-frame | Zero alloc when idle; follow-mouse only reads Input position |
|
||||
| Trigger | IPointerEnter/Exit interfaces, no delegates allocated per hover |
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
Packages/com.jovian.popup-system/
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── Runtime/
|
||||
│ ├── Jovian.PopupSystem.asmdef
|
||||
│ ├── PopupCategory.cs
|
||||
│ ├── PopupCategoryJsonConverter.cs
|
||||
│ ├── PopupSettings.cs
|
||||
│ ├── IPopupSystem.cs
|
||||
│ ├── PopupSystem.cs
|
||||
│ ├── IPopupAnimator.cs
|
||||
│ ├── FadePopupAnimator.cs
|
||||
│ ├── PopupContentBuilder.cs
|
||||
│ └── UI/
|
||||
│ ├── PopupView.cs
|
||||
│ └── PopupTrigger.cs
|
||||
├── Editor/
|
||||
│ ├── Jovian.PopupSystem.Editor.asmdef
|
||||
│ └── PopupSettingsProvider.cs
|
||||
└── Samples~/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `com.unity.textmeshpro` (TMP_Text for content elements)
|
||||
- `com.unity.inputsystem` (for pointer position in follow-mouse mode)
|
||||
- `com.unity.nuget.newtonsoft-json` (for PopupCategory serialization)
|
||||
|
||||
## Integration Example
|
||||
|
||||
```csharp
|
||||
// In GameModeGameState or similar:
|
||||
var popupSettings = Addressables.LoadAssetAsync<PopupSettings>("PopupSettings").WaitForCompletion();
|
||||
var popupViewPrefab = Addressables.LoadAssetAsync<PopupView>("PopupViewPrefab").WaitForCompletion();
|
||||
var popupSystem = new PopupSystem(popupSettings, popupViewPrefab);
|
||||
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
|
||||
popupSystem.RegisterCategory(PopupCategory.Item, priority: 5);
|
||||
|
||||
// Pass popupSystem to views/play modes via constructor DI
|
||||
|
||||
// In PartyGuiView — attach PopupTrigger to each slot:
|
||||
var trigger = slot.GetComponent<PopupTrigger>();
|
||||
trigger.Initialize(popupSystem, builder => {
|
||||
builder.AddHeader(member.Name)
|
||||
.AddSeparator()
|
||||
.AddStat("Health", member.Stats.GetValue(StatType.Health))
|
||||
.AddStat("Mana", member.Stats.GetValue(StatType.Mana))
|
||||
.AddStat("Level", member.Stats.GetValue(StatType.Level))
|
||||
.AddText($"{member.Race} {member.Class}");
|
||||
});
|
||||
|
||||
// In Tick:
|
||||
popupSystem.Tick(Time.deltaTime);
|
||||
|
||||
// On state exit:
|
||||
popupSystem.Dispose();
|
||||
```
|
||||
|
||||
## Prefab Setup
|
||||
|
||||
### PopupView prefab
|
||||
|
||||
1. Root: Canvas (Screen Space Overlay, sorting order from settings), CanvasGroup (alpha=0)
|
||||
2. Child "Content": RectTransform, VerticalLayoutGroup (Child Force Expand Width: true, Height: false, Spacing: 4, Padding: 8), ContentSizeFitter (Vertical Fit: Preferred Size, Horizontal Fit: Preferred Size up to maxPopupWidth)
|
||||
3. Background Image behind Content
|
||||
4. Optional: RectMask2D on root
|
||||
|
||||
Pre-create cached elements as children of Content (all deactivated):
|
||||
- 2x Header (TMP_Text, bold, larger font)
|
||||
- 4x Text (TMP_Text, normal)
|
||||
- 4x Stat (horizontal layout: TMP_Text label + TMP_Text value)
|
||||
- 2x Image (Image component)
|
||||
- 3x Separator (Image, 1px height, horizontal stretch)
|
||||
999
docs/plans/2026-04-06-popup-system-implementation.md
Normal file
999
docs/plans/2026-04-06-popup-system-implementation.md
Normal file
@@ -0,0 +1,999 @@
|
||||
# Popup System Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build `com.jovian.popup-system`, a low-allocation popup/tooltip package with category-based isolation, fluent content builder, grow-only element cache, screen edge clamping, and extensible animations.
|
||||
|
||||
**Architecture:** Per-game-state `IPopupSystem` instances with registered categories. Each category lazily creates one `PopupView` MonoBehaviour that reuses cached content elements. `PopupTrigger` MonoBehaviour handles hover detection. `PopupContentBuilder` struct operates directly on cached elements. Float-based timers in `Tick()` — no coroutines.
|
||||
|
||||
**Tech Stack:** Unity 6 / C# 9, TextMeshPro, Unity Input System, Newtonsoft.Json
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Package scaffold
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/package.json`
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/Jovian.PopupSystem.asmdef`
|
||||
- Create: `Packages/com.jovian.popup-system/Editor/Jovian.PopupSystem.Editor.asmdef`
|
||||
- Create: `Packages/com.jovian.popup-system/Samples~/README.md`
|
||||
|
||||
**Step 1: Create package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Create Runtime asmdef**
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Create Editor asmdef**
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Create Samples~/README.md**
|
||||
|
||||
```markdown
|
||||
# Samples
|
||||
|
||||
This folder is reserved for sample scenes and scripts demonstrating the Popup System.
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/
|
||||
git commit -m "feat: scaffold com.jovian.popup-system package"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: PopupCategory + JSON converter + enums
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/PopupCategory.cs`
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/PopupCategoryJsonConverter.cs`
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/PopupEnums.cs`
|
||||
|
||||
**Step 1: Implement PopupCategory** (same pattern as LogChannel)
|
||||
|
||||
```csharp
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Implement JSON converter**
|
||||
|
||||
```csharp
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Implement enums**
|
||||
|
||||
```csharp
|
||||
namespace Jovian.PopupSystem {
|
||||
public enum AnchorSide {
|
||||
Below,
|
||||
Above,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
public enum PopupPositionMode {
|
||||
AnchorToElement,
|
||||
FollowMouse
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/
|
||||
git commit -m "feat: add PopupCategory, enums, and JSON converter"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: PopupSettings ScriptableObject
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/PopupSettings.cs`
|
||||
|
||||
**Step 1: Implement settings**
|
||||
|
||||
```csharp
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/PopupSettings.cs
|
||||
git commit -m "feat: add PopupSettings ScriptableObject with full configuration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: IPopupAnimator + FadePopupAnimator
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs`
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs`
|
||||
|
||||
**Step 1: Implement interface**
|
||||
|
||||
```csharp
|
||||
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; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Implement FadePopupAnimator**
|
||||
|
||||
```csharp
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs
|
||||
git commit -m "feat: add IPopupAnimator interface and FadePopupAnimator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: PopupView MonoBehaviour with element cache
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/UI/PopupView.cs`
|
||||
|
||||
**Step 1: Implement PopupView with grow-only element cache, positioning, and screen clamping**
|
||||
|
||||
```csharp
|
||||
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; // contains label + value TMP_Text
|
||||
[SerializeField] Image imagePrefab;
|
||||
[SerializeField] Image separatorPrefab;
|
||||
|
||||
// Element caches (grow-only)
|
||||
readonly List<TMP_Text> headerCache = new();
|
||||
readonly List<TMP_Text> textCache = new();
|
||||
readonly List<RectTransform> statCache = new();
|
||||
readonly List<Image> imageCache = new();
|
||||
readonly List<Image> separatorCache = new();
|
||||
|
||||
int headerIndex;
|
||||
int textIndex;
|
||||
int statIndex;
|
||||
int imageIndex;
|
||||
int separatorIndex;
|
||||
|
||||
// State
|
||||
PopupPositionMode positionMode;
|
||||
RectTransform anchorTarget;
|
||||
AnchorSide anchorSide;
|
||||
Vector2 followOffset;
|
||||
float screenEdgePadding;
|
||||
bool isVisible;
|
||||
|
||||
public CanvasGroup CanvasGroup => canvasGroup;
|
||||
public RectTransform Content => content;
|
||||
public bool IsVisible => isVisible;
|
||||
|
||||
public void SetVisible(bool visible) {
|
||||
isVisible = visible;
|
||||
gameObject.SetActive(visible);
|
||||
if(!visible) {
|
||||
canvasGroup.alpha = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearContent() {
|
||||
for(int i = 0; i < headerIndex; i++) {
|
||||
headerCache[i].gameObject.SetActive(false);
|
||||
}
|
||||
for(int i = 0; i < textIndex; i++) {
|
||||
textCache[i].gameObject.SetActive(false);
|
||||
}
|
||||
for(int i = 0; i < statIndex; i++) {
|
||||
statCache[i].gameObject.SetActive(false);
|
||||
}
|
||||
for(int i = 0; i < imageIndex; i++) {
|
||||
imageCache[i].gameObject.SetActive(false);
|
||||
}
|
||||
for(int i = 0; i < separatorIndex; i++) {
|
||||
separatorCache[i].gameObject.SetActive(false);
|
||||
}
|
||||
headerIndex = 0;
|
||||
textIndex = 0;
|
||||
statIndex = 0;
|
||||
imageIndex = 0;
|
||||
separatorIndex = 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() {
|
||||
var rt = GetOrCreate(statCache, statPrefab, ref statIndex);
|
||||
var children = rt.GetComponentsInChildren<TMP_Text>(true);
|
||||
return (children[0], children[1]);
|
||||
}
|
||||
|
||||
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 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 popupRect = GetScreenRect((RectTransform)transform);
|
||||
var popupSize = popupRect.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;
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(content);
|
||||
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 Rect GetScreenRect(RectTransform rt) {
|
||||
var corners = new Vector3[4];
|
||||
rt.GetWorldCorners(corners);
|
||||
var min = new Vector2(corners[0].x, corners[0].y);
|
||||
var max = new Vector2(corners[2].x, corners[2].y);
|
||||
return new Rect(min, max - min);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/UI/PopupView.cs
|
||||
git commit -m "feat: add PopupView with grow-only element cache and positioning"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: PopupContentBuilder struct
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs`
|
||||
|
||||
**Step 1: Implement fluent builder**
|
||||
|
||||
```csharp
|
||||
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 = text.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs
|
||||
git commit -m "feat: add PopupContentBuilder fluent API"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: IPopupSystem + PopupSystem implementation
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs`
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/PopupSystem.cs`
|
||||
|
||||
**Step 1: Implement interface**
|
||||
|
||||
```csharp
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Implement PopupSystem**
|
||||
|
||||
```csharp
|
||||
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 IPopupAnimator animator;
|
||||
readonly Dictionary<PopupCategory, ViewState> categories = new();
|
||||
|
||||
public PopupSystem(PopupSettings settings, PopupView viewPrefab, IPopupAnimator animator = null) {
|
||||
this.settings = settings;
|
||||
this.viewPrefab = viewPrefab;
|
||||
this.animator = animator ?? 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)
|
||||
};
|
||||
}
|
||||
|
||||
public void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
|
||||
RectTransform anchor = null, AnchorSide? anchorSide = null) {
|
||||
if(!categories.TryGetValue(category, out var state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dismiss lower-priority visible popups
|
||||
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) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Update follow-mouse positions
|
||||
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.SetFollowMouseMode(settings.followMouseOffset, settings.screenEdgePadding);
|
||||
state.isFollowMouse = false; // positioned at fixed screen point
|
||||
}
|
||||
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();
|
||||
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);
|
||||
|
||||
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;
|
||||
animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => {
|
||||
state.view.SetVisible(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ViewState {
|
||||
public PopupView view;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs Packages/com.jovian.popup-system/Runtime/PopupSystem.cs
|
||||
git commit -m "feat: add IPopupSystem and PopupSystem with priority and delay"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: PopupTrigger MonoBehaviour
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs`
|
||||
|
||||
**Step 1: Implement trigger**
|
||||
|
||||
```csharp
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs
|
||||
git commit -m "feat: add PopupTrigger MonoBehaviour for hover detection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: README.md
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/README.md`
|
||||
|
||||
**Step 1: Write README** covering: quick start, PopupCategory, PopupSettings, content builder API, PopupTrigger setup, code-only triggers, prefab setup, IPopupAnimator extensibility.
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/README.md
|
||||
git commit -m "feat: add README for popup system package"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Editor — PopupSettingsProvider
|
||||
|
||||
**Files:**
|
||||
- Create: `Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs`
|
||||
|
||||
**Step 1: Implement settings provider** for Project Settings > Jovian > Popup System (same pattern as SaveSystemSettingsProvider).
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs
|
||||
git commit -m "feat: add PopupSettingsProvider for Project Settings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
| File | Type | Purpose |
|
||||
|------|------|---------|
|
||||
| `Packages/com.jovian.popup-system/package.json` | Create | Package manifest |
|
||||
| `Packages/com.jovian.popup-system/Runtime/Jovian.PopupSystem.asmdef` | Create | Runtime assembly |
|
||||
| `Packages/com.jovian.popup-system/Editor/Jovian.PopupSystem.Editor.asmdef` | Create | Editor assembly |
|
||||
| `Packages/com.jovian.popup-system/Samples~/README.md` | Create | Samples placeholder |
|
||||
| `Packages/com.jovian.popup-system/Runtime/PopupCategory.cs` | Create | Category readonly struct |
|
||||
| `Packages/com.jovian.popup-system/Runtime/PopupCategoryJsonConverter.cs` | Create | Newtonsoft converter |
|
||||
| `Packages/com.jovian.popup-system/Runtime/PopupEnums.cs` | Create | AnchorSide, PopupPositionMode |
|
||||
| `Packages/com.jovian.popup-system/Runtime/PopupSettings.cs` | Create | ScriptableObject config |
|
||||
| `Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs` | Create | Animation interface |
|
||||
| `Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs` | Create | Default fade animation |
|
||||
| `Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs` | Create | Fluent builder struct |
|
||||
| `Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs` | Create | System interface |
|
||||
| `Packages/com.jovian.popup-system/Runtime/PopupSystem.cs` | Create | System implementation |
|
||||
| `Packages/com.jovian.popup-system/Runtime/UI/PopupView.cs` | Create | MonoBehaviour with element cache |
|
||||
| `Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs` | Create | Hover trigger MonoBehaviour |
|
||||
| `Packages/com.jovian.popup-system/README.md` | Create | Package documentation |
|
||||
| `Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs` | Create | Project Settings UI |
|
||||
Reference in New Issue
Block a user