pytauri_core/ext_mod_impl/
tray.rs

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
22/// See also: [tauri::tray::TrayIconId]
23///
24/// Remember use [TrayIconId::intern] to create a new instance.
25pub type TrayIconId = PyString;
26
27/// See also: [tauri::tray::TrayIcon]
28#[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        // TODO, PERF: release the GIL?
80        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        // Delegate to [ext_mod::AppHandle::on_menu_event] as their implementation is the same:
86        // - <https://docs.rs/tauri/2.2.5/tauri/tray/struct.TrayIcon.html#method.on_menu_event>
87        // - <https://docs.rs/tauri/2.2.5/tauri/struct.AppHandle.html#method.on_menu_event>
88        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                        // See: <https://github.com/tauri-apps/tauri/blob/8e9339e8807338597132ffd8688fb9da00f4102b/crates/tauri/src/app.rs#L2185-L2205>,
101                        // The `tray_icon` argument is always the `TrayIcon` instance that calls this method,
102                        // so we can directly use the same PyObject.
103                        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                                // TODO: maybe we should only `write_unraisable` and log it instead of `panic` here?
108                                .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    // TODO, PERF: `pyo3` didn't implement `FromPyObject` for `&path`,
156    // see: <https://github.com/PyO3/pyo3/blob/2c732a7ab42af4b11c2a9a8da9f838b592712d95/src/conversions/std/path.rs#L22>
157    #[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/// See also: [tauri::tray::TrayIconEvent]
180#[pyclass(frozen)]
181#[non_exhaustive]
182pub enum TrayIconEvent {
183    // use `Py<T>` to avoid creating new obj every time visiting the field,
184    // see: <https://pyo3.rs/v0.23.4/faq.html#pyo3get-clones-my-field>
185    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        /// See also: [tauri::tray::MouseButton]
296        #[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        /// See also: [tauri::tray::MouseButtonState]
330        #[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}