Improve tray MacOS

This commit is contained in:
Lennard Kittner
2026-03-19 16:45:12 +01:00
parent 2073e4473e
commit 1c89bd6f0c

View File

@@ -5,21 +5,14 @@ use std::{
use hyper_headset::devices::{DeviceEvent, DeviceProperties, PropertyType}; use hyper_headset::devices::{DeviceEvent, DeviceProperties, PropertyType};
use tray_icon::{ use tray_icon::{
menu::{IconMenuItem, Menu, MenuEvent, MenuId, Submenu}, menu::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu},
TrayIcon, TrayIconBuilder, TrayIcon, TrayIconBuilder,
}; };
use winit::{application::ApplicationHandler, event::StartCause, event_loop::ControlFlow}; use winit::{application::ApplicationHandler, event::StartCause};
//TODO: maybe use MenuItem instead of IconMenuItem but than I probably have to patch Muda because
//it crashes sometimes when trying to handle an image with zero size
const NO_COMPATIBLE_DEVICE: &str = "No compatible device found. Is the dongle plugged in?"; const NO_COMPATIBLE_DEVICE: &str = "No compatible device found. Is the dongle plugged in?";
const HEADSET_NOT_CONNECTED: &str = "Headset is not connected"; const HEADSET_NOT_CONNECTED: &str = "Headset is not connected";
fn placeholder_icon() -> tray_icon::menu::Icon {
tray_icon::menu::Icon::from_rgba(vec![0, 0, 0, 0], 1, 1).unwrap()
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn create_tray_icon() -> tray_icon::Icon { fn create_tray_icon() -> tray_icon::Icon {
// embed a headset .ico/.png at compile time — no file needed at runtime // embed a headset .ico/.png at compile time — no file needed at runtime
@@ -36,8 +29,6 @@ pub struct TrayApp {
pub sender: Sender<DeviceEvent>, pub sender: Sender<DeviceEvent>,
callbacks: CallbackMap, callbacks: CallbackMap,
current_state: Option<Option<DeviceProperties>>, current_state: Option<Option<DeviceProperties>>,
//TODO: maybe not needed anymore?
pending_update: Option<Option<DeviceProperties>>,
} }
impl ApplicationHandler<Option<DeviceProperties>> for TrayApp { impl ApplicationHandler<Option<DeviceProperties>> for TrayApp {
@@ -54,7 +45,7 @@ impl ApplicationHandler<Option<DeviceProperties>> for TrayApp {
.unwrap(), .unwrap(),
); );
} }
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "macos")]
{ {
self.tray_icon = Some( self.tray_icon = Some(
TrayIconBuilder::new() TrayIconBuilder::new()
@@ -72,21 +63,10 @@ impl ApplicationHandler<Option<DeviceProperties>> for TrayApp {
fn user_event( fn user_event(
&mut self, &mut self,
el: &winit::event_loop::ActiveEventLoop, _el: &winit::event_loop::ActiveEventLoop,
device_properties: Option<DeviceProperties>, device_properties: Option<DeviceProperties>,
) { ) {
// Don't call set_menu here — macOS menu is still active at this point. self.update(device_properties);
// Buffer the update and apply it once the event loop is idle.
self.pending_update = Some(device_properties);
el.set_control_flow(ControlFlow::Poll); // wake about_to_wait immediately
}
// Called once the event loop has drained all pending events — menu is closed by now
fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
if let Some(props) = self.pending_update.take() {
self.update(props);
}
el.set_control_flow(ControlFlow::Wait); // go back to sleeping
} }
fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {} fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}
@@ -120,7 +100,6 @@ impl TrayApp {
sender, sender,
callbacks, callbacks,
current_state: None, current_state: None,
pending_update: None,
} }
} }
@@ -134,19 +113,18 @@ impl TrayApp {
return; return;
}; };
let quit_item = IconMenuItem::new("Quit", true, Some(placeholder_icon()), None);
let menu = Menu::new(); let menu = Menu::new();
let mut new_callbacks: HashMap<MenuId, Box<dyn Fn() + Send + Sync>> = HashMap::new(); let mut new_callbacks: HashMap<MenuId, Box<dyn Fn() + Send + Sync>> = HashMap::new();
let Some(device_properties) = device_properties else { let Some(device_properties) = device_properties else {
let _ = tray.set_tooltip(Some(NO_COMPATIBLE_DEVICE)); let _ = tray.set_tooltip(Some(NO_COMPATIBLE_DEVICE));
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "macos")]
tray.set_title(Some(&format!("🎧?"))); tray.set_title(Some(&format!("🎧?")));
let status_item = let status_item = MenuItem::new(NO_COMPATIBLE_DEVICE, false, None);
IconMenuItem::new(NO_COMPATIBLE_DEVICE, false, Some(placeholder_icon()), None);
menu.append(&status_item).unwrap(); menu.append(&status_item).unwrap();
menu.append(&quit_item).unwrap(); menu.append(&PredefinedMenuItem::separator()).unwrap();
new_callbacks.insert(quit_item.id().clone(), Box::new(|| std::process::exit(0))); menu.append(&PredefinedMenuItem::quit(Some("Quit")))
.unwrap();
*self.callbacks.lock().unwrap() = new_callbacks; *self.callbacks.lock().unwrap() = new_callbacks;
tray.set_menu(Some(Box::new(menu))); tray.set_menu(Some(Box::new(menu)));
@@ -156,13 +134,13 @@ impl TrayApp {
if !device_properties.connected.unwrap_or(false) { if !device_properties.connected.unwrap_or(false) {
let _ = tray.set_tooltip(Some(HEADSET_NOT_CONNECTED)); let _ = tray.set_tooltip(Some(HEADSET_NOT_CONNECTED));
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "macos")]
tray.set_title(Some(&format!("🎧?"))); tray.set_title(Some(&format!("🎧?")));
let status_item = let status_item = MenuItem::new(HEADSET_NOT_CONNECTED, false, None);
IconMenuItem::new(HEADSET_NOT_CONNECTED, false, Some(placeholder_icon()), None);
menu.append(&status_item).unwrap(); menu.append(&status_item).unwrap();
menu.append(&quit_item).unwrap(); menu.append(&PredefinedMenuItem::separator()).unwrap();
new_callbacks.insert(quit_item.id().clone(), Box::new(|| std::process::exit(0))); menu.append(&PredefinedMenuItem::quit(Some("Quit")))
.unwrap();
*self.callbacks.lock().unwrap() = new_callbacks; *self.callbacks.lock().unwrap() = new_callbacks;
tray.set_menu(Some(Box::new(menu))); tray.set_menu(Some(Box::new(menu)));
@@ -170,7 +148,7 @@ impl TrayApp {
return; return;
} }
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "macos")]
let _ = tray.set_tooltip(Some( let _ = tray.set_tooltip(Some(
device_properties device_properties
.to_string_with_padding(0) .to_string_with_padding(0)
@@ -191,7 +169,7 @@ impl TrayApp {
.join("\n"), .join("\n"),
)); ));
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "macos")]
if let Some(battery_level) = device_properties.battery_level { if let Some(battery_level) = device_properties.battery_level {
tray.set_title(Some(&format!("🎧 {battery_level}%"))); tray.set_title(Some(&format!("🎧 {battery_level}%")));
} }
@@ -202,10 +180,9 @@ impl TrayApp {
let Some(current_value) = property.data else { let Some(current_value) = property.data else {
continue; continue;
}; };
let menu_item = IconMenuItem::new( let menu_item = MenuItem::new(
format!("{} {}{}", property.prefix, current_value, property.suffix), format!("{} {}{}", property.prefix, current_value, property.suffix),
false, false,
Some(placeholder_icon()),
None, None,
); );
let _ = menu.append(&menu_item); let _ = menu.append(&menu_item);
@@ -220,12 +197,8 @@ impl TrayApp {
); );
for item_value in items { for item_value in items {
let entry = IconMenuItem::new( let entry =
format!("{}{}", item_value, property.suffix), MenuItem::new(format!("{}{}", item_value, property.suffix), true, None);
true,
Some(placeholder_icon()),
None,
);
submenu.append(&entry).unwrap(); submenu.append(&entry).unwrap();
let create_event = property.create_event; let create_event = property.create_event;
@@ -249,11 +222,10 @@ impl TrayApp {
}; };
let create_event = property.create_event; let create_event = property.create_event;
let update_sender = self.sender.clone(); let update_sender = self.sender.clone();
let menu_item = IconMenuItem::new( let menu_item = MenuItem::new(
format!("{} {}{}", property.prefix, current_value, property.suffix), format!("{} {}{}", property.prefix, current_value, property.suffix),
property.property_type == PropertyType::ReadWrite property.property_type == PropertyType::ReadWrite
&& property.data.is_some(), && property.data.is_some(),
Some(placeholder_icon()),
None, None,
); );
let _ = menu.append(&menu_item); let _ = menu.append(&menu_item);
@@ -271,10 +243,9 @@ impl TrayApp {
let Some(current_value) = property.data else { let Some(current_value) = property.data else {
continue; continue;
}; };
let menu_item = IconMenuItem::new( let menu_item = MenuItem::new(
format!("{} {}{}", property.prefix, current_value, property.suffix), format!("{} {}{}", property.prefix, current_value, property.suffix),
false, false,
Some(placeholder_icon()),
None, None,
); );
let _ = menu.append(&menu_item); let _ = menu.append(&menu_item);
@@ -282,8 +253,9 @@ impl TrayApp {
} }
} }
menu.append(&quit_item).unwrap(); menu.append(&PredefinedMenuItem::separator()).unwrap();
new_callbacks.insert(quit_item.id().clone(), Box::new(|| std::process::exit(0))); menu.append(&PredefinedMenuItem::quit(Some("Quit")))
.unwrap();
*self.callbacks.lock().unwrap() = new_callbacks; *self.callbacks.lock().unwrap() = new_callbacks;
tray.set_menu(Some(Box::new(menu))); tray.set_menu(Some(Box::new(menu)));