Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
516 lines
18 KiB
Rust
516 lines
18 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
//! System tray integration.
|
|
//!
|
|
//! This module hosts the `SystemTrayIcon` native item (the element exposed to `.slint`) and
|
|
//! wraps the platform-specific tray icon backends: `ksni` on Linux/BSD, AppKit
|
|
//! (`NSStatusBar` / `NSStatusItem`) on macOS, and `Shell_NotifyIconW` on Windows.
|
|
|
|
#![allow(unsafe_code)]
|
|
|
|
use crate::cursor::MouseCursorInner;
|
|
use crate::graphics::Image;
|
|
use crate::input::{
|
|
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
|
|
KeyEventResult, MouseEvent,
|
|
};
|
|
use crate::item_rendering::CachedRenderingData;
|
|
use crate::items::{ColorScheme, Item, ItemConsts, ItemRc, Orientation, RenderingResult, VoidArg};
|
|
use crate::layout::LayoutInfo;
|
|
use crate::lengths::{LogicalRect, LogicalSize};
|
|
#[cfg(feature = "rtti")]
|
|
use crate::rtti::*;
|
|
use crate::window::WindowAdapter;
|
|
use crate::{Callback, Coord, Property, SharedString};
|
|
use alloc::boxed::Box;
|
|
use alloc::rc::Rc;
|
|
use const_field_offset::FieldOffsets;
|
|
use core::pin::Pin;
|
|
use i_slint_core_macros::*;
|
|
|
|
// Pick the per-platform tray backend. The `dummy` arm catches anything without a
|
|
// real native tray (the `system-tray` feature is off, or Android, WASM, embedded
|
|
// targets, …) so a `SystemTrayIcon`-rooted component constructs without surfacing
|
|
// an icon to any host shell.
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(all(feature = "system-tray", target_os = "macos"))] {
|
|
mod appkit;
|
|
use self::appkit::PlatformTray;
|
|
} else if #[cfg(all(feature = "system-tray", target_os = "windows"))] {
|
|
mod windows;
|
|
use self::windows::PlatformTray;
|
|
} else if #[cfg(all(feature = "system-tray", target_family = "unix", not(target_vendor = "apple"), not(target_os = "android")))] {
|
|
mod ksni;
|
|
use self::ksni::PlatformTray;
|
|
} else {
|
|
mod dummy;
|
|
use self::dummy::PlatformTray;
|
|
}
|
|
}
|
|
|
|
/// Parameters passed to the platform-specific tray backend when building a tray icon.
|
|
pub struct Params<'a> {
|
|
pub icon: &'a Image,
|
|
pub tooltip: &'a str,
|
|
pub title: &'a str,
|
|
}
|
|
|
|
/// Errors raised while constructing a platform tray icon.
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, derive_more::Display)]
|
|
pub enum Error {
|
|
#[display("Failed to create a rgba8 buffer from an icon image")]
|
|
Rgba8,
|
|
#[display("{_0}")]
|
|
PlatformError(crate::platform::PlatformError),
|
|
#[display("{_0}")]
|
|
EventLoopError(crate::api::EventLoopError),
|
|
#[display(
|
|
"no system tray backend compiled in (missing `system-tray` feature or unsupported platform)"
|
|
)]
|
|
Unsupported,
|
|
}
|
|
|
|
/// Owning handle to a live platform tray icon. Dropping it removes the icon.
|
|
pub struct SystemTrayIconHandle(PlatformTray);
|
|
|
|
impl SystemTrayIconHandle {
|
|
pub fn new(
|
|
params: Params,
|
|
self_weak: crate::item_tree::ItemWeak,
|
|
context: &crate::SlintContext,
|
|
) -> Result<Self, Error> {
|
|
PlatformTray::new(params, self_weak, context).map(Self)
|
|
}
|
|
|
|
pub fn rebuild_menu(
|
|
&self,
|
|
menu: vtable::VRef<'_, crate::menus::MenuVTable>,
|
|
entries_out: &mut alloc::vec::Vec<crate::items::MenuEntry>,
|
|
) {
|
|
self.0.rebuild_menu(menu, entries_out);
|
|
}
|
|
|
|
pub fn set_visible(&self, visible: bool) {
|
|
self.0.set_visible(visible);
|
|
}
|
|
|
|
pub fn set_icon(&self, icon: &Image) {
|
|
self.0.set_icon(icon);
|
|
}
|
|
|
|
pub fn set_tooltip(&self, tooltip: &str) {
|
|
self.0.set_tooltip(tooltip);
|
|
}
|
|
|
|
pub fn set_title(&self, title: &str) {
|
|
self.0.set_title(title);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Native `SystemTrayIcon` item, exposed to `.slint`.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[repr(C)]
|
|
/// Wraps the internal data structure for the SystemTrayIcon
|
|
pub struct SystemTrayIconDataBox(core::ptr::NonNull<SystemTrayIconData>);
|
|
|
|
impl Default for SystemTrayIconDataBox {
|
|
fn default() -> Self {
|
|
SystemTrayIconDataBox(Box::leak(Box::<SystemTrayIconData>::default()).into())
|
|
}
|
|
}
|
|
impl Drop for SystemTrayIconDataBox {
|
|
fn drop(&mut self) {
|
|
// Safety: the self.0 was constructed from a Box::leak in SystemTrayIconDataBox::default
|
|
drop(unsafe { Box::from_raw(self.0.as_ptr()) });
|
|
}
|
|
}
|
|
|
|
impl core::ops::Deref for SystemTrayIconDataBox {
|
|
type Target = SystemTrayIconData;
|
|
fn deref(&self) -> &Self::Target {
|
|
// Safety: initialized in SystemTrayIconDataBox::default
|
|
unsafe { self.0.as_ref() }
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct SystemTrayIconData {
|
|
inner: core::cell::OnceCell<SystemTrayIconHandle>,
|
|
change_tracker: crate::properties::ChangeTracker,
|
|
visible_tracker: crate::properties::ChangeTracker,
|
|
icon_tracker: crate::properties::ChangeTracker,
|
|
tooltip_tracker: crate::properties::ChangeTracker,
|
|
title_tracker: crate::properties::ChangeTracker,
|
|
/// Whether this tray currently contributes to the SlintContext keepalive
|
|
/// counter. Flipped in lockstep with `acquire_keepalive`/`release_keepalive`
|
|
/// so that a re-fired tracker can't double-increment.
|
|
keepalive_live: core::cell::Cell<bool>,
|
|
menu: core::cell::RefCell<Option<MenuState>>,
|
|
/// The context of the component this tray belongs to, handed over by the generated
|
|
/// code. A tray has no window, so this is the only way it can tell which context it
|
|
/// is part of. Empty for a component built with `new()`, which uses the thread's.
|
|
context: core::cell::OnceCell<crate::SlintContextWeak>,
|
|
}
|
|
|
|
impl SystemTrayIconData {
|
|
/// The context this tray belongs to: the one its component was built with, falling
|
|
/// back to the thread's for a component built with `new()`.
|
|
fn context(&self) -> Option<crate::SlintContext> {
|
|
match self.context.get() {
|
|
Some(ctx) => ctx.upgrade(),
|
|
None => crate::context::GLOBAL_CONTEXT.with(|p| p.get().cloned()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for SystemTrayIconData {
|
|
fn drop(&mut self) {
|
|
if self.keepalive_live.get()
|
|
&& let Some(ctx) = self.context()
|
|
{
|
|
ctx.release_keepalive();
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MenuState {
|
|
menu_vrc: vtable::VRc<crate::menus::MenuVTable>,
|
|
entries: alloc::vec::Vec<crate::items::MenuEntry>,
|
|
tracker: Pin<Box<crate::properties::PropertyTracker<false, MenuDirtyHandler>>>,
|
|
}
|
|
|
|
struct MenuDirtyHandler {
|
|
self_weak: crate::item_tree::ItemWeak,
|
|
}
|
|
|
|
impl crate::properties::PropertyDirtyHandler for MenuDirtyHandler {
|
|
fn notify(self: Pin<&Self>) {
|
|
let self_weak = self.self_weak.clone();
|
|
let Some(item_rc) = self_weak.upgrade() else { return };
|
|
let Some(tray) = item_rc.downcast::<SystemTrayIcon>() else { return };
|
|
let Some(ctx) = tray.as_pin_ref().data.context() else { return };
|
|
ctx.single_shot(Default::default(), move || {
|
|
let Some(item_rc) = self_weak.upgrade() else { return };
|
|
let Some(tray) = item_rc.downcast::<SystemTrayIcon>() else { return };
|
|
tray.as_pin_ref().rebuild_menu();
|
|
});
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct SystemTrayIcon {
|
|
pub icon: Property<Image>,
|
|
pub tooltip: Property<SharedString>,
|
|
pub title: Property<SharedString>,
|
|
pub visible: Property<bool>,
|
|
pub color_scheme: Property<ColorScheme>,
|
|
pub clicked: Callback<VoidArg>,
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
data: SystemTrayIconDataBox,
|
|
}
|
|
|
|
impl SystemTrayIcon {
|
|
/// Called from a tray-rooted component's `new_with_context` to tell the item which
|
|
/// context it belongs to. Without it the item would fall back to the thread's context,
|
|
/// which is the wrong one when the component was built on another.
|
|
pub fn set_context(self: Pin<&Self>, ctx: &crate::SlintContext) {
|
|
let _ = self.data.context.set(ctx.downgrade());
|
|
}
|
|
|
|
/// Called from generated code (via the `SetupSystemTrayIcon` builtin) to hand off the
|
|
/// lowered menu's `VRc<MenuVTable>` to the native item. The item walks the menu via
|
|
/// this vtable inside its own `PropertyTracker`, so property changes inside the menu
|
|
/// tree automatically trigger a rebuild of the platform tray menu. Subsequent calls
|
|
/// replace any previously installed menu.
|
|
pub fn set_menu(
|
|
self: Pin<&Self>,
|
|
self_rc: &ItemRc,
|
|
menu_vrc: vtable::VRc<crate::menus::MenuVTable>,
|
|
) {
|
|
let tracker = Box::pin(crate::properties::PropertyTracker::new_with_dirty_handler(
|
|
MenuDirtyHandler { self_weak: self_rc.downgrade() },
|
|
));
|
|
*self.data.menu.borrow_mut() =
|
|
Some(MenuState { menu_vrc, entries: alloc::vec::Vec::new(), tracker });
|
|
// If the platform tray is already up (icon was set before the menu), populate
|
|
// the menu now; otherwise the icon tracker's notify will call rebuild_menu
|
|
// once the handle exists.
|
|
self.rebuild_menu();
|
|
}
|
|
|
|
fn rebuild_menu(self: Pin<&Self>) {
|
|
let Some(handle) = self.data.inner.get() else { return };
|
|
let mut menu_borrow = self.data.menu.borrow_mut();
|
|
let Some(MenuState { menu_vrc, entries, tracker }) = menu_borrow.as_mut() else {
|
|
return;
|
|
};
|
|
tracker.as_ref().evaluate(|| {
|
|
handle.rebuild_menu(vtable::VRc::borrow(menu_vrc), entries);
|
|
});
|
|
}
|
|
|
|
pub fn set_color_scheme(self: Pin<&Self>, scheme: ColorScheme) {
|
|
Self::FIELD_OFFSETS.color_scheme().apply_pin(self).set(scheme);
|
|
}
|
|
|
|
/// Reconcile the SlintContext keepalive counter with this tray's state.
|
|
/// A tray contributes to the counter only while it has a live platform
|
|
/// handle and its `visible` property is `true`; everything else is a
|
|
/// no-op so a re-fired tracker can't double-increment.
|
|
fn update_keepalive(self: Pin<&Self>) {
|
|
let want_live = self.data.inner.get().is_some() && self.visible();
|
|
let was_live = self.data.keepalive_live.get();
|
|
if want_live == was_live {
|
|
return;
|
|
}
|
|
let Some(ctx) = self.data.context() else {
|
|
return;
|
|
};
|
|
if want_live {
|
|
ctx.acquire_keepalive();
|
|
self.data.keepalive_live.set(true);
|
|
} else {
|
|
self.data.keepalive_live.set(false);
|
|
ctx.release_keepalive();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Item for SystemTrayIcon {
|
|
fn init(self: Pin<&Self>, self_rc: &ItemRc) {
|
|
self.data.change_tracker.init_delayed(
|
|
self_rc.downgrade(),
|
|
|_| true,
|
|
|self_weak, has_icon| {
|
|
let Some(tray_rc) = self_weak.upgrade() else {
|
|
return;
|
|
};
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
|
|
return;
|
|
};
|
|
if !*has_icon {
|
|
return;
|
|
}
|
|
let Some(ctx) = tray.as_pin_ref().data.context() else {
|
|
return;
|
|
};
|
|
let tray = tray.as_pin_ref();
|
|
let handle = match SystemTrayIconHandle::new(
|
|
Params { icon: &tray.icon(), tooltip: &tray.tooltip(), title: &tray.title() },
|
|
self_weak.clone(),
|
|
&ctx,
|
|
) {
|
|
Ok(handle) => handle,
|
|
Err(err) => {
|
|
crate::debug_log!("Slint: Failed to create system tray icon: {err}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let _ = tray.data.inner.set(handle);
|
|
// If a menu was already installed before the icon was set, build it now
|
|
// that we have a platform handle.
|
|
tray.rebuild_menu();
|
|
tray.update_keepalive();
|
|
},
|
|
);
|
|
|
|
self.data.visible_tracker.init_delayed(
|
|
self_rc.downgrade(),
|
|
|self_weak| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return false };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return false };
|
|
tray.as_pin_ref().visible()
|
|
},
|
|
|self_weak, visible| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
|
|
let tray = tray.as_pin_ref();
|
|
if let Some(handle) = tray.data.inner.get() {
|
|
handle.set_visible(*visible);
|
|
}
|
|
tray.update_keepalive();
|
|
// If the platform handle isn't up yet, the icon-driven init path
|
|
// will create it later and call update_keepalive itself.
|
|
},
|
|
);
|
|
|
|
// Push live icon / title changes through to the platform handle. The
|
|
// initial spawn always uses the latest values (the icon-driven init
|
|
// path reads them at fire time), so these trackers are no-ops until
|
|
// the user mutates the property after the tray is up.
|
|
self.data.icon_tracker.init_delayed(
|
|
self_rc.downgrade(),
|
|
|self_weak| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return Image::default() };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
|
|
return Image::default();
|
|
};
|
|
tray.as_pin_ref().icon()
|
|
},
|
|
|self_weak, icon| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
|
|
if let Some(handle) = tray.as_pin_ref().data.inner.get() {
|
|
handle.set_icon(icon);
|
|
}
|
|
},
|
|
);
|
|
|
|
self.data.tooltip_tracker.init_delayed(
|
|
self_rc.downgrade(),
|
|
|self_weak| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return SharedString::default() };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
|
|
return SharedString::default();
|
|
};
|
|
tray.as_pin_ref().tooltip()
|
|
},
|
|
|self_weak, tooltip| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
|
|
if let Some(handle) = tray.as_pin_ref().data.inner.get() {
|
|
handle.set_tooltip(tooltip.as_str());
|
|
}
|
|
},
|
|
);
|
|
|
|
self.data.title_tracker.init_delayed(
|
|
self_rc.downgrade(),
|
|
|self_weak| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return SharedString::default() };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else {
|
|
return SharedString::default();
|
|
};
|
|
tray.as_pin_ref().title()
|
|
},
|
|
|self_weak, title| {
|
|
let Some(tray_rc) = self_weak.upgrade() else { return };
|
|
let Some(tray) = tray_rc.downcast::<SystemTrayIcon>() else { return };
|
|
if let Some(handle) = tray.as_pin_ref().data.inner.get() {
|
|
handle.set_title(title.as_str());
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo::default()
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
_: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
_: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut MouseCursorInner,
|
|
) -> InputEventResult {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
FocusEventResult::FocusIgnored
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut &mut dyn crate::item_rendering::ItemRenderer,
|
|
_self_rc: &ItemRc,
|
|
_size: LogicalSize,
|
|
) -> RenderingResult {
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for SystemTrayIcon {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
|
|
Self::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
/// # Safety
|
|
/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
|
|
/// hold a SystemTrayIconDataBox
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_system_tray_icon_data_init(data: *mut SystemTrayIconDataBox) {
|
|
unsafe { core::ptr::write(data, SystemTrayIconDataBox::default()) };
|
|
}
|
|
|
|
/// # Safety
|
|
/// This must be called using a non-null pointer pointing to an initialized SystemTrayIconDataBox
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_system_tray_icon_data_free(data: *mut SystemTrayIconDataBox) {
|
|
unsafe { core::ptr::drop_in_place(data) };
|
|
}
|
|
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_system_tray_icon_set_menu(
|
|
system_tray: &SystemTrayIcon,
|
|
item_rc: &ItemRc,
|
|
menu_vrc: &vtable::VRc<crate::menus::MenuVTable>,
|
|
) {
|
|
unsafe { Pin::new_unchecked(system_tray) }.set_menu(item_rc, menu_vrc.clone());
|
|
}
|