gpui-kit 无样式 Accordion 原语:基于 GPUI Base 构建可访问、可控状态的折叠面板组件
2026/9/15 12:46:27 网站建设 项目流程

gpui-kit 无样式 Accordion 原语:基于 GPUI Base 构建可访问、可控状态的折叠面板组件

【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit

导读

Accordion(折叠面板)是桌面应用中组织内容、节省纵向空间的常见交互组件。gpui-kit 的 Base 层为它提供了一套"只负责行为与语义、不规定任何视觉"的无样式原语(unstyled primitives),由AccordionAccordionItemAccordionHeaderAccordionTriggerAccordionPanel五个公开类型组合而成。本文将基于 website/zh-CN/base/primitives/accordion.md 文档,结合 crates/base/src/accordion.rs 源码与 showcase 示例,讲解如何导入、组合、受控管理状态,并打通键盘操作与无障碍语义,让你能在自己的设计系统中直接复刻这套折叠面板。

设计定位:行为与语义分离,视觉完全交给应用

GPUI Base 原语的设计哲学是:组件只提供行为结构和语义结构,不内置任何产品视觉语言。这一点在 Accordion 上体现得尤为彻底——源码文档注释开宗明义地写着 "An unstyled accordion root for application-owned items"(一个无样式、应用自持条目的折叠根节点),见 crates/base/src/accordion.rs。

落到实践中意味着:

  • 标题、触发器的外观(字号、颜色、边框、图标、悬停态)全部由你的样式代码决定;
  • 展开/收起动画、图标旋转、间距等视觉细节由应用层实现;
  • Base 层只负责:受控展开状态、点击回调、aria-expanded等无障碍状态、标题与区域的语义角色、面板的条件挂载。

因此文档强调:请使用 GPUI 样式(Styledtrait 提供的一系列方法)并组合导出的部件,使其符合你的设计系统。

导入与运行示例

导入公开类型

use gpui_kit::base::{Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger};

这五个类型从crates/base/src/lib.rspub use accordion::{...}统一导出(见 crates/base/src/lib.rs)。

运行原生示例

Accordion 的示例位于 showcase 组件集中,原生与 WASM 预览共用同一份实现(同一文件被两条构建路径引用)。本地运行:

cargo run -p gpui-base-examples -- accordion

这条命令对应的二进制是 crates/base/examples/native/src/bin/components.rs,它把命令行第一个参数作为要展示的组件名传给showcase::run;包名定义在 crates/base/examples/native/Cargo.toml(name = "gpui-base-examples",默认运行components这个 bin)。Accordion 被注册在 showcase 的组件清单中(crates/base/examples/showcase/mod.rs),并在页面渲染的分发逻辑中映射为self.accordion(cx)(见 crates/base/examples/showcase/mod.rs)。

启动后会打开一个居中窗口,左侧目录里选择 "accordion" 即可看到示例页;如果只输入cargo run -p gpui-base-examples不传参数,则默认进入overview概览页。

结构模型:五个部件各司其职

从源码的类型设计(crates/base/src/accordion.rs)可以清晰梳理出五层结构:

类型职责关键方法
Accordion折叠组根节点,容器角色new(id),实现Styled/ParentElement/InteractiveElement,渲染为Role::Group
AccordionItem一个"触发器 + 面板"的条目open(bool)disabled(bool)header(...)panel(...)
AccordionHeader标题容器,拥有触发器new(trigger)level(usize)id(...),渲染为Role::Heading并带aria_level
AccordionTrigger可点击、可聚焦的开关new(id)open(bool)disabled(bool)on_change(...),渲染为Role::Button并投影aria-expanded
AccordionPanel展开时挂载的内容区open(bool)keep_mounted(bool)id(...),渲染为Role::Region

几个值得注意的实现细节:

  1. 根节点与触发器是有状态元素AccordionAccordionTrigger内部都包裹了ObservedElement<gpui::Stateful<Div>>,因此可以响应点击、跟踪焦点(track_focus),并且配合test_support()获得测试辅助能力。
  2. Header 默认标题级别为 3AccordionHeader::new的默认level3(crates/base/src/accordion.rs),可用.level(2)等按文档大纲需求调整,最终通过aria_level暴露给辅助技术。
  3. Trigger 激活语义是"请求相反状态"。源码中let next_open = !self.open;(crates/base/src/accordion.rs),即点击时把"当前状态取反"作为请求结果交给on_change,实际是否更新由应用决定——这正是受控组件的标志。
  4. Panel 支持条件挂载。默认keep_mounted = false,当!open && !keep_mounted时渲染为gpui::Empty(完全不进入元素树,见 crates/base/src/accordion.rs);如果你希望面板内容在关闭时仍保留在树中(例如为了缓存滚动位置或做展开动画),用.keep_mounted(true)

状态与事件:受控模式是唯一推荐路径

文档明确指出:受控状态应保存在父渲染类型或 GPUI entity 中。每次触发器的on_change回调收到"下一次展开状态"后,应用在回调中更新自己的状态字段,并调用cx.notify()触发重渲染;不要在渲染函数里每次重建持久 entity——那会丢失焦点与内部状态。

从示例 crates/base/examples/showcase/components/accordion.rs 可以看到完整的受控闭环:

let open = self.accordion_items[index]; let entity = cx.entity().downgrade(); AccordionItem::new() .open(open) .header(AccordionHeader::new( AccordionTrigger::new(format!("accordion-trigger-{index}")) .on_change(move |next, _, _, cx| { _ = entity.update(cx, |this, cx| { this.accordion_items[index] = next; cx.notify(); }); }) // ... 样式链 )) .panel(AccordionPanel::new() /* ... */ .child(answer))

状态字段accordion_items: [bool; 3]定义在 showcase 结构体里(crates/base/examples/showcase/mod.rs),初始值为[true, false, false](第一个面板默认展开,见 crates/base/examples/showcase/mod.rs)。关键点:

  • on_change的签名Fn(bool, &ClickEvent, &mut Window, &mut App),第一个bool即"请求的下一状态"(源码见 crates/base/src/accordion.rs);
  • 回调里通过entity.update写回父级状态并cx.notify()
  • cx.entity().downgrade()捕获弱引用,避免回调持有父实体造成引用环;
  • 由于open是受控的,AccordionTriggerAccordionPanel接收到的open永远来自应用状态,渲染结果与状态一致。

样式组合:触发器与面板的完整示例链

示例展示了如何在不触碰语义结构的前提下,用 GPUI 样式方法把原语"装修"成可用的 UI(完整源码见 crates/base/examples/showcase/components/accordion.rs):

Accordion::new("example-accordion") .w(px(270.)) .border_t_1() .border_color(example_rgb(0xd4d4d4)) .children( items.into_iter().enumerate().map(|(index, (question, answer))| { let open = self.accordion_items[index]; // ... AccordionItem::new() .open(open) .header(AccordionHeader::new( AccordionTrigger::new(format!("accordion-trigger-{index}")) .on_change(...) .w_full().flex().items_center().justify_between() .h_7().border_b_1().border_color(example_rgb(0xd4d4d4)) .text_xs() .child(question) .child(div().text_color(example_rgb(0x737373)) .child(if open { "−" } else { "+" })), )) .panel( AccordionPanel::new() .px_1().py_1().border_b_1() .border_color(example_rgb(0xd4d4d4)) .text_xs().text_color(example_rgb(0x525252)) .child(answer), ) }), )

这段代码传达了几个可复用的模式:

  • Root 用稳定 Element IDAccordion::new("example-accordion")AccordionTrigger::new("accordion-trigger-{index}")。文档"注意事项"特别提醒:在支持的位置使用稳定元素 ID,这在测试、焦点管理和无障碍树里都依赖它。
  • 展开指示符由应用绘制:示例用文本/+表达展开/收起,你完全可以用图标组件替换——这正是"视觉属于应用"的体现。
  • Trigger 自己决定布局flex+justify_between让标题文本与指示符分居两端,h_7控制行高,均为 GPUIStyled方法。
  • Panel 通过.open(open)同步受控状态:关闭时该面板直接从元素树卸载(未启用keep_mounted时)。

键盘操作与可访问性

文档对 Accordion 的可访问性要求可以总结为三点:触发器可聚焦、可用键盘操作、向辅助技术暴露展开状态。对照源码,这三点的实现依据是:

  1. 可聚焦AccordionTrigger实现InteractiveElement并提供track_focus(handle)(crates/base/src/accordion.rs),应用可把FocusHandle绑定到触发器上纳入焦点管理。
  2. 键盘操作:触发器渲染为Role::Button,天然继承 GPUI 对按钮的键盘激活语义(空格/回车触发点击)。
  3. 暴露展开状态:渲染时aria_expanded(self.open)直接把受控的open状态写入无障碍树(crates/base/src/accordion.rs)。

结构语义同样完整:AccordionHeaderRole::Heading+aria_level暴露标题层级(crates/base/src/accordion.rs),AccordionPanelRole::Region暴露内容区(crates/base/src/accordion.rs),根节点以Role::Group组织整组(crates/base/src/accordion.rs)。

这些语义有单元测试背书。在 crates/base/src/accordion.rs 的tests模块中:

  • trigger_projects_expanded_accessibility_state:断言触发器无障碍节点角色为Buttonis_expanded() == Some(true),同时确认仅渲染(不点击)不会触发on_change
  • pointer_requests_next_controlled_state_and_respects_disabled:通过simulate_click验证点击open=false的触发器会请求true,而disabled=true的触发器点击后不会产生任何请求——即禁用状态在 Base 层就被拦截;
  • header_and_panel_project_structural_roles:验证 Header 与 Panel 分别投影HeadingRegion角色。

注意事项与消费端验收清单

文档最后给出两条工程化建议,结合源码可以做更具体的落地:

  1. 使用稳定元素 IDAccordionAccordionTrigger、以及可选的AccordionHeader::id(...)AccordionPanel::id(...)都接受ElementId。稳定的 ID 让 GPUI 的元素差异比对(diffing)在重渲染时保持一致,避免焦点与动画状态漂移。
  2. 在消费端设计系统中验证完整状态矩阵。由于 Base 层不提供视觉,你需要自行确认以下状态在你的主题下都成立:
    • 悬停(hover)、按下(active)、聚焦(focus)与聚焦可见(focus-visible)的样式差异;
    • 选中/展开态的视觉反馈;
    • 禁用(disabled(true))态的弱化样式——注意禁用逻辑在 Base 层已实现(点击不会产生变更请求),但视觉反馈需要你画;
    • prefers-reduced-motion(减少动态效果)下的降级,尤其是如果你用keep_mounted(true)配合展开动画时;
    • 高对比度(high contrast)主题下的边框与文本可读性。

总结

gpui-kit 的 Base Accordion 是一组把"交互逻辑"与"视觉呈现"完全解耦的原语:受控的open状态、请求式的on_change回调、aria-expanded/Heading/Region/Group语义角色、可选的keep_mounted挂载策略,以及开箱即用的禁用拦截,全部在 crates/base/src/accordion.rs 这一个文件里实现完毕。你要做的只是导入五个类型、把状态放进自己的 entity、再用 GPUI 样式画出你的设计语言。文档原文与可运行示例位于 website/zh-CN/base/primitives/accordion.md 和 crates/base/examples/showcase/components/accordion.rs,动手跑一遍cargo run -p gpui-base-examples -- accordion即可获得最直观的感受。

【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询