From b288f0d155e475ae945ce2ef54c156a7e6774c11 Mon Sep 17 00:00:00 2001 From: nessi Date: Tue, 17 Mar 2026 19:56:46 +0100 Subject: [PATCH] feat: add system tray icon with minimize-to-tray behavior Enable tray-icon feature in Tauri dependencies. Add system tray with Open and Quit menu items. Implement tray icon click handlers to restore main window. Add window event handlers to hide window on close/minimize instead of exiting application. Add restore_main_window and hide_main_window helper functions for window visibility management. --- desktop-client/src-tauri/Cargo.toml | 2 +- desktop-client/src-tauri/src/lib.rs | 68 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/desktop-client/src-tauri/Cargo.toml b/desktop-client/src-tauri/Cargo.toml index c932a8c..8b56a53 100644 --- a/desktop-client/src-tauri/Cargo.toml +++ b/desktop-client/src-tauri/Cargo.toml @@ -18,5 +18,5 @@ rand_core = { version = "0.6", features = ["getrandom"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tauri = { version = "2.10.1", features = [] } +tauri = { version = "2.10.1", features = ["tray-icon"] } x25519-dalek = { version = "2.0", features = ["static_secrets"] } diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index 35cc572..b25670b 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -6,10 +6,15 @@ use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; use rand_core::OsRng; use reqwest::Client; use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Manager, State}; +use tauri::{ + menu::{MenuBuilder, MenuItemBuilder}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Manager, State, WebviewWindow, WindowEvent, +}; use x25519_dalek::{PublicKey, StaticSecret}; const PROFILE_NAME: &str = "NexaVPN"; +const MAIN_WINDOW_LABEL: &str = "main"; struct AppState { session: Mutex>, @@ -263,12 +268,73 @@ fn ensure_app_dir(app: &AppHandle) -> Result { Ok(dir) } +fn restore_main_window(window: &WebviewWindow) { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); +} + +fn hide_main_window(window: &WebviewWindow) { + let _ = window.hide(); +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .setup(|app| { + let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN").build(app)?; + let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN").build(app)?; + let menu = MenuBuilder::new(app).items(&[&open_item, &quit_item]).build()?; + + let mut tray = TrayIconBuilder::new().menu(&menu).show_menu_on_left_click(false); + if let Some(icon) = app.default_window_icon() { + tray = tray.icon(icon.clone()); + } + + tray + .on_menu_event(|app, event| match event.id().as_ref() { + "open" => { + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + restore_main_window(&window); + } + } + "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(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + restore_main_window(&window); + } + } + }) + .build(app)?; + + Ok(()) + }) .manage(AppState { session: Mutex::new(None), }) + .on_window_event(|window, event| match event { + WindowEvent::CloseRequested { api, .. } => { + api.prevent_close(); + hide_main_window(window); + } + WindowEvent::Resized(_) => { + if window.is_minimized().unwrap_or(false) { + hide_main_window(window); + } + } + _ => {} + }) .invoke_handler(tauri::generate_handler![load_state, enroll_device, connect_tunnel, disconnect_tunnel]) .run(tauri::generate_context!()) .expect("error while running tauri application");