mirror of
https://github.com/italicsjenga/winit-sonoma-fix.git
synced 2024-12-26 23:21:30 +11:00
209 lines
8.7 KiB
Rust
209 lines
8.7 KiB
Rust
|
// Normally when you run or distribute a macOS app, it's bundled: it's in one
|
||
|
// of those fun little folders that you have to right click "Show Package
|
||
|
// Contents" on, and usually contains myriad delights including, but not
|
||
|
// limited to, plists, icons, and of course, your beloved executable. However,
|
||
|
// when you use `cargo run`, your app is unbundled - it's just a lonely, bare
|
||
|
// executable.
|
||
|
//
|
||
|
// Apple isn't especially fond of unbundled apps, which is to say, they seem to
|
||
|
// barely be supported. If you move the mouse while opening a winit window from
|
||
|
// an unbundled app, the window will fail to activate and be in a grayed-out
|
||
|
// uninteractable state. Switching to another app and back is the only way to
|
||
|
// get the winit window into a normal state. None of this happens if the app is
|
||
|
// bundled, i.e. when running via Xcode.
|
||
|
//
|
||
|
// To workaround this, we just switch focus to the Dock and then switch back to
|
||
|
// our app. We only do this for unbundled apps, and only when they fail to
|
||
|
// become active on their own.
|
||
|
//
|
||
|
// This solution was derived from this Godot PR:
|
||
|
// https://github.com/godotengine/godot/pull/17187
|
||
|
// (which appears to be based on https://stackoverflow.com/a/7602677)
|
||
|
// The curious specialness of mouse motions is touched upon here:
|
||
|
// https://github.com/godotengine/godot/issues/8653#issuecomment-358130512
|
||
|
//
|
||
|
// We omit the 2nd step of the solution used in Godot, since it appears to have
|
||
|
// no effect - I speculate that it's just technical debt picked up from the SO
|
||
|
// answer; the API used is fairly exotic, and was historically used for very
|
||
|
// old versions of macOS that didn't support `activateIgnoringOtherApps`, i.e.
|
||
|
// in previous versions of SDL:
|
||
|
// https://hg.libsdl.org/SDL/file/c0bcc39a3491/src/video/cocoa/SDL_cocoaevents.m#l322
|
||
|
//
|
||
|
// The `performSelector` delays in the Godot solution are used for sequencing,
|
||
|
// since refocusing the app will fail if the call is made before it finishes
|
||
|
// unfocusing. The delays used there are much smaller than the ones in the
|
||
|
// original SO answer, presumably because they found the fastest delay that
|
||
|
// works reliably through trial and error. Instead of using delays, we just
|
||
|
// handle `applicationDidResignActive`; despite the app not activating reliably,
|
||
|
// that still triggers when we switch focus to the Dock.
|
||
|
//
|
||
|
// The Godot solution doesn't appear to skip the hack when an unbundled app
|
||
|
// activates normally. Checking for this is difficult, since if you call
|
||
|
// `isActive` too early, it will always be `NO`. Even though we receive
|
||
|
// `applicationDidResignActive` when switching focus to the Dock, we never
|
||
|
// receive a preceding `applicationDidBecomeActive` if the app fails to
|
||
|
// activate normally. I wasn't able to find a proper point in time to perform
|
||
|
// the `isActive` check, so we instead check for the cause of the quirk: if
|
||
|
// any mouse motion occurs prior to us receiving `applicationDidResignActive`,
|
||
|
// we assume the app failed to become active.
|
||
|
//
|
||
|
// Fun fact: this issue is still present in GLFW
|
||
|
// (https://github.com/glfw/glfw/issues/1515)
|
||
|
//
|
||
|
// A similar issue was found in SDL, but the resolution doesn't seem to work
|
||
|
// for us: https://bugzilla.libsdl.org/show_bug.cgi?id=3051
|
||
|
|
||
|
use super::util;
|
||
|
use cocoa::{
|
||
|
appkit::{NSApp, NSApplicationActivateIgnoringOtherApps},
|
||
|
base::id,
|
||
|
foundation::NSUInteger,
|
||
|
};
|
||
|
use objc::runtime::{Object, Sel, BOOL, NO, YES};
|
||
|
use std::{
|
||
|
os::raw::c_void,
|
||
|
sync::atomic::{AtomicBool, Ordering},
|
||
|
};
|
||
|
|
||
|
#[derive(Debug, Default)]
|
||
|
pub struct State {
|
||
|
// Indicates that the hack has either completed or been skipped.
|
||
|
activated: AtomicBool,
|
||
|
// Indicates that the mouse has moved at some point in time.
|
||
|
mouse_moved: AtomicBool,
|
||
|
// Indicates that the hack is in progress, and that we should refocus when
|
||
|
// the app resigns active.
|
||
|
needs_refocus: AtomicBool,
|
||
|
}
|
||
|
|
||
|
impl State {
|
||
|
pub fn name() -> &'static str {
|
||
|
"activationHackState"
|
||
|
}
|
||
|
|
||
|
pub fn new() -> *mut c_void {
|
||
|
let this = Box::new(Self::default());
|
||
|
Box::into_raw(this) as *mut c_void
|
||
|
}
|
||
|
|
||
|
pub unsafe fn free(this: *mut Self) {
|
||
|
Box::from_raw(this);
|
||
|
}
|
||
|
|
||
|
pub unsafe fn get_ptr(obj: &Object) -> *mut Self {
|
||
|
let this: *mut c_void = *(*obj).get_ivar(Self::name());
|
||
|
assert!(!this.is_null(), "`activationHackState` pointer was null");
|
||
|
this as *mut Self
|
||
|
}
|
||
|
|
||
|
pub unsafe fn set_activated(obj: &Object, value: bool) {
|
||
|
let this = Self::get_ptr(obj);
|
||
|
(*this).activated.store(value, Ordering::Release);
|
||
|
}
|
||
|
|
||
|
unsafe fn get_activated(obj: &Object) -> bool {
|
||
|
let this = Self::get_ptr(obj);
|
||
|
(*this).activated.load(Ordering::Acquire)
|
||
|
}
|
||
|
|
||
|
pub unsafe fn set_mouse_moved(obj: &Object, value: bool) {
|
||
|
let this = Self::get_ptr(obj);
|
||
|
(*this).mouse_moved.store(value, Ordering::Release);
|
||
|
}
|
||
|
|
||
|
pub unsafe fn get_mouse_moved(obj: &Object) -> bool {
|
||
|
let this = Self::get_ptr(obj);
|
||
|
(*this).mouse_moved.load(Ordering::Acquire)
|
||
|
}
|
||
|
|
||
|
pub unsafe fn set_needs_refocus(obj: &Object, value: bool) {
|
||
|
let this = Self::get_ptr(obj);
|
||
|
(*this).needs_refocus.store(value, Ordering::Release);
|
||
|
}
|
||
|
|
||
|
unsafe fn get_needs_refocus(obj: &Object) -> bool {
|
||
|
let this = Self::get_ptr(obj);
|
||
|
(*this).needs_refocus.load(Ordering::Acquire)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This is the entry point for the hack - if the app is unbundled and a mouse
|
||
|
// movement occurs before the app activates, it will trigger the hack. Because
|
||
|
// mouse movements prior to activation are the cause of this quirk, they should
|
||
|
// be a reliable way to determine if the hack needs to be performed.
|
||
|
pub extern "C" fn mouse_moved(this: &Object, _: Sel, _: id) {
|
||
|
trace!("Triggered `activationHackMouseMoved`");
|
||
|
unsafe {
|
||
|
if !State::get_activated(this) {
|
||
|
// We check if `CFBundleName` is undefined to determine if the
|
||
|
// app is unbundled.
|
||
|
if let None = util::app_name() {
|
||
|
info!("App detected as unbundled");
|
||
|
unfocus(this);
|
||
|
} else {
|
||
|
info!("App detected as bundled");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
trace!("Completed `activationHackMouseMoved`");
|
||
|
}
|
||
|
|
||
|
// Switch focus to the dock.
|
||
|
unsafe fn unfocus(this: &Object) {
|
||
|
// We only perform the hack if the app failed to activate, since otherwise,
|
||
|
// there'd be a gross (but fast) flicker as it unfocused and then refocused.
|
||
|
// However, we only enter this function if we detect mouse movement prior
|
||
|
// to activation, so this should always be `NO`.
|
||
|
//
|
||
|
// Note that this check isn't necessarily reliable in detecting a violation
|
||
|
// of the invariant above, since it's not guaranteed that activation will
|
||
|
// resolve before this point. In other words, it can spuriously return `NO`.
|
||
|
// This is also why the mouse motion approach was chosen, since it's not
|
||
|
// obvious how to sequence this check - if someone knows how to, then that
|
||
|
// would almost surely be a cleaner approach.
|
||
|
let active: BOOL = msg_send![NSApp(), isActive];
|
||
|
if active == YES {
|
||
|
error!("Unbundled app activation hack triggered on an app that's already active; this shouldn't happen!");
|
||
|
} else {
|
||
|
info!("Performing unbundled app activation hack");
|
||
|
let dock_bundle_id = util::ns_string_id_ref("com.apple.dock");
|
||
|
let dock_array: id = msg_send![
|
||
|
class!(NSRunningApplication),
|
||
|
runningApplicationsWithBundleIdentifier: *dock_bundle_id
|
||
|
];
|
||
|
let dock_array_len: NSUInteger = msg_send![dock_array, count];
|
||
|
if dock_array_len == 0 {
|
||
|
error!("The Dock doesn't seem to be running, so switching focus to it is impossible");
|
||
|
} else {
|
||
|
State::set_needs_refocus(this, true);
|
||
|
let dock: id = msg_send![dock_array, objectAtIndex: 0];
|
||
|
// This will trigger `applicationDidResignActive`, which will in
|
||
|
// turn call `refocus`.
|
||
|
let status: BOOL = msg_send![
|
||
|
dock,
|
||
|
activateWithOptions: NSApplicationActivateIgnoringOtherApps
|
||
|
];
|
||
|
if status == NO {
|
||
|
error!("Failed to switch focus to Dock");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Switch focus back to our app, causing the user to rejoice!
|
||
|
pub unsafe fn refocus(this: &Object) {
|
||
|
if State::get_needs_refocus(this) {
|
||
|
State::set_needs_refocus(this, false);
|
||
|
let app: id = msg_send![class!(NSRunningApplication), currentApplication];
|
||
|
// Simply calling `NSApp activateIgnoringOtherApps` doesn't work. The
|
||
|
// nuanced difference isn't clear to me, but hey, I tried.
|
||
|
let success: BOOL = msg_send![
|
||
|
app,
|
||
|
activateWithOptions: NSApplicationActivateIgnoringOtherApps
|
||
|
];
|
||
|
if success == NO {
|
||
|
error!("Failed to refocus app");
|
||
|
}
|
||
|
}
|
||
|
}
|