一键导入
adding-tauri-system-tray
Guides the user through implementing Tauri system tray functionality, including tray icon setup, tray menu creation, handling tray events, and updating the tray at runtime in the notification area.
菜单
Guides the user through implementing Tauri system tray functionality, including tray icon setup, tray menu creation, handling tray events, and updating the tray at runtime in the notification area.
Guides users through distributing Tauri applications on Windows, including creating MSI and NSIS installers, customizing installer behavior, configuring WebView2 installation modes, and submitting apps to the Microsoft Store.
Assists with managing Tauri application resources including app icons setup and generation, embedding static files and assets, accessing bundled resources at runtime, and implementing thread-safe state management patterns.
Creates and edits Tauri capability JSON files, configures plugin permissions and per-window access control, and sets up platform-specific security boundaries. Use when working with capability.json, Tauri allowlist, IPC permissions, plugin access, Tauri security config, or per-window access control.
Helps users create and initialize new Tauri v2 projects for building cross-platform desktop and mobile applications. Covers system prerequisites and setup requirements for macOS, Windows, and Linux. Guides through project creation using create-tauri-app or manual Tauri CLI initialization. Explains project directory structure and configuration files. Supports vanilla JavaScript, TypeScript, React, Vue, Svelte, Angular, SolidJS, and Rust-based frontends. Use when the user wants to start a new Tauri project, set up Tauri v2, or asks about Tauri project initialization and prerequisites.
Build and debug Tauri desktop applications using the Rust backend, webview frontend, IPC commands/events, and capability-based security model. Use when scaffolding a Tauri project, defining Rust commands, configuring tauri.conf.json, setting up capabilities and permissions, or debugging cross-platform desktop app issues.
Scaffolds Tauri v2 plugins with `tauri plugin new`, implements Rust commands with desktop/mobile split, configures plugin permissions in TOML, writes Kotlin and Swift native bindings for Android and iOS. Use when creating or extending Tauri plugins, configuring plugin permissions, writing platform-specific native code, or building Tauri plugins for mobile platforms.
| name | adding-tauri-system-tray |
| description | Guides the user through implementing Tauri system tray functionality, including tray icon setup, tray menu creation, handling tray events, and updating the tray at runtime in the notification area. |
This skill covers implementing system tray (notification area) functionality in Tauri v2 applications.
Enable the tray-icon feature in src-tauri/Cargo.toml:
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
Create a tray icon in src-tauri/src/lib.rs:
use tauri::tray::TrayIconBuilder;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// Use TrayIconBuilder::with_id() to reference the tray later
let tray = TrayIconBuilder::with_id(app, "main-tray")
.icon(app.default_window_icon().unwrap().clone())
.tooltip("My Tauri App")
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?;
let tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.menu_on_left_click(false) // Only show menu on right-click
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem, Submenu},
tray::TrayIconBuilder,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let option1 = MenuItem::with_id(app, "option1", "Option 1", true, None::<&str>)?;
let option2 = MenuItem::with_id(app, "option2", "Option 2", true, None::<&str>)?;
let options_submenu = Submenu::with_items(app, "Options", true, &[&option1, &option2])?;
let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let separator = PredefinedMenuItem::separator(app)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(
app,
&[&show_item, &options_submenu, &separator, &quit_item],
)?;
let tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
Manager,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?;
let tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| {
match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => app.exit(0),
_ => println!("Unhandled menu item: {:?}", event.id),
}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.on_tray_icon_event(|tray, event| {
match event {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
let _ = window.show();
let _ = window.set_focus();
}
}
}
TrayIconEvent::Enter { .. } => println!("Mouse entered tray"),
TrayIconEvent::Leave { .. } => println!("Mouse left tray"),
_ => {}
}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Note: Enter, Move, and Leave events are not supported on Linux.
use tauri::{image::Image, tray::TrayIconBuilder, Manager};
#[tauri::command]
fn update_tray_icon(app: tauri::AppHandle, icon_path: String) -> Result<(), String> {
if let Some(tray) = app.tray_by_id("main-tray") {
let icon = Image::from_path(&icon_path).map_err(|e| e.to_string())?;
tray.set_icon(Some(icon)).map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
fn update_tray_tooltip(app: tauri::AppHandle, tooltip: String) -> Result<(), String> {
if let Some(tray) = app.tray_by_id("main-tray") {
tray.set_tooltip(Some(&tooltip)).map_err(|e| e.to_string())?;
}
Ok(())
}
use std::sync::Mutex;
use tauri::{
menu::{Menu, MenuItem, MenuItemKind},
tray::TrayIconBuilder,
Manager,
};
struct AppState {
menu: Mutex<Option<Menu<tauri::Wry>>>,
}
#[tauri::command]
fn toggle_menu_item(app: tauri::AppHandle, item_id: String, enabled: bool) -> Result<(), String> {
let state = app.state::<AppState>();
if let Some(menu) = state.menu.lock().unwrap().as_ref() {
if let Some(MenuItemKind::MenuItem(item)) = menu.get(&item_id) {
item.set_enabled(enabled).map_err(|e| e.to_string())?;
}
}
Ok(())
}
#[tauri::command]
fn update_menu_text(app: tauri::AppHandle, item_id: String, text: String) -> Result<(), String> {
let state = app.state::<AppState>();
if let Some(menu) = state.menu.lock().unwrap().as_ref() {
if let Some(MenuItemKind::MenuItem(item)) = menu.get(&item_id) {
item.set_text(&text).map_err(|e| e.to_string())?;
}
}
Ok(())
}
For tray toggles that reflect a boolean backing state (always-on-top, save-position, autostart, etc.), use CheckMenuItem instead of MenuItem. To keep the visible check-mark synced with the underlying setting after a click, store the CheckMenuItem handles in managed state and flip .set_checked(...) from the menu handler:
use std::sync::Arc;
use tauri::{menu::CheckMenuItem, Manager, Wry};
// Managed struct holds clones of each check item so handlers can update them.
pub struct TrayHandles {
pub always_on_top: CheckMenuItem<Wry>,
pub save_position: CheckMenuItem<Wry>,
}
pub fn setup(app: &tauri::AppHandle) -> tauri::Result<()> {
// Read initial state from your config / window / plugin so the checkmarks
// show the real starting value, not a hardcoded default.
let aot_initial = app.get_webview_window("main")
.and_then(|w| w.is_always_on_top().ok())
.unwrap_or(true);
let always_on_top = CheckMenuItem::with_id(
app, "always_on_top", "Always on top", /* enabled */ true,
/* checked */ aot_initial, None::<&str>,
)?;
let save_position = CheckMenuItem::with_id(
app, "save_position", "Save position on exit", true, false, None::<&str>,
)?;
// ...build menu and tray as usual, then:
app.manage(TrayHandles {
always_on_top: always_on_top.clone(),
save_position: save_position.clone(),
});
Ok(())
}
fn toggle_always_on_top(app: &tauri::AppHandle) {
let Some(window) = app.get_webview_window("main") else { return };
let new_state = !window.is_always_on_top().unwrap_or(false);
let _ = window.set_always_on_top(new_state);
// Sync the tray check-mark to the new window state.
if let Some(handles) = app.try_state::<TrayHandles>() {
let _ = handles.always_on_top.set_checked(new_state);
}
}
Two points specific to CheckMenuItem vs MenuItem:
checked: bool positional argument before the accelerator..is_checked() / .set_checked(bool) both return tauri::Result<…>; .cloned() is cheap (internally Arc-backed), so storing multiple references for later updates is fine.Alternative without managed handles: retrieve the item on demand via menu.get("<id>") and downcast with if let Some(MenuItemKind::Check(item)) = .... Works, but the managed-handle approach avoids the downcast and keeps handler bodies small.
use tauri::{menu::{Menu, MenuItem}, Manager};
#[tauri::command]
fn set_connected_menu(app: tauri::AppHandle) -> Result<(), String> {
if let Some(tray) = app.tray_by_id("main-tray") {
let disconnect = MenuItem::with_id(&app, "disconnect", "Disconnect", true, None::<&str>)
.map_err(|e| e.to_string())?;
let status = MenuItem::with_id(&app, "status", "Connected", false, None::<&str>)
.map_err(|e| e.to_string())?;
let quit = MenuItem::with_id(&app, "quit", "Quit", true, None::<&str>)
.map_err(|e| e.to_string())?;
let menu = Menu::with_items(&app, &[&status, &disconnect, &quit])
.map_err(|e| e.to_string())?;
tray.set_menu(Some(menu)).map_err(|e| e.to_string())?;
}
Ok(())
}
use std::sync::Mutex;
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager,
};
struct TrayState {
is_paused: Mutex<bool>,
}
#[tauri::command]
fn get_tray_status(state: tauri::State<TrayState>) -> bool {
*state.is_paused.lock().unwrap()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(TrayState { is_paused: Mutex::new(false) })
.setup(|app| {
let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let hide = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?;
let sep = PredefinedMenuItem::separator(app)?;
let pause = MenuItem::with_id(app, "pause", "Pause", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &hide, &sep, &pause, &quit])?;
let _tray = TrayIconBuilder::with_id(app, "main-tray")
.icon(app.default_window_icon().unwrap().clone())
.tooltip("My Tauri App - Running")
.menu(&menu)
.menu_on_left_click(false)
.on_menu_event(|app, event| {
match event.id.as_ref() {
"show" => {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
"hide" => {
if let Some(w) = app.get_webview_window("main") {
let _ = w.hide();
}
}
"pause" => {
let state = app.state::<TrayState>();
let mut paused = state.is_paused.lock().unwrap();
*paused = !*paused;
if let Some(tray) = app.tray_by_id("main-tray") {
let tip = if *paused { "Paused" } else { "Running" };
let _ = tray.set_tooltip(Some(tip));
}
}
"quit" => app.exit(0),
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up, ..
} = event {
let app = tray.app_handle();
if let Some(w) = app.get_webview_window("main") {
if w.is_visible().unwrap_or(false) {
let _ = w.hide();
} else {
let _ = w.show();
let _ = w.set_focus();
}
}
}
})
.build(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![get_tray_status])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
| Platform | Support |
|---|---|
| Windows | Full support for all tray events |
| macOS | Full support for all tray events |
| Linux | Enter, Move, Leave events not supported |
Tray icon not appearing:
tray-icon feature is enabled in Cargo.tomlbuild() is called and result is storedMenu not showing:
.menu(&menu)menu_on_left_click settingEvents not firing:
build()tray_by_id()