using System; using System.Collections.Generic; using System.Threading; using BriarQueen.Framework.Managers.UI.Base; using Cysharp.Threading.Tasks; using PrimeTween; using TMPro; using UnityEngine; using UnityEngine.UI; namespace BriarQueen.UI.HUD { public class InfoPopup : MonoBehaviour, IPopup { [SerializeField] private TextMeshProUGUI _text; [SerializeField] private RectTransform _rectTransform; [SerializeField] private CanvasGroup _canvasGroup; [Header("Sizing")] [SerializeField] private float _minWidth = 160f; [SerializeField] private float _maxWidth = 500f; [SerializeField] private float _minHeight = 60f; [SerializeField] private float _extraWidthSafety = 4f; [SerializeField] private float _extraHeightSafety = 4f; [Header("Animation")] [SerializeField] private float _slideDuration = 1.2f; [SerializeField] private Vector2 _hiddenAnchoredPosition = new(0f, -250f); [SerializeField] private Vector2 _shownAnchoredPosition = new(0f, 0f); [SerializeField] private float _shownAlpha = 1f; [Header("Queue")] [SerializeField] private bool _suppressDuplicateMessages = true; [Header("Editor Debug")] [SerializeField] private bool _debugResizeInEditor = true; private readonly Queue _queue = new(); private string _currentMessage = string.Empty; private CancellationTokenSource _destroyCts; private bool _isProcessingQueue; private string _lastEditorText = string.Empty; private float _lastHeight = -1f; private float _lastWidth = -1f; private Sequence _sequence; private CancellationTokenSource _sequenceCts; public bool IsModal => false; public GameObject GameObject => gameObject; private void Awake() { _destroyCts = new CancellationTokenSource(); if (_rectTransform == null) _rectTransform = GetComponent(); if (_canvasGroup == null) _canvasGroup = GetComponent(); if (_rectTransform != null) _rectTransform.anchoredPosition = _hiddenAnchoredPosition; if (_canvasGroup != null) { _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } } #if UNITY_EDITOR private void Update() { if (Application.isPlaying) return; if (_text == null) return; if (_lastEditorText != _text.text) ResizeToFitText(); } #endif private void OnDestroy() { CancelTweenIfRunning(); if (_destroyCts != null) { _destroyCts.Cancel(); _destroyCts.Dispose(); _destroyCts = null; } _queue.Clear(); } private void OnValidate() { if (_rectTransform == null) _rectTransform = GetComponent(); if (_canvasGroup == null) _canvasGroup = GetComponent(); ResizeToFitText(); } public async UniTask Show() { if (_rectTransform == null || _canvasGroup == null) return; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; CancelTweenIfRunning(); var localTweenCts = new CancellationTokenSource(); _sequenceCts = localTweenCts; _rectTransform.anchoredPosition = _hiddenAnchoredPosition; _canvasGroup.alpha = 0f; _sequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.UIAnchoredPosition(_rectTransform, new TweenSettings { startValue = _hiddenAnchoredPosition, endValue = _shownAnchoredPosition, settings = new TweenSettings { duration = _slideDuration, ease = Ease.OutCubic, useUnscaledTime = true } })) .Group(Tween.Alpha(_canvasGroup, new TweenSettings { startValue = 0f, endValue = _shownAlpha, settings = new TweenSettings { duration = _slideDuration, ease = Ease.OutCubic, useUnscaledTime = true } })); try { await _sequence.ToUniTask(cancellationToken: localTweenCts.Token); _rectTransform.anchoredPosition = _shownAnchoredPosition; _canvasGroup.alpha = _shownAlpha; } catch (OperationCanceledException) { } finally { if (ReferenceEquals(_sequenceCts, localTweenCts)) _sequenceCts = null; localTweenCts.Dispose(); _sequence = default; } } public async UniTask Hide() { if (_rectTransform == null || _canvasGroup == null) return; CancelTweenIfRunning(); var localTweenCts = new CancellationTokenSource(); _sequenceCts = localTweenCts; _sequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.UIAnchoredPosition(_rectTransform, new TweenSettings { startValue = _rectTransform.anchoredPosition, endValue = _hiddenAnchoredPosition, settings = new TweenSettings { duration = _slideDuration, ease = Ease.InCubic, useUnscaledTime = true } })) .Group(Tween.Alpha(_canvasGroup, new TweenSettings { startValue = _canvasGroup.alpha, endValue = 0f, settings = new TweenSettings { duration = _slideDuration, ease = Ease.InCubic, useUnscaledTime = true } })); try { await _sequence.ToUniTask(cancellationToken: localTweenCts.Token); _rectTransform.anchoredPosition = _hiddenAnchoredPosition; _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } catch (OperationCanceledException) { } finally { if (ReferenceEquals(_sequenceCts, localTweenCts)) _sequenceCts = null; localTweenCts.Dispose(); _sequence = default; } } public void SetText(string text) { if (_text != null) _text.text = text ?? string.Empty; ResizeToFitText(); } public UniTask Play(string text, float duration) { if (string.IsNullOrWhiteSpace(text)) return UniTask.CompletedTask; if (_suppressDuplicateMessages && IsDuplicateMessage(text)) return UniTask.CompletedTask; _queue.Enqueue(new PopupRequest(text, duration)); if (!_isProcessingQueue) ProcessQueue().Forget(); return UniTask.CompletedTask; } public void ClearQueue() { _queue.Clear(); _currentMessage = string.Empty; CancelTweenIfRunning(); if (_rectTransform != null) _rectTransform.anchoredPosition = _hiddenAnchoredPosition; if (_canvasGroup != null) { _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } SetText(string.Empty); _isProcessingQueue = false; } private bool IsDuplicateMessage(string text) { if (!string.IsNullOrEmpty(_currentMessage) && string.Equals(_currentMessage, text, StringComparison.Ordinal)) return true; foreach (var queued in _queue) if (string.Equals(queued.Text, text, StringComparison.Ordinal)) return true; return false; } private async UniTaskVoid ProcessQueue() { if (_isProcessingQueue) return; _isProcessingQueue = true; try { while (_destroyCts != null && !_destroyCts.IsCancellationRequested && _queue.Count > 0) { var request = _queue.Dequeue(); _currentMessage = request.Text; SetText(request.Text); await Show(); if (request.Duration > 0f) await UniTask.Delay( TimeSpan.FromSeconds(request.Duration), cancellationToken: _destroyCts.Token); await Hide(); _currentMessage = string.Empty; } } catch (OperationCanceledException) { } finally { _currentMessage = string.Empty; _isProcessingQueue = false; } } private void ResizeToFitText() { if (_text == null || _rectTransform == null) return; _text.ForceMeshUpdate(); var margin = _text.margin; var leftInset = margin.x; var topInset = margin.y; var rightInset = margin.z; var bottomInset = margin.w; var totalHorizontalInset = leftInset + rightInset; var totalVerticalInset = topInset + bottomInset; var maxTextWidth = Mathf.Max(0f, _maxWidth - totalHorizontalInset - _extraWidthSafety); var preferredSize = _text.GetPreferredValues(_text.text, maxTextWidth, Mathf.Infinity); var width = Mathf.Clamp( preferredSize.x + totalHorizontalInset + _extraWidthSafety, _minWidth, _maxWidth); var availableTextWidth = Mathf.Max(0f, width - totalHorizontalInset - _extraWidthSafety); var wrappedPreferredSize = _text.GetPreferredValues(_text.text, availableTextWidth, Mathf.Infinity); var height = Mathf.Max( _minHeight, wrappedPreferredSize.y + totalVerticalInset + _extraHeightSafety); _rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width); _rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height); LayoutRebuilder.ForceRebuildLayoutImmediate(_rectTransform); Canvas.ForceUpdateCanvases(); #if UNITY_EDITOR var changed = !Mathf.Approximately(width, _lastWidth) || !Mathf.Approximately(height, _lastHeight) || _lastEditorText != _text.text; if (_debugResizeInEditor && changed) Debug.Log( $"[InfoPopup] '{_text.text}'\n" + $"Preferred: {preferredSize.x:F1} x {preferredSize.y:F1}\n" + $"Wrapped: {wrappedPreferredSize.x:F1} x {wrappedPreferredSize.y:F1}\n" + $"Insets: L{leftInset:F1} T{topInset:F1} R{rightInset:F1} B{bottomInset:F1}\n" + $"Final Size: {width:F1} x {height:F1}", this); #endif _lastEditorText = _text.text; _lastWidth = width; _lastHeight = height; } private void CancelTweenIfRunning() { if (_sequence.isAlive) _sequence.Stop(); _sequence = default; if (_sequenceCts != null) { _sequenceCts.Cancel(); _sequenceCts.Dispose(); _sequenceCts = null; } } private struct PopupRequest { public readonly string Text; public readonly float Duration; public PopupRequest(string text, float duration) { Text = text; Duration = duration; } } } }