Skip to main content

i_slint_core/items/
system_tray.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! System tray integration.
5//!
6//! This module hosts the `SystemTrayIcon` native item (the element exposed to `.slint`) and
7//! wraps the platform-specific tray icon backends: `ksni` on Linux/BSD, AppKit
8//! (`NSStatusBar` / `NSStatusItem`) on macOS, and `Shell_NotifyIconW` on Windows.
9
10#![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
33// Pick the per-platform tray backend. The `dummy` arm catches anything without a
34// real native tray (the `system-tray` feature is off, or Android, WASM, embedded
35// targets, …) so a `SystemTrayIcon`-rooted component constructs without surfacing
36// an icon to any host shell.
37cfg_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
53/// Parameters passed to the platform-specific tray backend when building a tray icon.
54pub struct Params<'a> {
55    pub icon: &'a Image,
56    pub tooltip: &'a str,
57    pub title: &'a str,
58}
59
60/// Errors raised while constructing a platform tray icon.
61#[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
72/// Owning handle to a live platform tray icon. Dropping it removes the icon.
73pub 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// ---------------------------------------------------------------------------
110// Native `SystemTrayIcon` item, exposed to `.slint`.
111// ---------------------------------------------------------------------------
112
113#[repr(C)]
114/// Wraps the internal data structure for the SystemTrayIcon
115pub 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        // Safety: the self.0 was constructed from a Box::leak in SystemTrayIconDataBox::default
125        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        // Safety: initialized in SystemTrayIconDataBox::default
133        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    /// Whether this tray currently contributes to the SlintContext keepalive
146    /// counter. Flipped in lockstep with `acquire_keepalive`/`release_keepalive`
147    /// so that a re-fired tracker can't double-increment.
148    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    /// Called from generated code (via the `SetupSystemTrayIcon` builtin) to hand off the
199    /// lowered menu's `VRc<MenuVTable>` to the native item. The item walks the menu via
200    /// this vtable inside its own `PropertyTracker`, so property changes inside the menu
201    /// tree automatically trigger a rebuild of the platform tray menu. Subsequent calls
202    /// replace any previously installed menu.
203    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        // If the platform tray is already up (icon was set before the menu), populate
214        // the menu now; otherwise the icon tracker's notify will call rebuild_menu
215        // once the handle exists.
216        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    /// Reconcile the SlintContext keepalive counter with this tray's state.
235    /// A tray contributes to the counter only while it has a live platform
236    /// handle and its `visible` property is `true`; everything else is a
237    /// no-op so a re-fired tracker can't double-increment.
238    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                // The platform is set before any item's `init` runs (the public
273                // component's `new` calls `ensure_backend()` first), so the
274                // global context is populated by the time this tracker fires
275                // from the event loop. SystemTrayIcon has no `WindowAdapter` of
276                // its own, so we read the context directly rather than going
277                // through `tray_rc.window_adapter()`.
278                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                // If a menu was already installed before the icon was set, build it now
296                // that we have a platform handle.
297                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                // If the platform handle isn't up yet, the icon-driven init path
318                // will create it later and call update_keepalive itself.
319            },
320        );
321
322        // Push live icon / title changes through to the platform handle. The
323        // initial spawn always uses the latest values (the icon-driven init
324        // path reads them at fire time), so these trackers are no-ops until
325        // the user mutates the property after the tray is up.
326        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/// # Safety
469/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
470/// hold a SystemTrayIconDataBox
471#[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/// # Safety
478/// This must be called using a non-null pointer pointing to an initialized SystemTrayIconDataBox
479#[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}