Bevy 文本输入组件拆分迁移指南:从单体 EditableText 到 EditableText + TextInput 双组件模式
本篇指南基于 Bevy 的发布迁移文档,讲解文本输入组件的职责拆分:EditableText 从"状态持有者 + 输入控件"二合一的单体组件,拆分为仅负责状态的无头组件(bevy_text 中)与负责交互行为的 TextInput 控件组件(bevy_ui_widgets 中)。读完你将掌握如何把旧版单组件用法迁移到新的双组件组合、各组件的职责边界、读写模式(readonly/static)的取值含义,以及 TextInputPlugin 提供的键盘、指针、IME 处理链路。
一、迁移核心:一个组件变成两个
迁移文档(见 _release-content/migration-guides/text_input.md)的核心结论只有一句话:
过去,
EditableText组件同时扮演两个角色:可编辑文本的状态持有者,以及一个自带 observers 和键盘映射的独立控件。这两个职责现在被拆开了:要构造一个完整的、可工作的文本输入控件,你现在需要同时插入EditableText和TextInput两个组件。
对应的发布说明(_release-content/release-notes/text_input.md)补充了拆分动机与细节:
EditableText现居bevy::textcrate,只持有文本输入字段的状态,不再内置任何 observer——它不再是"控件"(无论有头无头),只是一个状态容器;- 所有"控件式行为"(响应按键等)移到了
bevy::ui_widgetscrate 的新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] 会自动附带一组排版相关组件:TextLayout、TextFont、TextColor、LineHeight、FontHinting、EditableTextGeneration 和 TextReadWriteMode(见 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;
两个关键点:
#[require(EditableText, ...)]:插入TextInput会自动要求实体上存在EditableText,并附带一个 AccessKit 的TextInput无障碍节点。也就是说,只写TextInput一个组件即可获得完整的"状态 + 交互"组合;- 与
bevy_text分离的原因:模块头部注释明确说明,该模块独立于核心bevy_textcrate,是为了避免给bevy_text引入对bevy_input的依赖——bevy_text本应可用于非交互场景(如渲染服务器、纯布局计算),而按键处理逻辑必然需要输入事件(见 模块文档注释)。
TextInputPlugin 注册了哪些行为
TextInputPlugin(crates/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。事件坐标会经UiGlobalTransform、ComputedUiRenderTargetInfo的scale_factor与UiScale换算到文本布局空间,并叠加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_lostobserver 在失焦时清除 IME 组合态并折叠选区(防止两个输入框间切换时残留 preedit);SelectAllOnFocus可选组件实现"聚焦即全选",若聚焦源于指针按下,则延迟到指针释放且无其他选区时才全选; - 组件登记:由于
bevy_text与bevy_ui之间存在循环依赖,EditableText对Node、TextNodeFlags、ContentSize的 required-components 登记放在TextInputPlugin::build中完成(见 build 尾部注释与调用)。
调度上,IME 相关系统按 ToggleWindowIMEInput → HandleEvents → UpdatePosition 链式排布于 PreUpdate,且在 InputSystems、InputFocusSystems::Dispatch、UiSystems::Focus 之后运行;TextEdit 的实际应用在 PostUpdate 的 EditableTextSystems 中执行——输入 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),
));
}
迁移检查清单:
- 为每个原
EditableText输入框实体补插TextInput组件。由于#[require]的关系,TextInput也保证EditableText存在,二者写在同一spawn元组中最直观; - 确认插件已启用:应用需加载
UiWidgetsPlugins(或单独添加TextInputPlugin),否则EditableText实体只会渲染、不会响应任何按键; - 只读/禁用逻辑改用
TextReadWriteMode:旧代码若靠其他手段做"只读展示",现在应写入TextReadWriteMode::ReadOnly(可选中不可改)或TextReadWriteMode::Static(完全只读)。Feathers 的输入控件即演示了这一联动:实体被加上InteractionDisabled时写入TextReadWriteMode::ReadOnly并切换为NotAllowed光标,移除时恢复Editable(见 Feathers 文本输入样式系统); - 不要直接改
editor:编辑操作请通过EditableText::queue_edit排队,由apply_text_edits系统在更新周期中统一应用; - 键盘语义注意 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!场景中依次给出TextInput、EditableText { cursor_width: 0.3, visible_width, max_characters }、ThemedText、TextLayout { linebreak: LineBreak::NoWrap }等(见 FeathersTextInput::scene),外层再由FeathersTextInputContainer提供圆角、底色与聚焦指示; - 回归测试可直接验证新行为:crates/bevy_ui_widgets/src/text_input.rs 末尾的
tests模块覆盖了 Steam 虚拟键盘未识别键插入文本、自动滚动速度与帧率无关性、Latin 布局逻辑键优先/非拉丁布局物理键兜底、以及"Escape 失焦但仍冒泡到窗口 observer"等关键路径。
五、小结
这次拆分的本质是关注点分离:bevy_text 里的 EditableText 回归纯状态(文本缓冲、光标、选区、视口、编辑队列、读写模式),不依赖 bevy_input;bevy_ui_widgets 里的 TextInput 承载全部交互(按键映射、指针选区、自动滚动、IME、无障碍节点),并可单独启停。迁移工作量很小——给既有输入框实体补一个 TextInput 组件、核对插件与只读模式即可——换来的是与其余 UI 控件一致的组织方式,以及"只读展示""纯展示"两种控件属性有了干净的一等表达。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00