using System.Text; using TMPro; using UnityEngine; using UnityEngine.UI; namespace AppUI.Localization { /// /// 按 UI 可用宽度在文案中插入零宽空格 \u200B,配合 TMP / Legacy Text 自动换行。 /// static class AppUILocalizationTextBreak { const char ZeroWidthSpace = '\u200B'; public static float ResolveMaxWidth(RectTransform rect, float maxWidth) { if (maxWidth > 0f) return maxWidth; if (rect == null) return 0f; float width = rect.rect.width; if (width > 0f) return width; return LayoutUtility.GetPreferredWidth(rect); } public static float ResolveMaxHeight(RectTransform rect, float maxHeight) { if (maxHeight > 0f) return maxHeight; if (rect == null) return 0f; float height = rect.rect.height; if (height > 0f) return height; return LayoutUtility.GetPreferredHeight(rect); } public static string InsertWidthBreaks(string text, TMP_Text tmpText, float maxWidth, float maxHeight = 0f) { if (string.IsNullOrEmpty(text) || tmpText == null) return text; maxWidth = ResolveMaxWidth(tmpText.rectTransform, maxWidth); if (maxWidth <= 0f) return text; if (FitsSingleLine(text, tmpText, maxWidth)) return text; var output = new StringBuilder(text.Length + 16); InsertWidthBreaksInto(text, output, maxWidth, line => tmpText.GetPreferredValues(line, 0, 0).x); string result = output.ToString(); maxHeight = ResolveMaxHeight(tmpText.rectTransform, maxHeight); if (maxHeight > 0f && tmpText.GetPreferredValues(result, maxWidth, 0).y > maxHeight) return result; return result; } public static string InsertWidthBreaks(string text, Text legacyText, float maxWidth, float maxHeight = 0f) { if (string.IsNullOrEmpty(text) || legacyText == null) return text; maxWidth = ResolveMaxWidth(legacyText.rectTransform, maxWidth); if (maxWidth <= 0f) return text; float Measure(string line) { var settings = legacyText.GetGenerationSettings(new Vector2(maxWidth, 0f)); settings.generateOutOfBounds = true; return new TextGenerator().GetPreferredWidth(line, settings) / legacyText.pixelsPerUnit; } if (Measure(text) <= maxWidth) return text; var output = new StringBuilder(text.Length + 16); InsertWidthBreaksInto(text, output, maxWidth, Measure); string result = output.ToString(); maxHeight = ResolveMaxHeight(legacyText.rectTransform, maxHeight); if (maxHeight > 0f) { var settings = legacyText.GetGenerationSettings(new Vector2(maxWidth, maxHeight)); settings.generateOutOfBounds = true; float height = new TextGenerator().GetPreferredHeight(result, settings) / legacyText.pixelsPerUnit; if (height > maxHeight) return result; } return result; } static bool FitsSingleLine(string text, TMP_Text tmpText, float maxWidth) { if (text.IndexOf('\n') >= 0) return false; Vector2 preferred = tmpText.GetPreferredValues(text, maxWidth, 0); if (preferred.x <= maxWidth) return true; return tmpText.GetPreferredValues(text, 0, 0).x <= maxWidth; } static void InsertWidthBreaksInto(string source, StringBuilder output, float maxWidth, System.Func measureWidth) { for (int i = 0; i < source.Length; i++) { char c = source[i]; if (c == '<') { int end = source.IndexOf('>', i); if (end > i) { output.Append(source, i, end - i + 1); i = end; continue; } } if (c == '\n') { output.Append(c); continue; } output.Append(c); while (GetVisibleLineLength(output) > 1 && measureWidth(GetCurrentLine(output)) > maxWidth) output.Insert(FindLastVisibleIndex(output), ZeroWidthSpace); } } static int FindLineStart(StringBuilder sb) { for (int i = sb.Length - 1; i >= 0; i--) { if (sb[i] == ZeroWidthSpace || sb[i] == '\n') return i + 1; } return 0; } static string GetCurrentLine(StringBuilder sb) { int start = FindLineStart(sb); return sb.ToString(start, sb.Length - start); } static int GetVisibleLineLength(StringBuilder sb) { string line = GetCurrentLine(sb); int count = 0; for (int i = 0; i < line.Length; i++) { if (line[i] == '<') { int end = line.IndexOf('>', i); if (end > i) { i = end; continue; } } count++; } return count; } static int FindLastVisibleIndex(StringBuilder sb) { int lineStart = FindLineStart(sb); for (int i = sb.Length - 1; i >= lineStart; i--) { if (sb[i] == ZeroWidthSpace || sb[i] == '\n') continue; if (sb[i] == '>') { int open = i; while (open >= lineStart && sb[open] != '<') open--; if (open >= lineStart) { i = open; continue; } } if (sb[i] == '<') continue; return i; } return sb.Length - 1; } } }