首页
/ Bevy 文本输入组件拆分迁移指南:从单体 EditableText 到 EditableText + TextInput 双组件模式

Bevy 文本输入组件拆分迁移指南:从单体 EditableText 到 EditableText + TextInput 双组件模式

2026-09-07 17:26:47作者:尤峻淳Whitney

本篇指南基于 Bevy 的发布迁移文档,讲解文本输入组件的职责拆分:EditableText 从"状态持有者 + 输入控件"二合一的单体组件,拆分为仅负责状态的无头组件(bevy_text 中)与负责交互行为的 TextInput 控件组件(bevy_ui_widgets 中)。读完你将掌握如何把旧版单组件用法迁移到新的双组件组合、各组件的职责边界、读写模式(readonly/static)的取值含义,以及 TextInputPlugin 提供的键盘、指针、IME 处理链路。

一、迁移核心:一个组件变成两个

迁移文档(见 _release-content/migration-guides/text_input.md)的核心结论只有一句话:

过去,EditableText 组件同时扮演两个角色:可编辑文本的状态持有者,以及一个自带 observers 和键盘映射的独立控件。这两个职责现在被拆开了:要构造一个完整的、可工作的文本输入控件,你现在需要同时插入 EditableTextTextInput 两个组件。

对应的发布说明(_release-content/release-notes/text_input.md)补充了拆分动机与细节:

  • EditableText 现居 bevy::text crate,只持有文本输入字段的状态,不再内置任何 observer——它不再是"控件"(无论有头无头),只是一个状态容器;
  • 所有"控件式行为"(响应按键等)移到了 bevy::ui_widgets crate 的新 TextInput 组件中;
  • 这一安排既与其他控件(widgets)的组织方式保持一致,也让只有控件才关心的属性(如 read-only 只读选项)有了合适的归属地。

因此,迁移的最小动作就是:在原有 EditableText 的实体上,额外插入一个 TextInput 组件

二、状态容器:EditableText 现在持有什么

EditableText 的定义位于 crates/bevy_text/src/editing.rs。它是一个"无头"(headless)组件:本身不提供边框、背景等任何视觉元素,需要与 UI 框架(如 Feathers)组合使用。其结构体字段完整列出了状态面:

字段 类型 含义
editor PlainEditor<TextBrush> 内部文本编辑器,同时管理文本内容与光标位置,按 Unicode 规则执行编辑(基于 parley)
viewport TextViewport 文本布局可见区域的边界
pending_edits Vec<TextEdit> 已请求但尚未应用的编辑操作,按 FIFO 顺序由 apply_text_edits 系统统一处理
pending_paste Option<ClipboardRead> 等待剪贴板 I/O 的粘贴操作(wasm32 上剪贴板读取是异步的)
cursor_margin Vec2 光标显形边距,取值为视口尺寸的比例(默认 0.2,钳制在 0.0..=0.5
cursor_width f32 光标宽度,相对字号(默认 0.2
cursor_blink_period Duration 光标闪烁周期(默认 1 秒)
max_characters Option<usize> 最大字符数,超出上限的编辑会被忽略
visible_lines Option<f32> 以可见行数设定输入框高度(默认 Some(1.0)
visible_width Option<f32> 以可见字形数设定输入框宽度
allow_newlines bool 是否允许换行(默认 false

插入 EditableText 时,#[require] 会自动附带一组排版相关组件:TextLayoutTextFontTextColorLineHeightFontHintingEditableTextGenerationTextReadWriteMode(见 editable.rs require 声明)。

读写模式:TextReadWriteMode 的三种取值

这是与本次拆分直接相关的"控件属性"。枚举定义在 crates/bevy_text/src/editing.rs

取值 行为
Editable(默认) 文本输入功能正常
ReadOnly 允许光标移动、选中与复制到剪贴板,但不允许任何修改
Static 纯展示,所有交互被禁用(连光标移动与选中都不允许)——Feathers 的数字输入控件在拖拽时使用该模式

注意:ReadOnly/Static 的判定发生在输入处理层。例如 TextInputPlugin 的键盘输入 observer 会在入队编辑前检查模式:ReadOnly 下仅放行"非破坏性"编辑(选区、复制类),而 Static 下指针按下/拖拽 observer 会直接提前返回(见 on_pointer_press 的模式判断)。

三、控件组件:TextInput 承担的全部交互

TextInput 是一个标记组件(marker component),定义在 crates/bevy_ui_widgets/src/text_input.rs

/// Editable text widget.
#[derive(Component, Clone, Default, Reflect)]
#[require(EditableText, AccessibilityNode(accesskit::Node::new(Role::TextInput)))]
#[reflect(Component)]
pub struct TextInput;

两个关键点:

  1. #[require(EditableText, ...)]:插入 TextInput 会自动要求实体上存在 EditableText,并附带一个 AccessKit 的 TextInput 无障碍节点。也就是说,只写 TextInput 一个组件即可获得完整的"状态 + 交互"组合;
  2. bevy_text 分离的原因:模块头部注释明确说明,该模块独立于核心 bevy_text crate,是为了避免给 bevy_text 引入对 bevy_input 的依赖——bevy_text 本应可用于非交互场景(如渲染服务器、纯布局计算),而按键处理逻辑必然需要输入事件(见 模块文档注释)。

TextInputPlugin 注册了哪些行为

TextInputPlugincrates/bevy_ui_widgets/src/text_input.rs)是"把 EditableText 变成控件"的全部所在,注册的 observer 与系统包括:

  • 键盘 observer on_focused_keyboard_input:把聚焦实体收到的 KeyboardInput 事件翻译为 TextEdit 动作。支持的操作包括:
    • 剪切/复制/粘贴(Key::Cut/Key::Copy/Key::Paste 及平台相关的 Ctrl/Cmd+C/X/V/A 快捷键,macOS 用 Super、其他平台用 Control,由 mac_host() 区分,wasm 下运行时探测宿主系统);
    • 光标移动:方向键、Home/End(行首/行尾或全文首/尾,取决于修饰键)、词级跳词(macOS 用 Alt、其他平台用 Control)、Backspace/Delete(含按词删除);
    • 选区扩展:Shift 组合任意光标移动、Shift+Delete 复制为剪切(非 macOS);
    • Escape:折叠选区并主动清空聚焦input_focus.clear()),同时不消费事件——同一次按键会继续冒泡到窗口层,供"取消/关闭对话框"等外层逻辑使用(源码注释特意说明这一行为,且有对应测试覆盖);
    • Enter:仅在 allow_newlines 为真时插入换行,否则放行以支持"提交"语义。
    • 快捷键匹配采用"逻辑键优先、物理键兜底"的混合策略(matches_edit_shortcut),使 AZERTY 等拉丁语系非 QWERTY 布局与西里尔等非拉丁布局都能正确触发 Ctrl+C 类操作;
  • 指针 observer on_pointer_press / on_pointer_drag:单击定位光标(MoveToPoint)、Shift+单击扩展选区(ShiftClickExtension)、双击选词(SelectWordAtPoint)、三次及以上点击全选;拖拽执行 ExtendSelectionToPoint。事件坐标会经 UiGlobalTransformComputedUiRenderTargetInfoscale_factorUiScale 换算到文本布局空间,并叠加 viewport.offset 处理滚动;
  • 自动滚动系统 text_input_autoscroll_system:拖拽选区时指针超出视口即自动滚动,速度随溢出距离在 0.75x(基准)到 2.0x(上限,视口尺寸的每秒比例)之间线性爬坡,且按帧时间归一化、帧率无关;
  • IME 支持on_ime_input 处理 Ime::Preedit/Commit/Enabled/Disabled 事件(preedit 文本不计入正式 value,提交时清掉 preedit 并插入承诺串);listen_for_ime_input_when_text_input_focused 按聚焦状态与读写模式开关窗口 IME;update_ime_position 让候选词窗口跟随光标(取光标区域底边,使候选框出现在当前行下方);
  • 聚焦行为on_focus_lost observer 在失焦时清除 IME 组合态并折叠选区(防止两个输入框间切换时残留 preedit);SelectAllOnFocus 可选组件实现"聚焦即全选",若聚焦源于指针按下,则延迟到指针释放且无其他选区时才全选;
  • 组件登记:由于 bevy_textbevy_ui 之间存在循环依赖,EditableTextNodeTextNodeFlagsContentSize 的 required-components 登记放在 TextInputPlugin::build 中完成(见 build 尾部注释与调用)。

调度上,IME 相关系统按 ToggleWindowIMEInput → HandleEvents → UpdatePosition 链式排布于 PreUpdate,且在 InputSystemsInputFocusSystems::DispatchUiSystems::Focus 之后运行;TextEdit 的实际应用在 PostUpdateEditableTextSystems 中执行——输入 observer 只负责把操作"排队"进 pending_edits,不立即修改文本。

TextInputPlugin 包含在 UiWidgetsPlugins 组中,也可以在只需要文本输入时单独添加(见 插件文档注释)。

四、迁移步骤与前后对照

迁移前(旧写法:单组件即可工作)

// 旧版本:EditableText 自带 observer 与键盘映射,插入即得完整控件
commands.spawn((
    Node {
        width: px(240.),
        height: px(32.),
        ..default()
    },
    EditableText::new("初始文本"),
));

迁移后(新写法:双组件组合)

use bevy::prelude::*;
use bevy::ui_widgets::TextInput;

fn setup(mut commands: Commands, assets: Res<AssetServer>) {
    commands.spawn((
        Node {
            width: px(240.),
            height: px(32.),
            ..default()
        },
        // ① 状态:文本内容、光标、选区、视口
        EditableText::new("初始文本"),
        // ② 控件:键盘/指针/IME 行为(会反向 require ①)
        TextInput,
        // 可选:聚焦时全选
        SelectAllOnFocus,
        // 可选:按字符过滤输入(组件挂在同一实体上)
        // EditableTextFilter(editable_text_filter),
    ));
}

迁移检查清单:

  1. 为每个原 EditableText 输入框实体补插 TextInput 组件。由于 #[require] 的关系,TextInput 也保证 EditableText 存在,二者写在同一 spawn 元组中最直观;
  2. 确认插件已启用:应用需加载 UiWidgetsPlugins(或单独添加 TextInputPlugin),否则 EditableText 实体只会渲染、不会响应任何按键;
  3. 只读/禁用逻辑改用 TextReadWriteMode:旧代码若靠其他手段做"只读展示",现在应写入 TextReadWriteMode::ReadOnly(可选中不可改)或 TextReadWriteMode::Static(完全只读)。Feathers 的输入控件即演示了这一联动:实体被加上 InteractionDisabled 时写入 TextReadWriteMode::ReadOnly 并切换为 NotAllowed 光标,移除时恢复 Editable(见 Feathers 文本输入样式系统);
  4. 不要直接改 editor:编辑操作请通过 EditableText::queue_edit 排队,由 apply_text_edits 系统在更新周期中统一应用;
  5. 键盘语义注意 IME:组合输入期间所有按键(含 Tab)都归 IME,TextInputPlugin 会停止事件冒泡;需要"Enter 提交"的逻辑应在 EditableText::is_composing() 为真时抑制提交,避免打断组合。

仓库中的真实参照

  • 最简示例 examples/ui/text/text_input.rs 展示了手搭 Node + EditableText + TextInput 的完整最小工程;多行输入见 examples/ui/text/multiline_text_input.rs,IME 支持见 examples/ui/text/ime_support.rs
  • Feathers 的 FeathersTextInput 场景函数是官方"双组件同实体"的标准写法——bsn! 场景中依次给出 TextInputEditableText { cursor_width: 0.3, visible_width, max_characters }ThemedTextTextLayout { linebreak: LineBreak::NoWrap } 等(见 FeathersTextInput::scene),外层再由 FeathersTextInputContainer 提供圆角、底色与聚焦指示;
  • 回归测试可直接验证新行为:crates/bevy_ui_widgets/src/text_input.rs 末尾的 tests 模块覆盖了 Steam 虚拟键盘未识别键插入文本、自动滚动速度与帧率无关性、Latin 布局逻辑键优先/非拉丁布局物理键兜底、以及"Escape 失焦但仍冒泡到窗口 observer"等关键路径。

五、小结

这次拆分的本质是关注点分离bevy_text 里的 EditableText 回归纯状态(文本缓冲、光标、选区、视口、编辑队列、读写模式),不依赖 bevy_inputbevy_ui_widgets 里的 TextInput 承载全部交互(按键映射、指针选区、自动滚动、IME、无障碍节点),并可单独启停。迁移工作量很小——给既有输入框实体补一个 TextInput 组件、核对插件与只读模式即可——换来的是与其余 UI 控件一致的组织方式,以及"只读展示""纯展示"两种控件属性有了干净的一等表达。

登录后查看全文
热门项目推荐
相关项目推荐