Files
unity-popup-system/Runtime/PopupView.cs
Sebastian Bularca 0f675b9981 added code from unity
2026-04-06 20:45:03 +02:00

225 lines
9.0 KiB
C#

using System.Collections.Generic;
using Jovian.PopupSystem.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace Jovian.PopupSystem {
/// <summary>
/// Behavior class for a popup view. Manages the generic grow-only element cache,
/// positioning, visibility, and layout. Operates on a <see cref="PopupReference"/>
/// MonoBehaviour for scene references.
/// </summary>
public sealed class PopupView {
readonly PopupReference reference;
readonly PopupSettings settings;
// Generic element cache: keyed by element type
readonly Dictionary<PopupElementType, List<GameObject>> elementCache = new();
readonly Dictionary<PopupElementType, int> elementIndex = new();
// Positioning state
PopupPositionMode positionMode;
RectTransform anchorTarget;
AnchorSide anchorSide;
Vector2 followOffset;
float screenEdgePadding;
float maxWidth;
bool isVisible;
bool layoutDirty;
Canvas rootCanvas;
Camera canvasCamera;
/// <summary>The underlying MonoBehaviour reference holder.</summary>
public PopupReference Reference => reference;
/// <summary>The CanvasGroup for animation control.</summary>
public CanvasGroup CanvasGroup => reference.CanvasGroup;
/// <summary>The content RectTransform where elements are parented.</summary>
public RectTransform Content => reference.Content;
/// <summary>The root Transform of the popup GameObject.</summary>
public Transform Transform => reference.transform;
/// <summary>Whether the popup is currently visible.</summary>
public bool IsVisible => isVisible;
/// <summary>
/// Creates a new popup view wrapping the given reference and settings.
/// </summary>
public PopupView(PopupReference reference, PopupSettings settings) {
this.reference = reference;
this.settings = settings;
}
// --- Element cache (generic, grow-only) ---
/// <summary>
/// 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.
/// </summary>
public GameObject GetElement(PopupElementType elementType) {
if(!elementCache.TryGetValue(elementType, out var cache)) {
cache = new List<GameObject>();
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;
}
/// <summary>
/// Deactivates all cached content elements and marks layout as dirty.
/// </summary>
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 ---
/// <summary>Shows or hides the popup GameObject. Resets alpha to 0 when hiding.</summary>
public void SetVisible(bool visible) {
isVisible = visible;
reference.gameObject.SetActive(visible);
if(!visible) {
reference.CanvasGroup.alpha = 0f;
}
}
/// <summary>Constrains the popup's horizontal size to the given maximum width in pixels.</summary>
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 ---
/// <summary>Configures the popup to anchor to a target element on the specified side.</summary>
public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) {
positionMode = PopupPositionMode.AnchorToElement;
anchorTarget = target;
anchorSide = side;
screenEdgePadding = edgePadding;
}
/// <summary>Configures the popup to follow the mouse cursor with the given offset.</summary>
public void SetFollowMouseMode(Vector2 offset, float edgePadding) {
positionMode = PopupPositionMode.FollowMouse;
followOffset = offset;
screenEdgePadding = edgePadding;
}
/// <summary>Positions the popup at a fixed screen coordinate with edge clamping.</summary>
public void SetFixedPosition(Vector2 screenPos, float edgePadding) {
positionMode = PopupPositionMode.AnchorToElement;
anchorTarget = null;
screenEdgePadding = edgePadding;
PositionAtScreenPoint(screenPos);
}
/// <summary>Updates the popup position based on the current mode (follow mouse or anchored).</summary>
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<Canvas>()?.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);
}
}
}