GPUI Kit 菜单组件实战指南:使用 PopupMenu 构建上下文菜单、下拉菜单与子菜单系统
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
导读
本文是 GPUI Kit(基于 GPUI 的跨平台桌面 UI 组件库)中 Menu 组件的完整实战指南。Menu 组件提供上下文菜单(右键菜单)与弹出式菜单(下拉菜单),支持图标、快捷键、子菜单、分隔线、勾选状态与自定义元素,并内置了无障碍支持与完整键盘导航。读完本文,你将掌握PopupMenu、PopupMenuItem、ContextMenuExt、DropdownMenu四大核心 API 的组合用法,并理解菜单与 GPUI Action/按键绑定系统深度集成的设计原理,能够为你的桌面应用快速搭建专业、可访问的菜单系统。
菜单组件总览:一个弹出内核,四种打开方式
在 crates/component/src/menu 目录下,菜单系统由五个源文件组成,其中popup_menu.rs是绝对核心:
| 文件 | 导出类型 | 职责 |
|---|---|---|
| popup_menu.rs | PopupMenu、PopupMenuItem | 菜单的渲染、键盘导航、动作分发与子菜单管理 |
| context_menu.rs | ContextMenu、ContextMenuExt | 右键上下文菜单的扩展 trait |
| dropdown_menu.rs | DropdownMenu | 按钮触发的下拉菜单扩展 trait |
| menu_item.rs | MenuItemElement(内部) | 单个菜单项的样式与交互渲染 |
| app_menu_bar.rs | AppMenuBar | Windows / Linux 下的应用级菜单栏 |
所有公共类型统一在 mod.rs 中重新导出。PopupMenu本身是一个Entity(视图实体),内部持有menu_items: Vec<PopupMenuItem>列表、focus_handle、action_context焦点句柄以及min_width / max_width / max_height / check_side / scrollable / external_link_icon等配置项(见 popup_menu.rs)。
一个值得注意的设计是:PopupMenu通过key_context("PopupMenu")注册按键上下文,并在init中绑定 Enter / Escape / 上 / 下 / 左 / 右 六个按键(popup_menu.rs),因此所有菜单形态(右键菜单、下拉菜单、子菜单)共享同一套键盘导航实现。
导入方式
use gpui_kit::component::{ menu::{PopupMenu, PopupMenuItem, ContextMenuExt, DropdownMenu}, Button }; use gpui_kit::{actions, Action};gpui_kit是应用入口门面 crate,gpui_kit::component即gpui-component(见 crates/kit/src/lib.rs),其中的actions!宏与Actiontrait 也已通过门面重新导出(crates/kit/src/lib.rs),应用无需直接依赖 gpui 即可定义动作。
上下文菜单 ContextMenu:右键即出
上下文菜单在元素上右键点击时出现,通过ContextMenuExttrait 的context_menu方法挂载。该方法会把目标元素改为relative定位,并追加一个absolute定位的ContextMenu子元素,因此不会影响父布局(context_menu.rs):
use gpui_kit::component::menu::ContextMenuExt; div() .id("my-element") .child("Right click me") .context_menu(|menu, window, cx| { menu.menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .menu("Delete", Box::new(Delete)) })从实现上看,context_menu会在元素稳定 ID 的基础上生成context-menu-{id}的子 ID,保证跨渲染的元素状态(菜单是否打开)不丢失;没有显式 ID 时则回退到调用处的代码位置(CodeLocation)。右键事件处理中,菜单通过window.defer在下一帧构建,以规避竞态条件,并在关闭时把焦点恢复到打开前的焦点句柄(context_menu.rs)。
下拉菜单 DropdownMenu:按钮触发
下拉菜单由按钮等可交互元素触发,通过DropdownMenutrait 提供。默认锚定在触发元素的左上角(Anchor::TopLeft,见 dropdown_menu.rs):
use gpui_kit::component::popup_menu::{PopupMenuExt as _, PopupMenuItem}; let view = cx.entity(); Button::new("menu-btn") .label("Open Menu") .dropdown_menu(|menu, window, cx| { menu.menu("New File", Box::new(NewFile)) .menu("Open File", Box::new(OpenFile)) .link("Documentation", "https://gpui-kit.com/") .separator() .item(PopupMenuItem::new("Custom Action") .on_click(window.listener_for(&view, |this, _, window, cx| { // Custom action logic here }) ) .separator() .menu("Exit", Box::new(Exit)) })实现上,DropdownMenu内部基于Popover封装,且只有Button默认实现了该 trait(impl DropdownMenu for Button {},dropdown_menu.rs)。它有一个关键优化:PopupMenu实体只创建一次并存入 keyed state 复用,只有收到DismissEvent时才会销毁,从而支持下次打开时动态重建菜单项(dropdown_menu.rs)。此外还可以通过on_open_change回调监听菜单的开关状态:
Button::new("menu-btn") .label("Options") .dropdown_menu(|menu, window, cx| { /* ... */ }) .on_open_change(|open, window, cx| { // open 为最新的打开状态(true / false) })锚点位置控制
dropdown_menu_with_anchor允许你控制菜单相对触发元素出现的位置:
use gpui_kit::Anchor; Button::new("menu-btn") .label("Options") .dropdown_menu_with_anchor(Anchor::TopRight, |menu, window, cx| { menu.menu("Option 1", Box::new(Action1)) .menu("Option 2", Box::new(Action2)) })Anchor支持TopLeft、TopRight等各角锚点,且菜单渲染时自带snap_to_window_with_margin贴窗逻辑,靠近窗口边缘时会自动翻转方向,不会跑出屏幕。
为什么用 Action 定义菜单项
文档中特别强调了一个设计理念:每个菜单项与一个Action关联,这是为了让菜单更好地融入 GPUI 的 action 与按键绑定系统——菜单项会自动展示对应的键盘快捷键。因此:
Action是定义菜单行为的推荐方式。菜单在渲染时调用Kbd::binding_for_action_in从action_context的焦点路径查找按键绑定,找不到时回退到应用级绑定(popup_menu.rs),这正是"自动显示快捷键"的底层机制;- 如果你不想为某个菜单项定义 Action,可以用
item方法配合PopupMenuItem直接挂on_click回调。
手动构建菜单项:PopupMenuItem
PopupMenuItem是一个枚举,覆盖了菜单中出现的所有条目类型(popup_menu.rs):
| 变体 | 含义 |
|---|---|
Separator | 分隔线 |
Label(SharedString) | 非交互的标题文本 |
Item { ... } | 标准菜单项(图标 / 标签 / 禁用 / 勾选 / Action 或点击回调) |
ElementItem { ... } | 自定义元素渲染的菜单项 |
Submenu { ... } | 打开另一个PopupMenu的子菜单项 |
PopupMenuItem提供了new、element、submenu、separator、label、link六个构造器,以及icon、action、disabled、checked、on_click五个链式配置方法。其中on_click的签名是Fn(&ClickEvent, &mut Window, &mut App),并会通过window.listener_for与视图绑定使用。不带 Action 的手动菜单项示例:
use gpui_kit::component::{menu::PopupMenuItem, Button}; Button::new("custom-item-menu") .label("Options") .dropdown_menu(|menu, window, cx| { menu.item( PopupMenuItem::new("Custom Action") .disabled(false) .icon(IconName::Star) .on_click(|window, cx| { // Custom click handler logic println!("Custom Action Clicked!"); }) ) .separator() .menu("Standard Action", Box::new(StandardAction)) })注意on_click与action同时存在时,点击会优先执行handler回调(见 popup_menu.rs 的confirm逻辑)。
图标:让菜单更清晰
menu_with_icon系列方法为菜单项添加图标,图标来自IconName枚举:
use gpui_kit::component::IconName; menu.menu_with_icon("Search", IconName::Search, Box::new(Search)) .menu_with_icon("Settings", IconName::Settings, Box::new(OpenSettings)) .separator() .menu_with_icon("Help", IconName::Help, Box::new(ShowHelp))图标实际渲染为Icon::new(...).xsmall(),并且整列菜单的图标列宽是自动对齐的——渲染时只要任一菜单项拥有左侧图标,所有菜单项都会为图标预留列宽(has_left_icon逻辑,见 popup_menu.rs),保证视觉整齐。
禁用状态
禁用项不可被点击、也不参与键盘导航(is_clickable会过滤掉disabled: true的项,见 popup_menu.rs),渲染时使用 muted 前景色:
menu.menu("Available Action", Box::new(Action1)) .menu_with_disabled("Disabled Action", Box::new(Action2), true) .menu_with_icon_and_disabled( "Unavailable", IconName::Lock, Box::new(Action3), true )PopupMenuItem上对应的链式方法是disabled(bool)。与之互补的menu_with_enable方法则接受enable: bool,内部取反后等价于menu_with_disabled(popup_menu.rs)。
勾选状态:Check State
带勾选的菜单项适合表达开关类状态,例如主题、侧边栏可见性等:
let is_enabled = true; menu.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) .menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar))默认情况下勾选图标显示在菜单项左侧;如果该菜单项本来有图标,左侧勾选图标会替换原图标(源码has_left_icon与render_icon的配合:有图标用图标,无图标且已勾选则渲染IconName::Check,见 popup_menu.rs 与 popup_menu.rs)。
你也可以把勾选图标放到右侧,通过check_side配置(注意:源码中的方法名是check_side,而非文档示例里的check_size):
menu.check_side(Side::Right) .menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature))check_side的默认值是Side::Left(见 popup_menu.rs)。右侧勾选图标渲染在标签之后、快捷键之前,同样为xsmall尺寸。
分隔线:分组菜单项
menu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() // Groups file operations .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() // Groups edit operations .menu("Exit", Box::new(Exit))源码对separator()做了两处智能处理,避免出现"悬挂"分隔线:
- 追加前校验:如果当前菜单项列表为空、或最后一项已经是分隔线,则直接返回、不再追加(popup_menu.rs);
- 渲染时过滤:如果最后一项恰好是分隔线,渲染时会被忽略(popup_menu.rs)。
分隔线在视觉上是一条 2px 的border_b,颜色取主题border(见 popup_menu.rs)。
标签:非交互的段落标题
menu.label("File Operations") .menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .label("Edit Operations") .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste))label创建不可交互的文本项(渲染为PopupMenuItem::Label),适用于把长菜单按逻辑段落分组,并配合separator()划分区块。
链接菜单项:打开外部链接
menu.link("Documentation", "https://docs.example.com") .link_with_icon( "GitHub", IconName::GitHub, "https://github.com/example/repo" ) .separator() .external_link_icon(false) // Hide external link icons .link("Support", "https://support.example.com")链接项的内部实现是PopupMenuItem::link(label, href),它在handler中直接调用cx.open_url(&href)打开系统浏览器(popup_menu.rs),因此无需定义 Action。默认情况下链接项会在标签右侧渲染一个IconName::ExternalLink小图标(muted 前景色),通过external_link_icon(false)可以隐藏该图标,external_link_icon默认值为true(见 popup_menu.rs)。
自定义元素菜单项
当标准"图标 + 文本 + 快捷键"布局无法满足需求时,使用menu_element系列方法注入任意 GPUI 元素。构建器闭包签名是Fn(&mut Window, &mut App) -> E,返回实现IntoElement的元素:
use gpui_kit::component::{h_flex, v_flex}; menu.menu_element(Box::new(CustomAction), |window, cx| { v_flex() .child("Custom Element") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("This is a subtitle") ) }) .menu_element_with_icon( IconName::Info, Box::new(InfoAction), |window, cx| { h_flex() .gap_1() .child("Status") .child( div() .text_sm() .text_color(cx.theme().success) .child("✓ Connected") ) } )源码中menu_element一族方法包括:menu_element(纯自定义元素)、menu_element_with_icon、menu_element_with_check(带勾选状态)、menu_element_with_disabled,它们最终都汇聚到menu_element_with_check_and_disabled内部实现(popup_menu.rs)。自定义元素同样受禁用、勾选、键盘导航等统一行为约束。
键盘快捷键:Action 的自动展示
菜单项若绑定了 Action,且该 Action 配置了按键绑定,菜单会自动显示快捷键(如 "Ctrl+C")。前提是先定义 actions 并注册KeyBinding:
// First define your actions and key bindings actions!(my_app, [Copy, Paste, Cut]); // In your app initialization cx.bind_keys([ KeyBinding::new("ctrl-c", Copy, Some("editor")), KeyBinding::new("ctrl-v", Paste, Some("editor")), KeyBinding::new("ctrl-x", Cut, Some("editor")), ]); // The menu will automatically show shortcuts menu.action_context(focus_handle) // Set context for shortcuts .menu("Copy", Box::new(Copy)) // Will show "Ctrl+C" .menu("Paste", Box::new(Paste)) // Will show "Ctrl+V" .menu("Cut", Box::new(Cut)) // Will show "Ctrl+X"自动展示的机制在render_key_binding:先尝试从action_context(或previous_focus_handle)的焦点路径解析按键绑定,解析不到再回退到应用级绑定(popup_menu.rs)。快捷键以Kbd元素渲染在菜单项右侧,使用 flex-nowrap 布局、透明背景,与菜单项保持统一风格。
子菜单与子菜单图标
submenu创建嵌套菜单,闭包内继续用同一个PopupMenu构建器:
menu.submenu("File", window, cx, |submenu, window, cx| { submenu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .menu("Recent", Box::new(ShowRecent)) }) .submenu("Edit", window, cx, |submenu, window, cx| { submenu.menu("Undo", Box::new(Undo)) .menu("Redo", Box::new(Redo)) })带图标的子菜单头使用submenu_with_icon(icon 参数是Option<Icon>):
menu.submenu_with_icon( Some(IconName::Folder.into()), "Project", window, cx, |submenu, window, cx| { submenu.menu("Open Project", Box::new(OpenProject)) .menu("Close Project", Box::new(CloseProject)) } )子菜单的实现有几个值得注意的源码细节:
- 层级绘制优先级:每层子菜单的
priority逐级 +1,保证多级子菜单永远绘制在浅层之上,避免底层内容(如列表)穿透子菜单背景——GPUI 对嵌套 deferred 绘制有深度上限,因此每层只保留一个 deferred 层(popup_menu.rs); - 锚点自动翻转:渲染前
update_submenu_menu_anchor会根据父菜单边界与窗口尺寸计算锚点,空间不足时自动切换为TopRight/BottomRight并调整偏移(popup_menu.rs); - 父子联动:子菜单持有
parent_menu弱引用,关闭子菜单会沿链条一路关闭父菜单(dismiss递归,popup_menu.rs); - 键盘联动:
←/→在子菜单与父菜单之间切换焦点(select_left/select_right,popup_menu.rs)。
可滚动菜单
:::warning 当菜单启用了scrollable()后,应避免在菜单内使用子菜单——滚动容器内无法正确展示子菜单弹出层,会造成可用性问题(源码render中亦有对应 TODO 注释,见 popup_menu.rs)。 :::
菜单项很多时启用滚动,配合max_h限制高度:
Button::new("large-menu") .label("Many Options") .dropdown_menu(|menu, window, cx| { let mut menu = menu .scrollable(true) .max_h(px(300.)) .label("Select an option"); for i in 0..100 { menu = menu.menu( format!("Option {}", i), Box::new(SelectOption(i)) ); } menu })scrollable(true)会在菜单内容超出max_h时启用overflow_y_scroll并挂载垂直滚动条;滚动时使用ScrollHandle跟踪,且键盘上下选择会自动scroll_to_item保证选中项可见(popup_menu.rs)。另外,当从OwnedMenu(应用菜单)构建菜单且条目超过 20 个时,也会自动开启滚动(popup_menu.rs)。
菜单尺寸控制
menu.min_w(px(200.)) // Minimum width .max_w(px(400.)) // Maximum width .max_h(px(300.)) // Maximum height .scrollable(true) // Enable scrolling when content exceeds max height各尺寸配置的默认值与实现位置如下(popup_menu.rs):
| 配置 | 默认值 | 说明 |
|---|---|---|
min_w | 8rem(约 120px,见 popup_menu.rs) | 菜单最小宽度 |
max_w | 500px(px(500.)) | 菜单最大宽度,子菜单锚点翻转判断也依赖它 |
max_h | 窗口高度的一半,且不超过 450px(popup_menu.rs) | 仅配合scrollable(true)生效 |
Action 上下文:焦点与动作分发
action_context用来设置处理菜单动作的焦点上下文。当菜单被关闭、或某个动作被触发前,焦点会归还给该句柄,随后动作从该句柄分发:
let focus_handle = cx.focus_handle(); menu.action_context(focus_handle) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste))源码中的相关实现(popup_menu.rs):
action_context会递归下发给所有子菜单(set_action_context遍历Submenu项),保证整个菜单树共享同一分发路径;dispatch_confirm_action在触发动作前先context.focus(window, cx)恢复焦点,再window.dispatch_action(action.boxed_clone(), cx)分发动作(popup_menu.rs)——这样on_action处理器可以挂在触发元素祖先链上,即使该祖先不在当前焦点路径上也能收到动作;- 菜单内部还维护
previous_focus_handle:关闭时若焦点没有被菜单项点击回调主动移走(例如打开了对话框并聚焦其输入框),焦点会被归还给之前聚焦的句柄,避免"焦点悬空"(popup_menu.rs)。
API 参考与源码索引
| API | 说明 | 源码位置 |
|---|---|---|
PopupMenu | 菜单主体,提供全部链式构建方法 | crates/component/src/menu/popup_menu.rs |
PopupMenuItem | 菜单项枚举与构建器 | crates/component/src/menu/popup_menu.rs |
ContextMenuExt::context_menu | 右键上下文菜单扩展 | crates/component/src/menu/context_menu.rs |
DropdownMenu | 按钮下拉菜单扩展 | crates/component/src/menu/dropdown_menu.rs |
AppMenuBar | 应用级菜单栏(Windows / Linux) | crates/component/src/menu/app_menu_bar.rs |
Action | 菜单项动作 trait,经gpui_kit门面导出 | crates/kit/src/lib.rs |
完整示例
文件管理器右键菜单
div() .id("file-manager") .child("Right-click for options") .context_menu(|menu, window, cx| { menu.menu_with_icon("Open", IconName::FolderOpen, Box::new(Open)) .separator() .menu_with_icon("Copy", IconName::Copy, Box::new(Copy)) .menu_with_icon("Cut", IconName::Scissors, Box::new(Cut)) .menu_with_icon("Paste", IconName::Clipboard, Box::new(Paste)) .separator() .submenu("New", window, cx, |submenu, window, cx| { submenu.menu_with_icon("File", IconName::File, Box::new(NewFile)) .menu_with_icon("Folder", IconName::Folder, Box::new(NewFolder)) }) .separator() .menu_with_icon("Delete", IconName::Trash, Box::new(Delete)) .separator() .menu("Properties", Box::new(ShowProperties)) })带快捷键的编辑器菜单
// Define actions with keyboard shortcuts actions!(editor, [Save, SaveAs, Find, Replace, ToggleWordWrap]); // Set up key bindings cx.bind_keys([ KeyBinding::new("ctrl-s", Save, Some("editor")), KeyBinding::new("ctrl-shift-s", SaveAs, Some("editor")), KeyBinding::new("ctrl-f", Find, Some("editor")), KeyBinding::new("ctrl-h", Replace, Some("editor")), ]); // Create menu with automatic shortcuts let editor_focus = cx.focus_handle(); Button::new("editor-menu") .label("Edit") .dropdown_menu(|menu, window, cx| { menu.action_context(editor_focus) .menu("Save", Box::new(Save)) // Shows "Ctrl+S" .menu("Save As...", Box::new(SaveAs)) // Shows "Ctrl+Shift+S" .separator() .menu("Find", Box::new(Find)) // Shows "Ctrl+F" .menu("Replace", Box::new(Replace)) // Shows "Ctrl+H" .separator() .menu_with_check("Word Wrap", true, Box::new(ToggleWordWrap)) })带自定义元素与链接的设置菜单
Button::new("settings") .label("Settings") .dropdown_menu(|menu, window, cx| { menu.label("Display") .menu_element_with_check(dark_mode, Box::new(ToggleDarkMode), |window, cx| { h_flex() .gap_2() .child("Dark Mode") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(if dark_mode { "On" } else { "Off" }) ) }) .separator() .label("Account") .menu_element_with_icon( IconName::User, Box::new(ShowProfile), |window, cx| { v_flex() .child("John Doe") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("john@example.com") ) } ) .separator() .link_with_icon("Help Center", IconName::Help, "https://help.example.com") .menu("Sign Out", Box::new(SignOut)) })键盘操作速查
菜单聚焦时,以下按键直接可用(其中六项由PopupMenu的 key context 直接绑定,见 popup_menu.rs):
| 按键 | 行为 |
|---|---|
↑/↓ | 在可点击菜单项之间导航(循环,自动跳过禁用项与分隔线) |
←/→ | 在子菜单与父菜单之间导航切换 |
Enter/Space | 激活当前选中的菜单项 |
Escape | 关闭菜单(连同整条父菜单链) |
Tab | 关闭菜单并将焦点移到下一个可聚焦元素 |
键盘选择的实现要点:select_up/select_down会调用cx.stop_propagation()拦截事件,并只在is_clickable()的项之间移动;confirm会先执行on_click回调或分发action,然后dismiss整条菜单链(popup_menu.rs)。
最佳实践
- 分组相关项:用
separator()把功能相关的菜单项分组,视觉上更易扫读; - 图标风格统一:全应用保持一致的图标语义(复制、粘贴、删除等),降低用户认知成本;
- 逻辑排序:把最常用的操作放在菜单顶部;
- 提供快捷键:为高频操作注册
KeyBinding,菜单会自动展示,用户学习成本低; - 上下文感知:只展示与当前上下文相关的菜单项,必要时配合禁用态而非删除项;
- 渐进披露:复杂层级用
submenu收敛,避免一级菜单过载; - 标签清晰:使用描述性、动作导向的标签(如 "Save As..." 而非 "File Operations" 用于可点击项);
- 控制条目规模:超过 10–15 个菜单项时启用
scrollable(true)。
行为验证:测试用例佐证
仓库在 crates/kit/tests/menu.rs 提供了两个端到端集成测试,可以直接验证本文提到的关键行为:
menu_skips_disabled_commands_confirms_and_restores_focus(crates/kit/tests/menu.rs):验证禁用项不会被触发(Unavailable动作的处理器直接 panic,证明其永远不会被分发)、Enter确认后菜单关闭、动作生效,且焦点恢复到触发菜单前的工作区;Escape可以关闭菜单;hovering_submenu_opens_and_clicking_item_dismisses_the_chain(crates/kit/tests/menu.rs):验证悬停子菜单会打开(出现submenu节点)、点击子菜单项后子菜单与父菜单整条链一起关闭。
此外,context_menu.rs 中还有一个回归测试action_bubbles_from_trigger_and_focus_restores_on_dismiss,专门验证"动作从触发元素祖先链冒泡分发 + 关闭后焦点归还"这两个关键行为。这些测试展示了菜单系统与 GPUI 测试支持框架(#[gpui_kit::test]、test_support()、window.within("popup-menu"))的配合方式,可作为你编写自己组件测试的参考模板。
结语
GPUI Kit 的 Menu 组件以PopupMenu为单一弹出内核,通过ContextMenuExt、DropdownMenu、AppMenuBar三种形态对外暴露,覆盖了桌面应用绝大多数菜单场景:右键菜单、按钮下拉、子菜单、图标、快捷键、勾选、禁用、链接、自定义元素与滚动。其与 GPUI Action / 按键绑定系统的深度集成(自动展示快捷键、焦点路径分发、关闭焦点归还)是区别于普通 UI 库菜单的最大特色。掌握本文的 API 组合与源码原理后,你可以在 GPUI Kit 应用中以极低的成本构建出行为专业、可无障碍访问的完整菜单体系。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考