feat: add tui runtime
- wire terminal input parsing to focused components - render the root component tree through the renderer - update example to exercise runtime dispatch
This commit is contained in:
+97
-63
@@ -1,90 +1,124 @@
|
||||
using TinyTUI;
|
||||
using TinyTUI.Components;
|
||||
using TinyTUI.Input;
|
||||
using TinyTUI.Rendering;
|
||||
using TinyTUI.Runtime;
|
||||
using TinyTUI.Stdio;
|
||||
using TinyTUI.Stdout;
|
||||
|
||||
using var input = new ConsoleTerminalInput();
|
||||
var output = new ConsoleTerminalOutput();
|
||||
using var terminalInput = new ConsoleTerminalInput();
|
||||
var terminalOutput = new ConsoleTerminalOutput();
|
||||
var parser = new DefaultInputParser();
|
||||
var done = new ManualResetEventSlim();
|
||||
var latestSize = input.CurrentSize;
|
||||
var renderer = new DemoRenderer(terminalOutput);
|
||||
using var runtime = new TuiRuntime(terminalInput, parser, renderer);
|
||||
|
||||
output.ClearScreen();
|
||||
output.HideCursor();
|
||||
Render(output, latestSize, "等待输入", []);
|
||||
var component = new EchoComponent(runtime);
|
||||
runtime.Add(component);
|
||||
runtime.SetFocus(component);
|
||||
|
||||
input.DataReceived += (_, data) =>
|
||||
{
|
||||
var events = parser.Parse(data);
|
||||
if (events.Any(static e => e is { Kind: TuiInputEventKind.Key, Value: KeyNames.Escape }))
|
||||
{
|
||||
done.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
Render(output, latestSize, $"原始输入: {FormatInput(data)}", events);
|
||||
};
|
||||
|
||||
input.Resized += (_, size) =>
|
||||
{
|
||||
latestSize = size;
|
||||
Render(output, latestSize, "窗口尺寸已变化", [new TuiInputEvent(TuiInputEventKind.Resize, $"{size.Columns}x{size.Rows}")]);
|
||||
};
|
||||
terminalOutput.HideCursor();
|
||||
|
||||
try
|
||||
{
|
||||
input.Start();
|
||||
done.Wait();
|
||||
runtime.Start();
|
||||
component.WaitForExit();
|
||||
}
|
||||
finally
|
||||
{
|
||||
input.Stop();
|
||||
output.ShowCursor();
|
||||
output.Write("\r\n");
|
||||
output.Flush();
|
||||
runtime.Stop();
|
||||
terminalOutput.ShowCursor();
|
||||
terminalOutput.Write("\r\n");
|
||||
terminalOutput.Flush();
|
||||
}
|
||||
|
||||
static void Render(ConsoleTerminalOutput output, TerminalSize size, string message, IReadOnlyList<TuiInputEvent> events)
|
||||
/// <summary>
|
||||
/// 用于手动验证 Runtime 的示例组件
|
||||
/// </summary>
|
||||
file sealed class EchoComponent(ITuiRuntime runtime) : IInputComponent
|
||||
{
|
||||
output.ClearScreen();
|
||||
output.Write("TinyTUI Input 示例\r\n");
|
||||
output.Write("===================\r\n");
|
||||
output.Write($"尺寸: {size.Columns} x {size.Rows}\r\n");
|
||||
output.Write($"{message}\r\n");
|
||||
output.Write("\r\n");
|
||||
output.Write("解析事件:\r\n");
|
||||
private readonly ManualResetEventSlim _done = new();
|
||||
private readonly List<string> _events = [];
|
||||
|
||||
if (events.Count == 0)
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Render(int width)
|
||||
{
|
||||
output.Write("- 无\r\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var inputEvent in events)
|
||||
var lines = new List<string>
|
||||
{
|
||||
output.Write($"- {inputEvent.Kind}: {FormatInput(inputEvent.Value)}\r\n");
|
||||
}
|
||||
"TinyTUI Runtime 示例",
|
||||
"====================",
|
||||
"测试方式:",
|
||||
"- 输入普通字符 验证 Text 事件会进入焦点组件",
|
||||
"- 按方向键 Enter Backspace Tab Delete 验证 Key 事件会进入焦点组件",
|
||||
"- 调整窗口大小 验证 Resize 事件会进入焦点组件并触发重渲染",
|
||||
"- 按 Esc 退出",
|
||||
string.Empty,
|
||||
"最近事件:",
|
||||
};
|
||||
|
||||
lines.AddRange(_events.TakeLast(10));
|
||||
return lines;
|
||||
}
|
||||
|
||||
output.Write("\r\n");
|
||||
output.Write("测试方式:\r\n");
|
||||
output.Write("- 输入普通字符 应解析为 Text\r\n");
|
||||
output.Write("- 按方向键 Home End Delete F1-F12 应解析为 Key\r\n");
|
||||
output.Write("- 改变终端窗口大小 应解析为 Resize\r\n");
|
||||
output.Write("- 按 Esc 退出\r\n");
|
||||
output.Flush();
|
||||
/// <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(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
static string FormatInput(string data)
|
||||
/// <summary>
|
||||
/// 用于手动验证 Runtime 的临时全量渲染器
|
||||
/// </summary>
|
||||
file sealed class DemoRenderer(ConsoleTerminalOutput output) : IRenderer
|
||||
{
|
||||
return string.Join(' ', data.Select(static c => c switch
|
||||
/// <inheritdoc />
|
||||
public void Render(IReadOnlyList<string> lines, TerminalSize size)
|
||||
{
|
||||
'\r' => "\\r",
|
||||
'\n' => "\\n",
|
||||
'\t' => "\\t",
|
||||
'\b' => "\\b",
|
||||
'\e' => "\\e",
|
||||
_ when char.IsControl(c) => $"\\x{(int)c:x2}",
|
||||
_ => c.ToString(),
|
||||
}));
|
||||
output.ClearScreen();
|
||||
output.Write($"尺寸: {size.Columns} x {size.Rows}\r\n");
|
||||
output.Write("\r\n");
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
output.Write(line);
|
||||
output.Write("\r\n");
|
||||
}
|
||||
|
||||
output.Flush();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
output.ClearScreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using TinyTUI.Components;
|
||||
using TinyTUI.Input;
|
||||
using TinyTUI.Rendering;
|
||||
using TinyTUI.Stdio;
|
||||
|
||||
namespace TinyTUI.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 默认 TUI 运行时实现
|
||||
/// </summary>
|
||||
public sealed class TuiRuntime : ITuiRuntime
|
||||
{
|
||||
private readonly ITerminalInput _terminalInput;
|
||||
private readonly IInputParser _inputParser;
|
||||
private readonly IRenderer _renderer;
|
||||
private readonly Container _root = new();
|
||||
private readonly Lock _renderLock = new();
|
||||
|
||||
private IComponent? _focusedComponent;
|
||||
private bool _started;
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认 TUI 运行时
|
||||
/// </summary>
|
||||
public TuiRuntime(ITerminalInput terminalInput, IInputParser inputParser, IRenderer renderer)
|
||||
{
|
||||
_terminalInput = terminalInput;
|
||||
_inputParser = inputParser;
|
||||
_renderer = renderer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(IComponent component)
|
||||
{
|
||||
_root.Add(component);
|
||||
RequestRender();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove(IComponent component)
|
||||
{
|
||||
_root.Remove(component);
|
||||
|
||||
if (_focusedComponent == component)
|
||||
{
|
||||
_focusedComponent = null;
|
||||
}
|
||||
|
||||
RequestRender();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetFocus(IComponent? component)
|
||||
{
|
||||
_focusedComponent = component;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RequestRender()
|
||||
{
|
||||
if (!_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_renderLock)
|
||||
{
|
||||
var size = _terminalInput.CurrentSize;
|
||||
var lines = _root.Render(size.Columns);
|
||||
_renderer.Render(lines, size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Start()
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_terminalInput.DataReceived += OnDataReceived;
|
||||
_terminalInput.Resized += OnResized;
|
||||
_terminalInput.Start();
|
||||
_renderer.Reset();
|
||||
RequestRender();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Stop()
|
||||
{
|
||||
if (!_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = false;
|
||||
_terminalInput.DataReceived -= OnDataReceived;
|
||||
_terminalInput.Resized -= OnResized;
|
||||
_terminalInput.Stop();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_terminalInput.Dispose();
|
||||
}
|
||||
|
||||
private void OnDataReceived(object? sender, string data)
|
||||
{
|
||||
foreach (var inputEvent in _inputParser.Parse(data))
|
||||
{
|
||||
Dispatch(inputEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResized(object? sender, TerminalSize size)
|
||||
{
|
||||
Dispatch(new TuiInputEvent(TuiInputEventKind.Resize, $"{size.Columns}x{size.Rows}"));
|
||||
}
|
||||
|
||||
private void Dispatch(TuiInputEvent inputEvent)
|
||||
{
|
||||
if (_focusedComponent is IInputComponent inputComponent)
|
||||
{
|
||||
inputComponent.HandleInput(inputEvent);
|
||||
}
|
||||
|
||||
RequestRender();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user