using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace Jovian.PopupSystem.UI {
///
/// MonoBehaviour reference holder for a popup view. Manages the grow-only element cache,
/// screen positioning, and visibility. One instance per registered .
///
public class PopupReference : 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 headerCache = new();
readonly List textCache = new();
readonly List statCache = new();
readonly List imageCache = new();
readonly List 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;
Canvas rootCanvas;
Camera canvasCamera;
public CanvasGroup CanvasGroup => canvasGroup;
public RectTransform Content => content;
public bool IsVisible => isVisible;
/// Constrains the popup's horizontal size to the given maximum width in pixels.
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;
}
}
/// Shows or hides the popup GameObject. Resets alpha to 0 when hiding.
public void SetVisible(bool visible) {
isVisible = visible;
gameObject.SetActive(visible);
if(!visible) {
canvasGroup.alpha = 0f;
}
}
/// Deactivates all cached content elements and marks layout as dirty.
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(List 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) ---
/// Returns the next available header element from the cache, or creates one.
public TMP_Text GetHeader() {
return GetOrCreate(headerCache, headerPrefab, ref headerIndex);
}
/// Returns the next available text element from the cache, or creates one.
public TMP_Text GetText() {
return GetOrCreate(textCache, textPrefab, ref textIndex);
}
/// Returns the next available stat row (label + value pair) from the cache, or creates one.
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(true);
var entry = new StatEntry { root = created, label = children[0], value = children[1] };
statCache.Add(entry);
statIndex++;
return (entry.label, entry.value);
}
/// Returns the next available image element from the cache, or creates one.
public Image GetImage() {
return GetOrCreate(imageCache, imagePrefab, ref imageIndex);
}
/// Returns the next available separator element from the cache, or creates one.
public Image GetSeparator() {
return GetOrCreate(separatorCache, separatorPrefab, ref separatorIndex);
}
private T GetOrCreate(List 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 ---
/// Configures the popup to anchor to a target element on the specified side.
public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) {
positionMode = PopupPositionMode.AnchorToElement;
anchorTarget = target;
anchorSide = side;
screenEdgePadding = edgePadding;
}
/// Configures the popup to follow the mouse cursor with the given offset.
public void SetFollowMouseMode(Vector2 offset, float edgePadding) {
positionMode = PopupPositionMode.FollowMouse;
followOffset = offset;
screenEdgePadding = edgePadding;
}
/// Positions the popup at a fixed screen coordinate with edge clamping.
public void SetFixedPosition(Vector2 screenPos, float edgePadding) {
positionMode = PopupPositionMode.AnchorToElement;
anchorTarget = null;
screenEdgePadding = edgePadding;
PositionAtScreenPoint(screenPos);
}
/// Updates the popup position based on the current mode (follow mouse or anchored).
public void UpdatePosition() {
if(positionMode == PopupPositionMode.FollowMouse) {
PositionAtScreenPoint(Mouse.current.position.ReadValue() + followOffset);
}
else if(anchorTarget != null) {
PositionAnchoredTo(anchorTarget, anchorSide);
}
}
private void CacheCanvas() {
if(rootCanvas != null) {
return;
}
rootCanvas = GetComponentInParent