mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-02 18:07:04 +00:00
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:
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user