gpui-kit 中readonly与read_only的 API 命名决策:跨生态调研与源码实践
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
本文基于 gpui-kit 仓库的命名调研文档 READONLY-NAMING-RESEARCH.md,系统梳理 Rust GUI 库中readonly/readOnly/read_only三种拼写在 12 个跨端生态中的实际用法,说明 gpui-kit(gpui-component/gpui-base)为什么统一选择readonly作为输入组件的只读模式标识符,并结合 crates/component/src/input/input.rs 与 crates/base/src/input/base/state.rs 的源码实现和集成测试,给出可直接落地的命名规范与迁移建议。读完本文,你可以在为自己的 Rust UI 项目设计只读 API 时,直接复用这套“规范—先例—实现”三层判定方法。
结论:gpui-component继续使用readonly
调研(日期 2026-08-14)的最终建议是:gpui-component继续使用readonly,典型调用形态为:
Input::new(state).readonly(true); state.set_readonly(true, cx);这个结论由三条理由支撑:
- Rust 先例:Rust 公共标识符要求
snake_case,但这并不意味着每个自然语言复合词都必须拆开。Rust 标准库本身就把该概念拼作Permissions::readonly()和Permissions::set_readonly()(std::fs::Permissions),因此readonly/set_readonly完全符合 Rust 的实际先例,并不违反 API Guidelines 的 C-CASE(函数与方法是snake_case)——readonly本身不含大写字母。 - Web 生态术语稳定:HTML 标记层使用
readonly,JavaScript、Dart、Kotlin 等 camelCase 语言 API 使用readOnly。在 Rust 中转成小写后自然就是readonly。 - 迁移成本:gpui-kit 当前已公开使用
readonly、set_readonly与.readonly(true)(见下文源码印证),改为read_only会制造迁移成本,却没有得到跨生态惯例或 Rust 标准库先例的支持。
配套的文字规范是:代码标识符用readonly,英文文档叙述中用形容词read-only,不建议新增read_only别名。
跨生态调研:12 个框架/平台的实际拼写
调研范围限定为规范、项目官方文档或项目官方源码。一个容易踩坑的前提是:对 Vue、Svelte 这类直接渲染原生元素的框架,必须把其“原生 HTML attribute 透传/书写规则”和 HTML 规范一起看——readonly是 HTML 定义的 content attribute,不是这些框架另行定义的组件 prop。
| 生态 | 面向使用者的拼写 | 一手资料与判定 |
|---|---|---|
| HTML 标记 | readonly | HTML Standard 将输入控件的布尔 content attribute 定义为readonly,示例为<input readonly> |
| DOM | readOnly | 同一规范中HTMLInputElement的 IDL 定义attribute boolean readOnly;因此 JavaScript 写input.readOnly = true。这说明标记拼写和编程语言属性拼写本来就不同 |
| React DOM | readOnly | React 官方<input>reference 把readOnly列为布尔 prop,示例<input value={something} readOnly={true} /> |
| Vue | 原生元素上为readonly | Vue 官方 Attribute Bindings 说明模板 attribute 的基本写法与v-bind缩写;用于原生<input>时 attribute 由 HTML Standard 定义为readonly,静态写readonly,动态写:readonly="flag";不是read_only |
| Angular | HTML/模板为readonly;Signals Forms API 为readonly(...) | 原生 attribute 仍是 HTML 的readonly;Angular 官方 Signals Forms 文档使用readonly(schemaPath.username),字段状态访问为readonly(),formField会自动绑定readonlyattribute |
| Svelte | 原生元素上为readonly | Svelte 官方 Attributes 文档说明元素 attribute 可按 HTML 方式书写,也可用表达式赋值;写法是readonly或readonly={flag};不是read_only |
| Flutter | readOnly | Flutter 官方TextField.readOnly声明为final bool readOnly;构造参数为TextField(readOnly: true) |
| Jetpack Compose | readOnly | AndroidX 官方TextField.kt源码的参数是readOnly: Boolean = false,KDoc 也以readOnly描述只读状态 |
| SwiftUI | 没有同名的通用TextField参数 | Apple 官方TextField把它定义为可编辑文本接口,公开 initializer 中没有readOnly/read_only参数;EditMode示例在非编辑状态改为展示Text;官方教程展示的.disabled(...)会禁用交互,并不等同于 Web 中“仍可聚焦/选择”的 readonly 语义 |
| egui | 没有同名 builder;用interactive或只读 buffer 表达 | egui 官方 API 文档说明TextEdit::interactive(false)会禁止用户选择文本;若要“可选择但不可编辑”,TextEdit文档建议传入&mut &str;它没有为该语义选择readonly或read_only |
| iced | 没有同名属性;通过是否提供编辑消息表达 | iced 官方TextInput::on_input文档说明不调用该方法会产生 disabledTextInput;它没有独立的 readonly 命名,因此不能作为read_only的先例 |
| Slint | Slint DSL 为read-only | Slint 官方LineEdit定义read-only: bool,语义是仍可选择和复制但不能编辑;Slint 标识符采用 kebab-case,不能直接推导 Rust API 应写成read_only |
这张表传递出的关键信息是:没有哪一个主流生态在“类名/方法名式 API”中选择了read_only。HTML/Vue/Svelte/Angular 的标记层是readonly(Slint 是其 DSL 风格的read-only),DOM/React/Flutter/Jetpack Compose 的 camelCase 语言 API 是readOnly,而真正相关的 Rust 标准库先例明确采用readonly()。egui 与 iced 甚至没有为该语义建立同名 API,不构成任何一方的先例。
跨生态规律:三层命名模型
把上面的调研归纳后,可以提炼为三层命名规律:
- 标记或 DSL 层:HTML/Vue/Svelte/Angular 使用
readonly,Slint 使用其 DSL 风格的read-only; - camelCase 语言 API 层:DOM、React、Flutter、Jetpack Compose 使用
readOnly; - Rust 层:被调查的 UI 库没有形成统一属性名,但真正相关的 Rust 标准库先例(
Permissions::readonly())明确采用readonly。
由此可以否定一种常见直觉:“Rust 一律把readOnly机械转换成read_only”并不是可靠规则。readonly在 Rust 中是一个合法的snake_case标识符(单字、无下划线分隔),标准库先例进一步消除了歧义。
gpui-kit 源码印证:readonly已经贯穿组件层与状态层
调研文档中的建议并非纸面规范,gpui-kit 的实际代码与之完全一致。以下证据链覆盖“组件 builder → 状态 setter → 语义判定 → 集成测试”四个环节。
组件层:Input/Editor/Textarea的 builder 方法
crates/component/src/input/input.rs 中的Input::readonly:
/// Set the input field to read-only, default is `false`. /// /// Unlike [`Self::disabled`], a read-only input keeps the normal appearance /// and still can be focused, selected and copied, it only rejects the changes /// made by the user. pub fn readonly(mut self, readonly: bool) -> Self { self.readonly = readonly; self }Editor(crates/component/src/input/editor.rs)与Textarea(crates/component/src/input/textarea.rs)提供了语义相同的.readonly(bool)builder。注意文档注释中明确区分了readonly与disabled的语义边界:只读输入保持正常外观,仍可聚焦、选择、复制,只拒绝用户发起的文本变更——这正是 Web 生态readonly语义在桌面端的对应实现。
在RenderOnce for Input的渲染路径中(input.rs),builder 收集的self.readonly会被下发到状态:
state.set_disabled(self.disabled, cx); state.set_readonly(self.readonly, cx);同时上下文菜单也感知只读状态:只读输入仍可导航(如“转到定义”),但“代码操作”等会修改文本的菜单项会被禁用(editable = enabled && !capabilities.is_readonly(),见 input.rs)。
状态层:set_readonly与is_editable的语义收敛
底层状态实现位于 crates/base/src/input/base/state.rs:
/// Set with read-only mode. /// /// Unlike [`Self::disabled`], a read-only input keeps the normal appearance, /// focus, cursor, selection and copy behavior, it only rejects any change /// of the text made by the user. pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) { if self.readonly == readonly { return; } self.readonly = readonly; if readonly { self.search_session.replace_mode = false; } cx.notify(); } /// Returns true if the user is allowed to change the text. /// /// This is false when the input is `disabled` or `readonly`, the programmatic /// APIs (e.g.: [`Self::set_value`], [`Self::insert`]) are not limited by this. pub fn is_editable(&self) -> bool { !self.disabled && !self.readonly }从源码结构看,有三点值得注意:
- 幂等短路:
set_readonly与set_disabled一样先比较旧值,未变化时不触发cx.notify(),避免无谓的重绘; - 副作用联动:进入只读模式会关闭搜索会话的
replace_mode,即禁用“查找替换”中的替换能力——这是只读语义在编辑子系统内的具体投影; is_editable作为唯一语义出口:disabled与readonly在“用户能否编辑”上收敛为同一个布尔值,但两者的 UI 表现(外观、聚焦、光标、选择、复制)保持独立,这与 HTML 中disabled/readonly的区分保持一致。
对外部用户,gpui-component通过 crates/component/src/input/state.rs 的set_readonly分发方法暴露同名 setter,调用链为:组件 builder.readonly(true)→ 渲染时state.set_readonly(bool, cx)→ 底层TextInputState::set_readonly。整个链路上拼写始终统一为readonly,没有出现read_only变体(在crates/的 Rust 源码中搜索read_only基本没有命中,印证了仓库对这一拼写的收敛)。
测试层:只读与禁用都拒绝键盘输入
crates/kit/tests/input.rs 中的集成测试readonly_and_disabled_inputs_reject_native_typing验证了两种模式的行为一致性——无论是disabled还是readonly,聚焦后键入的文本都会被拒绝,输入框的值保持"fixed"不变:
#[gpui::test] fn readonly_and_disabled_inputs_reject_native_typing(cx: &mut TestAppContext) { let handle = inputs(cx); for disabled in [false, true] { // 交替设置 disabled / readonly,键入 "ignored" 后断言值仍为 "fixed" } }该测试也体现了gpui-kit对readonly语义的取舍:只读输入仍然可以被聚焦与点击(window.click("guarded", cx)成功),只是输入被拦截。
落地建议:各场景的推荐拼写
调研文档给出了面向gpui-component(当前版本见 crates/component/Cargo.toml,crate 名gpui-component)的逐场景建议,可以直接作为你自己 Rust UI 项目的参照表:
| 场景 | 推荐 | 不推荐 |
|---|---|---|
| Builder 方法 | readonly(bool) | read_only(bool) |
| 状态 setter | set_readonly(bool, cx) | set_read_only(bool, cx) |
| 字段/能力标志 | readonly: bool | read_only: bool |
| 英文说明文字 | “read-only input/mode” | “readonly input/mode” |
| 与 DOM 交互的 JavaScript | node.readOnly | node.readonly,node.read_only |
两个补充说明:
- 文档文字与标识符刻意分开:代码里是
readonly,英文叙述中用带连字符的形容词read-only。gpui-kit 的源码注释正是这样写的(如 “Set the input field to read-only”),读者不会混淆。 - 不要顺手加别名:不建议新增
read_only的 API 别名。别名会让一个概念存在两种拼写,长期成本高于收益;而且read_only既无 Rust 标准库先例,也无跨生态支持。
边界情况:editable是另一个设计维度
如果未来需要表达的是“当前内容能否被编辑”这一正向语义,而不是一个 HTML 式模式开关,调研文档建议单独考虑editable/is_editable这类正向命名。事实上 crates/base/src/input/base/state.rs 内部已经提供了is_editable()作为disabled与readonly的统一判定出口,正属于这一维度。文档特别强调:这属于不同的 API 设计,不应与readonly/read_only的拼写迁移混在一起——前者解决“API 极性”,后者解决“拼写一致性”,两个问题需要分开决策。
小结
gpui-kit 的这次命名调研提供了一个可复用的判定框架:面对snake_case语言中的复合词 API 命名时,依次检查(1)语言标准库的真实先例而非抽象规则;(2)同一概念在标记层/camelCase 层的稳定术语;(3)现有公开 API 的迁移成本。按这三条,readonly同时满足 Rust 标准库先例(Permissions::readonly())、Web 生态术语继承与仓库现状,是gpui-component输入组件只读模式的确定选择。相关实现可继续参考 crates/component/src/input/input.rs、crates/base/src/input/base/state.rs 与 crates/kit/tests/input.rs。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考