forked from Shardstone/trail-into-darkness
238 lines
8.7 KiB
C#
238 lines
8.7 KiB
C#
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<TMP_Text> headerCache = new();
|
|
readonly List<TMP_Text> textCache = new();
|
|
readonly List<StatEntry> statCache = new();
|
|
readonly List<Image> imageCache = new();
|
|
readonly List<Image> 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;
|
|
|
|
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<T>(List<T> 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<TMP_Text>(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<T>(List<T> 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 CacheCanvas() {
|
|
if(rootCanvas != null) {
|
|
return;
|
|
}
|
|
rootCanvas = 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)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)transform;
|
|
if(layoutDirty) {
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(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);
|
|
// Convert world corners to screen space
|
|
var min = RectTransformUtility.WorldToScreenPoint(camera, cornersBuffer[0]);
|
|
var max = RectTransformUtility.WorldToScreenPoint(camera, cornersBuffer[2]);
|
|
return new Rect(min, max - min);
|
|
}
|
|
}
|
|
}
|