using DanroJump.UI.Focus; using UnityEngine; using UnityEngine.UIElements; namespace DanroJump.UI.Modals { /// /// Базовый класс UI Toolkit-модалки с управлением видимостью и локальным кольцом фокуса. /// public abstract class ModalWindowBase { private const string ModalButtonClass = "modal-button"; private const string FocusedModalButtonClass = "modal-button--focused"; private readonly UiFocusRing focusRing = new(); /// /// Создает модальное окно поверх корневого VisualElement. /// protected ModalWindowBase(VisualElement root) { Root = root; Root?.RegisterCallback(HandleModalButtonFocusIn, TrickleDown.TrickleDown); Root?.RegisterCallback(HandleModalButtonFocusOut, TrickleDown.TrickleDown); } /// /// Корневой VisualElement модального окна. /// public VisualElement Root { get; } /// /// Последний элемент внутри модалки, на котором был фокус. /// public VisualElement LastFocusedElement { get; private set; } /// /// Разрешает закрытие окна по Escape/Back. /// public virtual bool CanCloseByBack => true; /// /// Показывает, открыто ли модальное окно сейчас. /// public bool IsOpen { get; private set; } public event System.Action Opened; public event System.Action Closed; public event System.Action CloseRequested; /// /// Показывает модальное окно и пересобирает доступные элементы фокуса. /// public virtual void Open() { if (Root == null) { return; } Root.style.display = DisplayStyle.Flex; Root.SetEnabled(true); IsOpen = true; RebuildFocusRing(); Opened?.Invoke(this); } /// /// Скрывает модальное окно и сбрасывает запомненный фокус. /// public virtual void Close() { if (Root == null) { return; } Root.style.display = DisplayStyle.None; Root.SetEnabled(false); IsOpen = false; LastFocusedElement = null; ClearFocusedModalButtons(); Closed?.Invoke(this); } /// /// Просит внешний стек закрыть это окно с учетом порядка модалок. /// public void RequestClose() { CloseRequested?.Invoke(this); } /// /// Обновляет список элементов, между которыми можно перемещать фокус. /// public void RebuildFocusRing() { focusRing.Rebuild(Root); } /// /// Проверяет, принадлежит ли элемент визуальному дереву модалки. /// public bool Contains(VisualElement element) { return ContainsElement(Root, element); } /// /// Проверяет, известен ли элемент текущему кольцу фокуса. /// public bool IsKnownFocusable(VisualElement element) { return focusRing.Contains(element); } /// /// Запоминает последний сфокусированный элемент внутри модалки. /// public void RememberFocusedElement(VisualElement element) { if (Contains(element)) { LastFocusedElement = element; } } /// /// Возвращает фокус на последний активный элемент или на элемент по умолчанию. /// public void FocusDefault() { RebuildFocusRing(); (LastFocusedElement != null && focusRing.Contains(LastFocusedElement) ? LastFocusedElement : GetDefaultFocusElement() ?? focusRing.GetFirst())?.Focus(); } /// /// Сдвигает фокус внутри окна на один элемент вперед или назад. /// public bool FocusRelativeTo(VisualElement current, int direction) { RebuildFocusRing(); return focusRing.FocusRelativeTo(current, direction); } /// /// Позволяет конкретной модалке перехватить UI Toolkit navigation-событие. /// /// Элемент, с которого пришла навигация. /// Направление навигации. /// True, если модалка обработала событие сама. public virtual bool HandleNavigationMove(VisualElement current, NavigationMoveEvent.Direction direction) { return false; } /// /// Позволяет конкретной модалке перехватить клавиатурную навигацию. /// /// Элемент, с которого пришла клавиша. /// Код нажатой клавиши. /// True, если модалка обработала клавишу сама. public virtual bool HandleNavigationKey(VisualElement current, KeyCode keyCode) { return false; } /// /// Позволяет конкретной модалке выбрать стартовый элемент фокуса. /// protected virtual VisualElement GetDefaultFocusElement() { return null; } private static bool ContainsElement(VisualElement root, VisualElement element) { for (var current = element; current != null; current = current.parent) { if (current == root) { return true; } } return false; } private void HandleModalButtonFocusIn(FocusInEvent evt) { ResolveModalButton(evt.target as VisualElement)?.AddToClassList(FocusedModalButtonClass); } private void HandleModalButtonFocusOut(FocusOutEvent evt) { ResolveModalButton(evt.target as VisualElement)?.RemoveFromClassList(FocusedModalButtonClass); } private void ClearFocusedModalButtons() { Root?.Query