Files

433 lines
13 KiB
C#

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<PopupRequest> _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<RectTransform>();
if (_canvasGroup == null)
_canvasGroup = GetComponent<CanvasGroup>();
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<RectTransform>();
if (_canvasGroup == null)
_canvasGroup = GetComponent<CanvasGroup>();
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<Vector2>
{
startValue = _hiddenAnchoredPosition,
endValue = _shownAnchoredPosition,
settings = new TweenSettings
{
duration = _slideDuration,
ease = Ease.OutCubic,
useUnscaledTime = true
}
}))
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
{
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<Vector2>
{
startValue = _rectTransform.anchoredPosition,
endValue = _hiddenAnchoredPosition,
settings = new TweenSettings
{
duration = _slideDuration,
ease = Ease.InCubic,
useUnscaledTime = true
}
}))
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
{
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;
}
}
}
}