feat: add advanced components
- add editor select list markdown and loader components - update example to preview advanced components and overlay interactions
This commit is contained in:
+158
-31
@@ -18,19 +18,42 @@ using var runtime = new TuiRuntime(terminalInput, parser, renderer);
|
||||
var done = new ManualResetEventSlim();
|
||||
var eventsText = new Text("最近事件:\n- 无");
|
||||
var statusText = new Text();
|
||||
var loader = new Loader(textMeasurer) { Message = "运行中" };
|
||||
var selectPreview = new SelectList(textMeasurer) { Height = 4 };
|
||||
var markdownPreview = new Markdown(
|
||||
"# Markdown 预览\n" +
|
||||
"## 二级标题会转成大写\n" +
|
||||
"- 列表项会转成星号前缀\n" +
|
||||
"> 引用会转成竖线前缀\n" +
|
||||
"支持 **粗体标记清理** 和 `行内代码标记清理`");
|
||||
using var loaderCancellation = new CancellationTokenSource();
|
||||
var input = new TinyTUI.Components.Input(textMeasurer) { Prompt = "请输入: " };
|
||||
|
||||
selectPreview.SetItems(["Text", "Input", "Editor", "SelectList", "Markdown", "Loader", "Overlay"]);
|
||||
|
||||
input.OnChanged = value => UpdateStatus(statusText, value, textMeasurer, renderer);
|
||||
input.OnSubmitted = value =>
|
||||
{
|
||||
if (value.Trim() == "/help")
|
||||
switch (value.Trim())
|
||||
{
|
||||
AddEvent(eventsText, "Overlay: help opened");
|
||||
runtime.ShowOverlay(new HelpOverlay(runtime, textMeasurer));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddEvent(eventsText, $"Submitted: {value}");
|
||||
case "/help":
|
||||
AddEvent(eventsText, "Overlay: help opened");
|
||||
runtime.ShowOverlay(new HelpOverlay(runtime, textMeasurer));
|
||||
break;
|
||||
case "/select":
|
||||
AddEvent(eventsText, "Overlay: select opened");
|
||||
runtime.ShowOverlay(new SelectOverlay(runtime, message => AddEvent(eventsText, message), textMeasurer));
|
||||
break;
|
||||
case "/edit":
|
||||
AddEvent(eventsText, "Overlay: editor opened");
|
||||
runtime.ShowOverlay(new EditorOverlay(runtime, message => AddEvent(eventsText, message), textMeasurer));
|
||||
break;
|
||||
case "":
|
||||
AddEvent(eventsText, "Submitted empty input");
|
||||
break;
|
||||
default:
|
||||
AddEvent(eventsText, $"Submitted: {value}");
|
||||
break;
|
||||
}
|
||||
|
||||
input.Clear();
|
||||
@@ -40,20 +63,37 @@ 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 提交 /help 打开弹层 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 = "事件" });
|
||||
AddAll(
|
||||
page,
|
||||
new Text("TinyTUI 基础组件示例"),
|
||||
new Text("======================"),
|
||||
new Markdown(
|
||||
"# 操作说明\n" +
|
||||
"- Markdown Loader SelectList 预览会在页面启动时直接显示\n" +
|
||||
"- 输入文本后 Enter 提交\n" +
|
||||
"- /help 打开帮助弹层\n" +
|
||||
"- /select 打开可交互选择列表\n" +
|
||||
"- /edit 打开需要焦点的多行编辑器\n" +
|
||||
"- Esc 退出"),
|
||||
new Text(),
|
||||
new Box(statusText, textMeasurer) { Title = "状态" },
|
||||
new Text(),
|
||||
new Box(markdownPreview, textMeasurer) { Title = "Markdown 预览" },
|
||||
new Text(),
|
||||
new Box(loader, textMeasurer) { Title = "Loader" },
|
||||
new Text(),
|
||||
new Box(selectPreview, textMeasurer) { Title = "SelectList 预览" },
|
||||
new Text(),
|
||||
new Box(input, textMeasurer) { Title = "Input" },
|
||||
new Text(),
|
||||
new Box(eventsText, textMeasurer) { Title = "事件" });
|
||||
|
||||
runtime.Add(page);
|
||||
runtime.SetFocus(input);
|
||||
terminalOutput.ShowCursor();
|
||||
|
||||
var loaderTask = RunLoaderAsync(loader, runtime, loaderCancellation.Token);
|
||||
|
||||
try
|
||||
{
|
||||
runtime.Start();
|
||||
@@ -61,12 +101,35 @@ try
|
||||
}
|
||||
finally
|
||||
{
|
||||
StopLoader(loaderCancellation, loaderTask);
|
||||
runtime.Stop();
|
||||
terminalOutput.ShowCursor();
|
||||
terminalOutput.Write("\r\n");
|
||||
terminalOutput.Flush();
|
||||
}
|
||||
|
||||
static async Task RunLoaderAsync(Loader loader, ITuiRuntime runtime, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(180, cancellationToken).ConfigureAwait(false);
|
||||
loader.Tick();
|
||||
runtime.RequestRender();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
static void StopLoader(CancellationTokenSource cancellation, Task loaderTask)
|
||||
{
|
||||
cancellation.Cancel();
|
||||
loaderTask.Wait(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
static void UpdateStatus(
|
||||
Text statusText,
|
||||
string inputValue,
|
||||
@@ -89,19 +152,21 @@ static void AddEvent(Text eventsText, string message)
|
||||
eventsText.Value = "最近事件:\n" + string.Join('\n', lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于手动验证 overlay 的帮助弹层
|
||||
/// </summary>
|
||||
static void AddAll(Container container, params IComponent[] components)
|
||||
{
|
||||
foreach (var component in components) container.Add(component);
|
||||
}
|
||||
|
||||
file sealed class HelpOverlay(ITuiRuntime runtime, ITextMeasurer textMeasurer) : IInputComponent
|
||||
{
|
||||
private readonly Box _box = new(
|
||||
new Text(
|
||||
"Overlay 示例\n" +
|
||||
new Markdown(
|
||||
"# Overlay 示例\n" +
|
||||
"\n" +
|
||||
"这个弹层由 Runtime 显示\n" +
|
||||
"OverlayManager 会把它合成到基础页面上\n" +
|
||||
"显示时焦点会切到弹层\n" +
|
||||
"关闭后焦点会恢复到输入框\n" +
|
||||
"- 这个弹层由 Runtime 显示\n" +
|
||||
"- OverlayManager 会把它合成到基础页面上\n" +
|
||||
"- 显示时焦点会切到弹层\n" +
|
||||
"- 关闭后焦点会恢复到输入框\n" +
|
||||
"\n" +
|
||||
"按 Enter 或 Esc 关闭"),
|
||||
textMeasurer)
|
||||
@@ -109,13 +174,8 @@ file sealed class HelpOverlay(ITuiRuntime runtime, ITextMeasurer textMeasurer) :
|
||||
Title = "Help",
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Render(int width)
|
||||
{
|
||||
return _box.Render(width);
|
||||
}
|
||||
public IReadOnlyList<string> Render(int width) => _box.Render(width);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void HandleInput(TuiInputEvent input)
|
||||
{
|
||||
if (input is { Kind: TuiInputEventKind.Key, Value: KeyNames.Enter or KeyNames.Escape })
|
||||
@@ -124,3 +184,70 @@ file sealed class HelpOverlay(ITuiRuntime runtime, ITextMeasurer textMeasurer) :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class SelectOverlay : IInputComponent
|
||||
{
|
||||
private readonly ITuiRuntime _runtime;
|
||||
private readonly Action<string> _addEvent;
|
||||
private readonly SelectList _selectList;
|
||||
private readonly Box _box;
|
||||
|
||||
public SelectOverlay(ITuiRuntime runtime, Action<string> addEvent, ITextMeasurer textMeasurer)
|
||||
{
|
||||
_runtime = runtime;
|
||||
_addEvent = addEvent;
|
||||
_selectList = new SelectList(textMeasurer) { Height = 5 };
|
||||
_selectList.SetItems(["Text", "Input", "Editor", "SelectList", "Markdown", "Loader", "Overlay"]);
|
||||
_selectList.OnSubmitted = (item, _) =>
|
||||
{
|
||||
_addEvent($"Selected: {item}");
|
||||
_runtime.HideOverlay();
|
||||
};
|
||||
_selectList.OnCanceled = _runtime.HideOverlay;
|
||||
_box = new Box(_selectList, textMeasurer) { Title = "SelectList" };
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> Render(int width) => _box.Render(width);
|
||||
|
||||
public void HandleInput(TuiInputEvent input) => _selectList.HandleInput(input);
|
||||
}
|
||||
|
||||
file sealed class EditorOverlay : IInputComponent
|
||||
{
|
||||
private readonly ITuiRuntime _runtime;
|
||||
private readonly Action<string> _addEvent;
|
||||
private readonly Editor _editor;
|
||||
private readonly Box _box;
|
||||
|
||||
public EditorOverlay(ITuiRuntime runtime, Action<string> addEvent, ITextMeasurer textMeasurer)
|
||||
{
|
||||
_runtime = runtime;
|
||||
_addEvent = addEvent;
|
||||
_editor = new Editor(textMeasurer)
|
||||
{
|
||||
Height = 6,
|
||||
Value = "这里可以输入多行文本\nEnter 换行 Esc 提交并关闭",
|
||||
OnCanceled = SubmitAndClose,
|
||||
};
|
||||
_box = new Box(_editor, textMeasurer) { Title = "Editor" };
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> Render(int width) => _box.Render(width);
|
||||
|
||||
public void HandleInput(TuiInputEvent input)
|
||||
{
|
||||
if (input is { Kind: TuiInputEventKind.Key, Value: KeyNames.Escape })
|
||||
{
|
||||
SubmitAndClose();
|
||||
return;
|
||||
}
|
||||
|
||||
_editor.HandleInput(input);
|
||||
}
|
||||
|
||||
private void SubmitAndClose()
|
||||
{
|
||||
_addEvent($"Edited {Math.Max(1, _editor.Value.Split('\n').Length)} line(s)");
|
||||
_runtime.HideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user