- measure text with grapheme-aware terminal widths - add ansi-aware truncate slice and wrap checks
90 lines
2.8 KiB
C#
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;
|
|
}
|
|
}
|