422 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 int panelId; // К какому panelId привязан этот пресет
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 panelId)
{
// Ищем пресет по panelId, а не по индексу массива
TextInputPreset preset = null;
foreach (var p in presets)
{
if (p.panelId == panelId) { preset = p; break; }
}
if (preset == null)
{
Debug.LogWarning($"TextInput: пресет для panelId={panelId} не найден");
return;
}
this.key = preset.key;
this.slotsCount = preset.slotsCount;
this.alphabetString = preset.alphabetString;
this.title.text = preset.title;
this.description.text = preset.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()
{
// Если Initialize не выполнился — слоты не созданы, просто выходим
if (slotRects == null) return;
string result = GetText();
if (!string.IsNullOrEmpty(key) && saveDataOnClose)
PlayerPrefs.SetString(key, result);
if (animationCoroutine != null)
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<LayoutElement>(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<T>(GameObject go) where T : Component
{
var comp = go.GetComponent<T>();
if (comp == null) comp = go.AddComponent<T>();
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<RectTransform>();
slotRects[index] = rt;
slotGraphics[index] = slot.GetComponent<Graphic>();
slotLabels[index] = slot.transform.GetComponentInChildren<TextMeshProUGUI>();
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<LayoutElement>(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()
{
if (slotRects == null) return;
foreach (RectTransform rect in slotRects)
{
if (rect != null)
Destroy(rect.gameObject);
}
slotRects = null;
slotGraphics = null;
slotLabels = null;
}
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<LayoutElement>();
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);
}
}