Files
ttui/src/TinyTUI/Text/ITextMeasurer.cs
T
chuan 0be34a9c16 feat: improve ansi text utilities
- measure text with grapheme-aware terminal widths

- add ansi-aware truncate slice and wrap checks
2026-06-04 02:10:46 +08:00

90 lines
2.8 KiB
C#

namespace TinyTUI.Text;
/// <summary>
/// 按终端字符单元宽度规则测量和裁剪字符串
/// </summary>
public interface ITextMeasurer
{
/// <summary>
/// 获取字符串占用的终端字符单元数量
/// </summary>
int GetWidth(string value);
/// <summary>
/// 裁剪字符串使其渲染宽度不超过给定字符单元宽度
/// </summary>
string Truncate(string value, int maxWidth);
/// <summary>
/// 裁剪字符串并在确实发生裁剪时追加省略符
/// </summary>
string Truncate(string value, int maxWidth, string ellipsis, bool pad = false)
{
if (maxWidth <= 0)
return string.Empty;
var width = GetWidth(value);
if (width <= maxWidth)
return pad ? value + new string(' ', maxWidth - width) : value;
var ellipsisWidth = GetWidth(ellipsis);
if (ellipsisWidth >= maxWidth)
{
var ellipsisSlice = Slice(ellipsis, 0, maxWidth, strict: true);
var clipped = ellipsisSlice.Text.Length == 0 ? string.Empty : $"\e[0m{ellipsisSlice.Text}\e[0m";
return pad ? clipped + new string(' ', Math.Max(0, maxWidth - ellipsisSlice.Width)) : clipped;
}
var prefix = Slice(value, 0, maxWidth - ellipsisWidth, strict: true);
var result = ellipsis.Length == 0
? $"{prefix.Text}\e[0m"
: $"{prefix.Text}\e[0m{ellipsis}\e[0m";
return pad ? result + new string(' ', Math.Max(0, maxWidth - prefix.Width - ellipsisWidth)) : result;
}
/// <summary>
/// 按可见列切出字符串片段并返回片段实际宽度
/// </summary>
TextSlice Slice(string value, int startColumn, int maxWidth, bool strict = true)
{
if (startColumn <= 0)
{
var text = Truncate(value, maxWidth);
return new TextSlice(text, GetWidth(text));
}
var prefix = Truncate(value, startColumn);
var remainder = value[prefix.Length..];
var sliced = Truncate(remainder, maxWidth);
return new TextSlice(sliced, GetWidth(sliced));
}
/// <summary>
/// 按可见宽度换行并尽量保留终端样式状态
/// </summary>
IReadOnlyList<string> Wrap(string value, int width)
{
if (value.Length == 0)
return [string.Empty];
if (width <= 0)
return value.Split('\n').Select(_ => string.Empty).ToArray();
var lines = new List<string>();
foreach (var line in value.Split('\n'))
{
if (GetWidth(line) <= width)
{
lines.Add(line);
continue;
}
for (var column = 0; column < GetWidth(line); column += width)
lines.Add(Slice(line, column, width, strict: true).Text);
}
return lines.Count == 0 ? [string.Empty] : lines;
}
}