|
1 | | -# 系统托盘替换方案 |
2 | | - |
3 | | -## 目标 |
4 | | -将现有的基于 C 库 `tray` 的系统托盘实现替换为 Rust 的 `tray-icon` 库,以提高可维护性并减少跨平台适配代码。 |
5 | | - |
6 | | -## 总体方案 |
7 | | -1. 在项目根目录下创建 Rust 子目录 `rust_tray`,编写静态库实现与原 `tray` 库完全兼容的 C API(`tray_init`、`tray_update`、`tray_loop`、`tray_exit`)。 |
8 | | -2. 使用 `bindgen` 从原 `third-party/tray/src/tray.h` 生成 Rust 绑定,保证结构体布局一致。 |
9 | | -3. 修改 CMake 构建系统,移除对原 `tray` 库的编译,改为构建并链接 Rust 静态库。 |
10 | | -4. 保留 `third-party/tray` 子模块中的头文件,确保 C++ 代码 `#include "tray/src/tray.h"` 仍然有效。 |
11 | | -5. 不修改 `src/system_tray.cpp` 的业务逻辑,仅替换底层库的实现。 |
12 | | - |
13 | | -## 详细步骤 |
14 | | - |
15 | | -### 1. 创建 Rust 项目 |
16 | | -在 `rust_tray/` 下初始化 Cargo 库: |
17 | | - |
18 | | -``` |
19 | | -rust_tray/ |
20 | | -├── Cargo.toml |
21 | | -├── build.rs |
22 | | -└── src/ |
23 | | - └── lib.rs |
24 | | -``` |
25 | | - |
26 | | -**Cargo.toml 内容:** |
27 | | - |
28 | | -```toml |
29 | | -[package] |
30 | | -name = "sunshine_tray" |
31 | | -version = "0.1.0" |
32 | | -edition = "2021" |
33 | | - |
34 | | -[lib] |
35 | | -name = "tray" |
36 | | -crate-type = ["staticlib"] |
37 | | - |
38 | | -[dependencies] |
39 | | -tray-icon = { version = "0.10", default-features = false, features = ["tray"] } |
40 | | -anyhow = "1.0" |
41 | | -lazy_static = "1.4" |
42 | | -libc = "0.2" |
43 | | -log = "0.4" |
44 | | - |
45 | | -[build-dependencies] |
46 | | -bindgen = "0.69" |
47 | | -``` |
48 | | - |
49 | | -### 2. 生成 FFI 绑定(build.rs) |
50 | | -利用 `bindgen` 解析原头文件,生成与 C 完全一致的 Rust 结构体定义。 |
51 | | - |
52 | | -```rust |
53 | | -// build.rs |
54 | | -use std::env; |
55 | | -use std::path::PathBuf; |
56 | | - |
57 | | -fn main() { |
58 | | - let bindings = bindgen::Builder::default() |
59 | | - .header("third-party/tray/src/tray.h") |
60 | | - .parse_callbacks(Box::new(bindgen::CargoCallbacks)) |
61 | | - .generate() |
62 | | - .expect("Unable to generate bindings"); |
63 | | - |
64 | | - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); |
65 | | - bindings |
66 | | - .write_to_file(out_path.join("bindings.rs")) |
67 | | - .expect("Couldn't write bindings"); |
68 | | -} |
69 | | -``` |
70 | | - |
71 | | -### 3. 实现 C API(lib.rs) |
72 | | - |
73 | | -主要任务: |
74 | | - |
75 | | -- 全局状态管理(`OnceLock<Mutex<Option<TrayState>>>`),存储 `TrayIcon` 实例及菜单项到 C 菜单的映射。 |
76 | | -- `tray_init`:根据传入的 `tray` 结构体创建托盘图标和菜单,注册回调(调用 C 回调)。 |
77 | | -- `tray_update`:更新图标、工具提示、菜单文本、勾选状态等;若设置了 `notification_*` 字段,显示通知。 |
78 | | -- `tray_loop`:启动平台事件循环(例如 GTK 的 `main` 或 Windows 消息循环),阻塞直到 `tray_exit` 被调用。 |
79 | | -- `tray_exit`:退出事件循环,清理资源。 |
80 | | - |
81 | | -关键代码框架: |
82 | | - |
83 | | -```rust |
84 | | -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); |
85 | | - |
86 | | -use std::ffi::{CStr, CString}; |
87 | | -use std::os::raw::{c_char, c_int, c_void}; |
88 | | -use std::sync::{Mutex, OnceLock}; |
89 | | -use tray_icon::{TrayIcon, TrayIconBuilder, Menu, MenuItem, MenuId, TrayIconEvent}; |
90 | | -use anyhow::Result; |
91 | | - |
92 | | -struct TrayState { |
93 | | - icon: TrayIcon, |
94 | | - menu_map: Vec<(MenuId, *const tray_menu)>, // 用于回调查找 |
95 | | - // 事件循环句柄(例如 gtk::Application 或 winit 事件循环) |
96 | | - event_loop: Option<...>, |
97 | | -} |
98 | | - |
99 | | -static TRAY_STATE: OnceLock<Mutex<Option<TrayState>>> = OnceLock::new(); |
100 | | - |
101 | | -#[no_mangle] |
102 | | -pub extern "C" fn tray_init(t: *mut tray) -> c_int { |
103 | | - // 错误处理返回 -1,成功 0 |
104 | | - match unsafe { do_init(t) } { |
105 | | - Ok(_) => 0, |
106 | | - Err(_) => -1, |
107 | | - } |
108 | | -} |
109 | | - |
110 | | -unsafe fn do_init(t: *mut tray) -> Result<()> { |
111 | | - // 构建菜单(递归) |
112 | | - let (menu, menu_map) = build_menu((*t).menu)?; |
113 | | - |
114 | | - // 加载图标 |
115 | | - let icon = load_icon(CStr::from_ptr((*t).icon).to_str()?)?; |
116 | | - |
117 | | - let builder = TrayIconBuilder::new() |
118 | | - .with_icon(icon) |
119 | | - .with_tooltip(CStr::from_ptr((*t).tooltip).to_str()?) |
120 | | - .with_menu(Box::new(menu)); |
121 | | - |
122 | | - let tray_icon = builder.build()?; |
123 | | - |
124 | | - // 存储状态 |
125 | | - let state = TrayState { |
126 | | - icon: tray_icon, |
127 | | - menu_map, |
128 | | - event_loop: None, |
129 | | - }; |
130 | | - TRAY_STATE.get_or_init(|| Mutex::new(None)) |
131 | | - .lock() |
132 | | - .unwrap() |
133 | | - .replace(state); |
134 | | - |
135 | | - Ok(()) |
136 | | -} |
137 | | - |
138 | | -// 其他函数类似实现 |
139 | | -``` |
140 | | - |
141 | | -菜单构建时需递归处理子菜单,并为每个 `MenuItem` 设置回调:当用户点击时,根据 `MenuId` 从 `menu_map` 中找到对应的 C `tray_menu` 指针,调用其 `cb` 字段(若存在)。 |
142 | | - |
143 | | -### 4. 修改 CMake 构建 |
144 | | - |
145 | | -编辑 `cmake/targets/common.cmake`,注释或删除对原 `tray` 库的引用,替换为自定义命令构建 Rust 库。 |
146 | | - |
147 | | -```cmake |
148 | | -# 禁用原 tray 库 |
149 | | -# add_subdirectory(third-party/tray) |
150 | | -
|
151 | | -# 添加 Rust 托盘库构建 |
152 | | -set(RUST_TRAY_SOURCE_DIR "${CMAKE_SOURCE_DIR}/rust_tray") |
153 | | -set(RUST_TARGET_DIR "${CMAKE_BINARY_DIR}/rust_tray") |
154 | | -set(RUST_OUTPUT_DEBUG "${RUST_TARGET_DIR}/debug/libtray.a") |
155 | | -# Release 构建可根据需要选择 |
156 | | -add_custom_command( |
157 | | - OUTPUT ${RUST_OUTPUT_DEBUG} |
158 | | - COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${RUST_TARGET_DIR} cargo build --manifest-path ${RUST_TRAY_SOURCE_DIR}/Cargo.toml |
159 | | - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} |
160 | | - COMMENT "Building Rust Tray library (debug)" |
161 | | - VERBATIM |
162 | | -) |
163 | | -add_custom_target(rust_tray ALL DEPENDS ${RUST_OUTPUT_DEBUG}) |
164 | | -
|
165 | | -# 链接静态库 |
166 | | -target_link_libraries(sunshine PRIVATE ${RUST_OUTPUT_DEBUG}) |
167 | | -target_include_directories(sunshine PRIVATE third-party/tray/src) |
168 | | -``` |
169 | | - |
170 | | -注意:根据实际构建类型(Debug/Release)调整 cargo 参数和输出路径,也可同时构建 Release 版本并通过 CMake 变量选择。 |
171 | | - |
172 | | -### 5. 验证与测试 |
173 | | - |
174 | | -编译 Sunshine,确保无链接错误。运行后验证: |
175 | | -- 系统托盘图标显示正常。 |
176 | | -- 菜单点击功能正常(打开 UI、开关显示器、导入导出配置、语言切换等)。 |
177 | | -- 通知正常显示(例如开始/暂停串流、配对请求)。 |
178 | | -- 图标切换(播放、暂停、锁定)正常。 |
179 | | - |
180 | | -## 可能的问题及应对 |
181 | | - |
182 | | -- **回调中 C 字符串的生命周期**:`tray_menu.text` 在语言切换时会指向新的 `std::string` 内部数据,而 Rust 在 `tray_update` 时会重新拷贝字符串,因此安全。 |
183 | | -- **事件循环集成**:不同平台事件循环实现方式不同,需确保 `tray_loop` 阻塞且能正确响应退出。可参考 `tray-icon` 示例中的事件循环代码(如 winit、gtk)。 |
184 | | -- **Windows 系统权限问题**:原 `system_tray.cpp` 中的线程 DACL 修改和等待 Shell 的代码保留,不影响 Rust 库。 |
185 | | -- **跨平台图标格式**:原代码已通过宏区分各平台图标路径/名称,直接传递给 `tray-icon` 即可,该库会自动处理。 |
186 | | - |
187 | | -## 后续工作 |
188 | | - |
189 | | -- 移除 `third-party/tray` 中除头文件外的源文件(可选,但保留子模块可方便获取头文件)。 |
190 | | -- 完善 Rust 实现中的错误处理和日志记录。 |
191 | | -- 如有必要,在 CI 中增加 Rust 工具链安装步骤。 |
192 | | - |
193 | | -本方案通过最小侵入式修改实现了核心功能替换,可显著降低维护成本并提高跨平台稳定性。 |
| 1 | +# 系统托盘替换方案(已更新:大部分逻辑迁移至 Rust 库) |
| 2 | + |
| 3 | +要点结论: |
| 4 | +- 大部分托盘逻辑、i18n 与菜单处理已迁移到 Rust 库,主实现见 [`rust_tray/src/lib.rs`](rust_tray/src/lib.rs:1)。 |
| 5 | +- 对外 C API 以扩展接口为主:请使用 [`tray_init_ex`、`tray_loop`、`tray_exit` 等](rust_tray/include/rust_tray.h:61);旧 `tray_init` 为遗留且不推荐使用。 |
| 6 | +- C++ 端使用薄包装器 [`src/system_tray_rust.cpp`](src/system_tray_rust.cpp:1) 与 Rust 库交互,CMake 已改为始终链接 Rust 实现。 |
| 7 | + |
| 8 | +关键文件(快速索引): |
| 9 | +- Rust 实现:[`rust_tray/src/lib.rs`](rust_tray/src/lib.rs:1) |
| 10 | +- 国际化:[`rust_tray/src/i18n.rs`](rust_tray/src/i18n.rs:1) |
| 11 | +- 菜单/动作:[`rust_tray/src/actions.rs`](rust_tray/src/actions.rs:1) |
| 12 | +- C 头(导出 API):[`rust_tray/include/rust_tray.h`](rust_tray/include/rust_tray.h:1) |
| 13 | +- C++ 包装器:[`src/system_tray_rust.cpp`](src/system_tray_rust.cpp:1) |
| 14 | +- CMake 目标:[`cmake/targets/rust_tray.cmake`](cmake/targets/rust_tray.cmake:1) |
| 15 | + |
| 16 | +架构要点: |
| 17 | +1. Rust 负责:菜单结构、i18n、事件循环、图标/通知、动作映射(MenuAction -> TrayAction)。 |
| 18 | +2. C++ 负责:应用内响应(打开 UI、重启、退出等)和平台特殊处理(如 Windows 特权/进程管理)。 |
| 19 | +3. 边界:Rust 通过 C API 导出简单函数;C++ 通过回调接收用户操作事件。 |
| 20 | + |
| 21 | +构建与集成: |
| 22 | +- CMake 现在包含并构建 `rust_tray`,使用 `cargo build` 生成静态库并链接到主程序(见 [`cmake/compile_definitions/*`](cmake/compile_definitions/common.cmake:1) 的改动)。 |
| 23 | +- 头文件为 [`rust_tray/include/rust_tray.h`](rust_tray/include/rust_tray.h:1),C++ 仅需包含该头并注册回调。 |
| 24 | +- CI:需保证 Rust toolchain 可用;建议在 CI 中添加 Rust 安装步骤。 |
| 25 | + |
| 26 | +运行时与 API 变化: |
| 27 | +- 初始化:推荐使用 `tray_init_ex(icon_normal, icon_playing, icon_pausing, icon_locked, tooltip, locale, callback)`。 |
| 28 | +- 事件循环:使用 `tray_loop(blocking)` 驱动;返回 -1 表示要求退出。可在单线程或分线程中调用(包装器提供线程化入口)。 |
| 29 | +- 运行时更新:`tray_set_icon`、`tray_set_tooltip`、`tray_set_vdd_checked`、`tray_set_vdd_enabled`、`tray_set_locale`、`tray_show_notification`。 |
| 30 | +- 兼容层:实现了 `tray_update`(部分支持);但 `tray_init` 已被降级(返回错误并打印警告)。 |
| 31 | + |
| 32 | +i18n 与菜单: |
| 33 | +- i18n 数据与逻辑在 Rust 层管理,参见 [`rust_tray/src/i18n.rs`](rust_tray/src/i18n.rs:1)。 |
| 34 | +- 语言切换由 Rust 处理并原子更新菜单文本,必要时会重设 TrayIcon 的菜单以确保生效(Windows 行为)。 |
| 35 | + |
| 36 | +图标与通知: |
| 37 | +- 图标加载:Windows 优先使用 .ico(多分辨率),Linux 支持图标名称或文件路径,macOS 使用文件路径。 |
| 38 | +- 通知:当前为占位实现(日志输出),需要按平台补全真实通知接口(待办)。 |
| 39 | + |
| 40 | +测试清单(必验): |
| 41 | +- 编译通过并链接 Rust 静态库。 |
| 42 | +- 托盘图标在 Windows / Linux / macOS 显示正确。 |
| 43 | +- 菜单项触发后,C++ 回调收到匹配的 `TrayAction`(见头文件枚举)。 |
| 44 | +- 语言切换后菜单文本更新并在 UI 上可见。 |
| 45 | +- 图标切换与 tooltip 更新正常。 |
| 46 | +- 通知调用至少不会崩溃(后续完善行为)。 |
| 47 | + |
| 48 | +已知限制与后续工作: |
| 49 | +- 完成平台通知实现(Rust 层需要具体实现)。 |
| 50 | +- 若需更细粒度的日志或错误上报,考虑在 Rust 层引入更丰富的日志接口并暴露给 C++。 |
| 51 | +- 可选:清理 `third-party/tray` 中多余源文件,仅保留头文件以减小仓库体积。 |
| 52 | +- 在 CI 中加入交叉编译与多平台验证。 |
| 53 | + |
| 54 | +迁移结论: |
| 55 | +本次提交把「菜单、i18n、事件循环、图标管理、部分文件对话(导入/导出)」这些横跨平台且逻辑密集的功能迁移到 Rust,提高了可维护性与一致性。C++ 侧保留平台特性与应用逻辑,双方通过稳定的 C API 协作。 |
| 56 | + |
| 57 | +参考实现与调试入口: |
| 58 | +- 查看实现:[`rust_tray/src/lib.rs`](rust_tray/src/lib.rs:1) |
| 59 | +- 头文件:[`rust_tray/include/rust_tray.h`](rust_tray/include/rust_tray.h:1) |
| 60 | +- C++ 包装示例:[`src/system_tray_rust.cpp`](src/system_tray_rust.cpp:1) |
| 61 | +- i18n 数据:[`rust_tray/src/i18n.rs`](rust_tray/src/i18n.rs:1) |
| 62 | + |
| 63 | +完成。 |
0 commit comments