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;
}
}
}