feat: add basic components

- add text, box, and single-line input components

- support input change, submit, and cancel callbacks

- update example to compose the page from library components
This commit is contained in:
chuan
2026-06-03 22:46:18 +08:00
parent 9ec681b724
commit e2311f6e99
4 changed files with 213 additions and 76 deletions
+48 -76
View File
@@ -14,16 +14,41 @@ var textMeasurer = new TerminalTextMeasurer();
var renderer = new DifferentialRenderer(terminalOutput, textMeasurer);
using var runtime = new TuiRuntime(terminalInput, parser, renderer);
var component = new EchoComponent(runtime, textMeasurer, renderer);
runtime.Add(component);
runtime.SetFocus(component);
var done = new ManualResetEventSlim();
var eventsText = new Text("最近事件:\n- 无");
var statusText = new Text();
var input = new TinyTUI.Components.Input(textMeasurer) { Prompt = "请输入: " };
input.OnChanged = value => UpdateStatus(statusText, value, textMeasurer, renderer);
input.OnSubmitted = value =>
{
AddEvent(eventsText, $"Submitted: {value}");
input.Clear();
};
input.OnCanceled = () => done.Set();
UpdateStatus(statusText, input.Value, textMeasurer, renderer);
var page = new Container();
page.Add(new Text("TinyTUI 基础组件示例"));
page.Add(new Text("======================"));
page.Add(new Text("测试方式: 输入文本 Backspace 删除 Enter 提交 Esc 退出"));
page.Add(new Text(string.Empty));
page.Add(new Box(statusText, textMeasurer) { Title = "状态" });
page.Add(new Text(string.Empty));
page.Add(new Box(input, textMeasurer) { Title = "Input" });
page.Add(new Text(string.Empty));
page.Add(new Box(eventsText, textMeasurer) { Title = "事件" });
runtime.Add(page);
runtime.SetFocus(input);
terminalOutput.HideCursor();
try
{
runtime.Start();
component.WaitForExit();
done.Wait();
}
finally
{
@@ -33,77 +58,24 @@ finally
terminalOutput.Flush();
}
/// <summary>
/// 用于手动验证 Runtime 的示例组件
/// </summary>
file sealed class EchoComponent(ITuiRuntime runtime, ITextMeasurer textMeasurer, DifferentialRenderer renderer) : IInputComponent
static void UpdateStatus(
Text statusText,
string inputValue,
ITextMeasurer textMeasurer,
DifferentialRenderer renderer)
{
private readonly ManualResetEventSlim _done = new();
private readonly List<string> _events = [];
/// <inheritdoc />
public IReadOnlyList<string> Render(int width)
{
var lines = new List<string>
{
"TinyTUI Runtime 示例",
"====================",
$"当前渲染宽度: {width}",
$"全量重绘次数: {renderer.FullRedrawCount}",
$"差分重绘次数: {renderer.DifferentialRedrawCount}",
string.Empty,
"测试方式:",
"- 输入普通字符 验证 Text 事件会进入焦点组件",
"- 按方向键 Enter Backspace Tab Delete 验证 Key 事件会进入焦点组件",
"- 调整窗口大小 验证 Resize 事件会进入焦点组件并触发重渲染",
"- 按 Esc 退出",
string.Empty,
"宽度示例:",
$"字符串 \"abc\" 的终端宽度 = {textMeasurer.GetWidth("abc")}",
$"字符串 \"\" 的终端宽度 = {textMeasurer.GetWidth("")}",
$"字符串 \"😀\" 的终端宽度 = {textMeasurer.GetWidth("😀")}",
$"截断前 \"abc中文😀xyz\" 的终端宽度 = {textMeasurer.GetWidth("abc中文😀xyz")}",
$"截断到 8 列后的结果 = \"{textMeasurer.Truncate("abc中文😀xyz", 8)}\"",
string.Empty,
"最近事件:",
};
lines.AddRange(_events.TakeLast(10));
return lines;
}
/// <inheritdoc />
public void HandleInput(TuiInputEvent input)
{
if (input is { Kind: TuiInputEventKind.Key, Value: KeyNames.Escape })
{
_done.Set();
return;
}
_events.Add($"{input.Kind}: {FormatInput(input.Value)}");
runtime.RequestRender();
}
/// <summary>
/// 等待用户退出示例
/// </summary>
public void WaitForExit()
{
_done.Wait();
}
private static string FormatInput(string data)
{
return string.Join(' ', data.Select(static c => c switch
{
'\r' => "\\r",
'\n' => "\\n",
'\t' => "\\t",
'\b' => "\\b",
'\e' => "\\e",
_ when char.IsControl(c) => $"\\x{(int)c:x2}",
_ => c.ToString(),
}));
}
statusText.Value = string.Join(
'\n',
$"当前输入 = \"{inputValue}\"",
$"当前输入宽度 = {textMeasurer.GetWidth(inputValue)}",
$"字符串 \"abc中文😀xyz\" 宽度 = {textMeasurer.GetWidth("abc中文😀xyz")}",
$"截断到 8 列 = \"{textMeasurer.Truncate("abc中文😀xyz", 8)}\"",
$"全量重绘次数 = {renderer.FullRedrawCount}",
$"差分重绘次数 = {renderer.DifferentialRedrawCount}");
}
static void AddEvent(Text eventsText, string message)
{
var lines = eventsText.Value.Split('\n').Skip(1).Append($"- {message}").TakeLast(8);
eventsText.Value = "最近事件:\n" + string.Join('\n', lines);
}