using System; using System.Collections; using TMPro; using UnityEngine; using UnityEngine.UI; public class TextInput : SettingsPanelBase { [Header("Ключ prefs")] [SerializeField] private string key; [SerializeField] private bool loadData; [SerializeField] private bool saveDataOnClose; [Header("Описание страницы")] [SerializeField] private TextMeshProUGUI title; [SerializeField] private TextMeshProUGUI description; [Header("Пресеты настроек")] [SerializeField] private TextInputPreset[] presets; [Header("Настройки генерации поля")] [Tooltip("Якорь лево-низ")][SerializeField] private GameObject SlotPrefab; [SerializeField] private RectTransform slotsRoot; [SerializeField] private int slotsCount = 10; [SerializeField] private float rootPaddingX = 10; [SerializeField] private float rootPaddingY = 10; [SerializeField] private float gapSize = 10; [Header("Размер слота")] [Tooltip("Предпочитаемый размер стороны квадрата слота. Если слотов много, размер уменьшится, чтобы вместиться.")] [SerializeField] private float preferredSlotWidth = 250f; [SerializeField] private bool centerWhenSmaller = true; // Высота контейнера всегда рассчитывается по числу строк (rows) [Header("Настройки анимации")] [SerializeField] private float animationSpeed; [SerializeField] private float fontSizeNormal = 20; [SerializeField] private float fontSizeHighlighted = 25; [SerializeField] private Color fontColorNormal; [SerializeField] private Color fontColorHighlighted; [SerializeField] private float slotSizeMultiplier = 1.1f; [SerializeField] private Color slotColorNormal; [SerializeField] private Color slotColorHighlighted; private Vector2 slotSizeNormal; //расчет при генерации // Сохраняем дефолтный размер контейнера, чтобы все перерасчеты основывались на исходном размере private Vector2 defaultSlotsRootSize = Vector2.zero; [Header("Алфавит")] [SerializeField] private string alphabetString = "0123456789"; //из него генерирует массив символов [Header("Скорость переключения букв")] [SerializeField] private float MaxSpeedLetterPerSecond = 8; [SerializeField] private float MinSpeedLetterPerSecond = 3; [SerializeField] private float MaxSpeedTime = 3; [SerializeField] private float MinSpeedTime = 0.5f; private float switchButtonPressedTime = 0; private float letterSwitchSpeed = 0; private float switchDelayRest = 1; public override bool UsesContinuousInput => true; //тут массивы для доступа private string[] alphabet; // не забыь добавить пустую строку при генерации (пробел лучше) private RectTransform[] slotRects; private Graphic[] slotGraphics; private TextMeshProUGUI[] slotLabels; private Coroutine animationCoroutine; private int currentSlotIndex = 0; private int currentLetterindex = 0; private TextMeshProUGUI currentLabel => slotLabels[currentSlotIndex]; [System.Serializable] public class TextInputPreset { public string key; public int slotsCount = 10; public string alphabetString = "0123456789"; public string title = "Введите номер"; public string description = "Введите номер"; } public static event Action OnTextChanged; public override void Initialize(int prisetIndex) { if (prisetIndex < 0 ||prisetIndex >= presets.Length) { Debug.Log("Неверный индекс пресета"); return; } this.key = presets[prisetIndex].key; this.slotsCount = presets[prisetIndex].slotsCount; this.alphabetString = presets[prisetIndex].alphabetString; this.title.text = presets[prisetIndex].title; this.description.text = presets[prisetIndex].description; // Сохраняем дефолтный размер slotsRoot при первом открытии if (slotsRoot != null && defaultSlotsRootSize == Vector2.zero) { defaultSlotsRootSize = slotsRoot.rect.size; } Generate(); LoadSavedData(); if (animationCoroutine != null) StopCoroutine(animationCoroutine); animationCoroutine = StartCoroutine(SlotsAnimation()); } public override void Accept() { string result = GetText(); if (!string.IsNullOrEmpty(key) && saveDataOnClose) { PlayerPrefs.SetString(key, result); } StopCoroutine(animationCoroutine); Clear(); OnTextChanged?.Invoke(); } private void Generate() { if (slotsRoot == null || SlotPrefab == null) { Debug.LogError("TextInput.Generate: missing slotsRoot or SlotPrefab"); return; } slotsCount = Mathf.Max(1, slotsCount); ClearSlotsRootChildren(); // Базовый размер контейнера (из дефолта, если он сохранён) Vector2 baseRootSize = defaultSlotsRootSize == Vector2.zero ? slotsRoot.rect.size : defaultSlotsRootSize; Vector2 contentArea = baseRootSize - new Vector2(rootPaddingX * 2f, rootPaddingY * 2f); float preferred = preferredSlotWidth > 0f ? preferredSlotWidth : 250f; // Сначала рассчитываем размер стороны квадрата при равномерном распределении float totalGaps = Mathf.Max(0, slotsCount - 1) * gapSize; float availablePerSlotWidth = Mathf.Max(0f, (contentArea.x - totalGaps) / slotsCount); float slotSide = Mathf.Max(1f, Mathf.Min(preferred, availablePerSlotWidth, contentArea.y)); slotSizeNormal = new Vector2(slotSide, slotSide); // Считаем колонки/строки (wrap) на основе полученного slotSide int columns = Mathf.Max(1, Mathf.FloorToInt((contentArea.x + gapSize) / (slotSide + gapSize))); columns = Mathf.Clamp(columns, 1, slotsCount); int rows = Mathf.CeilToInt((float)slotsCount / columns); float contentWidth = columns * slotSide + (columns - 1) * gapSize; float contentHeight = rows * slotSide + (rows - 1) * gapSize; float newRootW = contentWidth + rootPaddingX * 2f; float newRootH = contentHeight + rootPaddingY * 2f; slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, newRootW); slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, newRootH); var rootLayout = GetOrAdd(slotsRoot.gameObject); SetupLayoutElement(rootLayout, newRootW, newRootH, false); Canvas.ForceUpdateCanvases(); UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(slotsRoot); Vector2 start = new Vector2(rootPaddingX, rootPaddingY); if (centerWhenSmaller && contentWidth < (slotsRoot.rect.width - rootPaddingX * 2f)) start.x += ((slotsRoot.rect.width - rootPaddingX * 2f) - contentWidth) * 0.5f; slotRects = new RectTransform[slotsCount]; slotGraphics = new Graphic[slotsCount]; slotLabels = new TextMeshProUGUI[slotsCount]; for (int i = 0; i < slotsCount; i++) { int col = i % columns; int row = i / columns; Vector2 pos = new Vector2(start.x + col * (slotSide + gapSize), -(start.y + row * (slotSide + gapSize))); CreateSlot(i, pos, slotSizeNormal); } // Алфавит alphabet = new string[alphabetString.Length + 1]; for (int i = 0; i < alphabetString.Length; i++) alphabet[i] = alphabetString[i].ToString(); alphabet[alphabet.Length - 1] = " "; currentSlotIndex = 0; } // --- вспомогательные методы --- private void ClearSlotsRootChildren() { for (int i = slotsRoot.childCount - 1; i >= 0; i--) { var c = slotsRoot.GetChild(i); if (Application.isPlaying) Destroy(c.gameObject); else DestroyImmediate(c.gameObject); } } private T GetOrAdd(GameObject go) where T : Component { var comp = go.GetComponent(); if (comp == null) comp = go.AddComponent(); return comp; } private void SetupLayoutElement(LayoutElement l, float w, float h, bool ignoreLayout) { if (l == null) return; l.preferredWidth = w; l.minWidth = w; l.preferredHeight = h; l.minHeight = h; l.flexibleWidth = 0f; l.flexibleHeight = 0f; l.ignoreLayout = ignoreLayout; } private void CreateSlot(int index, Vector2 anchoredPos, Vector2 size) { GameObject slot = Instantiate(SlotPrefab, slotsRoot); var rt = slot.GetComponent(); slotRects[index] = rt; slotGraphics[index] = slot.GetComponent(); slotLabels[index] = slot.transform.GetComponentInChildren(); rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f); rt.pivot = new Vector2(0f, 1f); rt.localScale = Vector3.one; rt.sizeDelta = size; rt.anchoredPosition = anchoredPos; var le = GetOrAdd(slot); SetupLayoutElement(le, size.x, size.y, true); if (slotGraphics[index] != null) slotGraphics[index].color = slotColorNormal; if (slotLabels[index] != null) { slotLabels[index].color = fontColorNormal; slotLabels[index].text = ""; slotLabels[index].fontSize = fontSizeNormal; } } private void LoadSavedData() { string data = ""; if (loadData) data = PlayerPrefs.GetString(key, ""); // Очищаем все слоты for (int i = 0; i < slotsCount; i++) { slotLabels[i].text = " "; } // Заполняем сохраненными данными if (!string.IsNullOrEmpty(data)) { int minLength = Mathf.Min(data.Length, slotsCount); for (int i = 0; i < minLength; i++) { slotLabels[i].text = data[i].ToString(); } } } public override void SwitchVertical(int continiousDirection) { continiousDirection = Mathf.Clamp(continiousDirection, -1, 1); if (continiousDirection == 0)//сброс переключения { switchDelayRest = 0; switchButtonPressedTime = 0; return; } Debug.Log("Залет"); if (switchDelayRest < 0) { //переключение currentLetterindex = (currentLetterindex + continiousDirection + alphabet.Length) % alphabet.Length; currentLabel.text = alphabet[currentLetterindex]; recalculateRest(); Debug.Log($"Переключение остаток: {switchDelayRest}"); } switchDelayRest -= Time.deltaTime; switchButtonPressedTime += Time.deltaTime; void recalculateRest() { float k = (MaxSpeedLetterPerSecond - MinSpeedLetterPerSecond) / (MaxSpeedTime - MinSpeedTime); float b = MinSpeedLetterPerSecond - k * MinSpeedTime; letterSwitchSpeed = k * switchButtonPressedTime + b; letterSwitchSpeed = Mathf.Clamp(letterSwitchSpeed, MinSpeedLetterPerSecond, MaxSpeedLetterPerSecond); switchDelayRest = 1 / letterSwitchSpeed; } } public override void SwitchHorizontal(int discreteDirection) { if (discreteDirection == 0) return; currentSlotIndex = (currentSlotIndex + discreteDirection + slotsCount) % slotsCount; string letter = currentLabel.text; currentLetterindex = Array.IndexOf(alphabet, letter); } public void Clear() { foreach (RectTransform rect in slotRects) { Destroy(rect.gameObject); } } public string GetText() { System.Text.StringBuilder sb = new(); foreach (TextMeshProUGUI label in slotLabels) { // Если текст пустой строка, считаем это пробелом if (!string.IsNullOrEmpty(label.text)) { sb.Append(label.text); } } return sb.ToString().Trim(); } IEnumerator SlotsAnimation() { float scaleSpeed = (slotSizeMultiplier - 1) / animationSpeed; float slotColorSpeed = Vector4.Distance(slotColorNormal, slotColorHighlighted) / animationSpeed; float fontColorSpeed = Vector4.Distance(fontColorNormal, fontColorHighlighted) / animationSpeed; float fontSizeSpeed = (fontSizeHighlighted - fontSizeNormal) / animationSpeed; while (true) { for (int i = 0; i < slotsCount; i++) { if (i == currentSlotIndex) //подсвечиваем { slotRects[i].localScale = Vector3.MoveTowards(slotRects[i].localScale, Vector3.one * slotSizeMultiplier, scaleSpeed); slotGraphics[i].color = Vector4.MoveTowards(slotGraphics[i].color, slotColorHighlighted, slotColorSpeed); slotLabels[i].color = Vector4.MoveTowards(slotLabels[i].color, fontColorHighlighted, fontColorSpeed); slotLabels[i].fontSize = Mathf.MoveTowards(slotLabels[i].fontSize, fontSizeHighlighted, fontSizeSpeed); } else //убираем подсветку { slotRects[i].localScale = Vector3.MoveTowards(slotRects[i].localScale, Vector3.one, scaleSpeed); slotGraphics[i].color = Vector4.MoveTowards(slotGraphics[i].color, slotColorNormal, slotColorSpeed); slotLabels[i].color = Vector4.MoveTowards(slotLabels[i].color, fontColorNormal, fontColorSpeed); slotLabels[i].fontSize = Mathf.MoveTowards(slotLabels[i].fontSize, fontSizeNormal, fontSizeSpeed); } } yield return null; } } // Восстановить slotsRoot к сохранённому дефолтному размеру public void ResetSlotsRootToDefault() { if (slotsRoot == null) return; if (defaultSlotsRootSize == Vector2.zero) return; slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, defaultSlotsRootSize.x); slotsRoot.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, defaultSlotsRootSize.y); var rootLayout = slotsRoot.GetComponent(); if (rootLayout != null) { rootLayout.preferredWidth = defaultSlotsRootSize.x; rootLayout.preferredHeight = defaultSlotsRootSize.y; rootLayout.minWidth = defaultSlotsRootSize.x; rootLayout.minHeight = defaultSlotsRootSize.y; } Canvas.ForceUpdateCanvases(); UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(slotsRoot); } }