use std::collections::HashMap;
use tray_icon::menu::{Menu, MenuEvent, MenuItem};
use tray_icon::{Icon, MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
use uzor_window_hub::RgbaIcon;
#[derive(Debug)]
pub enum TrayError {
Build(String),
Icon(String),
}
impl std::fmt::Display for TrayError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TrayError::Build(s) => write!(f, "tray build error: {s}"),
TrayError::Icon(s) => write!(f, "tray icon error: {s}"),
}
}
}
impl std::error::Error for TrayError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrayEvent {
MenuClick(String),
LeftClick,
RightClick,
DoubleClick,
}
pub struct TrayBuilder {
icon: Option<RgbaIcon>,
tooltip: Option<String>,
items: Vec<(String, String, bool)>,
}
impl Default for TrayBuilder {
fn default() -> Self {
Self::new()
}
}
impl TrayBuilder {
pub fn new() -> Self {
Self {
icon: None,
tooltip: None,
items: Vec::new(),
}
}
pub fn icon(mut self, icon: RgbaIcon) -> Self {
self.icon = Some(icon);
self
}
pub fn tooltip(mut self, t: impl Into<String>) -> Self {
self.tooltip = Some(t.into());
self
}
pub fn menu_item(mut self, id: impl Into<String>, label: impl Into<String>) -> Self {
self.items.push((id.into(), label.into(), true));
self
}
pub fn menu_item_disabled(
mut self,
id: impl Into<String>,
label: impl Into<String>,
) -> Self {
self.items.push((id.into(), label.into(), false));
self
}
pub fn build(self) -> Result<TrayHandle, TrayError> {
let menu = Menu::new();
let mut id_map: HashMap<tray_icon::menu::MenuId, String> = HashMap::new();
for (our_id, label, enabled) in &self.items {
let item = MenuItem::new(label, *enabled, None);
id_map.insert(item.id().clone(), our_id.clone());
menu.append(&item).map_err(|e| TrayError::Build(e.to_string()))?;
}
let mut builder = TrayIconBuilder::new()
.with_menu(Box::new(menu))
.with_menu_on_left_click(false);
if let Some(tooltip) = &self.tooltip {
builder = builder.with_tooltip(tooltip);
}
let icon_dims: Option<(u32, u32)> = self.icon.as_ref().map(|i| (i.width, i.height));
if let Some(rgba) = self.icon {
let icon = Icon::from_rgba(rgba.pixels, rgba.width, rgba.height)
.map_err(|e| TrayError::Build(e.to_string()))?;
builder = builder.with_icon(icon);
}
let tray = builder
.build()
.map_err(|e| TrayError::Build(e.to_string()))?;
Ok(TrayHandle {
tray,
id_map,
icon_dims: icon_dims.unwrap_or((32, 32)),
})
}
}
pub struct TrayHandle {
tray: TrayIcon,
id_map: HashMap<tray_icon::menu::MenuId, String>,
icon_dims: (u32, u32),
}
impl TrayHandle {
pub fn set_icon(&mut self, icon: RgbaIcon) -> Result<(), TrayError> {
self.icon_dims = (icon.width, icon.height);
let os_icon = Icon::from_rgba(icon.pixels, icon.width, icon.height)
.map_err(|e| TrayError::Icon(e.to_string()))?;
self.tray
.set_icon(Some(os_icon))
.map_err(|e| TrayError::Icon(e.to_string()))
}
pub fn set_tooltip(&mut self, text: &str) {
let _ = self.tray.set_tooltip(Some(text));
}
pub fn next_event(&self) -> Option<TrayEvent> {
if let Ok(menu_ev) = MenuEvent::receiver().try_recv() {
if let Some(our_id) = self.id_map.get(&menu_ev.id) {
return Some(TrayEvent::MenuClick(our_id.clone()));
}
}
if let Ok(tray_ev) = TrayIconEvent::receiver().try_recv() {
match tray_ev {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => return Some(TrayEvent::LeftClick),
TrayIconEvent::Click {
button: MouseButton::Right,
button_state: MouseButtonState::Up,
..
} => return Some(TrayEvent::RightClick),
TrayIconEvent::DoubleClick {
button: MouseButton::Left,
..
} => return Some(TrayEvent::DoubleClick),
_ => {}
}
}
None
}
pub fn icon_dims(&self) -> (u32, u32) {
self.icon_dims
}
}