1use std::path::PathBuf;
2
3use pyo3::{prelude::*, types::PyString};
4use pyo3_utils::{
5 py_wrapper::{PyWrapper, PyWrapperT0},
6 ungil::UnsafeUngilExt,
7};
8use tauri::tray;
9
10use crate::{
11 ext_mod::{
12 self, manager_method_impl,
13 menu::{context_menu_impl, ImplContextMenu},
14 ImplManager, PhysicalPositionF64, PyAppHandleExt as _, Rect,
15 },
16 tauri_runtime::Runtime,
17 utils::{delegate_inner, PyResultExt as _, TauriError},
18};
19
20type TauriTrayIcon = tray::TrayIcon<Runtime>;
21
22pub type TrayIconId = PyString;
26
27#[pyclass(frozen)]
29#[non_exhaustive]
30pub struct TrayIcon(pub PyWrapper<PyWrapperT0<TauriTrayIcon>>);
31
32impl TrayIcon {
33 pub(crate) fn new(tray_icon: TauriTrayIcon) -> Self {
34 Self(PyWrapper::new0(tray_icon))
35 }
36
37 #[inline]
38 fn new_impl(
39 py: Python<'_>,
40 manager: &impl tauri::Manager<Runtime>,
41 id: Option<impl Into<tray::TrayIconId> + Send>,
42 ) -> PyResult<Self> {
43 unsafe {
44 py.allow_threads_unsend(manager, |manager| {
45 let tray_icon_builder = if let Some(id) = id {
46 tray::TrayIconBuilder::with_id(id)
47 } else {
48 tray::TrayIconBuilder::new()
49 };
50 let tray_icon = tray_icon_builder.build(manager)?;
51
52 tauri::Result::Ok(Self::new(tray_icon))
53 })
54 }
55 .map_err(TauriError::from)
56 .map_err(PyErr::from)
57 }
58}
59
60#[pymethods]
61impl TrayIcon {
62 #[new]
63 fn __new__(py: Python<'_>, manager: ImplManager) -> PyResult<Self> {
64 manager_method_impl!(py, &manager, |py, manager| {
65 Self::new_impl(py, manager, None::<&str>)
66 })?
67 }
68
69 #[staticmethod]
70 fn with_id(py: Python<'_>, manager: ImplManager, id: String) -> PyResult<Self> {
71 let id = tray::TrayIconId(id);
72 manager_method_impl!(py, &manager, |py, manager| {
73 Self::new_impl(py, manager, Some(id))
74 })?
75 }
76
77 fn app_handle(&self, py: Python<'_>) -> Py<ext_mod::AppHandle> {
78 let tray_icon = self.0.inner_ref();
79 let app_handle = tray_icon.app_handle().py_app_handle().clone_ref(py);
81 app_handle
82 }
83
84 fn on_menu_event(&self, py: Python<'_>, handler: PyObject) {
85 let app_handle = self.app_handle(py);
89 ext_mod::AppHandle::on_menu_event(app_handle, py, handler);
90 }
91
92 fn on_tray_icon_event(slf: Py<Self>, py: Python<'_>, handler: PyObject) {
93 let moved_slf = slf.clone_ref(py);
94 py.allow_threads(|| {
95 slf.get()
96 .0
97 .inner_ref()
98 .on_tray_icon_event(move |_tray_icon, tray_icon_event| {
99 Python::with_gil(|py| {
100 let tray_icon: &Py<Self> = &moved_slf;
104 debug_assert_eq!(tray_icon.get().0.inner_ref().id(), _tray_icon.id());
105 let tray_icon_event: TrayIconEvent =
106 TrayIconEvent::from_tauri(py, &tray_icon_event)
107 .expect("Failed to convert rust `TrayIconEvent` to pyobject");
109
110 let handler = handler.bind(py);
111 let result = handler.call1((tray_icon, tray_icon_event));
112 result.unwrap_unraisable_py_result(py, Some(handler), || {
113 "Python exception occurred in `TrayIcon::on_tray_icon_event` handler"
114 });
115 })
116 })
117 })
118 }
119
120 fn id<'py>(&self, py: Python<'py>) -> Bound<'py, TrayIconId> {
121 let tray_icon = self.0.inner_ref();
122 TrayIconId::intern(py, &tray_icon.id().0)
123 }
124
125 #[pyo3(signature = (icon))]
126 fn set_icon(&self, py: Python<'_>, icon: Option<Py<ext_mod::image::Image>>) -> PyResult<()> {
127 let icon = icon.as_ref().map(|icon| icon.get().to_tauri(py));
128 py.allow_threads(|| delegate_inner!(self, set_icon, icon))
129 }
130
131 #[pyo3(signature = (menu))]
132 fn set_menu(&self, py: Python<'_>, menu: Option<ImplContextMenu>) -> PyResult<()> {
133 py.allow_threads(|| match menu {
134 Some(menu) => context_menu_impl!(&menu, |menu| {
135 delegate_inner!(self, set_menu, Some(menu.to_owned()))
136 }),
137 None => delegate_inner!(self, set_menu, None::<tauri::menu::Menu<Runtime>>),
138 })
139 }
140
141 #[pyo3(signature = (tooltip))]
142 fn set_tooltip(&self, py: Python<'_>, tooltip: Option<&str>) -> PyResult<()> {
143 py.allow_threads(|| delegate_inner!(self, set_tooltip, tooltip))
144 }
145
146 #[pyo3(signature = (title))]
147 fn set_title(&self, py: Python<'_>, title: Option<&str>) -> PyResult<()> {
148 py.allow_threads(|| delegate_inner!(self, set_title, title))
149 }
150
151 fn set_visible(&self, py: Python<'_>, visible: bool) -> PyResult<()> {
152 py.allow_threads(|| delegate_inner!(self, set_visible, visible))
153 }
154
155 #[pyo3(signature = (path))]
158 fn set_temp_dir_path(&self, py: Python<'_>, path: Option<PathBuf>) -> PyResult<()> {
159 py.allow_threads(|| delegate_inner!(self, set_temp_dir_path, path))
160 }
161
162 fn set_icon_as_template(&self, py: Python<'_>, is_template: bool) -> PyResult<()> {
163 py.allow_threads(|| delegate_inner!(self, set_icon_as_template, is_template))
164 }
165
166 fn set_show_menu_on_left_click(&self, py: Python<'_>, enable: bool) -> PyResult<()> {
167 py.allow_threads(|| delegate_inner!(self, set_show_menu_on_left_click, enable))
168 }
169
170 fn rect(&self, py: Python<'_>) -> PyResult<Option<Rect>> {
171 let rect = py.allow_threads(|| delegate_inner!(self, rect,))?;
172 match rect {
173 Some(rect) => Ok(Some(Rect::from_tauri(py, rect)?)),
174 None => Ok(None),
175 }
176 }
177}
178
179#[pyclass(frozen)]
181#[non_exhaustive]
182pub enum TrayIconEvent {
183 Click {
186 id: Py<TrayIconId>,
187 #[expect(private_interfaces)]
188 position: PhysicalPositionF64,
189 rect: Py<Rect>,
190 button: Py<MouseButton>,
191 button_state: Py<MouseButtonState>,
192 },
193 DoubleClick {
194 id: Py<TrayIconId>,
195 #[expect(private_interfaces)]
196 position: PhysicalPositionF64,
197 rect: Py<Rect>,
198 button: Py<MouseButton>,
199 },
200 Enter {
201 id: Py<TrayIconId>,
202 #[expect(private_interfaces)]
203 position: PhysicalPositionF64,
204 rect: Py<Rect>,
205 },
206 Move {
207 id: Py<TrayIconId>,
208 #[expect(private_interfaces)]
209 position: PhysicalPositionF64,
210 rect: Py<Rect>,
211 },
212 Leave {
213 id: Py<TrayIconId>,
214 #[expect(private_interfaces)]
215 position: PhysicalPositionF64,
216 rect: Py<Rect>,
217 },
218 _NonExhaustive(),
219}
220
221impl TrayIconEvent {
222 pub(crate) fn from_tauri(py: Python<'_>, event: &tray::TrayIconEvent) -> PyResult<Self> {
223 fn from_rs_id(py: Python<'_>, id: &tray::TrayIconId) -> Py<TrayIconId> {
224 TrayIconId::intern(py, &id.0).unbind()
225 }
226 fn from_rs_position(
227 py: Python<'_>,
228 position: tauri::PhysicalPosition<f64>,
229 ) -> PyResult<PhysicalPositionF64> {
230 PhysicalPositionF64::from_tauri(py, position)
231 }
232 fn from_rs_rect(py: Python<'_>, rect: tauri::Rect) -> PyResult<Py<Rect>> {
233 Ok(Rect::from_tauri(py, rect)?.into_pyobject(py)?.unbind())
234 }
235 fn from_rs_button(py: Python<'_>, button: tray::MouseButton) -> PyResult<Py<MouseButton>> {
236 Ok(MouseButton::from(button).into_pyobject(py)?.unbind())
237 }
238 fn from_rs_button_state(
239 py: Python<'_>,
240 button_state: tray::MouseButtonState,
241 ) -> PyResult<Py<MouseButtonState>> {
242 Ok(MouseButtonState::from(button_state)
243 .into_pyobject(py)?
244 .unbind())
245 }
246
247 let event = match event {
248 tray::TrayIconEvent::Click {
249 id,
250 position,
251 rect,
252 button,
253 button_state,
254 } => Self::Click {
255 id: from_rs_id(py, id),
256 position: from_rs_position(py, *position)?,
257 rect: from_rs_rect(py, *rect)?,
258 button: from_rs_button(py, *button)?,
259 button_state: from_rs_button_state(py, *button_state)?,
260 },
261 tray::TrayIconEvent::DoubleClick {
262 id,
263 position,
264 rect,
265 button,
266 } => Self::DoubleClick {
267 id: from_rs_id(py, id),
268 position: from_rs_position(py, *position)?,
269 rect: from_rs_rect(py, *rect)?,
270 button: from_rs_button(py, *button)?,
271 },
272 tray::TrayIconEvent::Enter { id, position, rect } => Self::Enter {
273 id: from_rs_id(py, id),
274 position: from_rs_position(py, *position)?,
275 rect: from_rs_rect(py, *rect)?,
276 },
277 tray::TrayIconEvent::Move { id, position, rect } => Self::Move {
278 id: from_rs_id(py, id),
279 position: from_rs_position(py, *position)?,
280 rect: from_rs_rect(py, *rect)?,
281 },
282 tray::TrayIconEvent::Leave { id, position, rect } => Self::Leave {
283 id: from_rs_id(py, id),
284 position: from_rs_position(py, *position)?,
285 rect: from_rs_rect(py, *rect)?,
286 },
287 _ => Self::_NonExhaustive(),
288 };
289 Ok(event)
290 }
291}
292
293macro_rules! mouse_button_impl {
294 ($ident:ident => : $($variant:ident),*) => {
295 #[pyclass(frozen, eq, eq_int)]
297 #[derive(PartialEq, Clone, Copy)]
298 pub enum $ident {
299 $($variant,)*
300 }
301
302 impl From<tauri::tray::MouseButton> for $ident {
303 fn from(val: tauri::tray::MouseButton) -> Self {
304 match val {
305 $(tauri::tray::MouseButton::$variant => $ident::$variant,)*
306 }
307 }
308 }
309
310 impl From<$ident> for tauri::tray::MouseButton {
311 fn from(val: $ident) -> Self {
312 match val {
313 $($ident::$variant => tauri::tray::MouseButton::$variant,)*
314 }
315 }
316 }
317 };
318}
319
320mouse_button_impl! {
321 MouseButton => :
322 Left,
323 Right,
324 Middle
325}
326
327macro_rules! mouse_button_state_impl {
328 ($ident:ident => : $($variant:ident),*) => {
329 #[pyclass(frozen, eq, eq_int)]
331 #[derive(PartialEq, Clone, Copy)]
332 pub enum $ident {
333 $($variant,)*
334 }
335
336 impl From<tauri::tray::MouseButtonState> for $ident {
337 fn from(val: tauri::tray::MouseButtonState) -> Self {
338 match val {
339 $(tauri::tray::MouseButtonState::$variant => $ident::$variant,)*
340 }
341 }
342 }
343
344 impl From<$ident> for tauri::tray::MouseButtonState {
345 fn from(val: $ident) -> Self {
346 match val {
347 $($ident::$variant => tauri::tray::MouseButtonState::$variant,)*
348 }
349 }
350 }
351 };
352}
353
354mouse_button_state_impl! {
355 MouseButtonState => :
356 Up,
357 Down
358}