using System; using System.Collections; using UnityEngine; using UnityEngine.UI; using UnityEngine.InputSystem; namespace RR.Common.UI { /// /// Базовый компонент модального окна. /// Автоматически регистрируется в ModalManager при активации. /// [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; /// /// Первый элемент для фокуса при открытии. /// public Selectable FirstSelected => firstSelected; /// /// Блокировать ли фон при открытии. /// public bool BlockBackground => blockBackground; /// /// Следить ли за фокусом и возвращать его внутрь окна. /// public bool EnforceFocus => enforceFocus; /// /// Событие, вызываемое перед открытием окна. /// public event Action OnOpening; /// /// Событие, вызываемое после открытия окна. /// public event Action OnOpened; /// /// Событие, вызываемое перед закрытием окна. /// public event Action OnClosing; /// /// Событие, вызываемое после закрытия окна. /// public event Action OnClosed; protected bool _isOpen; protected Coroutine _fadeCoroutine; /// /// Открыто ли модальное окно. /// public bool IsOpen => _isOpen; protected virtual void Awake() { if (closeButton) { closeButton.onClick.AddListener(Close); } // Если canvasGroup не указан или указан на родительском объекте, // создаем/получаем CanvasGroup на самом модальном окне if (!canvasGroup || canvasGroup.gameObject != gameObject) { canvasGroup = GetComponent(); if (!canvasGroup) { canvasGroup = gameObject.AddComponent(); } } // Изначально скрыто 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(); } /// /// Открывает модальное окно. /// public virtual void Open() { if (_isOpen) return; OnOpening?.Invoke(); gameObject.SetActive(true); _isOpen = true; if (canvasGroup) { if (_fadeCoroutine != null) { StopCoroutine(_fadeCoroutine); } // Проверяем, активен ли объект в иерархии после SetActive(true) // Если родитель неактивен, запускаем корутину через ModalManager if (gameObject.activeInHierarchy) { _fadeCoroutine = StartCoroutine(FadeCoroutine(0f, 1f, fadeDuration, () => { OnOpened?.Invoke(); })); } else { // Объект неактивен в иерархии, запускаем через ModalManager var modalManager = ModalManager.Instance; if (modalManager != null && modalManager.gameObject.activeInHierarchy) { _fadeCoroutine = modalManager.StartCoroutine(FadeCoroutine(0f, 1f, fadeDuration, () => { OnOpened?.Invoke(); })); } else { // Если ModalManager тоже недоступен, просто вызываем OnOpened без анимации Debug.LogWarning($"[Modal] Cannot start fade coroutine for '{gameObject.name}' - object is inactive and ModalManager is not available"); canvasGroup.alpha = 1f; OnOpened?.Invoke(); } } } else { OnOpened?.Invoke(); } } /// /// Закрывает модальное окно. /// public virtual void Close() { if (!_isOpen && !gameObject.activeSelf) return; OnClosing?.Invoke(); if (canvasGroup && fadeDuration > 0f) { if (_fadeCoroutine != null) { StopCoroutine(_fadeCoroutine); } // Проверяем активность объекта в иерархии if (gameObject.activeInHierarchy) { _fadeCoroutine = StartCoroutine(FadeCoroutine(canvasGroup.alpha, 0f, fadeDuration, () => { gameObject.SetActive(false); _isOpen = false; OnClosed?.Invoke(); })); } else { // Если объект неактивен, запускаем через ModalManager var modalManager = ModalManager.Instance; if (modalManager != null && modalManager.gameObject.activeInHierarchy) { _fadeCoroutine = modalManager.StartCoroutine(FadeCoroutine(canvasGroup.alpha, 0f, fadeDuration, () => { gameObject.SetActive(false); _isOpen = false; OnClosed?.Invoke(); })); } else { // Если ModalManager недоступен, просто закрываем без анимации 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(); } } /// /// Мгновенно открывает окно без анимации. /// 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(); } /// /// Мгновенно закрывает окно без анимации. /// 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(); } /// /// Переключает состояние окна (открыто/закрыто). /// 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; } /// /// Устанавливает первый элемент для фокуса (можно менять динамически). /// public void SetFirstSelected(Selectable selectable) { firstSelected = selectable; } #if UNITY_EDITOR protected virtual void Reset() { // Пытаемся автоматически найти CanvasGroup canvasGroup = GetComponent(); if (!canvasGroup) { canvasGroup = GetComponentInChildren(); } // Ищем кнопку закрытия Button[] buttons = GetComponentsInChildren