using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace DanroJump.UI.Focus { /// /// Собирает линейное кольцо фокусируемых UI Toolkit-элементов для навигации с клавиатуры или автомата. /// public sealed class UiFocusRing { private readonly List elements = new(); /// /// Текущий список фокусируемых элементов. /// public IReadOnlyList Elements => elements; /// /// Количество элементов в кольце фокуса. /// public int Count => elements.Count; /// /// Пересобирает список видимых и активных элементов, доступных для фокуса. /// public void Rebuild(VisualElement root) { elements.Clear(); if (root == null) { return; } CollectFocusableElements(root); } /// /// Проверяет, входит ли элемент в текущее кольцо фокуса. /// public bool Contains(VisualElement element) { return elements.Contains(element); } /// /// Возвращает первый элемент кольца фокуса. /// public VisualElement GetFirst() { return elements.Count > 0 ? elements[0] : null; } /// /// Возвращает следующий элемент с зацикливанием по краям списка. /// public VisualElement GetNext(VisualElement current, int direction) { if (elements.Count == 0) { return null; } var currentIndex = current != null ? elements.IndexOf(current) : -1; if (direction == 0) { return currentIndex >= 0 ? elements[currentIndex] : elements[0]; } if (currentIndex < 0) { return direction > 0 ? elements[0] : elements[^1]; } var targetIndex = (currentIndex + direction + elements.Count) % elements.Count; return elements[targetIndex]; } /// /// Ставит фокус на первый доступный элемент. /// public void FocusFirst() { TryFocus(GetFirst()); } /// /// Перемещает фокус относительно текущего элемента. /// public bool FocusRelativeTo(VisualElement current, int direction) { var next = GetNext(current, direction); if (next == null) { return false; } return TryFocus(next); } /// /// Ставит фокус на элемент, если он все еще доступен в текущем кольце. /// public bool TryFocus(VisualElement element) { if (element == null || !elements.Contains(element) || !IsFocusable(element)) { return false; } element.Focus(); return true; } /// /// Обходит визуальное дерево в порядке отображения и собирает доступные элементы. /// private void CollectFocusableElements(VisualElement root) { if (!CanTraverse(root)) { return; } if (IsFocusable(root)) { elements.Add(root); } for (var index = 0; index < root.childCount; index++) { CollectFocusableElements(root[index]); } } /// /// Проверяет базовые условия доступности элемента для фокуса. /// private static bool IsFocusable(VisualElement element) { return element.focusable && CanTraverse(element); } /// /// Проверяет, нужно ли учитывать элемент и его дочернее дерево при сборке фокуса. /// private static bool CanTraverse(VisualElement element) { return element != null && element.enabledInHierarchy && element.resolvedStyle.display != DisplayStyle.None && element.resolvedStyle.visibility != Visibility.Hidden; } } }