System Tray: Finalize API#11575
Conversation
ogoffart
left a comment
There was a problem hiding this comment.
Looks good, appart from the fact that run doesn't use the SlintContext from the system tray.
In fact, do we even want run in the SystemTray API? I believe it might have been a mistake to have that on the component instead of asking to use slint::run_event_loop
(side question, but i guess the system tray counts as a "window" as far as slint is concerned for the quitting the application when all window are hidden)
|
|
||
| pub fn run(&self) -> ::core::result::Result<(), slint::PlatformError> { | ||
| self.show()?; | ||
| slint::run_event_loop()?; |
There was a problem hiding this comment.
There is no context for a SystemTray?
Maybe there should be.
| pub fn show(&self) -> ::core::result::Result<(), slint::PlatformError> { | ||
| self.0.globals.get().unwrap().window_adapter_ref()?.window().show() | ||
| } | ||
|
|
||
| pub fn hide(&self) -> ::core::result::Result<(), slint::PlatformError> { | ||
| self.0.globals.get().unwrap().window_adapter_ref()?.window().hide() | ||
| } |
There was a problem hiding this comment.
Indeed, my plan was to route this to the system tray itself in a follow-up. But I'll then move that into this PR.
| } | ||
| } | ||
| ) | ||
| } else { |
There was a problem hiding this comment.
Most of this code is shared. Can it be put in a #component_impl or something like that?
There was a problem hiding this comment.
I tried splitting it first but it became quite an ugly quote! mess. Let me see how it looks like after the next round.
I'm fine with removing run. I was trying to keep it generic, but then I'll special case everything exactly for the SystemTray then. I wanted to do the removal of the Item nature of the SystemTray in a separate PR, but as it looks that will also have to go into this one then.
Sounds okay to me. |
22159ad to
2a19329
Compare
|
Converting this back to a draft as more work is needed to not use a window. |
2a19329 to
d1de060
Compare
2417f3a to
e5625e9
Compare
Mirror the Rust/C++ codegen change for the dynamic interpreter path that backs Node, Python, and live-preview. `ComponentInstance::show`/ `hide` for a SystemTray-rooted component now set the `visible` property rather than going through the (non-real) window adapter; the change-tracker on the SystemTray native item dispatches to the platform handle. `run()` still exists on the trait but is unchanged here — invoking it on a tray-rooted instance would already panic via the window adapter, matching the pre-existing limitation.
SNI has no hide operation, so toggling visibility means dropping the registered handle (deregisters from the watcher) and spawning a fresh one on show. Move the icon, title, event sender, and menu cache onto PlatformTray so a respawn can rebuild the KsniTray without the caller having to re-supply state.
Previously the icon and title properties were only read once at handle creation. Add a SystemTrayHandle::set_icon / set_title pair and watch the corresponding properties via change trackers, so mutations made after the tray is up reach the platform backend. Implemented for ksni (via Handle::update); stubbed on AppKit and Windows alongside the existing set_visible TODOs.
NSStatusItem has a real visibility toggle since macOS 10.12, so the AppKit backend doesn't need the deregister/respawn dance the SNI backend has to do. Just forward to setVisible:.
Shell_NotifyIconW supports toggling the icon's visibility via NIM_MODIFY with NIF_STATE + NIS_HIDDEN, which keeps the icon registered (HWND, HICON, tooltip, and the WM_TRAYICON routing all survive). That's a real toggle, not a re-add, so unlike the SNI backend there's no recreation and no flicker.
Replace the set_icon / set_title stubs with real implementations using Shell_NotifyIconW(NIM_MODIFY). The hicon and tooltip slots on Inner are now interior-mutable so the TaskbarCreated re-add path in the wnd proc sees the latest values. On set_icon failure the new HICON is freed rather than leaked; on success the previous one is destroyed only after the modify is confirmed.
Forward set_icon and set_title to the status item button's setImage and setToolTip. On icon-conversion failure the previous image is left in place rather than blanked.
A tray-only program no longer materializes a hidden `WindowAdapter` as a side effect of constructing the component.
…ponents Mirror the Rust parent commit: tray-rooted public components no longer eagerly invoke `self->m_globals.window()` in `create`.
Annotate SystemTray with //-is_non_item_type so ElementType::lookup_property returns Invalid for any property not declared on the builtin instead of falling through to the auto-injected reserved_properties (x, y, width, height, z, padding, layout-info, accessibility, ...). Setting those on a tray would have no effect at runtime — the OS shell decides where to place the tray icon — so a clear 'Unknown property' diagnostic at compile time is preferable to silent dead code. Same annotation pattern as Menu / MenuItem / MenuSeparator / MenuBar, which are all logically non-rendered items.
A SystemTray must be the root of an exported component: the platform
tray APIs only know how to bind to a top-level SystemTray-rooted
component, so nesting one inside another element has no meaningful
lowering. Promote this case to a compile-time error in
`warn_about_child_windows` (sibling to the existing Window-child
warning) and add a syntax test covering both the direct-builtin case
and the user-subclass case.
Also harden the `lower_menus` pass to only call `process_system_tray`
on elements whose `base_type` is the directly-Builtin SystemTray. Before
this change, a still-Component subclass child (`MyTray {}`) would slip
through the `builtin_type()` filter and trip an `as_builtin()` panic
because lower_menus runs before inlining. The legitimate root case is
unaffected — `visit_all_used_components` enters user components
directly, where the root_element IS the Builtin SystemTray.
Spell out how the generated handle differs from a Window-rooted one: no `run()`, no `window()`, and `show()`/`hide()` route through the `visible` property. Include a table comparing the two, the per-platform behavior of `show`/`hide`, Rust and C++ entry-point examples driving the event loop themselves, and a note that a tray-only program never materializes a `WindowAdapter` even though it still goes through the backend selector for platform initialization.
The property is the hover-text shown over the tray icon on every backend. Calling it "title" was a leak of SNI's terminology — SNI's `Title` is a separate descriptive-name slot, not the hover tooltip — and it didn't match what users see on macOS or Windows, where the property had always mapped to a real OS tooltip (NSStatusItem.button toolTip / Shell_NotifyIcon szTip). Rename the slint property to `tooltip` so the name matches the user-visible behavior. On Linux, also fix the SNI mapping: the tooltip now populates the SNI `ToolTip` property (i.e. the hover slot the spec actually intends), not SNI `Title`. The SNI `Title` slot is left at its default for now; a separate `title` property targeting it is a future addition. The rename touches the builtin declaration, the native item, all three backends (ksni / AppKit / Win32), the rust test case, and the reference page.
Separate from `tooltip` (the hover text), `title` is the descriptive
name shown next to / for the icon. The platform mapping reflects what
each shell actually renders:
- Linux (SNI): populate `Tray::title()`, the descriptive-name slot
watchers use for accessibility and overflow listings.
- macOS: `NSStatusBarButton.setTitle:` — a real visible text label
next to the icon (battery indicator, clock, mixer-style).
- Windows: no native equivalent; the notification area renders only
the icon. The `set_title` hook on the Win32 backend is a documented
no-op so a cross-platform binding still compiles.
Plumb the property through `Params`, `SystemTrayHandle::set_title`,
the `title_tracker` change tracker, and each backend; document the
per-platform behavior in the reference page.
The Slint event loop already auto-quits when the last window is hidden,
via a counter on `SlintContext` (`window_count`) that
`WindowInner::show`/`hide` increment and decrement. Extend the same
mechanism so a visible system tray icon counts too, and an invisible
one doesn't:
- Add `acquire_keepalive` / `release_keepalive` helpers on
`SlintContext` that wrap the counter math + post-decrement
quit-on-zero. `WindowInner::show`/`hide` now call them in place
of the inline logic.
- On `SystemTray`, track an explicit `keepalive_live: Cell<bool>`
and reconcile it through a small `update_keepalive` method that
fires from the icon-driven tracker (when the platform handle comes
up) and from the `visible_tracker` (on every visibility flip).
The tray contributes to the counter exactly while it has a live
platform handle and `visible == true`.
- `SystemTrayData::drop` releases the counter if the tray was still
live, so dropping the last tray-only program quits the loop.
This makes a tray-only program work without anyone explicitly calling
`quit_event_loop`, and a hybrid window+tray program keep the loop
alive across hide/show cycles of either side.
Update the public docs everywhere the "last window closed" trigger is
described. With the previous commit a visible SystemTray contributes to
the same keepalive counter as a visible window, so the loop quits only
once nothing is visible — windows or tray icons.
Touches:
- `slint::run_event_loop` and `slint::run_event_loop_until_quit` in the
Rust API doc-comments.
- `EventLoopMode` and `slint::run_event_loop` in the C++ header.
- `ComponentHandle.run` and `runEventLoop`'s `quitOnLastWindowClosed`
parameter doc in the NodeJS binding.
- The SystemTray reference page, which had been telling tray-only users
to "drive the event loop yourself". They still call
`run_event_loop` directly, but the loop now stays alive while the
tray is visible without needing `run_event_loop_until_quit".
Replace the icon-only demo with a window that drives the tray and a
tray that drives the window:
- Window has LineEdit / CheckBox controls for the tray's tooltip,
title, and visibility, plus a 'Hide to tray' button and a Quit
button. Edits propagate via per-property *-changed callbacks.
- ExampleTray exposes `tray-tooltip` / `tray-title` /
`tray-visible` two-way-aliased to the inherited SystemTray
properties, so Rust gets typed `set_*` accessors. The inherited
properties on the builtin aren't auto-exposed on the public
component handle.
- Left-click on the tray (or the 'Show / hide window' menu item)
toggles the window via `window().is_visible()`. The OS close
button still hides via the default
`CloseRequestResponse::HideWindow`, so the tray's keepalive
keeps the loop running until the user picks Quit.
- The window has an 'activation log' line that records the most
recent tray click, so it's obvious the wiring is live.
Move the .slint sources out of the inline `slint::slint!` macro into a
dedicated `system_tray.slint` so the same UI and tray definitions can be
loaded from every binding. Add main.{cpp,py,js} entrypoints with
matching CMakeLists.txt / pyproject.toml / package.json scaffolding,
mirroring the layout of examples/memory.
Per-binding caveats:
- C++: gated behind `SLINT_FEATURE_EXPERIMENTAL` in the example's
CMakeLists. The host examples/CMakeLists.txt now picks up
examples/system_tray/ alongside the other compiler-driven examples.
- Python: `window` is not yet exposed on the binding's component
handle, so the toggle tracks the window's visibility in a small
Python-side cell. Sets `SLINT_ENABLE_EXPERIMENTAL_FEATURES=1`
before importing slint.
- Node.js: uses `main_window.window` for the visibility check, which
is conditionally installed for Window-rooted components.
Sets `process.env.SLINT_ENABLE_EXPERIMENTAL_FEATURES` before
importing slint-ui.
The Rust main.rs is now a thin wiring layer (12 lines of slint! are
gone) that loads the same .slint file the other bindings load.
Drop the line in `typeregister::TypeRegister::builtin` that removes
`SystemTray` from the non-experimental element set. SystemTray and
its plumbing are now stable enough — visibility, icon/title/tooltip
mapping, the keepalive integration, and per-platform behaviour are all
landed and documented — that hiding it behind
`SLINT_ENABLE_EXPERIMENTAL_FEATURES` no longer adds anything.
Tidy up everything that was setting the env var only to unlock
SystemTray:
- examples/system_tray: delete build.rs (and its slint-build dep) and
drop the env-var lines in main.py / main.js. The C++ CMakeLists
drops its EXPERIMENTAL feature gate.
- api/node and api/python tests: stop setting the env var in the
'non-windowed components have no `window` property' /
'test_system_tray_has_no_window_attribute' tests.
Test drivers (test-driver-rust / interpreter / cpp / nodejs / python)
keep setting `SLINT_ENABLE_EXPERIMENTAL_FEATURES` because they need
it for other experimental features.
Mass rename of the slint type and its supporting Rust / C++ symbols.
The new name is more descriptive about what the element actually
controls (an icon in the system tray), and matches conventions like
NSStatusItem on macOS.
Renamed identifiers:
- .slint type: `SystemTray` -> `SystemTrayIcon`
- Rust types in internal/core/items/system_tray.rs:
`SystemTray` -> `SystemTrayIcon`
`SystemTrayHandle` -> `SystemTrayIconHandle`
`SystemTrayData` -> `SystemTrayIconData`
`SystemTrayDataBox` -> `SystemTrayIconDataBox`
`SystemTrayVTable` -> `SystemTrayIconVTable`
- LLR enum: `TopLevelComponentType::SystemTray` -> `SystemTrayIcon`
- Builtin function: `SetupSystemTray` -> `SetupSystemTrayIcon`
- Helpers: `inherits_system_tray` -> `inherits_system_tray_icon`,
`process_system_tray` -> `process_system_tray_icon`
- FFI symbols: `slint_system_tray_*` -> `slint_system_tray_icon_*`
Updated:
- all .slint files (tests, examples, docs)
- internal compiler / interpreter / codegen
- C++ binding header + cbindgen config
- Node TypeScript wrapper + python tests
- examples/system_tray (the directory name and the .slint filename
are kept; only the type name inside changes)
- docs reference page renamed
docs/.../reference/window/systemtray.mdx -> systemtrayicon.mdx
plus linkMap entry in internal/core-macros/link-data.json updated to
"SystemTrayIcon" / "reference/window/systemtrayicon/". The single
cross-reference in globals.mdx now points at the new key.
Verified:
- cargo check -p i-slint-compiler / -p slint-interpreter / -p i-slint-core
- cargo test -p i-slint-compiler --test syntax_tests
- SLINT_TEST_FILTER=systemtray cargo test -p test-driver-rust --test elements
- astro check (docs type-check) reports 0 errors / warnings
Mirror the existing MenuBar pattern: `if cond : Menu { ... }` inside a
SystemTrayIcon is now lowered to a closure that gates the menu's shadow
tree, so the platform menu is empty (no popup contents) while the
condition is false and re-populates when it flips back to true.
- lower_menus.rs: take the `repeated` info off the Menu element; the
conditional case threads its model expression as a trailing argument
to `SetupSystemTrayIcon`. The `for`-loop case keeps erroring, with
the message tightened to "cannot be in a repeated element".
- generator/rust.rs: SetupSystemTrayIcon codegen destructures with
`rest @ ..` and switches between `MenuFromItemTree::new` and
`new_with_condition`, mirroring SetupMenuBar's shape.
- generator/cpp.rs: the same; `create_menu_wrapper` already takes the
optional condition pointer.
- interpreter/eval.rs: forward `rest.first()` as the
`condition: Option<&Expression>` argument to `make_menu_item_tree`.
Updated the syntax test (case A is now a positive test for
`if show-menu : Menu { ... }`; case B keeps the error with the new
phrasing) and added a runtime test
`tests/cases/elements/systemtray_conditional_menu.slint` that toggles
the condition. Both `elements_systemtray_menu` and
`elements_systemtray_conditional_menu` pass under
`SLINT_TEST_FILTER=systemtray cargo test -p test-driver-rust --test elements`.
Wire the `NSStatusItem` button's target/action to the `MenuAction`
ObjC class so click events are routed back to slint's `activated`
callback. AppKit only invokes the action when no menu is set on the
status item, so:
- A `SystemTrayIcon` with no `Menu` declared gets no `setMenu`
call (we never enter `rebuild_menu` for that case), the action
fires on click, and `activated` is invoked. This brings macOS in
line with Linux/Windows for the common menu-less tray.
- A `SystemTrayIcon` with `if cond : Menu { ... }` works the same
way dynamically: `rebuild_menu` now calls `setMenu(None)` when the
wrapper reports zero entries (which is what the conditional
evaluates to while the condition is false), and `setMenu(Some)`
when entries are present. AppKit forwards clicks to the action
while the menu is detached and pops the menu open while it's
attached.
Updated the activation-behavior table in the SystemTrayIcon reference
page to drop the "macOS doesn't fire `activated`" caveat and describe
the new semantics.
Cross-checked clean for `aarch64-apple-darwin`. No public-API change.
Wire a `tray-menu-enabled` checkbox in the window through to a
`menu-enabled` property on `ExampleTray`, which now wraps the slint
`Menu` in `if root.menu-enabled : Menu { ... }`. Toggling the checkbox
exercises the conditional-menu lowering and, on macOS specifically,
the new `setMenu(None)` <-> button-action behavior:
- menu-enabled = true -> Menu attached. Click on the tray pops the
menu open. `activated` does not fire.
- menu-enabled = false -> the conditional reports zero entries, the
appkit backend detaches the NSMenu, and a click on the tray fires
`activated` (which routes through `toggle-window` and updates the
activation log so the change is visible).
The wiring follows the same shape as the existing tray-{title,tooltip,
visible} state pushes — one initial sync plus one `*-changed` callback
per binding (Rust / C++ / NodeJS / Python).
…cing the quit() handler with the typicaly window show pattern
So a `SystemTrayIcon`-rooted component still constructs on Android, WASM, and other targets without a real tray; the icon simply never reaches a host shell.
Follow the PR #11356 pattern: the SystemTrayIcon documentation now lives as doc comments on the component in builtins.slint, with a `\doc-file:window/systemtrayicon.mdx` annotation; the standalone mdx is auto-generated by slint-doc-generator and ignored by git. The generator gains an auto-import for `Tabs` / `TabItem` (used in the "Language Bindings" footer to show per-language code in tabs), and the SystemTrayIcon properties are reordered to match the doc order.
As concluded in #6053 (comment) , this public API in Rust, C++, etc. when exporting a SystemTray sub-class should not be a standard Window/ComponentHandle.
This PR does that and fixes up various other parts of the API match what we want, including visible property, conditional menu support, and a rename to SystemTrayIcon.