1#![allow(unsafe_code)]
11
12use crate::cursor::MouseCursorInner;
13use crate::graphics::Image;
14use crate::input::{
15 FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
16 KeyEventResult, MouseEvent,
17};
18use crate::item_rendering::CachedRenderingData;
19use crate::items::{ColorScheme, Item, ItemConsts, ItemRc, Orientation, RenderingResult, VoidArg};
20use crate::layout::LayoutInfo;
21use crate::lengths::{LogicalRect, LogicalSize};
22#[cfg(feature = "rtti")]
23use crate::rtti::*;
24use crate::window::WindowAdapter;
25use crate::{Callback, Coord, Property, SharedString};
26use alloc::boxed::Box;
27use alloc::rc::Rc;
28use const_field_offset::FieldOffsets;
29use core::pin::Pin;
30use i_slint_core_macros::*;
31
32cfg_if::cfg_if! {
37 if #[cfg(all(feature = "system-tray", target_os = "macos"))] {
38 mod appkit;
39 use self::appkit::PlatformTray;
40 } else if #[cfg(all(feature = "system-tray", target_os = "windows"))] {
41 mod windows;
42 use self::windows::PlatformTray;
43 } else if #[cfg(all(feature = "system-tray", target_family = "unix", not(target_vendor = "apple"), not(target_os = "android")))] {
44 mod ksni;
45 use self::ksni::PlatformTray;
46 } else {
47 mod dummy;
48 use self::dummy::PlatformTray;
49 }
50}
51
52pub struct Params<'a> {
54 pub icon: &'a Image,
55 pub tooltip: &'a str,
56 pub title: &'a str,
57}
58
59#[allow(dead_code)]
61#[derive(Debug, derive_more::Display)]
62pub enum Error {
63 #[display("Failed to create a rgba8 buffer from an icon image")]
64 Rgba8,
65 #[display("{_0}")]
66 PlatformError(crate::platform::PlatformError),
67 #[display("{_0}")]
68 EventLoopError(crate::api::EventLoopError),
69 #[display(
70 "no system tray backend compiled in (missing `system-tray` feature or unsupported platform)"
71 )]
72 Unsupported,
73}
74
75pub struct SystemTrayIconHandle(PlatformTray);
77
78impl SystemTrayIconHandle {
79 pub fn new(
80 params: Params,
81 self_weak: crate::item_tree::ItemWeak,
82 context: &crate::SlintContext,
83 ) -> Result<Self, Error> {
84 PlatformTray::new(params, self_weak, context).map(Self)
85 }
86
87 pub fn rebuild_menu(
88 &self,
89 menu: vtable::VRef<'_, crate::menus::MenuVTable>,
90 entries_out: &mut alloc::vec::Vec<crate::items::MenuEntry>,
91 ) {
92 self.0.rebuild_menu(menu, entries_out);
93 }
94
95 pub fn set_visible(&self, visible: bool) {
96 self.0.set_visible(visible);
97 }
98
99 pub fn set_icon(&self, icon: &Image) {
100 self.0.set_icon(icon);
101 }
102
103 pub fn set_tooltip(&self, tooltip: &str) {
104 self.0.set_tooltip(tooltip);
105 }
106
107 pub fn set_title(&self, title: &str) {
108 self.0.set_title(title);
109 }
110}
111
112#[repr(C)]
117pub struct SystemTrayIconDataBox(core::ptr::NonNull<SystemTrayIconData>);
119
120impl Default for SystemTrayIconDataBox {
121 fn default() -> Self {
122 SystemTrayIconDataBox(Box::leak(Box::<SystemTrayIconData>::default()).into())
123 }
124}
125impl Drop for SystemTrayIconDataBox {
126 fn drop(&mut self) {
127 drop(unsafe { Box::from_raw(self.0.as_ptr()) });
129 }
130}
131
132impl core::ops::Deref for SystemTrayIconDataBox {
133 type Target = SystemTrayIconData;
134 fn deref(&self) -> &Self::Target {
135 unsafe { self.0.as_ref() }
137 }
138}
139
140#[derive(Default)]
141pub struct SystemTrayIconData {
142 inner: core::cell::OnceCell<SystemTrayIconHandle>,
143 change_tracker: crate::properties::ChangeTracker,
144 visible_tracker: crate::properties::ChangeTracker,
145 icon_tracker: crate::properties::ChangeTracker,
146 tooltip_tracker: crate::properties::ChangeTracker,
147 title_tracker: crate::properties::ChangeTracker,
148 keepalive_live: core::cell::Cell<bool>,
152 menu: core::cell::RefCell<Option<MenuState>>,
153 context: core::cell::OnceCell<crate::SlintContextWeak>,
157}
158
159impl SystemTrayIconData {
160 fn context(&self) -> Option<crate::SlintContext> {
163 match self.context.get() {
164 Some(ctx) => ctx.upgrade(),
165 None => crate::context::GLOBAL_CONTEXT.with(|p| p.get().cloned()),
166 }
167 }
168}
169
170impl Drop for SystemTrayIconData {
171 fn drop(&mut self) {
172 if self.keepalive_live.get()
173 && let Some(ctx) = self.context()
174 {
175 ctx.release_keepalive();
176 }
177 }
178}
179
180struct MenuState {
181 menu_vrc: vtable::VRc<crate::menus::MenuVTable>,
182 entries: alloc::vec::Vec<crate::items::MenuEntry>,
183 tracker: Pin<Box<crate::properties::PropertyTracker<false, MenuDirtyHandler>>>,
184}
185
186struct MenuDirtyHandler {
187 self_weak: crate::item_tree::ItemWeak,
188}
189
190impl crate::properties::PropertyDirtyHandler for MenuDirtyHandler {
191 fn notify(self: Pin<&Self>) {
192 let self_weak = self.self_weak.clone();
193 let Some(item_rc) = self_weak.upgrade() else { return };
194 let Some(tray) = item_rc.downcast::<SystemTrayIcon>() else { return };
195 let Some(ctx) = tray.as_pin_ref().data.context() else { return };
196 ctx.single_shot(Default::default(), move || {
197 let Some(item_rc) = self_weak.upgrade() else { return };
198 let Some(tray) = item_rc.downcast::<SystemTrayIcon>() else { return };
199 tray.as_pin_ref().rebuild_menu();
200 });
201 }
202}
203
204#[repr(C)]
205#[derive(FieldOffsets, Default, SlintElement)]
206#[pin]
207pub struct SystemTrayIcon {
208 pub icon: Property<Image>,
209 pub tooltip: Property<SharedString>,
210 pub title: Property<SharedString>,
211 pub visible: Property<bool>,
212 pub color_scheme: Property<ColorScheme>,
213 pub clicked: Callback<VoidArg>,
214 pub cached_rendering_data: CachedRenderingData,
215 data: SystemTrayIconDataBox,
216}
217
218impl SystemTrayIcon {
219 pub fn set_context(self: Pin<&Self>, ctx: &crate::SlintContext) {
223 let _ = self.data.context.set(ctx.downgrade());
224 }
225
226 pub fn set_menu(
232 self: Pin<&Self>,
233 self_rc: &ItemRc,
234 menu_vrc: vtable::VRc<crate::menus::MenuVTable>,
235 ) {
236 let tracker = Box::pin(crate::properties::PropertyTracker::new_with_dirty_handler(
237 MenuDirtyHandler { self_weak: self_rc.downgrade() },
238 ));
239 *self.data.menu.borrow_mut() =
240 Some(MenuState { menu_vrc, entries: alloc::vec::Vec::new(), tracker });
241 self.rebuild_menu();
245 }
246
247 fn rebuild_menu(self: Pin<&Self>) {
248 let Some(handle) = self.data.inner.get() else { return };
249 let mut menu_borrow = self.data.menu.borrow_mut();
250 let Some(MenuState { menu_vrc, entries, tracker }) = menu_borrow.as_mut() else {
251 return;
252 };
253 tracker.as_ref().evaluate(|| {
254 handle.rebuild_menu(vtable::VRc::borrow(menu_vrc), entries);
255 });
256 }
257
258 pub fn set_color_scheme(self: Pin<&Self>, scheme: ColorScheme) {
259 Self::FIELD_OFFSETS.color_scheme().apply_pin(self).set(scheme);
260 }
261
262 fn update_keepalive(self: Pin<&Self>) {
267 let want_live = self.data.inner.get().is_some() && self.visible();
268 let was_live = self.data.keepalive_live.get();
269 if want_live == was_live {
270 return;
271 }
272 let Some(ctx) = self.data.context() else {
273 return;
274 };
275 if want_live {
276 ctx.acquire_keepalive();
277 self.data.keepalive_live.set(true);
278 } else {
279 self.data.keepalive_live.set(false);
280 ctx.release_keepalive();
281 }
282 }
283}
284
285impl Item for SystemTrayIcon {
286 fn init(self: Pin<&Self>, self_rc: &ItemRc) {
287 self.data.change_tracker.init_delayed(
288 self_rc.downgrade(),
289 |_| true,
290 |self_weak, has_icon| {
291 let Some(tray_rc) = self_weak.upgrade() else {
292 return;
293 };
294 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
295 return;
296 };
297 if !*has_icon {
298 return;
299 }
300 let Some(ctx) = tray.as_pin_ref().data.context() else {
301 return;
302 };
303 let tray = tray.as_pin_ref();
304 let handle = match SystemTrayIconHandle::new(
305 Params { icon: &tray.icon(), tooltip: &tray.tooltip(), title: &tray.title() },
306 self_weak.clone(),
307 &ctx,
308 ) {
309 Ok(handle) => handle,
310 Err(err) => {
311 crate::debug_log!("Slint: Failed to create system tray icon: {err}");
312 return;
313 }
314 };
315
316 let _ = tray.data.inner.set(handle);
317 tray.rebuild_menu();
320 tray.update_keepalive();
321 },
322 );
323
324 self.data.visible_tracker.init_delayed(
325 self_rc.downgrade(),
326 |self_weak| {
327 let Some(tray_rc) = self_weak.upgrade() else { return false };
328 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return false };
329 tray.as_pin_ref().visible()
330 },
331 |self_weak, visible| {
332 let Some(tray_rc) = self_weak.upgrade() else { return };
333 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
334 let tray = tray.as_pin_ref();
335 if let Some(handle) = tray.data.inner.get() {
336 handle.set_visible(*visible);
337 }
338 tray.update_keepalive();
339 },
342 );
343
344 self.data.icon_tracker.init_delayed(
349 self_rc.downgrade(),
350 |self_weak| {
351 let Some(tray_rc) = self_weak.upgrade() else { return Image::default() };
352 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
353 return Image::default();
354 };
355 tray.as_pin_ref().icon()
356 },
357 |self_weak, icon| {
358 let Some(tray_rc) = self_weak.upgrade() else { return };
359 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
360 if let Some(handle) = tray.as_pin_ref().data.inner.get() {
361 handle.set_icon(icon);
362 }
363 },
364 );
365
366 self.data.tooltip_tracker.init_delayed(
367 self_rc.downgrade(),
368 |self_weak| {
369 let Some(tray_rc) = self_weak.upgrade() else { return SharedString::default() };
370 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
371 return SharedString::default();
372 };
373 tray.as_pin_ref().tooltip()
374 },
375 |self_weak, tooltip| {
376 let Some(tray_rc) = self_weak.upgrade() else { return };
377 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
378 if let Some(handle) = tray.as_pin_ref().data.inner.get() {
379 handle.set_tooltip(tooltip.as_str());
380 }
381 },
382 );
383
384 self.data.title_tracker.init_delayed(
385 self_rc.downgrade(),
386 |self_weak| {
387 let Some(tray_rc) = self_weak.upgrade() else { return SharedString::default() };
388 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
389 return SharedString::default();
390 };
391 tray.as_pin_ref().title()
392 },
393 |self_weak, title| {
394 let Some(tray_rc) = self_weak.upgrade() else { return };
395 let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
396 if let Some(handle) = tray.as_pin_ref().data.inner.get() {
397 handle.set_title(title.as_str());
398 }
399 },
400 );
401 }
402
403 fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
404
405 fn layout_info(
406 self: Pin<&Self>,
407 _orientation: Orientation,
408 _cross_axis_constraint: Coord,
409 _window_adapter: &Rc<dyn WindowAdapter>,
410 _self_rc: &ItemRc,
411 ) -> LayoutInfo {
412 LayoutInfo::default()
413 }
414
415 fn input_event_filter_before_children(
416 self: Pin<&Self>,
417 _: &MouseEvent,
418 _window_adapter: &Rc<dyn WindowAdapter>,
419 _self_rc: &ItemRc,
420 _: &mut MouseCursorInner,
421 ) -> InputEventFilterResult {
422 InputEventFilterResult::ForwardAndIgnore
423 }
424
425 fn input_event(
426 self: Pin<&Self>,
427 _: &MouseEvent,
428 _window_adapter: &Rc<dyn WindowAdapter>,
429 _self_rc: &ItemRc,
430 _: &mut MouseCursorInner,
431 ) -> InputEventResult {
432 InputEventResult::EventIgnored
433 }
434
435 fn capture_key_event(
436 self: Pin<&Self>,
437 _: &InternalKeyEvent,
438 _window_adapter: &Rc<dyn WindowAdapter>,
439 _self_rc: &ItemRc,
440 ) -> KeyEventResult {
441 KeyEventResult::EventIgnored
442 }
443
444 fn key_event(
445 self: Pin<&Self>,
446 _: &InternalKeyEvent,
447 _window_adapter: &Rc<dyn WindowAdapter>,
448 _self_rc: &ItemRc,
449 ) -> KeyEventResult {
450 KeyEventResult::EventIgnored
451 }
452
453 fn focus_event(
454 self: Pin<&Self>,
455 _: &FocusEvent,
456 _window_adapter: &Rc<dyn WindowAdapter>,
457 _self_rc: &ItemRc,
458 ) -> FocusEventResult {
459 FocusEventResult::FocusIgnored
460 }
461
462 fn render(
463 self: Pin<&Self>,
464 _backend: &mut &mut dyn crate::item_rendering::ItemRenderer,
465 _self_rc: &ItemRc,
466 _size: LogicalSize,
467 ) -> RenderingResult {
468 RenderingResult::ContinueRenderingChildren
469 }
470
471 fn bounding_rect(
472 self: core::pin::Pin<&Self>,
473 _window_adapter: &Rc<dyn WindowAdapter>,
474 _self_rc: &ItemRc,
475 geometry: LogicalRect,
476 ) -> LogicalRect {
477 geometry
478 }
479
480 fn clips_children(self: core::pin::Pin<&Self>) -> bool {
481 false
482 }
483}
484
485impl ItemConsts for SystemTrayIcon {
486 const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
487 Self::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
488}
489
490#[cfg(feature = "ffi")]
494#[unsafe(no_mangle)]
495pub unsafe extern "C" fn slint_system_tray_icon_data_init(data: *mut SystemTrayIconDataBox) {
496 unsafe { core::ptr::write(data, SystemTrayIconDataBox::default()) };
497}
498
499#[cfg(feature = "ffi")]
502#[unsafe(no_mangle)]
503pub unsafe extern "C" fn slint_system_tray_icon_data_free(data: *mut SystemTrayIconDataBox) {
504 unsafe { core::ptr::drop_in_place(data) };
505}
506
507#[cfg(feature = "ffi")]
508#[unsafe(no_mangle)]
509pub unsafe extern "C" fn slint_system_tray_icon_set_menu(
510 system_tray: &SystemTrayIcon,
511 item_rc: &ItemRc,
512 menu_vrc: &vtable::VRc<crate::menus::MenuVTable>,
513) {
514 unsafe { Pin::new_unchecked(system_tray) }.set_menu(item_rc, menu_vrc.clone());
515}