Files
qiming/qimingclaw/specs/agent-client/vendorsdoc/tray-icon.md

524 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tray-icon
## 项目概述
跨平台系统托盘图标库,基于 muda 库实现,支持 Windows、macOS 和 Linux。
**GitHub**: https://github.com/tauri-apps/tray-icon.git
**本地路径**: `vendors/tray-icon`
## 目录结构
```
tray-icon/
├── src/
│ ├── lib.rs # 公共 API
│ ├── icon.rs # 图标处理
│ ├── menu.rs # 菜单构建
│ └── platform_impl/ # 平台特定实现
│ ├── mod.rs
│ ├── windows.rs
│ ├── macos.rs
│ └── linux.rs
├── Cargo.toml
└── README.md
```
## 核心依赖
```toml
[dependencies]
muda = "1.0" # 跨平台菜单库
crossbeam-channel = "0.5"
once_cell = "1.18"
thiserror = "1.0"
# 平台依赖
objc2 = { version = "0.5", optional = true }
gtk = { version = "0.18", optional = true, features = ["v3_22"] }
libappindicator = { version = "0.8", optional = true }
windows = { version = "0.48", optional = true }
```
## 核心 API
### TrayIconBuilder
```rust
// lib.rs
pub struct TrayIconBuilder {
id: TrayIconId,
attrs: TrayIconAttributes,
}
impl TrayIconBuilder {
pub fn new() -> Self;
/// 设置托盘图标
pub fn with_icon(mut self, icon: Icon) -> Self;
/// 设置提示文本
pub fn with_tooltip<S: AsRef<str>>(self, s: S) -> Self;
/// 设置标题macOS
pub fn with_title<S: AsRef<str>>(self, title: S) -> Self;
/// 设置上下文菜单
pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self;
/// 左键点击显示菜单(默认右键)
pub fn with_menu_on_left_click(self, enable: bool) -> Self;
/// 设置临时目录路径
pub fn with_temp_dir_path<P: AsRef<Path>>(self, s: P) -> Self;
/// 构建托盘图标
pub fn build(self) -> Result<TrayIcon>;
}
```
### TrayIcon
```rust
pub struct TrayIcon {
id: TrayIconId,
tray: Rc<RefCell<platform_impl::TrayIcon>>,
}
impl TrayIcon {
/// 设置图标
pub fn set_icon(&self, icon: Option<Icon>) -> Result<()>;
/// 设置菜单
pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>);
/// 设置提示文本
pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()>;
/// 设置可见性
pub fn set_visible(&self, visible: bool) -> Result<()>;
/// 获取托盘区域矩形
pub fn rect(&self) -> Option<Rect>;
}
```
### TrayIconEvent
```rust
pub enum TrayIconEvent {
Click {
id: TrayIconId,
position: Position,
rect: Rect,
button: MouseButton,
button_state: ButtonState,
},
DoubleClick {
id: TrayIconId,
position: Position,
rect: Rect,
button: MouseButton,
},
Enter {
id: TrayIconId,
position: Position,
rect: Rect,
},
Move {
id: TrayIconId,
position: Position,
rect: Rect,
},
Leave {
id: TrayIconId,
position: Position,
rect: Rect,
},
}
impl TrayIconEvent {
/// 获取事件接收器
pub fn receiver() -> &'a TrayIconEventReceiver;
/// 设置事件处理函数
pub fn set_event_handler<F>(f: Option<F>)
where
F: Fn(TrayIconEvent, &dyn TrayIconHandle) + Send + 'static;
}
```
## 使用示例
```rust
use tray_icon::{TrayIconBuilder, icon::Icon, menu::MenuBuilder};
fn main() -> Result<()> {
// 加载图标
let icon = Icon::from_resource(1, None);
// 创建菜单
let menu = MenuBuilder::new()
.item("Show Window", "show_window", true, None)?
.separator()
.item("Settings", "settings", true, None)?
.separator()
.quit("Quit")
.build()?;
// 创建托盘图标
let _tray_icon = TrayIconBuilder::new()
.with_icon(icon)
.with_tooltip("My App")
.with_menu(Box::new(menu))
.with_on_tray_event(|event, tray| {
match event {
TrayIconEvent::Click { button, .. } => {
if button == MouseButton::Left {
show_window();
}
}
TrayIconEvent::MenuItemClick { id, .. } => {
match id.as_ref() {
"quit" => std::process::exit(0),
"settings" => open_settings(),
"show_window" => show_window(),
_ => {}
}
}
_ => {}
}
})
.build()?;
Ok(())
}
```
## 与 agent-client 集成场景
### 场景1完整的托盘管理器
```rust
// agent-client 托盘管理器
use tray_icon::{TrayIconBuilder, icon::Icon, menu::{Menu, MenuItem, MenuBuilder}};
use std::sync::atomic::{AtomicBool, Ordering};
pub struct TrayManager {
// 托盘图标
tray_icon: Option<TrayIcon>,
// 是否显示主窗口
window_visible: AtomicBool,
// 连接状态
connection_state: Arc<dyn ConnectionStatusProvider>,
// 应用退出标志
exit_flag: Arc<AtomicBool>,
}
impl TrayManager {
pub fn new(
connection_state: Arc<dyn ConnectionStatusProvider>,
exit_flag: Arc<AtomicBool>,
) -> Result<Self> {
let icon = Self::load_icon()?;
let menu = MenuBuilder::new()
.item("Show Window", "show_window", true, None)?
.separator()
.item("Agent Status", "status", true, None)?
.item("Open Settings", "settings", true, None)?
.separator()
.item("Quit", "quit", true, None)?
.build()?;
let tray_icon = TrayIconBuilder::new()
.with_icon(icon)
.with_tooltip("qiming-agent")
.with_menu(Box::new(menu))
.with_menu_on_left_click(true) // 左键也显示菜单
.build()?;
Ok(Self {
tray_icon: Some(tray_icon),
window_visible: AtomicBool::new(true),
connection_state,
exit_flag,
})
}
fn load_icon() -> Result<Icon> {
// 从资源文件加载图标
let icon_data = include_bytes!("../../assets/tray_icon.png");
let image = image::load_from_memory(icon_data)?
.to_rgba8()
.into_raw();
Icon::from_rgba(icon_data.to_vec(), 32, 32)
}
/// 设置托盘事件处理
pub fn setup_event_handler(&self) {
TrayIconEvent::set_event_handler(Some(move |event, _tray| {
match event {
TrayIconEvent::Click { button, .. } => {
if button == MouseButton::Left {
// 左键点击切换窗口可见性
// 发送事件到主窗口
}
}
TrayIconEvent::MenuItemClick { id, .. } => {
match id.as_ref() {
"show_window" => {
// 显示主窗口
}
"settings" => {
// 打开设置窗口
}
"quit" => {
// 退出应用
}
_ => {}
}
}
_ => {}
}
}));
}
/// 更新托盘图标(根据连接状态)
pub fn update_icon(&self, status: &ConnectionState) {
let icon = match status {
ConnectionState::Connected { .. } => self.load_connected_icon(),
ConnectionState::Connecting => self.load_loading_icon(),
ConnectionState::Disconnected { .. } => self.load_disconnected_icon(),
ConnectionState::Error { .. } => self.load_error_icon(),
};
if let Some(tray) = &self.tray_icon {
tray.set_icon(Some(icon)).ok();
}
}
/// 更新托盘提示文本
pub fn update_tooltip(&self, text: &str) {
if let Some(tray) = &self.tray_icon {
tray.set_tooltip(Some(text)).ok();
}
}
/// 销毁托盘
pub fn destroy(&mut self) {
if let Some(tray) = self.tray_icon.take() {
tray.set_visible(false).ok();
}
}
}
```
### 场景2托盘菜单与主窗口通信
```rust
// 使用 channel 进行托盘事件通信
use tokio::sync::mpsc;
pub enum TrayEvent {
ShowWindow,
OpenSettings,
Quit,
}
pub struct TrayEventChannel {
sender: mpsc::UnboundedSender<TrayEvent>,
receiver: Arc<Mutex<mpsc::UnboundedReceiver<TrayEvent>>>,
}
impl TrayEventChannel {
pub fn new() -> Self {
let (sender, receiver) = mpsc::unbounded_channel();
Self {
sender,
receiver: Arc::new(Mutex::new(receiver)),
}
}
pub fn sender(&self) -> mpsc::UnboundedSender<TrayEvent> {
self.sender.clone()
}
pub fn receiver(&self) -> Arc<Mutex<mpsc::UnboundedReceiver<TrayEvent>>> {
self.receiver.clone()
}
}
/// 在托盘事件中发送消息
fn setup_tray_events(channel: &TrayEventChannel) {
TrayIconEvent::set_event_handler(Some(move |event, _tray| {
if let TrayIconEvent::MenuItemClick { id, .. } = event {
let event = match id.as_ref() {
"show_window" => TrayEvent::ShowWindow,
"settings" => TrayEvent::OpenSettings,
"quit" => TrayEvent::Quit,
_ => return,
};
channel.sender().send(event).ok();
}
}));
}
/// 在主窗口中监听托盘事件
async fn handle_tray_events(channel: &TrayEventChannel, window: &WindowHandle) {
let mut receiver = channel.receiver().lock().await;
while let Some(event) = receiver.recv().await {
match event {
TrayEvent::ShowWindow => {
window.activate().ok();
}
TrayEvent::OpenSettings => {
// 打开设置窗口
}
TrayEvent::Quit => {
// 退出应用
std::process::exit(0);
}
}
}
}
```
### 场景3动态托盘菜单
```rust
// 根据状态动态更新菜单
pub struct DynamicTrayMenu {
menu: Menu,
status_item: MenuItem,
settings_item: MenuItem,
}
impl DynamicTrayMenu {
pub fn new() -> Result<Self> {
let menu = MenuBuilder::new()
.item("Show Window", "show_window", true, None)?
.separator()
.item("Status: Disconnected", "status", true, None)?
.item("Open Settings", "settings", true, None)?
.separator()
.item("Quit", "quit", true, None)?
.build()?;
// 获取子菜单项的引用
let status_item = menu.items().find(|item| item.id() == "status").unwrap();
let settings_item = menu.items().find(|item| item.id() == "settings").unwrap();
Ok(Self {
menu,
status_item,
settings_item,
})
}
/// 更新状态文本
pub fn update_status(&mut self, status: &ConnectionState) {
let text = match status {
ConnectionState::Connected { mode, latency } => {
match mode {
ConnectionMode::P2P => format!("Status: Connected (P2P) - {}ms", latency),
ConnectionMode::Relay => format!("Status: Connected (Relay) - {}ms", latency),
}
}
ConnectionState::Connecting => "Status: Connecting...".into(),
ConnectionState::Disconnected { .. } => "Status: Disconnected".into(),
ConnectionState::Error { .. } => "Status: Error".into(),
};
// 更新菜单项文本(如果支持)
// self.status_item.set_text(Some(&text));
}
/// 根据连接状态启用/禁用菜单项
pub fn update_items(&mut self, connected: bool) {
// 如果已连接,可能禁用某些选项
// 如果未连接,可能启用重连选项
}
}
```
### 场景4托盘图标动画
```rust
// 连接状态动画图标
pub struct AnimatedTrayIcon {
frames: Vec<Icon>,
current_frame: usize,
timer: Option<JoinHandle<()>>,
}
impl AnimatedTrayIcon {
pub fn new_loading_animation() -> Result<Self> {
// 创建加载动画帧
let frames = (0..8).map(|i| {
let degrees = i * 45;
Self::create_rotated_icon(degrees)
}).collect::<Result<Vec<_>>>()?;
Ok(Self {
frames,
current_frame: 0,
timer: None,
})
}
/// 开始动画
pub fn start_animation(&mut self, tray: &TrayIcon) {
let tray = tray.clone();
self.timer = Some(tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(100));
loop {
interval.tick().await;
// 下一帧
}
}));
}
/// 停止动画
pub fn stop_animation(&mut self) {
if let Some(timer) = self.timer.take() {
timer.abort();
}
}
fn create_rotated_icon(degrees: u32) -> Result<Icon> {
// 创建旋转后的图标
// 使用图像处理库生成旋转帧
Ok(Icon::from_rgba(vec![], 32, 32).unwrap())
}
}
```
## 在本项目中的使用
用于实现 agent-client 的任务栏常驻和托盘菜单功能:
```
agent-client
├── tray-icon (托盘图标)
│ │
│ ├── 托盘图标显示 (根据连接状态变化)
│ ├── 右键菜单 (显示窗口、设置、退出)
│ ├── 左键点击 (切换窗口可见性)
│ └── 动态提示文本 (显示连接状态)
└── muda (菜单系统)
├── Show Window
├── Agent Status (只读)
├── Open Settings
└── Quit
```