use std::path::PathBuf;
use pyo3::{prelude::*, types::PyString};
use pyo3_utils::{
py_wrapper::{PyWrapper, PyWrapperT0},
ungil::UnsafeUngilExt,
};
use tauri::tray;
use crate::{
ext_mod::{
self, manager_method_impl,
menu::{context_menu_impl, ImplContextMenu},
ImplManager, PhysicalPositionF64, PyAppHandleExt as _, Rect,
},
tauri_runtime::Runtime,
utils::{delegate_inner, PyResultExt as _, TauriError},
};
type TauriTrayIcon = tray::TrayIcon<Runtime>;
pub type TrayIconId = PyString;
#[pyclass(frozen)]
#[non_exhaustive]
pub struct TrayIcon(pub PyWrapper<PyWrapperT0<TauriTrayIcon>>);
impl TrayIcon {
pub(crate) fn new(tray_icon: TauriTrayIcon) -> Self {
Self(PyWrapper::new0(tray_icon))
}
#[inline]
fn new_impl(
py: Python<'_>,
manager: &impl tauri::Manager<Runtime>,
id: Option<impl Into<tray::TrayIconId> + Send>,
) -> PyResult<Self> {
unsafe {
py.allow_threads_unsend(manager, |manager| {
let tray_icon_builder = if let Some(id) = id {
tray::TrayIconBuilder::with_id(id)
} else {
tray::TrayIconBuilder::new()
};
let tray_icon = tray_icon_builder.build(manager)?;
tauri::Result::Ok(Self::new(tray_icon))
})
}
.map_err(TauriError::from)
.map_err(PyErr::from)
}
}
#[pymethods]
impl TrayIcon {
#[new]
fn __new__(py: Python<'_>, manager: ImplManager) -> PyResult<Self> {
manager_method_impl!(py, &manager, |py, manager| {
Self::new_impl(py, manager, None::<&str>)
})?
}
#[staticmethod]
fn with_id(py: Python<'_>, manager: ImplManager, id: String) -> PyResult<Self> {
let id = tray::TrayIconId(id);
manager_method_impl!(py, &manager, |py, manager| {
Self::new_impl(py, manager, Some(id))
})?
}
fn app_handle(&self, py: Python<'_>) -> Py<ext_mod::AppHandle> {
let tray_icon = self.0.inner_ref();
let app_handle = tray_icon.app_handle().py_app_handle().clone_ref(py);
app_handle
}
fn on_menu_event(&self, py: Python<'_>, handler: PyObject) {
let app_handle = self.app_handle(py);
ext_mod::AppHandle::on_menu_event(app_handle, py, handler);
}
fn on_tray_icon_event(slf: Py<Self>, py: Python<'_>, handler: PyObject) {
let moved_slf = slf.clone_ref(py);
py.allow_threads(|| {
slf.get()
.0
.inner_ref()
.on_tray_icon_event(move |_tray_icon, tray_icon_event| {
Python::with_gil(|py| {
let tray_icon: &Py<Self> = &moved_slf;
debug_assert_eq!(tray_icon.get().0.inner_ref().id(), _tray_icon.id());
let tray_icon_event: TrayIconEvent =
TrayIconEvent::from_tauri(py, &tray_icon_event)
.expect("Failed to convert rust `TrayIconEvent` to pyobject");
let handler = handler.bind(py);
let result = handler.call1((tray_icon, tray_icon_event));
result.unwrap_unraisable_py_result(py, Some(handler), || {
"Python exception occurred in `TrayIcon::on_tray_icon_event` handler"
});
})
})
})
}
fn id<'py>(&self, py: Python<'py>) -> Bound<'py, TrayIconId> {
let tray_icon = self.0.inner_ref();
TrayIconId::intern(py, &tray_icon.id().0)
}
#[pyo3(signature = (icon))]
fn set_icon(&self, py: Python<'_>, icon: Option<Py<ext_mod::image::Image>>) -> PyResult<()> {
let icon = icon.as_ref().map(|icon| icon.get().to_tauri(py));
py.allow_threads(|| delegate_inner!(self, set_icon, icon))
}
#[pyo3(signature = (menu))]
fn set_menu(&self, py: Python<'_>, menu: Option<ImplContextMenu>) -> PyResult<()> {
py.allow_threads(|| match menu {
Some(menu) => context_menu_impl!(&menu, |menu| {
delegate_inner!(self, set_menu, Some(menu.to_owned()))
}),
None => delegate_inner!(self, set_menu, None::<tauri::menu::Menu<Runtime>>),
})
}
#[pyo3(signature = (tooltip))]
fn set_tooltip(&self, py: Python<'_>, tooltip: Option<&str>) -> PyResult<()> {
py.allow_threads(|| delegate_inner!(self, set_tooltip, tooltip))
}
#[pyo3(signature = (title))]
fn set_title(&self, py: Python<'_>, title: Option<&str>) -> PyResult<()> {
py.allow_threads(|| delegate_inner!(self, set_title, title))
}
fn set_visible(&self, py: Python<'_>, visible: bool) -> PyResult<()> {
py.allow_threads(|| delegate_inner!(self, set_visible, visible))
}
#[pyo3(signature = (path))]
fn set_temp_dir_path(&self, py: Python<'_>, path: Option<PathBuf>) -> PyResult<()> {
py.allow_threads(|| delegate_inner!(self, set_temp_dir_path, path))
}
fn set_icon_as_template(&self, py: Python<'_>, is_template: bool) -> PyResult<()> {
py.allow_threads(|| delegate_inner!(self, set_icon_as_template, is_template))
}
fn set_show_menu_on_left_click(&self, py: Python<'_>, enable: bool) -> PyResult<()> {
py.allow_threads(|| delegate_inner!(self, set_show_menu_on_left_click, enable))
}
fn rect(&self, py: Python<'_>) -> PyResult<Option<Rect>> {
let rect = py.allow_threads(|| delegate_inner!(self, rect,))?;
match rect {
Some(rect) => Ok(Some(Rect::from_tauri(py, rect)?)),
None => Ok(None),
}
}
}
#[pyclass(frozen)]
#[non_exhaustive]
pub enum TrayIconEvent {
Click {
id: Py<TrayIconId>,
#[expect(private_interfaces)]
position: PhysicalPositionF64,
rect: Py<Rect>,
button: Py<MouseButton>,
button_state: Py<MouseButtonState>,
},
DoubleClick {
id: Py<TrayIconId>,
#[expect(private_interfaces)]
position: PhysicalPositionF64,
rect: Py<Rect>,
button: Py<MouseButton>,
},
Enter {
id: Py<TrayIconId>,
#[expect(private_interfaces)]
position: PhysicalPositionF64,
rect: Py<Rect>,
},
Move {
id: Py<TrayIconId>,
#[expect(private_interfaces)]
position: PhysicalPositionF64,
rect: Py<Rect>,
},
Leave {
id: Py<TrayIconId>,
#[expect(private_interfaces)]
position: PhysicalPositionF64,
rect: Py<Rect>,
},
_NonExhaustive(),
}
impl TrayIconEvent {
pub(crate) fn from_tauri(py: Python<'_>, event: &tray::TrayIconEvent) -> PyResult<Self> {
fn from_rs_id(py: Python<'_>, id: &tray::TrayIconId) -> Py<TrayIconId> {
TrayIconId::intern(py, &id.0).unbind()
}
fn from_rs_position(
py: Python<'_>,
position: tauri::PhysicalPosition<f64>,
) -> PyResult<PhysicalPositionF64> {
PhysicalPositionF64::from_tauri(py, position)
}
fn from_rs_rect(py: Python<'_>, rect: tauri::Rect) -> PyResult<Py<Rect>> {
Ok(Rect::from_tauri(py, rect)?.into_pyobject(py)?.unbind())
}
fn from_rs_button(py: Python<'_>, button: tray::MouseButton) -> PyResult<Py<MouseButton>> {
Ok(MouseButton::from(button).into_pyobject(py)?.unbind())
}
fn from_rs_button_state(
py: Python<'_>,
button_state: tray::MouseButtonState,
) -> PyResult<Py<MouseButtonState>> {
Ok(MouseButtonState::from(button_state)
.into_pyobject(py)?
.unbind())
}
let event = match event {
tray::TrayIconEvent::Click {
id,
position,
rect,
button,
button_state,
} => Self::Click {
id: from_rs_id(py, id),
position: from_rs_position(py, *position)?,
rect: from_rs_rect(py, *rect)?,
button: from_rs_button(py, *button)?,
button_state: from_rs_button_state(py, *button_state)?,
},
tray::TrayIconEvent::DoubleClick {
id,
position,
rect,
button,
} => Self::DoubleClick {
id: from_rs_id(py, id),
position: from_rs_position(py, *position)?,
rect: from_rs_rect(py, *rect)?,
button: from_rs_button(py, *button)?,
},
tray::TrayIconEvent::Enter { id, position, rect } => Self::Enter {
id: from_rs_id(py, id),
position: from_rs_position(py, *position)?,
rect: from_rs_rect(py, *rect)?,
},
tray::TrayIconEvent::Move { id, position, rect } => Self::Move {
id: from_rs_id(py, id),
position: from_rs_position(py, *position)?,
rect: from_rs_rect(py, *rect)?,
},
tray::TrayIconEvent::Leave { id, position, rect } => Self::Leave {
id: from_rs_id(py, id),
position: from_rs_position(py, *position)?,
rect: from_rs_rect(py, *rect)?,
},
_ => Self::_NonExhaustive(),
};
Ok(event)
}
}
macro_rules! mouse_button_impl {
($ident:ident => : $($variant:ident),*) => {
#[pyclass(frozen, eq, eq_int)]
#[derive(PartialEq, Clone, Copy)]
pub enum $ident {
$($variant,)*
}
impl From<tauri::tray::MouseButton> for $ident {
fn from(val: tauri::tray::MouseButton) -> Self {
match val {
$(tauri::tray::MouseButton::$variant => $ident::$variant,)*
}
}
}
impl From<$ident> for tauri::tray::MouseButton {
fn from(val: $ident) -> Self {
match val {
$($ident::$variant => tauri::tray::MouseButton::$variant,)*
}
}
}
};
}
mouse_button_impl! {
MouseButton => :
Left,
Right,
Middle
}
macro_rules! mouse_button_state_impl {
($ident:ident => : $($variant:ident),*) => {
#[pyclass(frozen, eq, eq_int)]
#[derive(PartialEq, Clone, Copy)]
pub enum $ident {
$($variant,)*
}
impl From<tauri::tray::MouseButtonState> for $ident {
fn from(val: tauri::tray::MouseButtonState) -> Self {
match val {
$(tauri::tray::MouseButtonState::$variant => $ident::$variant,)*
}
}
}
impl From<$ident> for tauri::tray::MouseButtonState {
fn from(val: $ident) -> Self {
match val {
$($ident::$variant => tauri::tray::MouseButtonState::$variant,)*
}
}
}
};
}
mouse_button_state_impl! {
MouseButtonState => :
Up,
Down
}