using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace Jovian.PopupSystem.UI { 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; 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(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) --- 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(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(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 --- 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); } } }