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