using UnityEngine;
using UnityEngine.EventSystems;
namespace RR.Common.UI
{
///
/// Указатель-стрелка, который плавно перемещается к произвольному UI-элементу.
/// Управляется извне через или .
///
public class UIRowPointer : MonoBehaviour
{
[Header("Указатель")]
[Tooltip("RectTransform указателя (стрелка, иконка и т.д.).")]
[SerializeField] private RectTransform pointer;
[Header("Цели (опционально)")]
[Tooltip("Фиксированные цели для SetTarget(int). Можно не заполнять, если используется SetTarget(RectTransform).")]
[SerializeField] private RectTransform[] targets;
[Header("Движение")]
[Tooltip("Скорость плавного перемещения. 0 — мгновенно.")]
[SerializeField] private float smoothSpeed = 12f;
[Tooltip("Смещение указателя относительно центра цели (в локальных координатах родителя указателя).")]
[SerializeField] private Vector2 offset;
[Header("Покачивание")]
[Tooltip("Включить покачивание указателя по горизонтали.")]
[SerializeField] private bool wobble = true;
[Tooltip("Амплитуда покачивания (пикселей).")]
[SerializeField] private float wobbleAmplitude = 4f;
[Tooltip("Скорость покачивания (циклов в секунду).")]
[SerializeField] private float wobbleSpeed = 2f;
[Header("Selection Tracking")]
[Tooltip("Задержка перед скрытием указателя, если выбранный элемент временно не найден.")]
[SerializeField] private float unmatchedHideDelay = 0.15f;
// ── Состояние ──
private RectTransform _currentTarget;
private Vector2 _targetPos;
private Vector2 _baseX;
private bool _ready;
private bool _followSelection;
private Transform _selectionScope;
private bool _hideWhenSelectionUnmatched;
private readonly System.Collections.Generic.List _selectionBindings = new();
private float _unmatchedSelectionTime;
private sealed class SelectionBinding
{
public Transform SelectionRoot;
public RectTransform PointerTarget;
public bool HidePointer;
}
// ────────────── Публичный API ──────────────
///
/// Перемещает указатель к произвольному .
///
public void SetTarget(RectTransform target)
{
if (target == null) return;
_currentTarget = target;
_targetPos = CalculatePosition(target);
if (!_ready)
{
if (pointer) pointer.anchoredPosition = _targetPos;
_ready = true;
}
ShowPointer();
}
///
/// Перемещает указатель к цели по индексу из массива .
///
public void SetTarget(int index)
{
if (targets == null || index < 0 || index >= targets.Length) return;
SetTarget(targets[index]);
}
///
/// Скрывает указатель.
///
public void Hide()
{
if (pointer) pointer.gameObject.SetActive(false);
_currentTarget = null;
}
///
/// Задаёт массив целей из кода (если не задан через инспектор).
///
public void SetTargets(RectTransform[] newTargets)
{
targets = newTargets;
}
///
/// Переключает указатель в режим следования за выбранным UI-элементом.
///
public void FollowSelection(Transform scope, bool hideWhenSelectionUnmatched = false)
{
_selectionScope = scope;
_hideWhenSelectionUnmatched = hideWhenSelectionUnmatched;
_followSelection = true;
_unmatchedSelectionTime = 0f;
}
///
/// Отключает режим следования за выбранным UI-элементом.
///
public void StopFollowingSelection()
{
_followSelection = false;
_selectionScope = null;
_hideWhenSelectionUnmatched = false;
_unmatchedSelectionTime = 0f;
}
///
/// Очищает runtime-привязки выбранных элементов к строкам указателя.
///
public void ClearSelectionBindings()
{
_selectionBindings.Clear();
}
///
/// Привязывает выбранный UI-элемент или его дочерние элементы к строке указателя.
///
public void RegisterSelectionBinding(Transform selectionRoot, RectTransform pointerTarget)
{
if (selectionRoot == null)
{
return;
}
_selectionBindings.Add(new SelectionBinding
{
SelectionRoot = selectionRoot,
PointerTarget = pointerTarget,
HidePointer = false
});
}
///
/// Привязывает выбранный UI-элемент к скрытию указателя.
///
public void RegisterHideBinding(Transform selectionRoot)
{
if (selectionRoot == null)
{
return;
}
_selectionBindings.Add(new SelectionBinding
{
SelectionRoot = selectionRoot,
PointerTarget = null,
HidePointer = true
});
}
// ────────────── Lifecycle ──────────────
private void Awake()
{
if (pointer) _baseX = new Vector2(pointer.anchoredPosition.x, 0f);
}
private void LateUpdate()
{
UpdateSelectionTracking();
if (!pointer || _currentTarget == null) return;
// Пересчитываем позицию (цель может двигаться из-за layout)
_targetPos = CalculatePosition(_currentTarget);
// Плавное перемещение
Vector2 pos = smoothSpeed > 0
? Vector2.Lerp(pointer.anchoredPosition, _targetPos, Time.unscaledDeltaTime * smoothSpeed)
: _targetPos;
// Покачивание
if (wobble)
pos.x += Mathf.Sin(Time.unscaledTime * wobbleSpeed * Mathf.PI * 2f) * wobbleAmplitude;
pointer.anchoredPosition = pos;
}
// ────────────── Внутреннее ──────────────
private void UpdateSelectionTracking()
{
if (!_followSelection)
{
return;
}
GameObject selected = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null;
if (selected == null)
{
HandleUnmatchedSelection();
return;
}
Transform selectedTransform = selected.transform;
if (_selectionScope != null && !selectedTransform.IsChildOf(_selectionScope))
{
HandleUnmatchedSelection();
return;
}
SelectionBinding binding = FindSelectionBinding(selectedTransform);
if (binding != null)
{
_unmatchedSelectionTime = 0f;
if (binding.HidePointer || binding.PointerTarget == null)
{
Hide();
return;
}
SetTarget(binding.PointerTarget);
return;
}
RectTransform trackedTarget = FindTrackedTarget(selectedTransform);
if (trackedTarget != null)
{
_unmatchedSelectionTime = 0f;
SetTarget(trackedTarget);
return;
}
HandleUnmatchedSelection();
}
private SelectionBinding FindSelectionBinding(Transform selectedTransform)
{
for (int i = 0; i < _selectionBindings.Count; i++)
{
SelectionBinding binding = _selectionBindings[i];
if (binding?.SelectionRoot == null)
{
continue;
}
if (selectedTransform == binding.SelectionRoot || selectedTransform.IsChildOf(binding.SelectionRoot))
{
return binding;
}
}
return null;
}
private RectTransform FindTrackedTarget(Transform selectedTransform)
{
if (targets == null)
{
return null;
}
for (int i = 0; i < targets.Length; i++)
{
RectTransform target = targets[i];
if (target == null)
{
continue;
}
if (selectedTransform == target || selectedTransform.IsChildOf(target))
{
return target;
}
}
return null;
}
private void HandleUnmatchedSelection()
{
if (_hideWhenSelectionUnmatched)
{
_unmatchedSelectionTime += Time.unscaledDeltaTime;
if (_unmatchedSelectionTime >= unmatchedHideDelay)
{
Hide();
}
return;
}
_unmatchedSelectionTime = 0f;
}
private Vector2 CalculatePosition(RectTransform target)
{
RectTransform parent = pointer.parent as RectTransform;
if (parent == null) return pointer.anchoredPosition;
Vector3[] corners = new Vector3[4];
target.GetWorldCorners(corners);
Vector3 worldCenter = (corners[0] + corners[2]) * 0.5f;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parent,
RectTransformUtility.WorldToScreenPoint(null, worldCenter),
null,
out Vector2 localPos
);
return new Vector2(
_baseX.x + offset.x,
localPos.y + offset.y
);
}
private void ShowPointer()
{
if (pointer && !pointer.gameObject.activeSelf)
pointer.gameObject.SetActive(true);
}
}
}