11use dioxus:: prelude:: * ;
2- #[ cfg( feature = "desktop" ) ]
3- use n0_error:: Result ;
42use std:: sync:: OnceLock ;
53use tracing:: info;
64use tracing_appender:: non_blocking:: WorkerGuard ;
@@ -9,16 +7,16 @@ use tracing_subscriber::{fmt, prelude::*, EnvFilter};
97use crate :: components:: { Head , Splash , UpdateDialog } ;
108use crate :: state:: AppState ;
119use crate :: views:: {
12- Chrome , JoinProxy , Login , ProxiesList , SelectProject , Settings , TunnelBandwidth ,
10+ Chrome , JoinProxy , Login , ProxiesList , SelectProject , Settings , TrayNavHandler , TunnelBandwidth ,
1311} ;
1412
1513#[ cfg( feature = "desktop" ) ]
1614use dioxus_desktop:: {
1715 trayicon:: {
18- menu:: { Menu , MenuItem , PredefinedMenuItem } ,
16+ menu:: { IconMenuItem , Menu , MenuItem , NativeIcon , PredefinedMenuItem } ,
1917 Icon , TrayIcon , TrayIconBuilder ,
2018 } ,
21- use_tray_menu_event_handler , use_window,
19+ use_muda_event_handler , use_window,
2220} ;
2321
2422mod components;
@@ -51,6 +49,7 @@ static MANUAL_UPDATE_CHECK_FLAG: std::sync::atomic::AtomicBool =
5149#[ derive( Debug , Clone , Routable , PartialEq ) ]
5250#[ rustfmt:: skip]
5351enum Route {
52+ #[ layout( TrayNavHandler ) ]
5453 #[ route( "/" ) ]
5554 Login { } ,
5655 #[ layout( Chrome ) ]
@@ -95,9 +94,6 @@ fn main() {
9594 #[ cfg( all( feature = "desktop" , target_os = "linux" ) ) ]
9695 gtk:: init ( ) . unwrap ( ) ;
9796
98- #[ cfg( feature = "desktop" ) ]
99- let _tray_icon = init_menu_bar ( ) . unwrap ( ) ;
100-
10197 #[ cfg( feature = "desktop" ) ]
10298 {
10399 use dioxus_desktop:: { Config , LogicalSize , WindowBuilder , WindowCloseBehaviour } ;
@@ -300,36 +296,52 @@ fn App() -> Element {
300296 } ) ;
301297 }
302298
299+ let mut open_add_tunnel_from_tray = use_signal ( || false ) ;
300+ let mut login_state_for_tray = use_signal ( || None :: < lib:: datum_cloud:: LoginState > ) ;
301+
302+ // Create tray when app state is ready. Must run before early return.
303303 #[ cfg( feature = "desktop" ) ]
304- use_tray_menu_event_handler ( move |event| -> ( ) {
305- // The event ID corresponds to the menu item text
306- let _: ( ) = match event. id . 0 . as_str ( ) {
307- "About Datum" => {
308- let _ = open:: that ( "https://datum.net" ) ;
309- ( )
310- }
311- "Show Window" => {
312- use_window ( ) . set_visible ( true ) ;
313- ( )
314- }
315- "Hide" => {
316- use_window ( ) . set_visible ( false ) ;
317- ( )
318- }
319- "Check for Updates..." => {
320- manual_update_check. set ( true ) ;
321- update_check_in_progress. set ( true ) ;
322- ( )
323- }
324- "Quit" => {
325- std:: process:: exit ( 0 ) ;
304+ {
305+ let mut tray_holder = use_signal ( || None :: < TrayIcon > ) ;
306+
307+ use_effect ( move || {
308+ if !app_state_ready ( ) {
309+ return ;
326310 }
327- _ => {
328- eprintln ! ( "Unknown menu event: {}" , event. id. 0 ) ;
329- ( )
311+ if ( * tray_holder. peek ( ) ) . is_some ( ) {
312+ return ;
330313 }
331- } ;
332- } ) ;
314+ let tray = init_tray ( ) ;
315+ tray_holder. set ( Some ( tray) ) ;
316+ } ) ;
317+
318+ let window = use_window ( ) ;
319+ use_muda_event_handler ( move |event| -> ( ) {
320+ let _: ( ) = match event. id . 0 . as_str ( ) {
321+ "about" => {
322+ let _ = open:: that ( "https://datum.net" ) ;
323+ }
324+ "show" => {
325+ window. set_visible ( true ) ;
326+ window. set_focus ( ) ;
327+ }
328+ "hide" => {
329+ window. set_visible ( false ) ;
330+ }
331+ "new_tunnel" => {
332+ if login_state_for_tray ( ) == Some ( lib:: datum_cloud:: LoginState :: Missing )
333+ || login_state_for_tray ( ) . is_none ( )
334+ {
335+ return ;
336+ }
337+ window. set_visible ( true ) ;
338+ window. set_focus ( ) ;
339+ open_add_tunnel_from_tray. set ( true ) ;
340+ }
341+ _ => { }
342+ } ;
343+ } ) ;
344+ }
333345
334346 if !app_state_ready ( ) {
335347 return rsx ! {
@@ -345,6 +357,11 @@ fn App() -> Element {
345357 provide_context ( auth_changed) ;
346358
347359 let state_for_auth_watch = consume_context :: < AppState > ( ) ;
360+ use_effect ( move || {
361+ let _ = auth_changed ( ) ;
362+ let state = consume_context :: < AppState > ( ) ;
363+ login_state_for_tray. set ( Some ( state. datum ( ) . login_state ( ) ) ) ;
364+ } ) ;
348365 use_future ( move || {
349366 let state_for_auth_watch = state_for_auth_watch. clone ( ) ;
350367 let mut auth_changed = auth_changed;
@@ -365,6 +382,9 @@ fn App() -> Element {
365382 in_progress : update_check_in_progress,
366383 } ) ;
367384
385+ // Tray can trigger opening the add tunnel dialog; Chrome (inside Router) handles navigation + dialog.
386+ provide_context ( open_add_tunnel_from_tray) ;
387+
368388 rsx ! {
369389 div { class: "theme-alpha" ,
370390 TitleBar { }
@@ -387,43 +407,52 @@ fn App() -> Element {
387407}
388408
389409#[ cfg( feature = "desktop" ) ]
390- fn init_menu_bar ( ) -> Result < TrayIcon > {
391- // Initialize the tray menu
392-
410+ fn init_tray ( ) -> TrayIcon {
393411 use n0_error:: StdResultExt ;
412+
394413 let tray_menu = Menu :: new ( ) ;
414+ let about_item = MenuItem :: with_id ( "about" , "About Datum" , true , None ) ;
415+ let show_item = MenuItem :: with_id ( "show" , "Show Window" , true , None ) ;
416+ let hide_item = MenuItem :: with_id ( "hide" , "Hide" , true , None ) ;
417+
418+ #[ cfg( target_os = "macos" ) ]
419+ let new_tunnel_item = IconMenuItem :: with_id_and_native_icon (
420+ "new_tunnel" ,
421+ "New Tunnel" ,
422+ true ,
423+ Some ( NativeIcon :: Add ) ,
424+ None ,
425+ ) ;
426+ #[ cfg( not( target_os = "macos" ) ) ]
427+ let new_tunnel_item = IconMenuItem :: with_id ( "new_tunnel" , "New Tunnel" , true , None , None ) ;
395428
396- // Create menu items with IDs for event handling
397- let about_item = MenuItem :: new ( "About Datum" , true , None ) ;
398- let show_item = MenuItem :: new ( "Show Window" , true , None ) ;
399- let hide_item = MenuItem :: new ( "Hide" , true , None ) ;
400- let separator1 = PredefinedMenuItem :: separator ( ) ;
401- let check_updates_item = MenuItem :: new ( "Check for Updates..." , true , None ) ;
402- let separator2 = PredefinedMenuItem :: separator ( ) ;
403- let quit_item = MenuItem :: new ( "Quit" , true , None ) ;
429+ let version_item = MenuItem :: with_id (
430+ "version" ,
431+ format ! ( "Version {}" , env!( "CARGO_PKG_VERSION" ) ) ,
432+ false ,
433+ None ,
434+ ) ;
435+ let sep = PredefinedMenuItem :: separator ( ) ;
404436
405- // Build the menu structure (macOS-style: About, Show, Hide, sep, Check for Updates, sep, Quit)
406437 tray_menu
407438 . append_items ( & [
439+ & new_tunnel_item,
440+ & sep,
408441 & about_item,
409442 & show_item,
410443 & hide_item,
411- & separator1,
412- & check_updates_item,
413- & separator2,
414- & quit_item,
444+ & sep,
445+ & version_item,
415446 ] )
416447 . expect ( "Failed to build tray menu" ) ;
417448
418- let icon = icon ( ) ;
419-
420- // Build the tray icon
421449 TrayIconBuilder :: new ( )
422450 . with_menu ( Box :: new ( tray_menu) )
423451 . with_tooltip ( "Datum" )
424- . with_icon ( icon)
452+ . with_icon ( icon ( ) )
425453 . build ( )
426454 . std_context ( "building tray icon" )
455+ . expect ( "failed to build tray icon" )
427456}
428457
429458/// Load an icon from a PNG file for the tray
0 commit comments