macOS: show the window on the current desktop

Opening the launcher from the menu bar used to switch to the desktop
(Space) its window was last on, which is jarring when you're working on
another one. A new setting, "Show the window on the current desktop"
(on by default), sets AppKit's "move to active space" behavior on the
window whenever it's shown, so it comes to the desktop you're on instead.
Turning it off restores macOS's default of switching to the window.

macOS only: the code and its dependencies compile only for macOS, and
the setting appears only where the host has desktops. objc2,
objc2-foundation and objc2-app-kit are already in Cargo.lock through
winit and tray-icon; they're pinned to those versions, and only safe
AppKit calls are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-10 03:03:12 -04:00
parent e8c4b3d355
commit 8766e29547
8 changed files with 81 additions and 1 deletions
+7
View File
@@ -28,6 +28,13 @@ notify-rust = "4"
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
tray-icon = "0.25"
# macOS only: which desktop (Space) the window shows on (spaces.rs). Safe AppKit calls only, and
# already in Cargo.lock through winit and tray-icon; pinned exactly to those versions.
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "=0.6.5"
objc2-foundation = { version = "=0.3.2", default-features = false, features = ["std", "NSArray", "NSEnumerator"] }
objc2-app-kit = { version = "=0.3.2", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly"))'.dependencies]
tray-icon = { version = "0.25", default-features = false, features = ["ksni"] }
+24 -1
View File
@@ -5,6 +5,8 @@
#![cfg_attr(windows, windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(target_os = "macos")]
mod spaces;
mod tray;
use std::process::ExitCode;
@@ -91,6 +93,10 @@ impl Host for DesktopHost {
self.tray
}
fn has_desktops(&self) -> bool {
cfg!(target_os = "macos")
}
fn login_item(&self) -> Option<bool> {
Some(login::is_enabled())
}
@@ -111,6 +117,9 @@ struct Shell {
show_requests: Receiver<()>,
visible: bool,
quitting: bool,
/// The "show on the current desktop" setting last applied to the window (macOS).
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
applied_follow: Option<bool>,
}
impl Shell {
@@ -135,11 +144,19 @@ impl Shell {
} else if hidden {
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
}
Self { ui: LauncherUi::new(&ctx, Box::new(backend), settings), tray, host, show_requests, visible, quitting: false }
Self { ui: LauncherUi::new(&ctx, Box::new(backend), settings), tray, host, show_requests, visible, quitting: false, applied_follow: None }
}
fn show(&mut self, ctx: &egui::Context) {
self.visible = true;
// Before the window is ordered front, so it comes to this desktop rather than taking the
// user to its own (macOS; the setting doesn't exist elsewhere).
#[cfg(target_os = "macos")]
{
let on = self.ui.settings.show_on_current_desktop;
spaces::follow_current_desktop(on);
self.applied_follow = Some(on);
}
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
@@ -159,6 +176,12 @@ impl Shell {
impl eframe::App for Shell {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.ui.logic(ctx, &mut self.host);
#[cfg(target_os = "macos")]
if self.applied_follow != Some(self.ui.settings.show_on_current_desktop) {
let on = self.ui.settings.show_on_current_desktop;
spaces::follow_current_desktop(on);
self.applied_follow = Some(on);
}
if self.show_requests.try_iter().count() > 0 {
self.show(ctx);
}
+22
View File
@@ -0,0 +1,22 @@
//! macOS only: which desktop (Space) the launcher's window appears on when it's shown.
//!
//! By default macOS switches to the desktop the window was last on, which is jarring when the
//! launcher is opened from the menu bar on another desktop. With "move to active space" set, the
//! window comes to the desktop you're on instead (and leaves the one it was on). Other platforms
//! show a hidden window on the current desktop anyway, so this module only exists on macOS.
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSWindowCollectionBehavior};
/// Makes the launcher's windows follow the current desktop when shown (`true`), or stay on their
/// own desktop and switch to it (`false`, macOS's default). Must run on the main thread, as the
/// UI does; elsewhere it does nothing.
pub fn follow_current_desktop(on: bool) {
let Some(mtm) = MainThreadMarker::new() else { return };
let app = NSApplication::sharedApplication(mtm);
for window in app.windows().iter() {
let mut behavior = window.collectionBehavior();
behavior.set(NSWindowCollectionBehavior::MoveToActiveSpace, on);
window.setCollectionBehavior(behavior);
}
}