Skip to content

Commit e083a30

Browse files
feat(ui): Implement system tray with dynamic icon switching and menu controls
This commit introduces a fully functional system tray for the Mountain application, enhancing native OS integration and user accessibility. Key features include: - A customizable system tray icon loaded from `icons/32x32.png` - Dynamic icon switching capability via the new `SwitchTrayIcon` IPC command - Interactive tray menu with "Open", "Hide", and "Quit" options - Native tray event handling (e.g., left-click toggles window visibility) - Graceful error handling for icon loading and tray operations Additionally, this commit includes: - Updates to `Cargo.toml` to reorder and maintain Tauri features consistently - Enhancements to `Info.plist` for improved macOS compatibility, including specifying the app icon - Refreshed icon assets across multiple platforms (Android, Apple, and general use) - Minor formatting improvements in `Binary.rs` for readability The system tray is initialized during the application setup phase and integrates seamlessly with existing components such as the localhost server and logging infrastructure.
1 parent 55844d4 commit e083a30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+153
-10
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ tonic-prost-build = { workspace = true }
1414

1515
[dependencies]
1616
tauri = { workspace = true, features = [
17-
"wry",
18-
"tray-icon",
17+
"compression",
1918
"devtools",
19+
"image-png",
2020
"rustls-tls",
21-
"compression",
21+
"tray-icon",
22+
"wry",
2223
], default-features = false }
2324

2425
tauri-plugin-dialog = { workspace = true }

Info.plist

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
4-
<dict></dict>
4+
<dict>
5+
<key>CSResourcesFileMapped</key>
6+
<false />
7+
<key>LSRequiresCarbon</key>
8+
<false />
9+
<key>CFBundleIconFile</key>
10+
<string>icon.icns</string>
11+
</dict>
512
</plist>

Source/Binary.rs

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
// Responsibilities:
44
// - Orchestrate the entire application lifecycle.
55
// - Initialize logging (via Tauri plugin), the Echo scheduler,
6-
// ApplicationState, and ApplicationRunTime.
6+
// ApplicationState, and ApplicationRunTime.
77
// - Serve assets via http://localhost (determined by portpicker) to support
8-
// Service Workers.
8+
// Service Workers.
99
// - Parse command-line arguments to open a workspace.
1010
// - Bootstrap native command registration.
1111
// - Set up the Vine gRPC server and spawn the Cocoon sidecar process.
1212
// - Create and customize the main Tauri application window.
1313
// - Manage the main application event loop and graceful shutdown.
14+
// - [NEW] Manage System Tray and native OS integration events.
1415
//
1516
// Logging strategy:
1617
// - Release default: Info (low noise) unless RUST_LOG overrides.
@@ -20,7 +21,7 @@
2021
//
2122
// NOTE (Webview logs):
2223
// - To see Rust logs in the Webview console, enable TargetKind::Webview and
23-
// call attachConsole() in the frontend.
24+
// call attachConsole() in the frontend.
2425

2526
//! # Mountain Binary Entry Point
2627
//!
@@ -38,7 +39,15 @@ use std::{
3839

3940
use Echo::Scheduler::SchedulerBuilder::SchedulerBuilder;
4041
use log::{LevelFilter, debug, error, info, trace, warn};
41-
use tauri::{AppHandle, Manager, RunEvent, Wry};
42+
use tauri::{
43+
AppHandle,
44+
Manager,
45+
RunEvent,
46+
Wry,
47+
image::Image,
48+
menu::{MenuBuilder, MenuItem},
49+
tray::{MouseButton, TrayIconBuilder, TrayIconEvent},
50+
};
4251
use tauri_plugin_log::{RotationStrategy, Target, TargetKind, TimezoneStrategy};
4352

4453
use crate::{
@@ -92,6 +101,109 @@ async fn MountainGetWorkbenchConfiguration(
92101
Ok(Config)
93102
}
94103

104+
/// Dynamically switches the tray icon based on the theme (Light/Dark).
105+
/// Can be invoked from the frontend when the theme changes.
106+
#[tauri::command]
107+
fn SwitchTrayIcon(App:AppHandle, IsDarkMode:bool) {
108+
debug!("[UI] [Tray] Switching icon. IsDarkMode: {}", IsDarkMode);
109+
110+
const DARK_ICON_BYTES:&[u8] = include_bytes!("../icons/32x32.png");
111+
112+
const LIGHT_ICON_BYTES:&[u8] = include_bytes!("../icons/32x32.png");
113+
114+
let IconBytes = if IsDarkMode { DARK_ICON_BYTES } else { LIGHT_ICON_BYTES };
115+
116+
if let Some(Tray) = App.tray_by_id("tray") {
117+
match Image::from_bytes(IconBytes) {
118+
Ok(IconImage) => {
119+
if let Err(e) = Tray.set_icon(Some(IconImage)) {
120+
error!("[UI] [Tray] Failed to set icon: {}", e);
121+
}
122+
},
123+
Err(e) => error!("[UI] [Tray] Failed to load icon bytes: {}", e),
124+
}
125+
} else {
126+
warn!("[UI] [Tray] Tray with ID 'tray' not found.");
127+
}
128+
}
129+
130+
// =============================================================================
131+
// Tray Initialization Logic
132+
// =============================================================================
133+
134+
/// Configures and builds the system tray with menu and event handling.
135+
fn EnableTray(Application:&mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
136+
let Handle = Application.handle();
137+
138+
// Create menu items
139+
let OpenItem = MenuItem::with_id(Handle, "open", "Open Mountain", true, None::<&str>)?;
140+
141+
let HideItem = MenuItem::with_id(Handle, "hide", "Hide Mountain", true, None::<&str>)?;
142+
143+
let Separator = tauri::menu::PredefinedMenuItem::separator(Handle)?;
144+
145+
let QuitItem = MenuItem::with_id(Handle, "quit", "Quit", true, None::<&str>)?;
146+
147+
// Build menu structure
148+
let TrayMenu = MenuBuilder::new(Handle)
149+
.item(&OpenItem)
150+
.item(&HideItem)
151+
.item(&Separator)
152+
.item(&QuitItem)
153+
.build()?;
154+
155+
// Load initial icon (Defaulting to 32x32 from your icons folder)
156+
let IconBytes = include_bytes!("../icons/32x32.png");
157+
158+
let TrayIconImage = Image::from_bytes(IconBytes)?;
159+
160+
// Build the Tray
161+
TrayIconBuilder::with_id("tray")
162+
.icon(TrayIconImage)
163+
.menu(&TrayMenu)
164+
.tooltip("Mountain")
165+
// Handle Menu Item Clicks
166+
.on_menu_event(|AppHandle, Event| match Event.id.as_ref() {
167+
"open" => {
168+
if let Some(Window) = AppHandle.get_webview_window("main") {
169+
let _ = Window.show();
170+
171+
let _ = Window.set_focus();
172+
173+
}
174+
},
175+
"hide" => {
176+
if let Some(Window) = AppHandle.get_webview_window("main") {
177+
let _ = Window.hide();
178+
179+
}
180+
},
181+
"quit" => AppHandle.exit(0),
182+
_ => warn!("[UI] [Tray] Unhandled menu item: {:?}", Event.id),
183+
})
184+
// Handle Native Tray Events (Left Click to Toggle)
185+
.on_tray_icon_event(|Tray, Event| {
186+
if let TrayIconEvent::Click { button: MouseButton::Left, .. } = Event {
187+
let App = Tray.app_handle();
188+
189+
if let Some(Window) = App.get_webview_window("main") {
190+
if Window.is_visible().unwrap_or(false) {
191+
let _ = Window.hide();
192+
} else {
193+
let _ = Window.show();
194+
195+
let _ = Window.set_focus();
196+
}
197+
}
198+
}
199+
})
200+
.build(Application)?;
201+
202+
info!("[UI] [Tray] System tray enabled successfully.");
203+
204+
Ok(())
205+
}
206+
95207
// =============================================================================
96208
// Binary Entrypoint
97209
// =============================================================================
@@ -281,7 +393,9 @@ pub fn Fn() {
281393
.plugin(tauri_plugin_localhost::Builder::new(ServerPort)
282394
.on_request(|_, Response| {
283395
Response.add_header("Access-Control-Allow-Origin", "*");
396+
284397
Response.add_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD");
398+
285399
Response.add_header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin, Accept");
286400
})
287401
.build())
@@ -304,6 +418,17 @@ pub fn Fn() {
304418

305419
TraceStep!("[Lifecycle] [Setup] AppHandle acquired.");
306420

421+
// ---------------------------------------------------------
422+
// [UI] [Tray] Initialize System Tray
423+
// ---------------------------------------------------------
424+
debug!("[UI] [Tray] Initializing system tray...");
425+
426+
if let Err(Error) = EnableTray(Application) {
427+
error!("[UI] [Tray] Failed to enable tray: {}", Error);
428+
429+
// We do not crash the app if tray fails, but we log it.
430+
}
431+
307432
// ---------------------------------------------------------
308433
// [Lifecycle] [Commands] Bootstrap native commands
309434
// ---------------------------------------------------------
@@ -437,6 +562,7 @@ pub fn Fn() {
437562
);
438563

439564
ScanPathsGuard.push(LocalPath);
565+
440566
}
441567
}
442568

@@ -493,6 +619,7 @@ pub fn Fn() {
493619
// [IPC] Command routing
494620
// ---------------------------------------------------------------------
495621
.invoke_handler(tauri::generate_handler![
622+
SwitchTrayIcon,
496623
MountainGetWorkbenchConfiguration,
497624
Command::TreeView::GetTreeViewChildren,
498625
Command::LanguageFeature::MountainProvideHover,
@@ -535,10 +662,8 @@ pub fn Fn() {
535662
RunTime.inner().clone().Shutdown().await;
536663

537664
info!("[Lifecycle] [Shutdown] ApplicationRunTime stopped.");
538-
539665
} else {
540666
error!("[Lifecycle] [Shutdown] ApplicationRunTime not found.");
541-
542667
}
543668

544669
debug!("[Lifecycle] [Shutdown] Stopping Echo scheduler...");
@@ -555,6 +680,7 @@ pub fn Fn() {
555680

556681
ApplicationHandleClone.exit(0);
557682
});
683+
558684
}
559685
});
560686

app-icon.png

39.2 KB
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
4+
<background android:drawable="@color/ic_launcher_background"/>
5+
</adaptive-icon>
150 Bytes
-15 Bytes
268 Bytes
193 Bytes
18 Bytes

0 commit comments

Comments
 (0)