Files
ttui/src/TinyTUI/Components/Editor/Editor.cs
T
chuan 83967fa691 feat: add component infrastructure
- add focusable runtime state and invalidate entry

- add theme primitives and reusable components

- add component checks for focus and cancellation
2026-06-04 02:17:07 +08:00

211 lines
5.5 KiB
C#

using TinyTUI.Input;
using TinyTUI.Text;
namespace TinyTUI.Components;
/// <summary>
/// 支持多行文本编辑的基础编辑器组件
/// </summary>
public sealed partial class Editor(ITextMeasurer? textMeasurer = null, KeybindingRegistry? keybindings = null) : IInputComponent, IFocusableComponent
{
private readonly ITextMeasurer _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
private readonly KeybindingRegistry _keybindings = keybindings ?? KeybindingRegistry.CreateDefault();
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;
/// <inheritdoc />
public bool Focused { get; set; }
/// <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 }:
HandleKey(input);
break;
}
}
/// <summary>
/// 提交当前编辑内容
/// </summary>
public void Submit() => OnSubmitted?.Invoke(Value);
/// <summary>
/// 取消当前编辑
/// </summary>
public void Cancel() => OnCanceled?.Invoke();
/// <summary>
/// 清空编辑器内容
/// </summary>
public void Clear() => SetValue(string.Empty);
/// <summary>
/// 根据动作映射处理多行编辑器快捷键
/// </summary>
private void HandleKey(TuiInputEvent input)
{
if (_keybindings.Matches(input, TuiKeybindings.EditorUndo))
{
Undo();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorRedo))
{
Redo();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorClear))
{
Edit(ClearText);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorWordLeft))
{
MoveWordLeft();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorWordRight))
{
MoveWordRight();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorDeleteWordBackward))
{
Edit(DeleteWordBackward);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorDeleteWordForward))
{
Edit(DeleteWordForward);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorSubmit))
{
Submit();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorNewLine))
{
Edit(SplitLine);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorDeleteCharBackward))
{
Edit(Backspace);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorDeleteCharForward))
{
Edit(Delete);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorLeft))
{
MoveLeft();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorRight))
{
MoveRight();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorUp))
{
if (!TryNavigateHistory(-1)) MoveVertical(-1);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorDown))
{
if (!TryNavigateHistory(1)) MoveVertical(1);
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorLineStart))
{
MoveToLineStart();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.EditorCursorLineEnd))
{
MoveToLineEnd();
return;
}
if (_keybindings.Matches(input, TuiKeybindings.InputCancel))
Cancel();
}
}