334 lines
11 KiB
C#
334 lines
11 KiB
C#
using System;
|
||
using System.Collections;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.InputSystem;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
/// <summary>
|
||
/// Базовый компонент модального окна.
|
||
/// Автоматически регистрируется в ModalManager при активации.
|
||
/// </summary>
|
||
[DisallowMultipleComponent]
|
||
public class Modal : MonoBehaviour
|
||
{
|
||
[Header("Настройки модального окна")]
|
||
[Tooltip("Первый элемент, который получит фокус при открытии окна.")]
|
||
[SerializeField] private Selectable firstSelected;
|
||
|
||
[Tooltip("Блокировать интерактивность элементов за пределами модального окна.")]
|
||
[SerializeField] private bool blockBackground = true;
|
||
|
||
[Tooltip("Автоматически возвращать фокус внутрь окна, если он уходит наружу.")]
|
||
[SerializeField] private bool enforceFocus = true;
|
||
|
||
[Header("Анимация")]
|
||
[Tooltip("CanvasGroup для анимации появления/исчезновения.")]
|
||
[SerializeField] private CanvasGroup canvasGroup;
|
||
|
||
[Tooltip("Длительность анимации появления/исчезновения.")]
|
||
[SerializeField] private float fadeDuration = 0.15f;
|
||
|
||
[Header("Закрытие")]
|
||
[Tooltip("InputAction для закрытия модального окна (например, Escape).")]
|
||
[SerializeField] private InputActionReference closeAction;
|
||
|
||
[Tooltip("Кнопка закрытия модального окна.")]
|
||
[SerializeField] private Button closeButton;
|
||
|
||
/// <summary>
|
||
/// Первый элемент для фокуса при открытии.
|
||
/// </summary>
|
||
public Selectable FirstSelected => firstSelected;
|
||
|
||
/// <summary>
|
||
/// Блокировать ли фон при открытии.
|
||
/// </summary>
|
||
public bool BlockBackground => blockBackground;
|
||
|
||
/// <summary>
|
||
/// Следить ли за фокусом и возвращать его внутрь окна.
|
||
/// </summary>
|
||
public bool EnforceFocus => enforceFocus;
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое перед открытием окна.
|
||
/// </summary>
|
||
public event Action OnOpening;
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое после открытия окна.
|
||
/// </summary>
|
||
public event Action OnOpened;
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое перед закрытием окна.
|
||
/// </summary>
|
||
public event Action OnClosing;
|
||
|
||
/// <summary>
|
||
/// Событие, вызываемое после закрытия окна.
|
||
/// </summary>
|
||
public event Action OnClosed;
|
||
|
||
protected bool _isOpen;
|
||
protected Coroutine _fadeCoroutine;
|
||
|
||
/// <summary>
|
||
/// Открыто ли модальное окно.
|
||
/// </summary>
|
||
public bool IsOpen => _isOpen;
|
||
|
||
protected virtual void Awake()
|
||
{
|
||
if (closeButton)
|
||
{
|
||
closeButton.onClick.AddListener(Close);
|
||
}
|
||
|
||
// Если canvasGroup не указан или указан на родительском объекте,
|
||
// создаём/получаем CanvasGroup на самом модальном окне
|
||
if (!canvasGroup || canvasGroup.gameObject != gameObject)
|
||
{
|
||
canvasGroup = GetComponent<CanvasGroup>();
|
||
if (!canvasGroup)
|
||
{
|
||
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||
}
|
||
}
|
||
|
||
// Изначально скрыто
|
||
if (canvasGroup)
|
||
{
|
||
canvasGroup.alpha = 0f;
|
||
canvasGroup.interactable = false;
|
||
canvasGroup.blocksRaycasts = false;
|
||
}
|
||
|
||
gameObject.SetActive(false);
|
||
}
|
||
|
||
protected virtual void OnEnable()
|
||
{
|
||
// Регистрируемся в менеджере
|
||
ModalManager.Instance.Push(this);
|
||
|
||
// Подписываемся на действие закрытия
|
||
if (closeAction)
|
||
{
|
||
closeAction.action.Enable();
|
||
closeAction.action.performed += OnCloseActionPerformed;
|
||
}
|
||
}
|
||
|
||
protected virtual void OnDisable()
|
||
{
|
||
// Отписываемся от действия закрытия
|
||
if (closeAction)
|
||
{
|
||
closeAction.action.performed -= OnCloseActionPerformed;
|
||
}
|
||
|
||
// Удаляемся из менеджера
|
||
ModalManager.Instance.Pop(this);
|
||
}
|
||
|
||
private void OnCloseActionPerformed(InputAction.CallbackContext ctx)
|
||
{
|
||
Close();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Открывает модальное окно.
|
||
/// </summary>
|
||
public virtual void Open()
|
||
{
|
||
if (_isOpen) return;
|
||
|
||
OnOpening?.Invoke();
|
||
|
||
gameObject.SetActive(true);
|
||
_isOpen = true;
|
||
|
||
if (canvasGroup)
|
||
{
|
||
if (_fadeCoroutine != null)
|
||
{
|
||
StopCoroutine(_fadeCoroutine);
|
||
}
|
||
_fadeCoroutine = StartCoroutine(FadeCoroutine(0f, 1f, fadeDuration, () =>
|
||
{
|
||
OnOpened?.Invoke();
|
||
}));
|
||
}
|
||
else
|
||
{
|
||
OnOpened?.Invoke();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Закрывает модальное окно.
|
||
/// </summary>
|
||
public virtual void Close()
|
||
{
|
||
if (!_isOpen && !gameObject.activeSelf) return;
|
||
|
||
OnClosing?.Invoke();
|
||
|
||
if (canvasGroup && fadeDuration > 0f)
|
||
{
|
||
if (_fadeCoroutine != null)
|
||
{
|
||
StopCoroutine(_fadeCoroutine);
|
||
}
|
||
_fadeCoroutine = StartCoroutine(FadeCoroutine(canvasGroup.alpha, 0f, fadeDuration, () =>
|
||
{
|
||
gameObject.SetActive(false);
|
||
_isOpen = false;
|
||
OnClosed?.Invoke();
|
||
}));
|
||
}
|
||
else
|
||
{
|
||
if (canvasGroup)
|
||
{
|
||
canvasGroup.alpha = 0f;
|
||
canvasGroup.interactable = false;
|
||
canvasGroup.blocksRaycasts = false;
|
||
}
|
||
gameObject.SetActive(false);
|
||
_isOpen = false;
|
||
OnClosed?.Invoke();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Мгновенно открывает окно без анимации.
|
||
/// </summary>
|
||
public void OpenInstant()
|
||
{
|
||
if (_isOpen) return;
|
||
|
||
OnOpening?.Invoke();
|
||
|
||
gameObject.SetActive(true);
|
||
_isOpen = true;
|
||
|
||
if (canvasGroup)
|
||
{
|
||
canvasGroup.alpha = 1f;
|
||
canvasGroup.interactable = true;
|
||
canvasGroup.blocksRaycasts = true;
|
||
}
|
||
|
||
OnOpened?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Мгновенно закрывает окно без анимации.
|
||
/// </summary>
|
||
public void CloseInstant()
|
||
{
|
||
if (!_isOpen && !gameObject.activeSelf) return;
|
||
|
||
OnClosing?.Invoke();
|
||
|
||
if (canvasGroup)
|
||
{
|
||
canvasGroup.alpha = 0f;
|
||
canvasGroup.interactable = false;
|
||
canvasGroup.blocksRaycasts = false;
|
||
}
|
||
|
||
gameObject.SetActive(false);
|
||
_isOpen = false;
|
||
OnClosed?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Переключает состояние окна (открыто/закрыто).
|
||
/// </summary>
|
||
public void Toggle()
|
||
{
|
||
if (_isOpen)
|
||
Close();
|
||
else
|
||
Open();
|
||
}
|
||
|
||
private IEnumerator FadeCoroutine(float from, float to, float duration, Action onComplete = null)
|
||
{
|
||
if (!canvasGroup)
|
||
{
|
||
onComplete?.Invoke();
|
||
yield break;
|
||
}
|
||
|
||
// При открытии сразу делаем интерактивным
|
||
if (to > from)
|
||
{
|
||
canvasGroup.interactable = true;
|
||
canvasGroup.blocksRaycasts = true;
|
||
}
|
||
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.unscaledDeltaTime;
|
||
float t = Mathf.Clamp01(elapsed / duration);
|
||
canvasGroup.alpha = Mathf.Lerp(from, to, t);
|
||
yield return null;
|
||
}
|
||
|
||
canvasGroup.alpha = to;
|
||
|
||
// При закрытии выключаем интерактивность
|
||
if (to < from)
|
||
{
|
||
canvasGroup.interactable = false;
|
||
canvasGroup.blocksRaycasts = false;
|
||
}
|
||
|
||
onComplete?.Invoke();
|
||
_fadeCoroutine = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Устанавливает первый элемент для фокуса (можно менять динамически).
|
||
/// </summary>
|
||
public void SetFirstSelected(Selectable selectable)
|
||
{
|
||
firstSelected = selectable;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
protected virtual void Reset()
|
||
{
|
||
// Пытаемся автоматически найти CanvasGroup
|
||
canvasGroup = GetComponent<CanvasGroup>();
|
||
if (!canvasGroup)
|
||
{
|
||
canvasGroup = GetComponentInChildren<CanvasGroup>();
|
||
}
|
||
|
||
// Ищем кнопку закрытия
|
||
Button[] buttons = GetComponentsInChildren<Button>(true);
|
||
foreach (Button btn in buttons)
|
||
{
|
||
string btnName = btn.name.ToLower();
|
||
if (btnName.Contains("close") || btnName.Contains("back") || btnName.Contains("cancel"))
|
||
{
|
||
closeButton = btn;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Ищем первый Selectable для фокуса
|
||
firstSelected = GetComponentInChildren<Selectable>(false);
|
||
}
|
||
#endif
|
||
}
|
||
}
|