pub(crate) mod plugin;
use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener};
use crate::menu::ContextMenu;
use crate::menu::MenuEvent;
use crate::resources::Resource;
use crate::{
image::Image, menu::run_item_main_thread, AppHandle, Manager, PhysicalPosition, Rect, Runtime,
};
use serde::Serialize;
use std::path::Path;
pub use tray_icon::TrayIconId;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize)]
pub enum MouseButtonState {
Up,
Down,
}
impl Default for MouseButtonState {
fn default() -> Self {
Self::Up
}
}
impl From<tray_icon::MouseButtonState> for MouseButtonState {
fn from(value: tray_icon::MouseButtonState) -> Self {
match value {
tray_icon::MouseButtonState::Up => MouseButtonState::Up,
tray_icon::MouseButtonState::Down => MouseButtonState::Down,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize)]
pub enum MouseButton {
Left,
Right,
Middle,
}
impl Default for MouseButton {
fn default() -> Self {
Self::Left
}
}
impl From<tray_icon::MouseButton> for MouseButton {
fn from(value: tray_icon::MouseButton) -> Self {
match value {
tray_icon::MouseButton::Left => MouseButton::Left,
tray_icon::MouseButton::Right => MouseButton::Right,
tray_icon::MouseButton::Middle => MouseButton::Middle,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum TrayIconEvent {
#[serde(rename_all = "camelCase")]
Click {
id: TrayIconId,
position: PhysicalPosition<f64>,
rect: Rect,
button: MouseButton,
button_state: MouseButtonState,
},
DoubleClick {
id: TrayIconId,
position: PhysicalPosition<f64>,
rect: Rect,
button: MouseButton,
},
Enter {
id: TrayIconId,
position: PhysicalPosition<f64>,
rect: Rect,
},
Move {
id: TrayIconId,
position: PhysicalPosition<f64>,
rect: Rect,
},
Leave {
id: TrayIconId,
position: PhysicalPosition<f64>,
rect: Rect,
},
}
impl TrayIconEvent {
pub fn id(&self) -> &TrayIconId {
match self {
TrayIconEvent::Click { id, .. } => id,
TrayIconEvent::DoubleClick { id, .. } => id,
TrayIconEvent::Enter { id, .. } => id,
TrayIconEvent::Move { id, .. } => id,
TrayIconEvent::Leave { id, .. } => id,
}
}
}
impl From<tray_icon::TrayIconEvent> for TrayIconEvent {
fn from(value: tray_icon::TrayIconEvent) -> Self {
match value {
tray_icon::TrayIconEvent::Click {
id,
position,
rect,
button,
button_state,
} => TrayIconEvent::Click {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
button: button.into(),
button_state: button_state.into(),
},
tray_icon::TrayIconEvent::DoubleClick {
id,
position,
rect,
button,
} => TrayIconEvent::DoubleClick {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
button: button.into(),
},
tray_icon::TrayIconEvent::Enter { id, position, rect } => TrayIconEvent::Enter {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
},
tray_icon::TrayIconEvent::Move { id, position, rect } => TrayIconEvent::Move {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
},
tray_icon::TrayIconEvent::Leave { id, position, rect } => TrayIconEvent::Leave {
id,
position,
rect: Rect {
position: rect.position.into(),
size: rect.size.into(),
},
},
_ => todo!(),
}
}
}
#[derive(Default)]
pub struct TrayIconBuilder<R: Runtime> {
on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
inner: tray_icon::TrayIconBuilder,
}
impl<R: Runtime> TrayIconBuilder<R> {
pub fn new() -> Self {
Self {
inner: tray_icon::TrayIconBuilder::new(),
on_menu_event: None,
on_tray_icon_event: None,
}
}
pub fn with_id<I: Into<TrayIconId>>(id: I) -> Self {
let mut builder = Self::new();
builder.inner = builder.inner.with_id(id);
builder
}
pub fn menu<M: ContextMenu>(mut self, menu: &M) -> Self {
self.inner = self.inner.with_menu(menu.inner_context_owned());
self
}
pub fn icon(mut self, icon: Image<'_>) -> Self {
let icon = icon.try_into().ok();
if let Some(icon) = icon {
self.inner = self.inner.with_icon(icon);
}
self
}
pub fn tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
self.inner = self.inner.with_tooltip(s);
self
}
pub fn title<S: AsRef<str>>(mut self, title: S) -> Self {
self.inner = self.inner.with_title(title);
self
}
pub fn temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
self.inner = self.inner.with_temp_dir_path(s);
self
}
pub fn icon_as_template(mut self, is_template: bool) -> Self {
self.inner = self.inner.with_icon_as_template(is_template);
self
}
pub fn menu_on_left_click(mut self, enable: bool) -> Self {
self.inner = self.inner.with_menu_on_left_click(enable);
self
}
pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(
mut self,
f: F,
) -> Self {
self.on_menu_event.replace(Box::new(f));
self
}
pub fn on_tray_icon_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
mut self,
f: F,
) -> Self {
self.on_tray_icon_event.replace(Box::new(f));
self
}
pub fn id(&self) -> &TrayIconId {
self.inner.id()
}
pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
let id = self.id().clone();
let inner = self.inner.build()?;
let icon = TrayIcon {
id,
inner,
app_handle: manager.app_handle().clone(),
};
icon.register(
&icon.app_handle,
self.on_menu_event,
self.on_tray_icon_event,
);
Ok(icon)
}
}
#[tauri_macros::default_runtime(crate::Wry, wry)]
pub struct TrayIcon<R: Runtime> {
id: TrayIconId,
inner: tray_icon::TrayIcon,
app_handle: AppHandle<R>,
}
impl<R: Runtime> Clone for TrayIcon<R> {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
inner: self.inner.clone(),
app_handle: self.app_handle.clone(),
}
}
}
unsafe impl<R: Runtime> Sync for TrayIcon<R> {}
unsafe impl<R: Runtime> Send for TrayIcon<R> {}
impl<R: Runtime> TrayIcon<R> {
fn register(
&self,
app_handle: &AppHandle<R>,
on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
) {
if let Some(handler) = on_menu_event {
app_handle
.manager
.menu
.global_event_listeners
.lock()
.unwrap()
.push(handler);
}
if let Some(handler) = on_tray_icon_event {
app_handle
.manager
.tray
.event_listeners
.lock()
.unwrap()
.insert(self.id.clone(), handler);
}
app_handle
.manager
.tray
.icons
.lock()
.unwrap()
.push(self.clone());
}
pub fn app_handle(&self) -> &AppHandle<R> {
&self.app_handle
}
pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(&self, f: F) {
self
.app_handle
.manager
.menu
.global_event_listeners
.lock()
.unwrap()
.push(Box::new(f));
}
pub fn on_tray_icon_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
&self,
f: F,
) {
self
.app_handle
.manager
.tray
.event_listeners
.lock()
.unwrap()
.insert(self.id.clone(), Box::new(f));
}
pub fn id(&self) -> &TrayIconId {
&self.id
}
pub fn set_icon(&self, icon: Option<Image<'_>>) -> crate::Result<()> {
let icon = match icon {
Some(i) => Some(i.try_into()?),
None => None,
};
run_item_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into)
}
pub fn set_menu<M: ContextMenu + 'static>(&self, menu: Option<M>) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| self_
.inner
.set_menu(menu.map(|m| m.inner_context_owned())))
}
pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> crate::Result<()> {
let s = tooltip.map(|s| s.as_ref().to_string());
run_item_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into)
}
pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) -> crate::Result<()> {
let s = title.map(|s| s.as_ref().to_string());
run_item_main_thread!(self, |self_: Self| self_.inner.set_title(s))
}
pub fn set_visible(&self, visible: bool) -> crate::Result<()> {
run_item_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into)
}
pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) -> crate::Result<()> {
#[allow(unused)]
let p = path.map(|p| p.as_ref().to_path_buf());
#[cfg(target_os = "linux")]
run_item_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?;
Ok(())
}
pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> {
#[cfg(target_os = "macos")]
run_item_main_thread!(self, |self_: Self| self_
.inner
.set_icon_as_template(is_template))?;
Ok(())
}
pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> {
#[cfg(target_os = "macos")]
run_item_main_thread!(self, |self_: Self| self_
.inner
.set_show_menu_on_left_click(enable))?;
Ok(())
}
pub fn rect(&self) -> crate::Result<Option<crate::Rect>> {
run_item_main_thread!(self, |self_: Self| self_.inner.rect().map(|rect| {
Rect {
position: rect.position.into(),
size: rect.size.into(),
}
}))
}
}
impl<R: Runtime> Resource for TrayIcon<R> {
fn close(self: std::sync::Arc<Self>) {
self.app_handle.remove_tray_by_id(&self.id);
}
}
#[cfg(test)]
mod tests {
#[test]
fn tray_event_json_serialization() {
use super::*;
let event = TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
id: TrayIconId::new("id"),
position: crate::PhysicalPosition::default(),
rect: crate::Rect {
position: tray_icon::Rect::default().position.into(),
size: tray_icon::Rect::default().size.into(),
},
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(
value,
serde_json::json!({
"type": "Click",
"button": "Left",
"buttonState": "Down",
"id": "id",
"position": {
"x": 0.0,
"y": 0.0,
},
"rect": {
"size": {
"Physical": {
"width": 0,
"height": 0,
}
},
"position": {
"Physical": {
"x": 0,
"y": 0,
}
},
}
})
);
}
}