feat(slint): Linux system tray (ksni) + Wayland-safe close-to-tray

Port the Tauri tray to Slint on Linux via ksni: themed icon (auto/mono/
color), left-click window toggle, middle-click play/pause, scroll volume,
the 5-item menu, and a live tooltip reflecting the now-playing track. Tray
actions call the playback controller directly (QConnect-aware) instead of
emitting webview events. Settings reuse the shared qbz-app tray store
(per-user) wired through AppearanceState.

Window hide/show uses Slint window.hide()/show() (winit set_visible is a
no-op on Wayland) and the loop runs via run_event_loop_until_quit so a
tray-hide keeps the app alive. close-to-tray is honored on both the custom
titlebar close and the WM close (on_close_requested). minimize-to-tray is
hidden until custom window chrome exists.

cc
This commit is contained in:
vicrodh committed 2026-06-06 01:44:38 -06:00
1 parent 5b3ba963b0
commit 8a78c2250f
22 files changed
+1084 -26

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ import { AppShell } from "shell/AppShell.slint";
import { Toast } from "primitives/Toast.slint";
// Re-export the state globals so the Rust layer can populate them.
export { HomeState, HomeActions, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice } from "state.slint";
export { HomeState, HomeActions, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice, AppearanceState } from "state.slint";
// Which top-level screen is shown. The app starts on `splash` while it
// restores a saved session, then resolves to `shell` or `login`.
@@ -700,12 +700,11 @@ export component AppearanceSettings inherits VerticalLayout {
Rectangle { height: 12px; }
// ===================================================================
// SYSTEM TRAY
// SYSTEM TRAY (macOS: "MENU BAR")
// ===================================================================
GroupHeader { text: @tr("SYSTEM TRAY"); }
GroupHeader { text: AppearanceState.is-macos ? @tr("MENU BAR") : @tr("SYSTEM TRAY"); }
SettingRow {
unsupported: true;
label: @tr("Enable tray icon");
description: @tr("Show icon in system tray (requires restart)");
QbzToggle {
@@ -716,24 +715,18 @@ export component AppearanceSettings inherits VerticalLayout {
}
}
}
// NOTE: a separate "Minimize to tray" toggle is intentionally absent.
// Redirecting the window-manager minimize button to the tray requires
// owning that button (custom window chrome), which is not built yet, and
// Wayland forbids a client from intercepting the compositor's minimize.
// Until custom chrome lands, the option is HIDDEN (not shown disabled) and
// this row reads "Close to tray". When custom chrome exists, merge the two
// into a single "Minimize/Close to tray" control.
SettingRow {
unsupported: true;
label: @tr("Minimize to tray");
description: @tr("Hide window to tray when minimizing");
enabled: AppearanceState.tray-enable;
QbzToggle {
checked: AppearanceState.tray-minimize-to-tray;
enabled: AppearanceState.tray-enable;
toggled(v) => {
AppearanceState.tray-minimize-to-tray = v;
AppearanceState.appearance-bool("tray-minimize-to-tray", v);
}
}
}
SettingRow {
unsupported: true;
label: @tr("Close to tray");
description: @tr("Hide window to tray instead of quitting");
description: AppearanceState.is-macos
? @tr("Keep playing in the menu bar instead of quitting when you close the window")
: @tr("Keep playing in the tray instead of quitting when you close the window");
enabled: AppearanceState.tray-enable;
QbzToggle {
checked: AppearanceState.tray-close-to-tray;
@@ -744,8 +737,23 @@ export component AppearanceSettings inherits VerticalLayout {
}
}
}
// macOS-only: switch the activation policy to .accessory while closed to
// the menu bar (no Dock icon). Off = Spotify-style (keep the Dock icon).
SettingRow {
visible: AppearanceState.is-macos;
label: @tr("Hide Dock icon when closed to menu bar");
description: @tr("Run as a menu-bar-only app while the window is closed. Off keeps the Dock icon (like Spotify)");
enabled: AppearanceState.tray-enable && AppearanceState.tray-close-to-tray;
QbzToggle {
checked: AppearanceState.tray-mac-hide-dock;
enabled: AppearanceState.tray-enable && AppearanceState.tray-close-to-tray;
toggled(v) => {
AppearanceState.tray-mac-hide-dock = v;
AppearanceState.appearance-bool("tray-mac-hide-dock", v);
}
}
}
SettingRow {
unsupported: true;
label: @tr("Tray icon variant");
description: @tr("Pick a mono glyph to match your panel (Plasma, GNOME's permanently dark top bar) or the full colour vinyl logo");
QbzSelect {
+6
View File
@@ -1432,9 +1432,15 @@ export global AppearanceState {
in-out property <bool> show-purchases: false;
// --- SYSTEM TRAY -------------------------------------------------------
// `is-macos` is set once from Rust (cfg!(target_os = "macos")) so the tray
// section can swap the header to "Menu Bar" and show macOS-only controls.
in-out property <bool> is-macos: false;
in-out property <bool> tray-enable: true;
in-out property <bool> tray-minimize-to-tray: false;
in-out property <bool> tray-close-to-tray: false;
// macOS-only: hide the Dock icon when closed to the menu bar (.accessory
// activation policy). Off = Spotify-style (keep the Dock icon).
in-out property <bool> tray-mac-hide-dock: false;
in-out property <[string]> tray-icon-themes: ["Auto", "Mono light", "Mono dark", "Color"];
in-out property <int> tray-icon-theme-index: 0;