using System;
using System.Collections.Generic;
using Jovian.PopupSystem.UI;
using UnityEngine;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace Jovian.PopupSystem {
///
/// Core implementation of . Created per game state, not as a singleton.
/// Manages category registration, trigger discovery, popup lifecycle, priority dismissal, and
/// tick-driven delay timers and animations. Pass a canvasParent to auto-scan triggers on construction.
///
public sealed class PopupSystem : IPopupSystem {
readonly PopupSettings settings;
readonly PopupReference viewPrefab;
readonly Func animatorFactory;
readonly Transform canvasParent;
readonly Dictionary categories = new();
readonly List registeredTriggers = new();
///
/// Creates a new popup system instance.
///
/// Configuration ScriptableObject with delays, priorities, and display settings.
/// The PopupReference prefab to instantiate per category.
/// Optional parent Canvas transform. When provided, popup views are parented
/// here (inheriting CanvasScaler) and all PopupTrigger components are auto-scanned.
/// Optional factory for custom animators. Defaults to FadePopupAnimator.
public PopupSystem(PopupSettings settings, PopupReference viewPrefab, Transform canvasParent = null, Func animatorFactory = null) {
this.settings = settings;
this.viewPrefab = viewPrefab;
this.canvasParent = canvasParent;
this.animatorFactory = animatorFactory ?? (() => new FadePopupAnimator());
// Auto-scan if a parent was provided
if(canvasParent != null) {
ScanTriggers(canvasParent);
}
}
///
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 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 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 ScanTriggers(Transform parent) {
var triggers = parent.GetComponentsInChildren(true);
foreach(var trigger in triggers) {
if(!registeredTriggers.Contains(trigger)) {
BindTrigger(trigger);
}
}
}
///
public PopupTriggerView GetTriggerHandler(string gameObjectName) {
foreach(var trigger in registeredTriggers) {
if(trigger != null && trigger.gameObject.name == gameObjectName) {
return trigger.Handler;
}
}
return null;
}
///
public IReadOnlyList GetTriggerHandlers(PopupCategory category) {
var result = new List();
foreach(var trigger in registeredTriggers) {
if(trigger != null && trigger.Category == category && trigger.Handler != null) {
result.Add(trigger.Handler);
}
}
return result;
}
///
public void InitializeTriggersInChildren(Transform parent, Action configureTrigger) {
var triggers = parent.GetComponentsInChildren(true);
foreach(var trigger in triggers) {
if(!registeredTriggers.Contains(trigger)) {
BindTrigger(trigger);
}
configureTrigger(trigger, trigger.Handler);
}
}
private void BindTrigger(PopupTrigger trigger) {
var handler = new PopupTriggerView(this);
trigger.Bind(handler);
registeredTriggers.Add(trigger);
}
///
public void Dispose() {
foreach(var kvp in categories) {
if(kvp.Value.view?.Reference != null) {
Object.Destroy(kvp.Value.view.Reference.gameObject);
}
}
categories.Clear();
}
private void ShowImmediate(ViewState state) {
EnsureView(state);
state.view.ClearContent();
var builder = new PopupContentBuilder(state.view);
state.pendingBuild?.Invoke(builder);
// Activate before layout rebuild so Unity has an active hierarchy to calculate
state.view.CanvasGroup.alpha = 0f;
state.view.SetVisible(true);
// Force full layout rebuild so positioning has correct size on first show
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(state.view.Content);
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform)state.view.Transform);
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.UpdatePosition();
state.animator.Show(state.view.CanvasGroup, settings.fadeDuration, null);
}
private void EnsureView(ViewState state) {
if(state.view != null) {
return;
}
PopupReference popupRef;
if(canvasParent != null) {
popupRef = Object.Instantiate(viewPrefab, canvasParent);
}
else {
popupRef = Object.Instantiate(viewPrefab);
}
var canvas = popupRef.GetComponent