using System.Collections.Generic; using Jovian.PopupSystem.UI; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace Jovian.PopupSystem { /// /// Behavior class for a popup view. Manages the generic grow-only element cache, /// positioning, visibility, and layout. Operates on a /// MonoBehaviour for scene references. /// public sealed class PopupView { readonly PopupReference reference; readonly PopupSettings settings; // Generic element cache: keyed by element type readonly Dictionary> elementCache = new(); readonly Dictionary elementIndex = new(); // Positioning state PopupPositionMode positionMode; RectTransform anchorTarget; AnchorSide anchorSide; Vector2 followOffset; float screenEdgePadding; float maxWidth; bool isVisible; bool layoutDirty; Canvas rootCanvas; Camera canvasCamera; /// The underlying MonoBehaviour reference holder. public PopupReference Reference => reference; /// The CanvasGroup for animation control. public CanvasGroup CanvasGroup => reference.CanvasGroup; /// The content RectTransform where elements are parented. public RectTransform Content => reference.Content; /// The root Transform of the popup GameObject. public Transform Transform => reference.transform; /// Whether the popup is currently visible. public bool IsVisible => isVisible; /// /// Creates a new popup view wrapping the given reference and settings. /// public PopupView(PopupReference reference, PopupSettings settings) { this.reference = reference; this.settings = settings; } // --- Element cache (generic, grow-only) --- /// /// Returns the next available cached element for the given type, or instantiates /// a new one from the settings prefab registry. The element is activated and placed /// at the end of the content layout. /// public GameObject GetElement(PopupElementType elementType) { if(!elementCache.TryGetValue(elementType, out var cache)) { cache = new List(); elementCache[elementType] = cache; elementIndex[elementType] = 0; } var index = elementIndex[elementType]; if(index < cache.Count) { var existing = cache[index]; existing.SetActive(true); existing.transform.SetAsLastSibling(); elementIndex[elementType] = index + 1; return existing; } var prefab = settings.GetPrefab(elementType); if(prefab == null) { Debug.LogWarning($"[PopupView] No prefab registered for element '{elementType}'"); return null; } var created = Object.Instantiate(prefab, reference.Content); created.SetActive(true); created.transform.SetAsLastSibling(); cache.Add(created); elementIndex[elementType] = index + 1; return created; } /// /// Deactivates all cached content elements and marks layout as dirty. /// public void ClearContent() { foreach(var kvp in elementCache) { var cache = kvp.Value; var activeCount = elementIndex[kvp.Key]; for(int i = 0; i < activeCount; i++) { cache[i].SetActive(false); } elementIndex[kvp.Key] = 0; } layoutDirty = true; } // --- Visibility --- /// Shows or hides the popup GameObject. Resets alpha to 0 when hiding. public void SetVisible(bool visible) { isVisible = visible; reference.gameObject.SetActive(visible); if(!visible) { reference.CanvasGroup.alpha = 0f; } } /// 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)reference.transform; var size = rt.sizeDelta; size.x = Mathf.Min(size.x, maxWidth); rt.sizeDelta = size; } } // --- 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 = reference.GetComponentInParent()?.rootCanvas; canvasCamera = rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay ? rootCanvas.worldCamera : null; } private void PositionAnchoredTo(RectTransform target, AnchorSide side) { CacheCanvas(); var targetRect = GetScreenRect(target, canvasCamera); var popupRect = GetScreenRect((RectTransform)reference.transform, canvasCamera); 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) { CacheCanvas(); var rt = (RectTransform)reference.transform; if(layoutDirty) { LayoutRebuilder.ForceRebuildLayoutImmediate(reference.Content); layoutDirty = false; } var popupSize = GetScreenRect(rt, canvasCamera).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); // Convert screen position to parent local space var parentRt = rt.parent as RectTransform; if(parentRt != null) { RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRt, screenPos, canvasCamera, out var localPos); rt.localPosition = localPos; } else { rt.position = screenPos; } } private static readonly Vector3[] cornersBuffer = new Vector3[4]; private static Rect GetScreenRect(RectTransform rt, Camera camera) { rt.GetWorldCorners(cornersBuffer); var min = RectTransformUtility.WorldToScreenPoint(camera, cornersBuffer[0]); var max = RectTransformUtility.WorldToScreenPoint(camera, cornersBuffer[2]); return new Rect(min, max - min); } } }