using System; using RR.Common.Settings; using TMPro; using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; namespace RR.Common.UI { public class SettingUIItem : MonoBehaviour { [Header("Common")] public TMP_Text titleText; public TMP_Text descriptionText; [Header("Controls")] public Toggle toggle; public Slider slider; public TMP_Dropdown dropdown; public TMP_InputField input; public Button button; [Header("Option Picker")] public Button prevButton; public Button nextButton; public TMP_Text optionValueText; [Header("Optional")] public TMP_Text sliderValueText; // например справа от слайдера private SettingEntry entry; private bool showDescription; /// /// Безопасно устанавливает значение настройки с проверкой на уничтоженный SettingsManager /// private void SetSafe(string key, SettingValue value) { var manager = SettingsManager.Instance; if (!manager) { Debug.LogWarning($"[SettingUIItem] SettingsManager.Instance is null, cannot set {key}"); return; } manager.Set(key, value); } public void Bind(SettingEntry entryToBind, bool showDescriptionOption) { this.entry = entryToBind; this.showDescription = showDescriptionOption; if (titleText) titleText.text = string.IsNullOrWhiteSpace(entryToBind.displayName) ? entryToBind.key : entryToBind.displayName; if (descriptionText) { bool hasDesc = showDescriptionOption && !string.IsNullOrWhiteSpace(entryToBind.description); descriptionText.gameObject.SetActive(hasDesc); if (hasDesc) descriptionText.text = entryToBind.description; } // Отключаем все контролы, включим нужный if (toggle) toggle.gameObject.SetActive(false); if (slider) slider.gameObject.SetActive(false); if (dropdown) dropdown.gameObject.SetActive(false); if (input) input.gameObject.SetActive(false); if (button) button.gameObject.SetActive(false); if (prevButton) prevButton.gameObject.SetActive(false); if (nextButton) nextButton.gameObject.SetActive(false); if (optionValueText) optionValueText.gameObject.SetActive(false); // Биндим по applyTarget/valueType SetupControl(); RefreshFromSettings(); } public void BindExternalDescriptionTarget(TMP_Text targetText) { if (entry == null) return; var selectables = new List(); GetComponentsInChildren(true, selectables); string description = entry.description ?? string.Empty; foreach (Selectable selectable in selectables) { if (selectable == null) continue; SelectableDescriptionTarget descriptionTarget = selectable.GetComponent(); if (descriptionTarget == null) descriptionTarget = selectable.gameObject.AddComponent(); descriptionTarget.Configure(targetText, description); } } public void RefreshFromSettings() { if (entry == null) return; SettingsManager manager = SettingsManager.Instance; if (!manager) return; var v = manager.Get(entry.key); if (toggle && toggle.gameObject.activeSelf) toggle.SetIsOnWithoutNotify(v.b); if (slider && slider.gameObject.activeSelf) slider.SetValueWithoutNotify(slider.wholeNumbers ? v.i : v.f); if (dropdown && dropdown.gameObject.activeSelf) dropdown.SetValueWithoutNotify(Mathf.Clamp(v.i, 0, dropdown.options.Count - 1)); if (input && input.gameObject.activeSelf) input.SetTextWithoutNotify(v.s ?? ""); if (optionValueText && optionValueText.gameObject.activeSelf && entry.valueType == SettingValueType.OptionPicker) UpdateOptionPickerDisplay(entry.options); UpdateSliderValueText(); } private void SetupControl() { if (entry == null) return; // Настройки по valueType switch (entry.valueType) { case SettingValueType.Bool: UseToggle(v => SetSafe(entry.key, SettingValue.Bool(v))); break; case SettingValueType.Float: // Для ValueSelector не создаём слайдер - его обрабатывает ValueSelectorRow if (entry.floatDisplayFormat == FloatDisplayFormat.ValueSelector) { // ValueSelectorRow сам управляет значением } else { UseSliderFloat(entry.minFloat, entry.maxFloat, v => SetSafe(entry.key, SettingValue.Float(v))); } break; case SettingValueType.Int: // Для ValueSelector не создаём слайдер - его обрабатывает ValueSelectorRow if (entry.intDisplayFormat == IntDisplayFormat.ValueSelector) { // ValueSelectorRow сам управляет значением // SettingUIItem здесь только отображает label и description } else { // Int — делаем слайдер целочисленным (лучше чем инпут) UseSliderInt(entry.minInt, entry.maxInt, v => SetSafe(entry.key, SettingValue.Int(v))); } break; case SettingValueType.String: UseInput(s => SetSafe(entry.key, SettingValue.String(s))); break; case SettingValueType.Button: UseButton(() => { // Вызываем биндеры на сцене (SettingsButtonBinder) SettingsButtonDispatcher.Dispatch(entry.key); }); break; case SettingValueType.OptionPicker: UseOptionPicker(entry.options, idx => SetSafe(entry.key, SettingValue.Int(idx))); break; default: throw new ArgumentOutOfRangeException(); } } private void UseToggle(UnityEngine.Events.UnityAction onChanged) { if (!toggle) return; toggle.gameObject.SetActive(true); toggle.onValueChanged.RemoveAllListeners(); toggle.onValueChanged.AddListener(onChanged); } private void UseSliderFloat(float min, float max, UnityEngine.Events.UnityAction onChanged) { if (!slider) return; slider.gameObject.SetActive(true); slider.wholeNumbers = false; slider.minValue = Mathf.Min(min, max); slider.maxValue = Mathf.Max(min, max); slider.onValueChanged.RemoveAllListeners(); slider.onValueChanged.AddListener(v => { onChanged(v); UpdateSliderValueText(); }); } private void UseSliderInt(int min, int max, System.Action onChanged) { if (!slider) return; slider.gameObject.SetActive(true); slider.wholeNumbers = true; slider.minValue = Mathf.Min(min, max); slider.maxValue = Mathf.Max(min, max); slider.onValueChanged.RemoveAllListeners(); slider.onValueChanged.AddListener(v => { onChanged(Mathf.RoundToInt(v)); UpdateSliderValueText(); }); } private void UseDropdown(List options, System.Action onChanged) { if (!dropdown) return; dropdown.gameObject.SetActive(true); dropdown.ClearOptions(); dropdown.AddOptions(options ?? new List()); dropdown.onValueChanged.RemoveAllListeners(); dropdown.onValueChanged.AddListener(idx => onChanged(idx)); } private void UseInput(System.Action onEndEdit) { if (!input) return; input.gameObject.SetActive(true); input.onEndEdit.RemoveAllListeners(); input.onEndEdit.AddListener(text => onEndEdit(text)); } private void UseButton(System.Action onClick) { if (!button) return; button.gameObject.SetActive(true); // Убеждаемся что кнопка интерактивна button.interactable = true; // Навигация настраивается централизованно в SettingsUI.SetupExplicitNavigation() // Устанавливаем текст кнопки var buttonText = button.GetComponentInChildren(); if (buttonText && entry != null) { string text = !string.IsNullOrEmpty(entry.buttonText) ? entry.buttonText : entry.displayName; if (string.IsNullOrEmpty(text)) text = entry.key; buttonText.text = text; } // Устанавливаем цвет и спрайт кнопки if (entry != null) { var image = button.GetComponent(); if (image) { // Устанавливаем цвет напрямую на Image image.color = entry.buttonColor; // Устанавливаем спрайт if (entry.buttonSprite != null) { image.sprite = entry.buttonSprite; } } } button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => onClick()); } private void UseOptionPicker(List options, System.Action onChanged) { if (options == null || options.Count == 0) return; // Если есть dropdown - используем его if (dropdown) { UseDropdown(options, onChanged); return; } // Иначе используем кнопки prev/next if (!prevButton || !nextButton || !optionValueText) return; prevButton.gameObject.SetActive(true); nextButton.gameObject.SetActive(true); optionValueText.gameObject.SetActive(true); prevButton.onClick.RemoveAllListeners(); nextButton.onClick.RemoveAllListeners(); prevButton.onClick.AddListener(() => { if (entry == null) return; int current = SettingsManager.Instance.Get(entry.key).i; int newIndex = current - 1; if (newIndex < 0) newIndex = options.Count - 1; // Wrap around onChanged(newIndex); UpdateOptionPickerDisplay(options); }); nextButton.onClick.AddListener(() => { if (entry == null) return; int current = SettingsManager.Instance.Get(entry.key).i; int newIndex = (current + 1) % options.Count; // Wrap around onChanged(newIndex); UpdateOptionPickerDisplay(options); }); UpdateOptionPickerDisplay(options); } private void UpdateOptionPickerDisplay(List options) { if (!optionValueText || entry == null) return; int currentIndex = SettingsManager.Instance.Get(entry.key).i; if (currentIndex >= 0 && currentIndex < options.Count) optionValueText.text = options[currentIndex]; } private void UpdateSliderValueText() { if (!sliderValueText || !slider || !slider.gameObject.activeSelf) return; if (slider.wholeNumbers) { sliderValueText.text = Mathf.RoundToInt(slider.value).ToString(); } else { // Проверяем флаг displayAsPercentage if (entry != null && entry.displayAsPercentage) { // Отображаем как проценты (0-100%) float percentage = slider.value * 100f; sliderValueText.text = percentage.ToString("0") + "%"; } else { sliderValueText.text = slider.value.ToString("0.##"); } } } } }