桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载导读本文基于 BongoCat 的架构决策记录 ADR-0068系统讲解桌面宠物应用如何让系统托盘菜单与模型窗口右键菜单共用同一棵原生菜单树从而消除两个入口在层级与状态上的不一致。文章覆盖菜单树的单一 owner 设计bongocat-platform::SystemMenu、强类型SystemMenuAction的动作边界、由 settings snapshot 驱动的菜单 presentation、显隐/穿透/置顶/悬停隐藏四项 check item 的状态语义以及托盘图标先销毁、菜单项父级关系和主线程约束等生命周期不变量。读完本文你将理解这套“一个 popup 根、一个 owner、一套 action id”的实现原理并能依据 crates/bongocat-platform/src/system_menu_native.rs 与 crates/bongocat-platform/src/system_menu.rs 快速定位菜单相关代码与测试。背景为什么要统一托盘与右键菜单BongoCat 的托盘菜单和模型窗口右键菜单都代表同一个“模型窗口控制面”。早期的方案尝试按入口拆成两棵不同的菜单树但同一组模型窗口操作在两个入口会出现不同的层级和状态既增加维护成本也让验收测试难以收敛。ADR-0068docs/adr/0068-tray-and-overlay-context-menu-layering.md2026-09-25 修订为“托盘与模型窗口右键共用同一套菜单”明确了本次调整的边界只改变菜单树的复用方式不改变应用状态的所有权平台层仍只产生强类型SystemMenuActionoverlay 只发送右键请求runtime、settings service 和 renderer不感知菜单树菜单的呈现与动作分发被隔离在平台层与应用协调层。换句话说这是一次“结构收敛”而非“功能扩展”菜单少了但语义更稳定。决策一一棵 popup 根一个 ownerSystemMenu是唯一的菜单 owner在crates/bongocat-platform/src/lib.rs中平台 crate 导出SystemMenuAction、SystemMenuError与SystemMenuPresentationlib.rs而实际的 native 实现位于system_menu_native.rs。SystemMenu结构体的字段顺序本身就是一条设计约束system_menu_native.rspub struct SystemMenu { // Keep the tray icon first: the native menu handles must be released after // the status item stops using them. tray_icon: TrayIcon, menu: Menu, model_window: Submenu, items: NativeMenuItems, #[cfg(target_os windows)] tray_icon_id: TrayIconId, sender: SenderSystemMenuAction, receiver: ReceiverSystemMenuAction, visible: bool, }关键点TrayIcon字段必须先于菜单根、Submenu和菜单项声明。Rust 的字段析构顺序与声明顺序一致因此托盘图标总是先于菜单句柄释放避免“状态栏还在引用已被释放的菜单”这一经典资源顺序问题。托盘图标和模型窗口右键都展示同一棵菜单树menu根被同时挂到TrayIcon的.with_menu(...)上并在右键请求时通过show_context_menu_for_window呈现到 overlay 的真实窗口句柄。菜单树结构ADR-0068 原文可完整复现设置 ──────── 模型窗口 ├─ □ 隐藏模型窗口 ├─ □ 鼠标穿透 ├─ □ 始终置顶 └─ □ 鼠标悬停时隐藏 ──────── 检查更新仅在更新能力可用时创建 ──────── 退出 BongoCat这一布局在源码中由两个常量数组描述system_menu_native.rsconst MENU_ENTRIES: [MenuEntry] [ MenuEntry::OpenSettings, MenuEntry::Separator, MenuEntry::ModelWindow, MenuEntry::Separator, MenuEntry::CheckForUpdates, MenuEntry::Separator, MenuEntry::Quit, ]; const MODEL_WINDOW_ENTRIES: [MenuEntry] [ MenuEntry::ToggleOverlay, MenuEntry::ToggleClickThrough, MenuEntry::ToggleAlwaysOnTop, MenuEntry::ToggleHideOnPointerHover, ];MENU_ENTRIES定义根级项含“模型窗口”子菜单的挂载点MODEL_WINDOW_ENTRIES定义子菜单内的四个 check item。append_menu_entries在拼接时会做一项细节处理更新行可选当check_for_updates为None时跳过该行并折叠它两侧的分隔符previous_was_separator逻辑避免在无法更新的 channel 上留下一个空段system_menu_native.rs。菜单不再提供哪些行ADR-0068 明确菜单不再提供源码、重启、版本、缩放或透明度行。源码与版本已由设置页的 About 页面提供缩放由设置页和右键拖动提供透明度留在设置页检查更新由构建/channel 事实决定是否创建不显示永久禁用的空行。实现上check_for_updates字段是OptionMenuItem仅在presentation.update_check_available时构造system_menu_native.rs。Windows 托盘图标细节GUID 与 tooltipWindows 平台上托盘图标使用固定 GUID 注册TRAY_ICON_GUID: u128 0x123f3c6f_7d2a_4ca3_b8cb_9b1d1eaf2f10system_menu_native.rs。源码注释明确指出tray-icon 0.25.0的一个已知缺陷对 GUID 注册的图标调用set_tooltip会发出不带NIF_GUID的NIM_MODIFYshell 会忽略uID而要求后续每次调用都带同一个 GUID导致该调用总是失败。因此tooltip 只能在 owner 创建时设置一次生命周期内保持不变量修改 tooltip 需要替换整个托盘 owner而 ADR-0031 禁止在应用运行期间做这种替换system_menu_native.rs。决策二动作与状态边界强类型SystemMenuAction两个入口共用同一批 native item 实例和同一组稳定 action id由action_for_menu_id把muda::MenuId映射为强类型SystemMenuActionsystem_menu_native.rsfn action_for_menu_id(id: MenuId) - OptionSystemMenuAction { Some(match id.0.as_str() { OPEN_SETTINGS_ID SystemMenuAction::OpenSettings, TOGGLE_OVERLAY_VISIBILITY_ID SystemMenuAction::ToggleOverlayVisibility, TOGGLE_CLICK_THROUGH_ID SystemMenuAction::ToggleClickThrough, TOGGLE_ALWAYS_ON_TOP_ID SystemMenuAction::ToggleAlwaysOnTop, TOGGLE_HIDE_ON_POINTER_HOVER_ID SystemMenuAction::ToggleHideOnPointerHover, CHECK_FOR_UPDATES_ID SystemMenuAction::CheckForUpdates, QUIT_ID SystemMenuAction::Quit, _ return None, }) }SystemMenuAction枚举定义在平台层system_menu.rs是Copy Eq的纯值类型动作集合刻意保持小且稳定pub enum SystemMenuAction { OpenSettings, ToggleOverlayVisibility, // 会话级显隐check 状态表示“已隐藏” ToggleClickThrough, ToggleAlwaysOnTop, ToggleHideOnPointerHover, CheckForUpdates, Quit, }对应的稳定 id 字符串常量system_menu_native.rsActionMenuId打开设置bongocat.open-settings隐藏/显示模型窗口bongocat.toggle-overlay鼠标穿透bongocat.toggle-click-through始终置顶bongocat.toggle-always-on-top鼠标悬停时隐藏bongocat.toggle-hide-on-pointer-hover检查更新bongocat.check-for-updates退出 BongoCatbongocat.quit单测menu_action_mapping_keeps_only_the_current_action_set断言上述 7 个 id 都能映射为对应 action同时断言被删除的bongocat.open-source、bongocat.restart、bongocat.version甚至子菜单 idbongocat.model-window均返回None从测试层面锁死“已删除的 id 不会产生 action”system_menu_native.rs。事件接收同一MenuEventreceiver一个有界队列SystemMenu::try_recv统一轮询muda::MenuEvent::receiver()把命中action_for_menu_id的事件送入自身mpsc队列Windows 上还会额外轮询TrayIconEvent左键单击托盘图标被映射为OpenSettingssystem_menu_native.rspub fn try_recv(self) - OptionSystemMenuAction { while let Ok(event) MenuEvent::receiver().try_recv() { if let Some(action) action_for_menu_id(event.id) { let _ self.sender.send(action); } } #[cfg(target_os windows)] while let Ok(event) TrayIconEvent::receiver().try_recv() { if let TrayIconEvent::Click { id, button: MouseButton::Left, button_state: MouseButtonState::Up, .. } event id self.tray_icon_id { let _ self.sender.send(SystemMenuAction::OpenSettings); } } self.receiver.try_recv().ok() }注意菜单 tracking 期间只允许把事件送入既有有界队列不能在 callback 内销毁SystemMenu或 overlay。应用侧对 action 的分发同样不增加菜单专用弱类型协议——动作直接对应到 settings command 与 revisioned 配置写入。presentation 由 settings snapshot 驱动平台层只持有 native 菜单句柄绝不成为配置或本地化的第二来源。SystemMenuPresentation由应用层从 settings snapshot 投影生成crates/bongocat-app/src/system_menu.rspub(crate) fn system_menu_presentation(snapshot: SettingsSnapshot) - SystemMenuPresentation { let locale snapshot.resolved_language.catalog_locale(); let text |key| bongocat_i18n::text(locale, key).to_owned(); SystemMenuPresentation { title: text(system_menu.title), tooltip: text(system_menu.title), open_settings: text(system_menu.open_settings), model_window: text(navigation.model_window.title), hide_overlay: text(settings.overlay.hide_model_window.label), click_through: text(settings.overlay.click_through.label), always_on_top: text(settings.overlay.always_on_top.label), hide_on_pointer_hover: text(settings.overlay.hide_on_mouse_hover.label), check_for_updates: text(update.about.label), quit: text(system_menu.quit), overlay_visible: snapshot.overlay_visible, click_through_enabled: snapshot.overlay.click_through, always_on_top_enabled: snapshot.overlay.always_on_top, hide_on_pointer_hover_enabled: snapshot.overlay.hide_on_pointer_hover, update_check_available: /* 见下文 */, } }要点拆解文案复用偏好设置已有的 key显隐用settings.overlay.hide_model_window.label穿透/置顶/悬停隐藏分别复用settings.overlay.click_through.label、settings.overlay.always_on_top.label、settings.overlay.hide_on_mouse_hover.label检查更新复用 About 已有的update.about.label。菜单不新增重复文案 key——这正是 ADR 中“locale 双向 key 守门”约束的来源。“已隐藏”为选中值visibility_checked(overlay_visible)返回!overlay_visible即模型窗口隐藏时 check item 打勾启动默认未选中因此模型窗口默认可见system_menu_native.rs 与单测visibility_is_rendered_as_a_check_state_meaning_hidden。显隐是 runtime 会话状态不写入config.json菜单与设置页共享同一个 runtime snapshot 投影。其余三个开关穿透/置顶/悬停隐藏走set_overlay_settings写回配置。check item 失败后的回写ADR 规定若 check item 的 command 失败应用重新读取当前 snapshot 并回写 presentation避免 native menu 保留用户点击产生的乐观勾选状态。refresh_system_menu_presentationcrates/bongocat-app/src/system_menu.rs正是这套回写机制的入口它读取最新 snapshot、生成 presentation再通过ProductCoordinator上的system_menu.set_presentation(...)应用到两个菜单面。set_presentation只更新 item 文本与 check 状态items.update不重新创建菜单树system_menu_native.rs。决策三生命周期不变量ADR-0068 定义了四条生命周期不变量源码逐一印证字段析构顺序TrayIcon先于菜单根/子菜单/菜单项声明已在决策一说明显式 shutdown 先set_visible(false)隐藏托盘再按既定顺序停止 input/runtime/frame/renderer/overlaysystem_menu_native.rs。右键只使用真实窗口句柄overlay 右键弹出只使用真实 Windows HWND 或 macOS contentNSView不借用托盘隐藏窗口也不在 overlay 内创建第二个 owner。show_context_menu_for_window通过HasWindowHandle取得句柄后按平台分发system_menu_native.rsWindowsmenu.show_context_menu_for_hwnd(handle.hwnd.get(), None)macOS要求MainThreadMarker后调用menu.show_context_menu_for_nsview(handle.ns_view.as_ptr(), None)其他句柄类型返回SystemMenuError::UnsupportedWindowHandle。overlay 的HasWindowHandle实现在 product_session.rsmacOS 另有 macos/session.rs、Windows 在 windows/session.rs安全注释保证 overlay 的句柄实现会在同步 popup 期间保持窗口存活。主线程约束菜单根、模型窗口子菜单和所有更新/文字/勾选操作都在平台 UI 主线程执行macOS 侧用MainThreadMarker::new().ok_or(SystemMenuError::WrongThread)?在start_with_presentation与set_presentation入口强校验system_menu_native.rs。第三方类型不泄漏muda与tray-icon的版本、features 与替换边界继续由 ADR-0031 固定docs/adr/0031-tray-icon-boundary.md第三方类型不进入公共业务 API——平台 crate 只导出SystemMenuAction、SystemMenuError、SystemMenuPresentation三个自有类型。应用侧的右键调用链右键请求在应用主循环中处理。macOS 分支crates/bongocat-app/src/main.rsif context_menu_requested let Some(menu) coordinator.system_menu.as_ref() let Some(overlay) coordinator.overlay.as_ref() let Err(error) menu.show_context_menu_for_window(overlay) { // 记录 ServiceFailed 日志 record_failure }Windows 分支在cx.update中先借用coordinator.overlay.borrow()取得 overlay 再调用同一方法main.rs失败路径统一走frame_application_log的ApplicationLogEvent::new(ApplicationLogCode::ServiceFailed)并附带context_menu_failed原因。两平台共用menu.show_context_menu_for_window(overlay)这一入口验证了“overlay 只发送右键请求不感知菜单树”的边界。验证体系ADR-0068 的验证分四层全部有仓库证据平台 crate 单测system_menu_native.rsvisibility_is_rendered_as_a_check_state_meaning_hidden断言显隐勾选语义隐藏勾选both_menu_surfaces_use_the_same_layout断言根级项含ModelWindow/CheckForUpdates子菜单含四个 check itemmenu_action_mapping_keeps_only_the_current_action_set断言 7 个 action id 均映射、4 个已删除 id 不产生 action。菜单布局 contract断言托盘与模型窗口右键共用同一组根项且模型窗口子菜单包含四个 check item即上表两棵MenuEntry常量数组。locale 双向 key 守门确保navigation.model_window.title、偏好设置已有的显隐/穿透/置顶/悬停鼠标文案以及 About 已有的update.about.label可解析菜单不新增重复文案 key。macOS/Windows release system-menu smoke覆盖托盘显隐、设置恢复、显隐 action、runtime snapshot 变化和有序退出。它不替代真实 popup 展开与点击验证——ADR 明确列出发布前必须在两个目标平台实机确认的清单托盘与模型窗口右键展示同一层级、子菜单展开、cursor 定位、DPI/Retina、点击外部关闭、菜单项 action 派发、Explorer/菜单栏恢复和 shutdown 清理。另外system_menu_smoke是一个 opt-in 的构建选项见 crates/bongocat-app/src/binary_tests/product_options.rs 与 smoke_status.rsrelease 构建才启用避免开发构建误跑依赖原生菜单环境的冒烟测试。替换边界ADR-0068 把变更面压到最小替换点只有crates/bongocat-platform/src/system_menu_native.rs与 overlay 的HasWindowHandle实现。升级muda/tray-icon时必须重新验证以下事项且不能退回“由 overlay 持有菜单”或“把第三方类型泄漏到 runtime/UI”共用菜单根与菜单项父级关系析构顺序托盘图标先于菜单句柄释放同一个MenuEventreceiver 的事件分发Windows GUID/tooltip 缺陷见决策一的已知限制主线程约束macOSMainThreadMarkermacOSNSView生命周期两个平台的实机菜单层级。这套约束同时呼应 ADR-0032启动权限提示边界docs/adr/0032-startup-permission-prompt-boundary.md与 ADR-0035更新 worker 与更新窗口docs/adr/0035-update-worker-and-window.md中“平台能力只在平台层落地、业务侧只见强类型接口”的一贯原则。小结ADR-0068 的落地方案可以概括为三句话结构上一棵Menu根同时服务托盘图标与模型窗口右键唯一的 owner 是bongocat-platform::SystemMenu字段顺序保证析构安全语义上7 个稳定MenuId通过action_for_menu_id映射到强类型SystemMenuAction菜单文本与勾选状态完全由 settings snapshot 投影而来不产生第二份配置或本地化状态边界上overlay 只贡献真实窗口句柄runtime/settings/renderer 不感知菜单树muda/tray-icon的升级风险被收口到单一 native 文件。对维护者而言若要在未来调整菜单增删项、改层级核心改动点就是MENU_ENTRIES/MODEL_WINDOW_ENTRIES两个常量、NativeMenuItems::new的 item 构造以及action_for_menu_id的映射表三者必须同步更新并让menu_action_mapping_keeps_only_the_current_action_set等单测继续锁定新集合。赞分享桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载相关推荐用 PhantomData 为生命周期打品牌Comprehensive Rust 中生命周期子类型与不变量变型Token Types 系列 2/4用 PhantomData 为生命周期打品牌Comprehensive Rust 中生命周期子类型与不变量变型Token Types 系列 2/4 本文档教程LobeHub Desktop 菜单体系实战指南App 菜单、右键菜单与托盘菜单的配置原理LobeHub Desktop 菜单体系实战指南App 菜单、右键菜单与托盘菜单的配置原理 本篇基于 LobeHub 仓库中的 Desktop 菜单配置指南人工智能AI 应用大模型AI Agent多智能体工具调用前端后端BongoCat 托盘图标与系统菜单边界设计tray-icon 与 muda 的单一 Owner 架构实践BongoCat 托盘图标与系统菜单边界设计tray icon 与 muda 的单一 Owner 架构实践 导读 本文基于 BongoCat 项目的架构决策记桌面应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考