Files
ttui/src/TinyTUI/Components/Input.cs
T
chuan d84ca3a313 feat: share runtime keybindings with components
- add component-level keybinding injection contract

- inject runtime keybinding registry when components enter runtime or overlay

- cover focused component behavior with shared runtime keybindings
2026-06-04 02:54:07 +08:00

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());
}
}