ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

GPUI Kit 编码指南:面向可维护 Rust 桌面应用的分层架构、状态所有权与命名规范

GPUI Kit 编码指南:面向可维护 Rust 桌面应用的分层架构、状态所有权与命名规范 GPUI Kit 编码指南面向可维护 Rust 桌面应用的分层架构、状态所有权与命名规范【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit本篇指南以 gpui-kit 仓库中规范性的《Coding Guides》文档skills/gpui-kit/references/coding-guides.md为主体系统讲解如何用 GPUI Kit 构建架构清晰、可长期维护的跨平台桌面应用。读完你将掌握按能力而非文件类型组织 Rust crate 的分层架构、RenderOnce与EntityT的正确选型、状态所有权与受控组件模式、ElementId稳定身份、语义化主题与基于 rem 的字体缩放机制、公共 API 与命名约定、测试策略与性能规则。文中所有结论均可在仓库源码中找到对应实现。阅读前提本指南是规范性文档建议先阅读设计指南——代码结构应当承载产品意图而不是替代它。Must表示生命周期、正确性或生态约束should表示默认架构偏离必须有具体理由。对于精确的函数签名以当前源码与 API 文档为准。架构总览分层与依赖方向GPUI Kit 应用的分层架构遵循依赖向下原则高层拥有领域含义与编排orchestration低层拥有可复用的表现或行为。不要让一个可复用组件依赖某个应用屏幕也不要让gpui-base依赖 GPUI Component 的主题。五个边界依次为边界职责app shell组合窗口与特性 crate特性逻辑尽量不进入这一层feature crate把一个能力的模型、服务、视图、命令、对话框与工作流收敛在一个公共边界之后app component重复出现的、具有领域含义的模式应用自有组件gpui-component有主题的通用 UIgpui_kit::componentgpui-base不含产品表现的可复用行为与几何gpui_kit::base按能力组织大型应用在大型 Rust 应用中一个特性feature通常应该是一个 crate而不是全局views、models、modals目录里的又一个文件。同一个能力的模型、视图、命令、对话框与工作流必须放在一起编辑 workspace 的对话框属于 workspace 特性只有可复用的对话框原语才属于 UI 库。推荐目录结构crates/ ├── app/ │ └── src/main.rs # 组合窗口与特性 ├── workspace/ │ └── src/ │ ├── lib.rs # 特性的公共边界 │ ├── model.rs │ ├── commands.rs │ ├── workspace_view.rs │ └── rename_dialog.rs ├── search/ │ └── src/ │ ├── lib.rs │ ├── model.rs │ ├── commands.rs │ ├── search_view.rs │ └── filters.rs ├── settings/ │ └── src/ │ ├── lib.rs │ ├── model.rs │ ├── settings_view.rs │ └── account_dialog.rs └── shared/ └── src/ ├── lib.rs └── recent_items.rs # 有多个所有者的稳定能力不要反转成全局models/、views/、modals/、commands/目录——这些文件夹按实现角色分类文件却把每个特性散落在整个应用里。应用 shell 只负责组合特性 crate几乎不含特性逻辑。特性可以依赖稳定的共享能力与 UI 基础但不得依赖 shell也不得伸手进兄弟特性的内部。两个特性需要通信时优先用显式的命令command、事件、数据类型或小型共享服务而不是在两个视图之间建立依赖。只有当某个能力已经有了清晰的名字且不止一个真实所有者时才抽取共享 crate。crate 边界就是工程边界它让 Cargo 只重建和测试更小的依赖子图让所有权在Cargo.toml中可见并限制了变更的评审与回归面。它也让移除变得诚实——一个无法在不搜索全局视图/模态目录的情况下摘除的特性从来就没有被隔离过。当然不要为每个屏幕或辅助函数都建 crate。只有在某个能力拥有自己的状态与生命周期、有稳定的公共接缝、或实现量大到值得独立编译与测试时才拆分。保持依赖无环并指向更小、更稳定的 crate。启动引导与 Root 所有权在创建任何基于组件的视图之前只初始化一次 GPUI Component并在每个窗口的第一层放置Rootapp.run(move |cx| { gpui_kit::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let workspace cx.new(|cx| Workspace::new(window, cx)); cx.new(|cx| Root::new(workspace, window, cx)) }) .expect(failed to open window); }) .detach(); });在 crates/kit/src/lib.rs 中gpui_kit::init(cx)是一个门面函数启用component特性默认开启时它调用gpui_component::init后者同时初始化gpui-base应用只需依赖gpui-kit一个 crateGPUI 通过use gpui_kit::*;导入各层按名访问——gpui_kit::component带样式的组件、gpui_kit::base无样式的行为、gpui_kit::assets默认图标、gpui_kit::platform。Root协调窗口级别的组件设施例如 overlay 与通知。不要在一个窗口里为每个页面创建独立的 root。它还负责模态焦点恢复、焦点陷阱focus trap、tooltip/菜单层以及窗口作用域内的文本选择。绕过它静态观察可能一切正常但 overlay 嵌套或焦点快速变化时就会出问题。在 crates/component/src/root.rs 的Render实现中可以看到Root承担的职责之一window.set_rem_size(cx.theme().font_size)会把主题基础字体投影为窗口的 rem 基准详见后文Base 字体是应用缩放控制。理解 GPUI 的阶段与上下文GPUI 是保留状态retained state 声明式渲染declarative rendering实体跨帧存活render返回的元素树只是当前帧的一份全新描述。请始终区分这两者ContextSelf变更当前实体、创建绑定到它的监听器、发出它的事件、通知它的观察者App访问应用全局状态以及对实体的读/更新——不代表对渲染元素的拥有权Window拥有该窗口的焦点、动作、输入分发、元素键控状态element-keyed state、测量与动画帧请求layout、prepaint、paint是后续阶段只有确实需要已解析的几何时才使用它们的钩子。永远不要把mut Window、mut App或mut Context_保留到调用之外。应该保留类型化句柄——Entity、WeakEntity、FocusHandle、滚动句柄或领域 ID。选择合适的单元用RenderOnce处理值类元素当所有输入都能由调用方提供、且元素无需在帧之间保留应用状态时使用RenderOnce/IntoElement组件。这是表现型包装器和小型控件的常规选择#[derive(IntoElement)] struct EmptyState { title: SharedString, } impl RenderOnce for EmptyState { fn render(self, _: mut Window, cx: mut App) - impl IntoElement { div() .v_flex() .gap_2() .items_center() .text_color(cx.theme().muted_foreground) .child(self.title) } }用EntityT处理保留行为当行为跨帧存在或需要观察observation、订阅、焦点、异步工作、历史、测量或增量更新时使用实体支撑的Render视图。实体应存储在拥有它的视图中而不是在render里重建struct SearchView { query: EntityInputState, } impl SearchView { fn new(window: mut Window, cx: mut ContextSelf) - Self { let query cx.new(|cx| InputState::new(window, cx).placeholder(Search…)); Self { query } } }不要把所有视觉碎片都变成实体。实体边界有生命周期与协调成本只在保留身份retained identity真正重要时使用。仓库中Input、Select、Combobox、Slider、DatePicker、Calendar等都是有状态的组件持有Entity...State对应 crates/component/src 下的input、select、combobox等模块而Button、Checkbox、Switch、Radio、Badge等则是无状态的RenderOnce控件。元素、视图与行为系统是不同的事物不要把所有组件塞进同一种模板。生态中存在四类语义元素如Button、Checkbox、Link、Tabs复合行为根如Dialog、Popover、Select、Combobox实体支撑的系统如Input、Table、Tree、Dock、通知基础设施定位positioning、虚拟化、滚动、焦点陷阱、动效、历史、测量。一个元素内部可能很复杂但对调用方依然是值类的一个带状态的系统可能暴露 render 回调让应用拥有表现而不必重实现行为。公共接缝应当由行为决定而不是由它的 renderer 里有多少个div决定。状态所有权把每份状态放进能保持它正确的最小所有者领域状态 → 模型或特性视图瞬态视图状态 → 渲染它的那个视图可复用行为状态 → 为该行为设计的组件状态极小的元素局部状态 → GPUI 键控元素状态keyed element state共享的应用级服务 → GPUI globals。普通选择与开关优先使用受控值把当前值传入组件收到变更请求后更新所有者再渲染一次。回调只报告意图不应创建第二个隐藏的数据源Checkbox::new(show-hidden) .checked(self.show_hidden) .label(Show hidden files) .on_click(cx.listener(|this, checked, _, cx| { this.show_hidden *checked; cx.notify(); }))在 crates/component/src/checkbox.rs 中可以看到该 API 的实际签名new(id)接受ElementIdchecked(bool)与label(...)返回Selfon_click的回调接收bool——值由所有者驱动回调只报告意图与上述模式完全对应。配套规则变更会影响渲染后调用cx.notify()语义事件用cx.emit(...)交给所有者处理生命周期应跟随某个实体时用cx.subscribe(...)或cx.observe(...)需要保持返回的订阅存活时务必保留它不要因为读了一下值就 notify避免在render中无条件通知——那会再排一次渲染可能造成永久重绘循环多个字段构成一个不变量时一起更新、只通知一次一个无法拿到上下文的可复用状态类型必须明确说明这一限制并要求其所有者代为 emit/notify。避免状态反馈循环文本输入、选择、过滤器与受控弹层通常有两条路径外部所有者更新值以及用户交互请求新值。不要在同步过程中把所有者提供的值原路塞回用户回调。要追踪来源或比较一致的快照让每个逻辑变更只上报一次。当回调可能同步关闭、替换或更新调用它的组件时回调必须可重入安全re-entrancy-safe。稳定身份ElementIdElementId是行为的一部分。它给元素稳定身份并为元素局部或组件状态提供键组件也可以把它作为焦点、测量或动画身份的输入之一焦点与滚动仍由各自的专用句柄拥有。行、标签页、树节点与重复控件使用稳定的领域 ID同一控件重复出现时用其所属对象为子 ID 加命名空间绝不要从被翻译的标签或可插入/重排的列表索引派生身份不要在render期间生成新鲜随机 ID。Button::new((delete-project, project.id)) .danger() .label(Delete)ID 改变就意味着 UI 身份改变——把这种重置视为有意为之。同一规则适用于过渡通道transition channels、overlay token、滚动句柄与持久化 ID两个各自保留行为的行为体共享同一个键就会互相覆盖状态一个行为每帧更换键就永远累积不了状态。渲染与组合保持render声明式读取当前状态、推导表现值、组合元素。领域操作、解析与非平凡变更移到具名方法或服务里impl Render for ProjectView { fn render(mut self, _: mut Window, cx: mut ContextSelf) - impl IntoElement { div() .v_flex() .size_full() .child(self.render_toolbar(cx)) .child(self.render_content(cx)) } }当 helper 能命名一个有意义的区域、并减少读者需要同时记住的状态量时抽一个 render helper当区域有自己可复用的契约或保留生命周期时抽一个新组件——仅仅因为 builder 链很长而抽取是不够的一致地使用 GPUI Component 的流畅 traitSizable、Disableable、Selectable以及各组件自己的 builder小的条件修饰用.when(...)/.when_some(...)分支代表差异较大的接口时用普通 Rust 控制流先从标准语义组件组合起不要为了匹配某张截图而用通用div重造菜单、下拉、选择或命令面板——复用组件才能保住它的项几何、焦点转移、键盘导航、选择、禁用态、关闭与无障碍契约如果标准组件表达不了某个反复出现的合法模式应当改进它的显式 API而不是在每个调用点去样式化任意后代应用代码提供的render 回调应当无副作用列表项渲染器、菜单 builder、dock 面板渲染器可能在所有者需要测量或重绘时被反复调用绝不能执行业务操作、追加数据或注册无界订阅。行为与表现边界Base 层最持久的规则是Base 拥有可复用行为及其所需的几何表现层拥有产品的视觉语言。无头headless不等于一个空div。弹层碰撞检测、键盘导航、编辑、虚拟化、resize 运算、焦点陷阱与 dock 调和reconciliation都需要内部结构与状态。把这些工作搬给每个调用方不会换来灵活性只会复制脆弱的行为。反过来Base 不得选择品牌色、字体、密度、最终图标、组件变体或应用组合。表现应通过Styled、类型化的语义状态样式、显式部分parts、子槽child slots与项渲染器暴露。不要通过检查任意后代来发现标题、描述或关闭按钮——让语义部分显式化。主题与样式从当前主题读取语义值用 GPUI 的Styled方法布局div() .bg(cx.theme().background) .text_color(cx.theme().foreground) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius)规则不要硬编码产品颜色、圆角、间距或控件几何应用代码不得引入裸 hex、rgb/rgba、hsla——从cx.theme()读语义色或在产品主题里补充缺失的角色应用布局应使用 GPUI 的 rem 尺度辅助方法p_2()、gap_3()、w_64()、text_sm()而不是直接用px(...)语义 token 表达含义而不是调色板位置与状态无关的几何放进普通 builder 链运行时交互态用 GPUI 的hover、active、focus、focus_visible修饰符选中/按下/禁用外观用组件的语义状态样式弹层所有权放在显式状态里让 trigger 在关闭前能渲染打开/按下外观禁用控件不应响应 hover/active 修饰要做好守卫组件变体保持少而有意义不要为每个调用点加变体主样式绑定到决策区域真实的默认提交与 Enter 动作不要从动作数量、频率或工具栏位置推导Badge 与 Alert 变体保持语义化且克制普通元数据保持中性。样式生效优先级为实例样式 → 激活的语义值状态 → 禁用状态 → GPUI 运行时交互修饰。后面的层只替换它们设置的字段。新的应用级表现优先使用Theme::semantic_tokens()。语义 token 面包含通用颜色角色以及圆角、间距、排版、阴影尺度刻意不含组件名见 crates/base/src/theme_tokens.rs 中SemanticThemeTokens的字段colors、radius、spacing、typography、shadow。旧有的组件专属主题值仅为兼容保留不应成为每个应用控件的扩展点。有一个所有权上的注意点Theme::spacing_tokens()投影的是默认尺度而Theme::apply_semantic_tokens(...)不会存储自定义的 spacing 或 elevation 尺度。自定义这些尺度的应用必须自己保留SemanticThemeTokens或更窄的设计系统状态并使其对组件可用。不要写一份自定义 spacing 快照进全局主题然后指望之后的cx.theme().semantic_tokens()把它读回来。如果代码直接改动了全局 GPUI Component 主题之后要调用Theme::sync_base(cx)让 Base 拥有的滚动条与 resize 手柄收到新的投影Theme::change(...)会在一次完整主题变更中自动完成这一投影crates/component/src/theme/mod.rs 中可以看到它把base_theme写回全局并调用window.refresh()的完整流程sync_base在 L328-L333 会重建整个 Base 主题。向外扩展的焦点环需要物理空间带overflow_hidden()的祖先会把它裁掉。优先选择留出空间的布局如果产品必须重度裁剪用主题的焦点环策略并保留聚焦边框而不是默默隐藏所有键盘焦点。Base 字体是应用缩放控制Root::render调用window.set_rem_size(cx.theme().font_size)见 crates/component/src/root.rs。因此主题的基础字体不仅是正文字体更是应用 rem 设计尺度的参考长度。这刻意借鉴了 Tailwind 模型中有用的部分具名的类型/间距/尺寸阶梯共享一个相对基准而不是变成互不相关的像素常量。修改缩放 更新基础字体并刷新窗口Theme::global_mut(cx).font_size px(18.); Theme::sync_base(cx); window.refresh();基础字体本身是像素值因为它锚定整个尺度。其下的应用 UI 应使用相对辅助方法——text_sm()、gap_2()、px_3()、h_8()、size_4()——让文字、空白、控件与图标一起响应缩放。若某个自定义组件把 rem 文本与固定像素 padding 或图标几何混用必须说明该部分为何不应缩放。把应用 UI 中每个直接的px(...)和裸颜色构造都当作评审发现。只有在有文档记录的物理/平台边界、运行时测量几何、栅格/数据颜色或主题/token 定义本身时才可接受。方便与匹配截图都不是合法例外。任何从已解析布局缓存下来的东西其失效键都必须包含window.rem_size()直接或通过随它变化的修订号。这包括换行行高、文本 shaping/layout、虚拟列表测量、弹层与对话框几何、从文本派生的图标尺寸、自定义 canvas 度量。Command 组件的可变行高就是一个生态示例基础字体变大后同一固定宽度换行方式不同所以它在 rem 变化时会重新测量。不要把这个应用级缩放与Dock 面板缩放混淆。Dock 缩放是有状态布局操作让某个标签组或 tile 填满 DockArea同时保留容器 chrome 与退出方式。它绝不能修改窗口 rem 尺寸。事件、动作与焦点指针专属行为用 pointer 回调需要支持键绑定、菜单或多个输入源分发的命令用GPUI Actions。动作处理器放在拥有该命令的视图附近。一个逻辑桌面命令只建模一次工具栏Button、DropdownMenu项、ContextMenu项、菜单栏项与键绑定应分发同一个 Action 或调用同一个所有者方法而不是复制五份变更。可行时从一个命令策略command policy派生它们的标签、图标、快捷键与可用状态让所有入口无法互相矛盾。菜单拥有导航与关闭特性所有者仍然拥有命令是否允许、做什么。在元素选择上保留语义角色。即使想要安静的视觉处理也请用Button——选择outline、ghost或图标形态而不要用Link顶替。GPUI Kit 应用把Link保留给浏览器或邮件客户端打开的目标URL、网页文档、邮箱地址应用内跳转用相关导航组件命令用Button/Action。这是产品约定不是gpui_kit::base::Link的限制——它的open_with接缝可以把目标路由到别处。只有嵌套交互必须阻止父级处理同一事件时才停止传播。一刀切的传播停止会在难以诊断的方式下破坏菜单、选择、拖拽与窗口级命令。让焦点所有权显式在拥有键盘交互的实体里保留一个FocusHandle在适当的聚焦区域注册键上下文与动作打开 overlay 时转移焦点关闭时恢复渲染可见的focus_visible状态不要在render中无条件请求焦点。把key_context与其on_action处理器挂在同一个聚焦区域上。绑定是上下文相关的注册了 Action 却没有预期的焦点路径键盘交互就不成立。复合控件应实现完整的导航模型——方向键移动、合适的 Home/End 或翻页、确认、取消与 Tab 行为——而不是几个孤立的快捷键。模态表面必须陷阱焦点并在关闭时恢复到之前有效的焦点目标。嵌套 overlay 从顶层关闭。处理快速关/开序列时不要经由一个正在关闭的中间表面去恢复焦点。异步工作与副作用从事件、生命周期钩子或具名方法启动异步工作而不是把它变成render的无条件副作用工作不应让已关闭的视图存活时捕获弱实体weak entities任务完成时通过 GPUI 上下文更新状态处理实体或窗口已不存在的情况并在一次连贯的状态变更后只通知一次。用显式状态表示异步操作idle、loading、loaded、failed。刷新时尽量保留可用的旧数据。阻止重复的破坏性提交并在 UI 中呈现可恢复的错误——不要把日志当作给用户的反馈。昂贵解析或计算用后台执行器但 GPUI 实体变更保持在合适的应用上下文上。结果可能晚于请求、文档、视图或选择发生变化——附加修订号或身份并拒绝过期工作而不是把它应用到新状态上。布局、测量与滚动h_flex在交叉轴上居中子项v_flex保留 flexbox 默认的stretch。这与 Zed 的h_flex一致一行图标加标签就该居中所以它们看不出问题但一行全高列不是这样——放进裸h_flex的列不会撑满行高比行高的列被居中后顶部通常是头部会被裁出窗口顶部而列旁边没有任何线索说明原因。子项是列的行要说items_stretch()h_flex() .items_stretch() .size_full() .child(sidebar) .child(content)大多数 UI 应该用 GPUI 布局而不是自己测量。测量是弹层、虚拟化、编辑器、resize 手柄、图表等正确性依赖已解析几何的组件的深层行为工具测量与几何放在拥有该行为的层只有普通布局表达不了关系时才在 prepaint 观察 bounds绝不每个 prepaint 都变更无关的应用状态把测量数据视为帧级或修订级作用域排版、rem 尺寸、宽度、主题或内容变化后它可能过期集中共享几何如弹层翻转与视口夹取让每个 overlay 遵循同一边缘策略。对齐不变量优先构造而非修正兄弟区域应消费同一个间距 token 或共享 inset而不是重复等价的字面量。对关键的重复边缘、列与间隙加几何断言或视觉回归覆盖。多测几种窗口形态rem 缩放与显示缩放会把小数坐标变成单物理像素漂移即使默认截图看起来对齐了。评审精度时测量已解析的结果但不要把测出来的修正编码成px(...)微调。追到重复 padding、嵌套 inset、边框归属、字体度量或舍入的源头然后修复结构性所有者。再次强调h_flex()与v_flex()不是镜像关系——前者居中子项后者让子项拉伸。放在行里的列因此取内容高度而非行高比行高的列被居中后头部被推出顶边裁掉。当子项拥有必须相对行高解析的头部、底部或滚动区域时给全高列加h_full()或给行加items_start()/items_stretch()。每个可滚动区域必须只有一个所有者。flex 布局中对允许收缩的弹性子项应用min_w_0()或min_h_0()——flex 项只有在自身 overflow 非 visible 时才会放弃基于内容的最小尺寸所以普通的弹性子项默认拒绝围绕长内容收缩直到你明确放行。Scrollable会为自己的 wrapper 处理这一点但它在 flex 容器之间的普通div仍然需要释放最小值。避免意外的嵌套滚动把滚轮输入路由到目标轴API 不可移植时保留平台/wasm 差异。把Scrollable附着在拥有整个面板、编辑器或窗口视口的元素上让滚动条相对区域边缘解析。内容 inset 放在该滚动所有者内部而不是用带 padding 的容器包住滚动所有者。滚动条漂浮在内容与面板边界之间通常意味着滚动所有者选错了或 padding 放错了层。列表、表格与大数据数据可能超过小型有界集合时使用虚拟化。行身份与可见位置分离避免每次 render 克隆整个数据集。让有状态的列表/表格拥有导航、选择、滚动协调与可见范围计算项渲染器只负责行表现。把以下内容分开源数据与领域 ID过滤/排序状态选择状态视口/滚动状态行渲染。这使更新局部化防止视图树变成数据模型。仓库中VirtualListcrates/component/src/virtual_list.rs、List/ListDelegate、DataTable/TableDelegate、Tree都遵循这一结构——delegate 或 provider 是可插拔数据/行为所有者的命名模式。虚拟化是行为契约不只是性能开关。宽度、排版、rem 尺寸或行内容变化时必须使项测量失效键盘选择与滚动到项必须在模型坐标中操作即使当前帧里大多数元素并不存在。公共 API 设计可复用组件应遵循构造函数建立有效默认值builder 接收并返回Self使用领域语言回调描述请求的变更只在修饰键或指针细节有意义时才包含指针事件可演化的行为接缝私有字段 构造 builder 读取用 reader布尔 reader 用is_或has_与同名 builder 对应非布尔 setter 用with_让 reader 保留纯字段名显式复合部分优先于检查任意后代新增可复用行为不得强迫产品级视觉选择。私有字段是行为状态的默认它让行为无需破坏调用方即可演化。公共字段只适合刻意记录式的配置、主题 token、几何与序列化 schema。每个带公共字段的公共 struct 都必须带#[non_exhaustive]并提供构造函数、Default或 builder让调用方无需穷举 struct 字面量也能构造值——这保留了未来加字段而不破坏调用方的能力。保持公共模块路径稳定用带刻意 re-export 的模块接缝重组内部让文件夹变化不强迫下游 import 变化。优先采用平台控件术语与项目既有命名而不是 web 框架词汇。平台与能力边界不要假设每个原生或 web 目标都支持同样的设施窗口装饰、无障碍桥、系统通知、剪贴板行为、滚动手势、字体与计时都可能不同。把平台专属代码放在窄能力接缝之后并定义回退行为。平台分支即使表现不同也必须保住语义契约。例如系统通知的收回retraction支持可能不同但应用仍需连贯的投递状态。尽量同时测试共享状态机与平台适配器。文件与命名约定视图与实体按产品概念命名ProjectList、ProjectEditor、SettingsState事件处理器按意图命名confirm_delete、open_project、on_query_changed每个模块一个主要职责当状态所有权或生命周期不读无关行为就无法理解时拆分文件一起变化的组件模块、状态、事件与聚焦测试放在一起文档化不变量与令人意外的生命周期约束不要叙述显而易见的 builder 调用使用rustfmt并满足工作区 Clippy 规则避免用宽泛allow隐藏无关警告。词汇是 API 的一部分同一个概念在组件间使用同一个词。命名新方法前先在 GPUI、gpui-base与 GPUI Component 中搜索既有术语生态无先例时优先采用 macOS/Windows 控件术语。本地化文档在翻译会降低精度时保留精确的 API 标识符与既定 UI 框架术语。标识符用代码格式必要时解释保留术语不要为了显得专业而混用语言。概念命名模式示例值类渲染控件名词Button、Checkbox、Tab保留行为模型ControlStateInputState、TableState命令式共享引用ControlHandleDialogHandle、滚动句柄语义通知ControlEventTableEvent、SelectEvent键盘命令动词或意图名词Confirm、Cancel、SelectNext可插拔数据/行为所有者RoleDelegate/RoleProviderTableDelegate、CompletionProvider应用提供的表现render_part或part_rendererrender_item构造new或语义构造函数new、horizontal、vertical流畅属性名词/形容词label、disabled、selected、placement通用非布尔替换 builderwith_fieldwith_size、with_mode就地变更set_fieldset_items、set_selected_index布尔 readeris_形容词/has_名词is_open、is_closable、has_selection纯值 reader字段名词placement、selected_value回调注册on_事件或意图on_click、on_open_change渲染具名区域render_regionrender_toolbar、render_content新 API 的流畅 builder 省略set_消费并返回Self通过mut self变更用set_。更改既有公共名字会造成无谓搅动时保留它们。set_position这类旧 builder 名是兼容例外不是新 API 的模式。布尔 reader 二选一has_名词值持有某物或is_形容词描述状态或权限。有形容词就优先用形容词is_closable优于can_close、is_zoomable优于can_zoom、is_copyable优于can_copy。动作是没有形容词形式的动词短语时命名它需要的东西has_definition而不是can_go_to_definition。不要新增can_reader。布尔 builder 可以用字段名disabled(bool)reader 用is_disabled()。公共接缝 struct 含非布尔字段时构造用with_item_ix(...)、读取用item_ix()setter/getter 永不撞名。新局部或内部零基索引优先_ix保留selected_index这类既有公共术语不要引入_idx。调用方从不构造该接缝值时不要仅为对称发布 builder。让外层名称携带语境名字是在某个环境里被读到的字段在类型里、参数在方法里所以两者都不重复环境。with_item_ix(ix)而不是with_item_ix(item_ix)。一个类型的字段保持同一缩写层级。唯一一个拼全的字段会成为异类读者会去找使它不同的区别。因为 builder 叫with_field缩写字段会同步缩短它的 builder两者始终匹配。只在环境确实消歧时才缩写。当短形式在生态别处是另一个量的既定术语时在文档注释里说清指的是哪个——文档在调用点被阅读能解释长名字只能暗示的东西。使用精确的领域词selected是持久成员资格或活动项focused是当前键盘目标hovered是指针存在confirmed是激活结果。绝不可混用open/close描述 overlay 或展开状态show/hide是瞬态表现请求expand/collapse描述结构disabled阻止交互read-only允许导航与选择但阻止编辑loading在操作挂起时阻止重复工作index是当前位置坐标id是稳定身份IndexPath表示层级位置。不要按 index 持久化或键控可重排数据value是受控领域数据presentation是为渲染准备的只读快照state是保留行为placement是边或锚点策略position是已解析几何size是语义控件档位width/height/bounds是几何child/children遵循 GPUI 组合header、footer、trigger、content等具名槽位承载额外语义。避免data、item2、handle_action、update_ui、process、manager、config这类含糊公共名。Manager只在类型真正协调集合或生命周期时使用正如ToastManager。类型与模块风格Rust 类型与 Action 用UpperCamelCase模块、函数、方法、字段与局部变量用snake_case常量用SCREAMING_SNAKE_CASE以组件命名的模块拥有其公共接缝。内部文件夹可以拆分 state、element、geometry、platform adapter 与 tests而不把这些文件夹名泄漏进 import单一组件概念用单数模块名家族用生态既定名input、table、dock只有真正擦除类型边界时才对类型擦除包装加Any后缀如AnyInputState、AnyElement标识符加Id后缀零基索引加ix集合用有意义复数。不要在一个子系统里混用idx、index、ix谓词尽量正面命名。正面的enabled/visible契约比多个否定更易组合但保留disabled这类匹配控件语义的既定 API 术语。回调与事件措辞on_click只用于真正的点击级契约。Base 中的受控语义原语应优先on_change(next_value, ...)带样式的兼容组件在指针细节或既有 API 期望重要时可保留on_click。不要为模型驱动的变更发明ClickEvent。生命周期钩子命名要精确on_will_change可以否决或准备on_change观察请求/当前值契约on_confirm提交选择on_dismiss关闭瞬态表面。文档要说明回调运行在内部状态变更之前、之后还是替代它以及它是否可以同步重入组件。文档与文案风格公共文档以这个类型做什么、谁拥有它的状态开头。示例必须使用当前可编译的 API并展示稳定 ID。记录默认值、平台限制、焦点行为、回调顺序以及任何需要调用notify、emit或主题同步方法的要求。界面文案遵循设计指南的界面语言规则。每个领域对象、命令与状态只保留一个规范术语。翻译键描述稳定意图dialog.delete_project.title而不是源语言句子或屏幕坐标。绝不用翻译碎片组装句子也不要为恰好共享同一英文文本的不同含义复用同一个键。本地化的是意图而非语法让每个 locale 控制词序、复数、标点与所需语境。在组件内用真实数据审阅字符串。测试或 lint 应捕捉缺失键、英文资源中意外的 CJK 文本、三点省略号、未经审阅的全大写与不一致的固定术语人类审阅仍决定重复是否被语境证明合理。每条字符串都要在其组件内用真实内容、文本扩展与应用缩放验证。测试策略在能证明行为的最低层测试状态转移、几何、解析与排序的纯测试实体、事件与订阅的 GPUI 上下文测试使用VisualTestContext的交互测试焦点、键盘、指针、布局与渲染状态示例或应用冒烟测试完整工作流。交互组件要覆盖语义契约而非实现细节指针与键盘激活、受控值变更、禁用行为、焦点移动、事件次数/顺序、稳定身份、重要的空态与失败态。可确定性复现的 bug修复前先加回归测试。依赖真实窗口系统的 UI 行为通过无障碍树按角色、标签、值、启用状态、焦点与选择测试。每次改变状态的 action 后都要重新读取树因为元素索引是快照。语义树表达不了的视觉事实用截图坐标输入只作回退。自动化与人工证据分开报告。仓库中大量组件测试都走这条路线例如 crates/component/src/input/editor.rs 通过VisualTestContext::update在无头窗口内驱动绘制与输入再断言状态#[gpui_kit::test]宏运行测试gpui_kit::test模块操作与检查 UI见 crates/kit/src/lib.rs 与 skills/gpui-kit/references/gpui/test.md。性能规则不要在render中无条件变更状态或通知避免每帧重建实体、订阅、焦点句柄与昂贵数据结构一次连贯状态变更后通知最窄的拥有实体长集合虚拟化只渲染可见范围不要仅为满足闭包而克隆大字符串或集合——捕获稳定句柄或共享数据加缓存前先测量。缓存必须有清晰的失效所有者动画工作保持有界并尊重 reduced motion减弱动态效果偏好。常见失败模式一个实体装着整个应用互不相关的状态业务逻辑与网络请求嵌在超长render方法里对可重排内容使用随机或基于索引的ElementId字面量颜色与圆角破坏自定义主题在语义组件已提供焦点、键盘、禁用与无障碍行为的地方用自定义可点击div复制的局部状态与受控模型值漂移每次 render 都变更导致cx.notify()循环无明确所有者的嵌套滚动容器为一屏一次性场景新增组件变体对可逆、低风险操作弹确认对话框测试只调内部方法从不演练键盘或指针行为。面向编码 Agent 的规则编辑前Agent 必须阅读最近的实现、它的测试、re-export 接缝与相关组件文档。必须在当前源码中搜索真实签名而不是凭 React、CSS 或旧版 GPUI 示例类推翻译——一个看起来合理却不存在的方法是最常见的失败。每次变更Agent 应能说出行为所有者与表现所有者保留身份与状态生命周期指针、键盘、焦点与无障碍契约布局与 overflow 所有者主题 token 与有意的例外行为回归时会失败的测试。生成的代码必须由人来评审与测试。能编译不是 UI 质量线一个只让生成代码看起来整洁的宽泛重构不能替代与仓库架构的匹配。实现检查清单在打开变更供评审前确认状态与副作用所有权显式RenderOnce与EntityT是深思熟虑的选择重复元素有稳定的领域 ID主题 token 与组件尺寸取代孤立的视觉字面量键盘动作、焦点、禁用态与 overlay 协同工作loading、empty、error 与 cancellation 路径都已表示大数据集使用合适的虚拟化组件公共 API 新增保持依赖方向与封装测试在合适层证明行为格式化、Clippy、针对性测试与相关示例全部通过。应用搭建方式见快速开始组件页提供当前 API 细节。需要设计决策选型、布局、间距、层级、颜色、密度、交互态、overlay、动效、界面文案时先读设计指南。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表