- add focusable runtime state and invalidate entry - add theme primitives and reusable components - add component checks for focus and cancellation
211 lines
5.5 KiB
C#
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();
|
|
}
|
|
}
|