using DanroJump.UI.Focus;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanroJump.UI.Modals
{
///
/// Базовый класс UI Toolkit-модалки с управлением видимостью и локальным кольцом фокуса.
///
public abstract class ModalWindowBase
{
private readonly UiFocusRing focusRing = new();
///
/// Создает модальное окно поверх корневого VisualElement.
///
protected ModalWindowBase(VisualElement root)
{
Root = root;
}
///
/// Корневой 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;
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;
}
}
}