use std::sync::Arc;
pub enum TrayMenuItem {
Item {
label: String,
enabled: bool,
callback: Arc<dyn Fn() + Send + Sync>,
},
Separator,
Submenu {
label: String,
items: Vec<TrayMenuItem>,
},
}
impl TrayMenuItem {
pub fn item(label: impl Into<String>, callback: impl Fn() + Send + Sync + 'static) -> Self {
Self::Item {
label: label.into(),
enabled: true,
callback: Arc::new(callback),
}
}
pub fn item_disabled(label: impl Into<String>) -> Self {
Self::Item {
label: label.into(),
enabled: false,
callback: Arc::new(|| {}),
}
}
pub fn separator() -> Self {
Self::Separator
}
pub fn submenu(label: impl Into<String>, items: Vec<TrayMenuItem>) -> Self {
Self::Submenu {
label: label.into(),
items,
}
}
}
pub struct TrayIconBuilder {
tooltip: Option<String>,
icon_rgba: Option<(Vec<u8>, u32, u32)>,
menu_items: Vec<TrayMenuItem>,
}
impl TrayIconBuilder {
pub fn new() -> Self {
Self {
tooltip: None,
icon_rgba: None,
menu_items: Vec::new(),
}
}
pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn icon_rgba(mut self, rgba: Vec<u8>, width: u32, height: u32) -> Self {
self.icon_rgba = Some((rgba, width, height));
self
}
pub fn menu(mut self, items: Vec<TrayMenuItem>) -> Self {
self.menu_items = items;
self
}
pub fn build(self) -> Option<TrayHandle> {
#[cfg(feature = "tray-icon")]
{
use tray_icon::TrayIconBuilder as NativeTrayBuilder;
let menu = muda::Menu::new();
let mut callbacks: Vec<(muda::MenuId, Arc<dyn Fn() + Send + Sync>)> = Vec::new();
type MenuCallback = (muda::MenuId, Arc<dyn Fn() + Send + Sync>);
fn build_menu_items(
menu: &muda::Menu,
items: &[TrayMenuItem],
callbacks: &mut Vec<MenuCallback>,
) {
for item in items {
match item {
TrayMenuItem::Item {
label,
enabled,
callback,
} => {
let menu_item = muda::MenuItem::new(label, *enabled, None);
callbacks.push((menu_item.id().clone(), Arc::clone(callback)));
let _ = menu.append(&menu_item);
}
TrayMenuItem::Separator => {
let _ = menu.append(&muda::PredefinedMenuItem::separator());
}
TrayMenuItem::Submenu { label, items } => {
let submenu = muda::Submenu::new(label, true);
for sub_item in items {
match sub_item {
TrayMenuItem::Item {
label,
enabled,
callback,
} => {
let menu_item = muda::MenuItem::new(label, *enabled, None);
callbacks
.push((menu_item.id().clone(), Arc::clone(callback)));
let _ = submenu.append(&menu_item);
}
TrayMenuItem::Separator => {
let _ =
submenu.append(&muda::PredefinedMenuItem::separator());
}
_ => {}
}
}
let _ = menu.append(&submenu);
}
}
}
}
build_menu_items(&menu, &self.menu_items, &mut callbacks);
let icon = if let Some((rgba, w, h)) = self.icon_rgba {
tray_icon::Icon::from_rgba(rgba, w, h).ok()
} else {
let size = 32u32;
let mut rgba = vec![0u8; (size * size * 4) as usize];
for pixel in rgba.chunks_exact_mut(4) {
pixel[0] = 100; pixel[1] = 150; pixel[2] = 255; pixel[3] = 255; }
tray_icon::Icon::from_rgba(rgba, size, size).ok()
};
let icon = match icon {
Some(i) => i,
None => {
tracing::error!("Failed to create tray icon");
return None;
}
};
let mut builder = NativeTrayBuilder::new().with_icon(icon);
if let Some(ref tooltip) = self.tooltip {
builder = builder.with_tooltip(tooltip);
}
if !self.menu_items.is_empty() {
builder = builder.with_menu(Box::new(menu));
}
match builder.build() {
Ok(tray) => {
let callbacks = Arc::new(callbacks);
std::thread::spawn(move || {
let receiver = muda::MenuEvent::receiver();
while let Ok(event) = receiver.recv() {
for (id, cb) in callbacks.iter() {
if *id == event.id {
cb();
break;
}
}
}
});
Some(TrayHandle { _tray: tray })
}
Err(e) => {
tracing::error!("Failed to build tray icon: {}", e);
None
}
}
}
#[cfg(not(feature = "tray-icon"))]
{
tracing::warn!("System tray not available (tray-icon feature not enabled)");
None
}
}
}
impl Default for TrayIconBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct TrayHandle {
#[cfg(feature = "tray-icon")]
_tray: tray_icon::TrayIcon,
}