using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace RR.Common.Settings.UI
{
///
/// Компонент-Selectable для элемента настроек с выбором значения стрелками влево/вправо.
/// Поддерживает как int, так и float значения.
/// При зажатии кнопки скорость изменения плавно увеличивается.
/// Автоповтор целиком управляется Event System (inputActionsPerSecond).
/// Работает на одном префабе вместе с SettingUIItem.
///
public class ValueSelectorRow : Selectable, IMoveHandler
{
private enum SelectorMode
{
Int,
Float,
OptionPicker
}
[Header("References")]
[SerializeField] private TMP_Text valueText;
private SettingEntry _entry;
private SelectorMode _mode;
private int _currentInt;
private float _currentFloat;
private int _intStep = 1;
private float _floatStep = 0.1f;
private int _currentOptionIndex;
// ───────────────────── Ускорение при зажатии ─────────────────────
/// Максимальный множитель шага при длительном зажатии.
private const int MaxStepMultiplier = 10;
/// Сколько вызовов OnMove до начала роста множителя.
private const int MultiplierStartCount = 6;
/// Сколько вызовов OnMove до достижения максимального множителя.
private const int MultiplierMaxCount = 30;
/// Если между двумя OnMove прошло больше этого времени — сброс множителя.
private const float HoldResetTimeout = 0.25f;
private int _holdDirection; // -1 = left, +1 = right, 0 = idle
private int _holdCount; // Счётчик последовательных OnMove в одном направлении
private float _lastMoveTime; // Time.unscaledTime последнего OnMove
// ───────────────────── Int Setup ─────────────────────
public void Setup(SettingEntry entry, int currentValue)
{
if (entry == null)
{
Debug.LogError("[ValueSelectorRow] Cannot setup with null entry!");
return;
}
_entry = entry;
_mode = SelectorMode.Int;
if (entry.intStep <= 0)
{
Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.intStep} for key {entry.key}, using 1");
_intStep = 1;
}
else
{
_intStep = entry.intStep;
}
_currentInt = Mathf.Clamp(currentValue, entry.minInt, entry.maxInt);
UpdateUI();
}
// ───────────────────── Float Setup ─────────────────────
public void Setup(SettingEntry entry, float currentValue)
{
if (entry == null)
{
Debug.LogError("[ValueSelectorRow] Cannot setup with null entry!");
return;
}
_entry = entry;
_mode = SelectorMode.Float;
if (entry.floatStep <= 0f)
{
Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.floatStep} for key {entry.key}, using 0.1");
_floatStep = 0.1f;
}
else
{
_floatStep = entry.floatStep;
}
_currentFloat = Mathf.Clamp(currentValue, entry.minFloat, entry.maxFloat);
UpdateUI();
}
// ───────────────────── Option Picker Setup ─────────────────────
public void SetupOptionPicker(SettingEntry entry, int currentIndex)
{
if (entry == null)
{
Debug.LogError("[ValueSelectorRow] Cannot setup option picker with null entry!");
return;
}
_entry = entry;
_mode = SelectorMode.OptionPicker;
int maxIndex = entry.options != null && entry.options.Count > 0
? entry.options.Count - 1
: 0;
_currentOptionIndex = Mathf.Clamp(currentIndex, 0, maxIndex);
UpdateUI();
}
// ───────────────────── Navigation ─────────────────────
public new void OnMove(AxisEventData eventData)
{
if (_entry == null || !IsInteractable())
{
base.OnMove(eventData);
return;
}
switch (eventData.moveDir)
{
case MoveDirection.Left:
ApplyMove(-1);
eventData.Use();
break;
case MoveDirection.Right:
ApplyMove(1);
eventData.Use();
break;
case MoveDirection.Up:
case MoveDirection.Down:
ResetHold();
base.OnMove(eventData);
break;
}
}
// ───────────────────── Move Logic ─────────────────────
private void ApplyMove(int direction)
{
float now = Time.unscaledTime;
// Если направление сменилось или прошло слишком много времени — сбрасываем
if (_holdDirection != direction || (now - _lastMoveTime) > HoldResetTimeout)
{
_holdDirection = direction;
_holdCount = 0;
}
_lastMoveTime = now;
_holdCount++;
// Вычисляем множитель шага по количеству последовательных нажатий
int multiplier = 1;
if (_holdCount > MultiplierStartCount)
{
float progress = Mathf.Clamp01(
(float)(_holdCount - MultiplierStartCount) / (MultiplierMaxCount - MultiplierStartCount));
multiplier = Mathf.Max(1, Mathf.RoundToInt(Mathf.Lerp(1f, MaxStepMultiplier, progress)));
}
if (direction > 0)
IncreaseValue(multiplier);
else
DecreaseValue(multiplier);
}
private void ResetHold()
{
_holdDirection = 0;
_holdCount = 0;
}
protected override void OnDisable()
{
base.OnDisable();
ResetHold();
}
public override void OnDeselect(BaseEventData eventData)
{
base.OnDeselect(eventData);
ResetHold();
}
// ───────────────────── Value Changes ─────────────────────
private void DecreaseValue(int multiplier)
{
if (_mode == SelectorMode.Float)
{
float newValue = Mathf.Max(_entry.minFloat, _currentFloat - _floatStep * multiplier);
if (!Mathf.Approximately(newValue, _currentFloat))
SetValue(newValue);
}
else if (_mode == SelectorMode.OptionPicker)
{
int newValue = Mathf.Max(0, _currentOptionIndex - multiplier);
if (newValue != _currentOptionIndex)
SetValue(newValue);
}
else
{
int newValue = Mathf.Max(_entry.minInt, _currentInt - _intStep * multiplier);
if (newValue != _currentInt)
SetValue(newValue);
}
}
private void IncreaseValue(int multiplier)
{
if (_mode == SelectorMode.Float)
{
float newValue = Mathf.Min(_entry.maxFloat, _currentFloat + _floatStep * multiplier);
if (!Mathf.Approximately(newValue, _currentFloat))
SetValue(newValue);
}
else if (_mode == SelectorMode.OptionPicker)
{
int maxIndex = _entry.options != null && _entry.options.Count > 0
? _entry.options.Count - 1
: 0;
int newValue = Mathf.Min(maxIndex, _currentOptionIndex + multiplier);
if (newValue != _currentOptionIndex)
SetValue(newValue);
}
else
{
int newValue = Mathf.Min(_entry.maxInt, _currentInt + _intStep * multiplier);
if (newValue != _currentInt)
SetValue(newValue);
}
}
// ───────────────────── UI ─────────────────────
private void UpdateUI()
{
if (valueText == null) return;
if (_mode == SelectorMode.Float)
{
if (_entry != null && _entry.displayAsPercentage)
valueText.text = (_currentFloat * 100f).ToString("0") + "%";
else
valueText.text = _currentFloat.ToString("0.##");
}
else if (_mode == SelectorMode.OptionPicker)
{
if (_entry != null && _entry.options != null &&
_currentOptionIndex >= 0 && _currentOptionIndex < _entry.options.Count)
valueText.text = _entry.options[_currentOptionIndex];
else
valueText.text = string.Empty;
}
else
{
valueText.text = _currentInt.ToString();
}
}
// ───────────────────── Public API (int) ─────────────────────
public void SetValue(int value)
{
if (_entry == null) return;
if (_mode == SelectorMode.OptionPicker)
{
int maxIndex = _entry.options != null && _entry.options.Count > 0
? _entry.options.Count - 1
: 0;
int clampedOption = Mathf.Clamp(value, 0, maxIndex);
if (clampedOption != _currentOptionIndex)
{
_currentOptionIndex = clampedOption;
UpdateUI();
var manager = SettingsManager.Instance;
if (manager != null)
manager.SetInt(_entry.key, _currentOptionIndex);
else
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
return;
}
int clamped = Mathf.Clamp(value, _entry.minInt, _entry.maxInt);
if (clamped != _currentInt)
{
_currentInt = clamped;
UpdateUI();
var manager = SettingsManager.Instance;
if (manager != null)
manager.SetInt(_entry.key, _currentInt);
else
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
}
// ───────────────────── Public API (float) ─────────────────────
public void SetValue(float value)
{
if (_entry == null) return;
float clamped = Mathf.Clamp(value, _entry.minFloat, _entry.maxFloat);
if (!Mathf.Approximately(clamped, _currentFloat))
{
_currentFloat = clamped;
UpdateUI();
var manager = SettingsManager.Instance;
if (manager != null)
manager.SetFloat(_entry.key, _currentFloat);
else
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
}
public bool IsFloat => _mode == SelectorMode.Float;
public string Key => _entry?.key;
}
}