Bug description
On Linux, a SystemTrayIcon with a Menu sometimes shows its icon, but right-clicking
it opens nothing. The menu stays missing for the rest of the process's lifetime.
Restarting the app fixes it.
It happens when the app starts slowly. The first launch of a freshly built binary (cold
disk cache) triggers it reliably. It never happens on warm launches.
The app side is correct the whole time. Querying the item's dbusmenu directly returns
the full menu:
$ busctl --user --json=short call org.kde.StatusNotifierItem-<pid>-1 /MenuBar \
com.canonical.dbusmenu GetLayout iias 0 -- -1 0
{"type":"u(ia{sv}av)","data":[1,[0,{"children-display":...},[
{... "label":"Show Tagent" ...}, {... "label":"Settings…" ...},
{... "type":"separator" ...}, {... "label":"Quit" ...}]]]}
Cause (as far as I can tell)
In i-slint-core/items/system_tray/ksni.rs, PlatformTray::new calls spawn_tray(...)
with an empty menu (std::vec::Vec::new()). The real menu arrives slightly later:
SystemTrayIcon::init calls tray.rebuild_menu() after the handle exists, which runs
PlatformTray::rebuild_menu → handle.update(|tray| tray.menu = ...).
So between these two steps the StatusNotifierItem is already registered with the watcher
and exposes an empty /MenuBar layout. ksni does emit LayoutUpdated after the
update. But if the tray host fetched the layout inside that gap, GNOME's AppIndicator
extension evidently keeps the empty menu and never shows one on right-click. Normally
the gap is too short to matter; on a slow start it isn't.
A comment in spawn_tray says returning the handle synchronously "eliminates the
pending-menu race". That covers the menu being lost on the Slint side, but not the host
reading the menu before it's filled in.
Suggested fix
Build the menu before the item is registered. For example, pass the built menu into
spawn_tray on creation, the same way set_visible(true) already respawns the tray
with the cached self.menu. The alternative is to defer spawn_tray until
rebuild_menu has run once.
Reproducible code
export component TrayIcon inherits SystemTrayIcon {
icon: @image-url("tray.png");
tooltip: "Test";
Menu {
MenuItem { title: "Show"; }
MenuSeparator {}
MenuItem { title: "Quit"; activated => { /* quit */ } }
}
}
fn main() -> Result<(), slint::PlatformError> {
let _tray = TrayIcon::new()?;
slint::run_event_loop_until_quit()
}
Steps to reproduce
- Build the app in release mode.
- Evict the binary from the page cache to make startup slow, as on the first launch
after a build:
python3 -c "import os,sys; fd=os.open(sys.argv[1],os.O_RDONLY); os.posix_fadvise(fd,0,0,os.POSIX_FADV_DONTNEED)" target/release/app
- Run the app.
- Right-click the tray icon.
Expected: the menu opens. Actual: nothing opens (the icon itself is shown, and
left-click/Activate works). Without step 2, the menu opens every time.
Environment details
- Slint: 1.17.1 (
ksni 0.3.6, zbus 5.18.0). Observed on 1.17.1. The same code path
(PlatformTray::new spawning with an empty menu) is unchanged in v1.18.1 and on
master, so I expect it to reproduce there too.
- Platform/OS: Ubuntu 24.04.5 LTS, GNOME Shell 46.0, X11 session
- Tray host:
ubuntu-appindicators@ubuntu.com (gnome-shell-extension-appindicator 58-1ubuntu24.04.1)
- Backend/renderer: winit (default)
- Rust: 1.98.1
Additional notes
As a workaround, I tried re-registering the icon once after startup (toggling
visible false → true, so the respawned item carries the cached menu). If visible is
not bound to anything in the .slint file, hide() on the tray panics with
Constant property being changed (i-slint-core/properties.rs:788), because an unbound
built-in visible is compiled as a constant. That may be worth a look on its own, since
SystemTrayIcon's docs present show()/hide() as the way to toggle it.
Bug description
On Linux, a
SystemTrayIconwith aMenusometimes shows its icon, but right-clickingit opens nothing. The menu stays missing for the rest of the process's lifetime.
Restarting the app fixes it.
It happens when the app starts slowly. The first launch of a freshly built binary (cold
disk cache) triggers it reliably. It never happens on warm launches.
The app side is correct the whole time. Querying the item's dbusmenu directly returns
the full menu:
Cause (as far as I can tell)
In
i-slint-core/items/system_tray/ksni.rs,PlatformTray::newcallsspawn_tray(...)with an empty menu (
std::vec::Vec::new()). The real menu arrives slightly later:SystemTrayIcon::initcallstray.rebuild_menu()after the handle exists, which runsPlatformTray::rebuild_menu→handle.update(|tray| tray.menu = ...).So between these two steps the StatusNotifierItem is already registered with the watcher
and exposes an empty
/MenuBarlayout.ksnidoes emitLayoutUpdatedafter theupdate. But if the tray host fetched the layout inside that gap, GNOME's AppIndicator
extension evidently keeps the empty menu and never shows one on right-click. Normally
the gap is too short to matter; on a slow start it isn't.
A comment in
spawn_traysays returning the handle synchronously "eliminates thepending-menu race". That covers the menu being lost on the Slint side, but not the host
reading the menu before it's filled in.
Suggested fix
Build the menu before the item is registered. For example, pass the built menu into
spawn_trayon creation, the same wayset_visible(true)already respawns the traywith the cached
self.menu. The alternative is to deferspawn_trayuntilrebuild_menuhas run once.Reproducible code
Steps to reproduce
after a build:
python3 -c "import os,sys; fd=os.open(sys.argv[1],os.O_RDONLY); os.posix_fadvise(fd,0,0,os.POSIX_FADV_DONTNEED)" target/release/appExpected: the menu opens. Actual: nothing opens (the icon itself is shown, and
left-click/
Activateworks). Without step 2, the menu opens every time.Environment details
ksni0.3.6,zbus5.18.0). Observed on 1.17.1. The same code path(
PlatformTray::newspawning with an empty menu) is unchanged in v1.18.1 and onmaster, so I expect it to reproduce there too.
ubuntu-appindicators@ubuntu.com(gnome-shell-extension-appindicator58-1ubuntu24.04.1)Additional notes
As a workaround, I tried re-registering the icon once after startup (toggling
visiblefalse → true, so the respawned item carries the cached menu). Ifvisibleisnot bound to anything in the
.slintfile,hide()on the tray panics withConstant property being changed(i-slint-core/properties.rs:788), because an unboundbuilt-in
visibleis compiled as a constant. That may be worth a look on its own, sinceSystemTrayIcon's docs presentshow()/hide()as the way to toggle it.