@@ -26,7 +26,6 @@ use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}
2626use tauri:: webview:: WebviewWindowBuilder ;
2727use tauri:: {
2828 AppHandle , Manager , Monitor , PhysicalPosition , Rect as TauriRect , Runtime , WebviewUrl ,
29- WindowEvent ,
3029} ;
3130
3231use crate :: i18n:: menu_labels;
@@ -41,20 +40,35 @@ const TRAY_WINDOW_HEIGHT: f64 = 600.0;
4140
4241/// Click-toggle dedupe window, in milliseconds.
4342///
44- /// macOS dispatches the tray window's focus-loss event before the
45- /// status item 's click handler when the user clicks the tray icon
46- /// while the mini window is open. Without this dedupe, the focus-loss
47- /// handler hides the window and the click handler immediately shows
48- /// it again — turning a "click to dismiss" into a no-op flicker.
49- /// Recording the auto-hide timestamp and skipping `show` when the click
50- /// arrives within this window gives us toggle semantics.
43+ /// Some platforms can deliver a tray- window auto-hide before the tray
44+ /// icon 's click handler when the user clicks the icon while the mini
45+ /// window is open. Without this dedupe, the auto-hide path hides the
46+ /// window and the click handler immediately shows it again — turning a
47+ /// "click to dismiss" into a no-op flicker. Recording the auto-hide
48+ /// timestamp and skipping `show` when the click arrives within this
49+ /// window gives us toggle semantics.
5150const TRAY_TOGGLE_DEDUPE_MS : u64 = 300 ;
5251
53- /// Wall-clock millis of the last focus-loss-driven auto-hide of the
54- /// tray window. 0 means "never". `AtomicU64` keeps it lock-free and
55- /// safe to read/write from any thread.
52+ /// macOS can deliver the status-item mouse-down to our global
53+ /// click-outside monitor after the tray click handler has already
54+ /// scheduled or shown the mini window. Suppress global auto-hide for a
55+ /// very short window after an icon-triggered show so the opening click
56+ /// can't immediately dismiss the window it just requested, while still
57+ /// allowing an intentional outside click right after opening to close it.
58+ #[ cfg( target_os = "macos" ) ]
59+ const TRAY_GLOBAL_HIDE_SUPPRESS_AFTER_ICON_CLICK_MS : u64 = 150 ;
60+
61+ /// Wall-clock millis of the last auto-hide of the tray window. 0 means
62+ /// "never". `AtomicU64` keeps it lock-free and safe to read/write from
63+ /// any thread.
5664static LAST_TRAY_AUTO_HIDE_MS : AtomicU64 = AtomicU64 :: new ( 0 ) ;
5765
66+ /// Wall-clock millis of the last tray-icon click that requested a
67+ /// mini-window show. Used only to dedupe macOS global-monitor events
68+ /// from the same physical click.
69+ #[ cfg( target_os = "macos" ) ]
70+ static LAST_TRAY_ICON_SHOW_CLICK_MS : AtomicU64 = AtomicU64 :: new ( 0 ) ;
71+
5872fn now_ms ( ) -> u64 {
5973 SystemTime :: now ( )
6074 . duration_since ( UNIX_EPOCH )
@@ -114,7 +128,7 @@ pub fn install_tray<R: Runtime>(app: &AppHandle<R>) -> Result<(), tauri::Error>
114128 Ok ( ( ) )
115129}
116130
117- fn handle_left_click < R : Runtime > (
131+ fn handle_left_click < R : Runtime + ' static > (
118132 app : & AppHandle < R > ,
119133 cursor : PhysicalPosition < f64 > ,
120134 icon_rect : TauriRect ,
@@ -129,16 +143,15 @@ fn handle_left_click<R: Runtime>(
129143 } ;
130144 if mini_enabled {
131145 // Toggle semantics: a click on the icon while the mini window
132- // is open should dismiss it. macOS status-item clicks do not
133- // always steal key-window status from the tray window , so we
134- // can't rely on the focus-loss path alone — handle both:
146+ // is open should dismiss it. Tray icon clicks and auto-hide
147+ // paths can interleave differently across platforms , so handle
148+ // both:
135149 //
136- // (a) status-item click did NOT blur the tray window —
137- // the window is still visible here; hide it explicitly.
138- // (b) status-item click DID blur the tray window — the
139- // focus-loss handler already hid it and stamped
140- // LAST_TRAY_AUTO_HIDE_MS; skip the show so the dismissal
141- // sticks instead of immediately re-showing.
150+ // (a) the tray window is still visible here; hide it
151+ // explicitly.
152+ // (b) an auto-hide path already hid it and stamped
153+ // LAST_TRAY_AUTO_HIDE_MS; skip re-showing so the
154+ // dismissal sticks.
142155 if let Some ( window) = app. get_webview_window ( TRAY_WINDOW_LABEL ) {
143156 if window. is_visible ( ) . unwrap_or ( false ) {
144157 let _ = window. hide ( ) ;
@@ -149,11 +162,34 @@ fn handle_left_click<R: Runtime>(
149162 if last_hide != 0 && now_ms ( ) . saturating_sub ( last_hide) < TRAY_TOGGLE_DEDUPE_MS {
150163 return ;
151164 }
165+ show_tray_window_from_tray_click ( app, cursor, icon_rect) ;
166+ } else {
167+ show_main_window ( app) ;
168+ }
169+ }
170+
171+ fn show_tray_window_from_tray_click < R : Runtime + ' static > (
172+ app : & AppHandle < R > ,
173+ cursor : PhysicalPosition < f64 > ,
174+ icon_rect : TauriRect ,
175+ ) {
176+ #[ cfg( target_os = "macos" ) ]
177+ {
178+ LAST_TRAY_ICON_SHOW_CLICK_MS . store ( now_ms ( ) , Ordering :: Relaxed ) ;
179+ let app_for_show = app. clone ( ) ;
180+ if let Err ( e) = app. run_on_main_thread ( move || {
181+ if let Err ( e) = show_tray_window ( & app_for_show, cursor, icon_rect) {
182+ log:: warn!( "failed to show mini window: {e}" ) ;
183+ }
184+ } ) {
185+ log:: warn!( "failed to schedule mini window show: {e}" ) ;
186+ }
187+ }
188+ #[ cfg( not( target_os = "macos" ) ) ]
189+ {
152190 if let Err ( e) = show_tray_window ( app, cursor, icon_rect) {
153191 log:: warn!( "failed to show mini window: {e}" ) ;
154192 }
155- } else {
156- show_main_window ( app) ;
157193 }
158194}
159195
@@ -349,22 +385,23 @@ fn show_tray_window<R: Runtime>(
349385 . map_err ( |e| e. to_string ( ) ) ?;
350386 }
351387
352- // On macOS, both `window.show()` and `window.set_focus()` call
353- // `makeKeyAndOrderFront:`, which activates the app via
354- // `activateIgnoringOtherApps:`. When the main window is visible
355- // but unfocused (e.g. another app was active), that activation
356- // surfaces the main window into key — visible as a brief focus
357- // flicker on the main window. Bypass Tauri here and call
358- // `orderFrontRegardless` directly: it puts the tray on top across
359- // app boundaries without activating our app, so main keeps its
360- // state. The first click inside the tray webview will activate
361- // our app naturally and make the tray the key window.
388+ // On macOS, avoid Tauri's `set_focus()` because it unconditionally
389+ // activates the whole app and can surface the main window. If the
390+ // app is inactive, though, the tray window must activate the app or
391+ // it remains a painted-but-not-interactive window that disappears
392+ // as soon as the pointer leaves the status item. Activate only in
393+ // that inactive case, then make the tray window key.
362394 #[ cfg( target_os = "macos" ) ]
363395 {
364- use objc2:: { msg_send, runtime:: AnyObject } ;
396+ use objc2:: { class , msg_send, runtime:: AnyObject } ;
365397 let ns_window = window. ns_window ( ) . map_err ( |e| e. to_string ( ) ) ? as * mut AnyObject ;
366398 unsafe {
367- let _: ( ) = msg_send ! [ ns_window, orderFrontRegardless] ;
399+ let ns_app: * mut AnyObject = msg_send ! [ class!( NSApplication ) , sharedApplication] ;
400+ let app_is_active: bool = msg_send ! [ ns_app, isActive] ;
401+ if !app_is_active {
402+ let _: ( ) = msg_send ! [ ns_app, activateIgnoringOtherApps: true ] ;
403+ }
404+ let _: ( ) = msg_send ! [ ns_window, makeKeyAndOrderFront: std:: ptr:: null:: <AnyObject >( ) ] ;
368405 }
369406 }
370407 #[ cfg( not( target_os = "macos" ) ) ]
@@ -399,22 +436,25 @@ fn create_tray_window<R: Runtime>(
399436 . shadow ( true )
400437 . build ( ) ?;
401438
402- let window_for_handler = window. clone ( ) ;
403- window. on_window_event ( move |event| {
404- // Hide on focus loss so the popover behaves like a real
405- // menubar mini-window: click outside → it disappears.
406- // Only stamp the auto-hide timestamp + call hide when the
407- // window is still visible — otherwise this is the trailing
408- // blur from a hide we already performed in handle_left_click,
409- // and re-stamping would freeze the next click out of show via
410- // the dedupe window.
411- if let WindowEvent :: Focused ( false ) = event {
412- if window_for_handler. is_visible ( ) . unwrap_or ( false ) {
413- LAST_TRAY_AUTO_HIDE_MS . store ( now_ms ( ) , Ordering :: Relaxed ) ;
414- let _ = window_for_handler. hide ( ) ;
439+ #[ cfg( not( target_os = "macos" ) ) ]
440+ {
441+ let window_for_handler = window. clone ( ) ;
442+ window. on_window_event ( move |event| {
443+ // Hide on focus loss so the popover behaves like a real
444+ // tray mini-window: click outside -> it disappears.
445+ // Only stamp the auto-hide timestamp + call hide when the
446+ // window is still visible -- otherwise this is the trailing
447+ // blur from a hide we already performed in handle_left_click,
448+ // and re-stamping would freeze the next click out of show via
449+ // the dedupe window.
450+ if let tauri:: WindowEvent :: Focused ( false ) = event {
451+ if window_for_handler. is_visible ( ) . unwrap_or ( false ) {
452+ LAST_TRAY_AUTO_HIDE_MS . store ( now_ms ( ) , Ordering :: Relaxed ) ;
453+ let _ = window_for_handler. hide ( ) ;
454+ }
415455 }
416- }
417- } ) ;
456+ } ) ;
457+ }
418458
419459 Ok ( window)
420460}
@@ -584,7 +624,7 @@ fn install_dismiss_monitors<R: Runtime>(app: &AppHandle<R>) {
584624 // the desktop, the menu bar (incl. the tray icon itself).
585625 let app_for_global = app. clone ( ) ;
586626 let global_block = RcBlock :: new ( move |_event : * mut AnyObject | {
587- hide_tray_if_visible ( & app_for_global) ;
627+ hide_tray_if_visible_unless_recent_icon_show ( & app_for_global) ;
588628 } ) ;
589629
590630 // Local monitor: clicks inside our app — fires for the tray window
@@ -630,6 +670,17 @@ fn install_dismiss_monitors<R: Runtime>(app: &AppHandle<R>) {
630670 std:: mem:: forget ( local_block) ;
631671}
632672
673+ #[ cfg( target_os = "macos" ) ]
674+ fn hide_tray_if_visible_unless_recent_icon_show < R : Runtime > ( app : & AppHandle < R > ) {
675+ let last_icon_show = LAST_TRAY_ICON_SHOW_CLICK_MS . load ( Ordering :: Relaxed ) ;
676+ if last_icon_show != 0
677+ && now_ms ( ) . saturating_sub ( last_icon_show) < TRAY_GLOBAL_HIDE_SUPPRESS_AFTER_ICON_CLICK_MS
678+ {
679+ return ;
680+ }
681+ hide_tray_if_visible ( app) ;
682+ }
683+
633684#[ cfg( target_os = "macos" ) ]
634685fn hide_tray_if_visible < R : Runtime > ( app : & AppHandle < R > ) {
635686 if let Some ( tray) = app. get_webview_window ( TRAY_WINDOW_LABEL ) {
0 commit comments