- add component-level keybinding injection contract - inject runtime keybinding registry when components enter runtime or overlay - cover focused component behavior with shared runtime keybindings
129 lines
3.7 KiB
C#
129 lines
3.7 KiB
C#
using System.Text;
|
|
using TinyTUI.Input;
|
|
using TinyTUI.Rendering;
|
|
using TinyTUI.Text;
|
|
using TinyTUI.Theme;
|
|
|
|
namespace TinyTUI.Components;
|
|
|
|
/// <summary>
|
|
/// 支持文本输入 提交和取消的单行输入组件
|
|
/// </summary>
|
|
public sealed class Input(
|
|
ITextMeasurer? textMeasurer = null,
|
|
KeybindingRegistry? keybindings = null,
|
|
ITuiTheme? theme = null) : IInputComponent, IFocusableComponent, IKeybindingComponent
|
|
{
|
|
private readonly ITextMeasurer _textMeasurer = textMeasurer ?? new TerminalTextMeasurer();
|
|
private readonly ITuiTheme _theme = theme ?? TuiTheme.Default;
|
|
private KeybindingRegistry _keybindings = keybindings ?? KeybindingRegistry.CreateDefault();
|
|
|
|
/// <summary>
|
|
/// 获取或设置输入框前缀
|
|
/// </summary>
|
|
public string Prompt { get; set; } = "> ";
|
|
|
|
/// <summary>
|
|
/// 获取当前输入值
|
|
/// </summary>
|
|
public string Value { get; private set; } = string.Empty;
|
|
|
|
/// <inheritdoc />
|
|
public bool Focused { get; set; }
|
|
|
|
/// <summary>
|
|
/// 在输入值变化时触发
|
|
/// </summary>
|
|
public Action<string>? OnChanged { get; set; }
|
|
|
|
/// <summary>
|
|
/// 在用户提交输入时触发
|
|
/// </summary>
|
|
public Action<string>? OnSubmitted { get; set; }
|
|
|
|
/// <summary>
|
|
/// 在用户取消输入时触发
|
|
/// </summary>
|
|
public Action? OnCanceled { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<string> Render(int width)
|
|
{
|
|
// 只有运行时确认获得焦点后才发出 marker 避免多个输入组件争抢硬件光标
|
|
var marker = Focused ? CursorMarker.Marker : string.Empty;
|
|
var cursor = Focused ? _theme.InputCursor(" ") : string.Empty;
|
|
var visible = $"{Prompt}{Value}{marker}{cursor}";
|
|
return [_textMeasurer.Truncate(visible, width)];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetKeybindings(KeybindingRegistry keybindings)
|
|
=> _keybindings = keybindings ?? throw new ArgumentNullException(nameof(keybindings));
|
|
|
|
/// <inheritdoc />
|
|
public void HandleInput(TuiInputEvent input)
|
|
{
|
|
switch (input)
|
|
{
|
|
case { Kind: TuiInputEventKind.Text }:
|
|
SetValue(Value + input.Value);
|
|
break;
|
|
case { Kind: TuiInputEventKind.Paste }:
|
|
SetValue(Value + input.Value.Replace("\r\n", "\n").Replace('\n', ' '));
|
|
break;
|
|
case { Kind: TuiInputEventKind.Key }:
|
|
HandleKey(input);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空当前输入值
|
|
/// </summary>
|
|
public void Clear() => SetValue(string.Empty);
|
|
|
|
/// <summary>
|
|
/// 根据动作映射处理单行输入快捷键
|
|
/// </summary>
|
|
private void HandleKey(TuiInputEvent input)
|
|
{
|
|
if (_keybindings.Matches(input, TuiKeybindings.EditorDeleteCharBackward))
|
|
{
|
|
Backspace();
|
|
return;
|
|
}
|
|
|
|
if (_keybindings.Matches(input, TuiKeybindings.InputSubmit))
|
|
{
|
|
OnSubmitted?.Invoke(Value);
|
|
return;
|
|
}
|
|
|
|
if (_keybindings.Matches(input, TuiKeybindings.InputCancel))
|
|
OnCanceled?.Invoke();
|
|
}
|
|
|
|
private void SetValue(string value)
|
|
{
|
|
Value = value;
|
|
OnChanged?.Invoke(Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 删除最后一个 Unicode Rune
|
|
/// </summary>
|
|
private void Backspace()
|
|
{
|
|
if (Value.Length == 0)
|
|
return;
|
|
|
|
var builder = new StringBuilder();
|
|
var runes = Value.EnumerateRunes().ToArray();
|
|
|
|
// 用 Rune 删除避免把 emoji 或代理对拆坏
|
|
foreach (var rune in runes[..^1]) builder.Append(rune);
|
|
|
|
SetValue(builder.ToString());
|
|
}
|
|
}
|