Files
ttui/src/TinyTUI/Components/Editor/Editor.cs
T
chuan 31766b2a78 feat: improve editor layout flow
- split editor implementation by responsibility

- add editor submit events and soft wrap layout

- support modified enter input for editor submission
2026-06-04 01:24:22 +08:00

150 lines
5.2 KiB
C#

using TinyTUI.Input;
using TinyTUI.Text;
namespace TinyTUI.Components;
/// <summary>
/// 支持多行文本编辑的基础编辑器组件
/// </summary>
public sealed partial class Editor(ITextMeasurer? textMeasurer = null) : IInputComponent
{
private readonly ITextMeasurer _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
private readonly List<string> _lines = [string.Empty];
private readonly Stack<EditorSnapshot> _undoStack = [];
private readonly Stack<EditorSnapshot> _redoStack = [];
private readonly List<string> _history = [];
private int _cursorRow;
private int _cursorColumn;
private int _historyIndex = -1;
private string _historyDraft = string.Empty;
private int _lastRenderWidth = 80;
private int _scrollOffset;
private int? _preferredVisualColumn;
/// <summary>
/// 获取或设置编辑器高度
/// </summary>
public int Height { get; set; } = 5;
/// <summary>
/// 获取或设置是否按可用宽度软换行
/// </summary>
public bool SoftWrap { get; set; } = true;
/// <summary>
/// 获取或设置空编辑器的输入提示
/// </summary>
public string Placeholder { get; set; } = string.Empty;
/// <summary>
/// 获取当前编辑器文本
/// </summary>
public string Value
{
get => string.Join('\n', _lines);
set => SetValue(value);
}
/// <summary>
/// 在文本变化时触发
/// </summary>
public Action<string>? OnChanged { get; set; }
/// <summary>
/// 在用户取消编辑时触发
/// </summary>
public Action? OnCanceled { get; set; }
/// <summary>
/// 在用户提交编辑内容时触发
/// </summary>
public Action<string>? OnSubmitted { get; set; }
/// <inheritdoc />
public void HandleInput(TuiInputEvent input)
{
switch (input)
{
case { Kind: TuiInputEventKind.Text }:
Edit(() => InsertText(input.Value));
break;
case { Kind: TuiInputEventKind.Paste }:
Edit(() => InsertText(input.Value.Replace("\r\n", "\n")));
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl("z"):
Undo();
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl("y"):
Redo();
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl("c"):
Edit(ClearText);
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl(KeyNames.Left):
MoveWordLeft();
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl(KeyNames.Right):
MoveWordRight();
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl(KeyNames.Backspace):
Edit(DeleteWordBackward);
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl(KeyNames.Delete):
Edit(DeleteWordForward);
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Enter }:
Edit(SplitLine);
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl(KeyNames.Enter):
Submit();
break;
case { Kind: TuiInputEventKind.Key, Value: var key } when key == KeyNames.Ctrl("s"):
Submit();
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Backspace }:
Edit(Backspace);
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Delete }:
Edit(Delete);
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Left }:
MoveLeft();
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Right }:
MoveRight();
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Up }:
if (!TryNavigateHistory(-1)) MoveVertical(-1);
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Down }:
if (!TryNavigateHistory(1)) MoveVertical(1);
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Home }:
MoveToLineStart();
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.End }:
MoveToLineEnd();
break;
case { Kind: TuiInputEventKind.Key, Value: KeyNames.Escape }:
Cancel();
break;
}
}
/// <summary>
/// 提交当前编辑内容
/// </summary>
public void Submit() => OnSubmitted?.Invoke(Value);
/// <summary>
/// 取消当前编辑
/// </summary>
public void Cancel() => OnCanceled?.Invoke();
/// <summary>
/// 清空编辑器内容
/// </summary>
public void Clear() => SetValue(string.Empty);
}