fix(macos): prevent Escape from exiting native fullscreen
This commit is contained in:
1 parent
3442acd52c
commit
6eb8b43701
3 files changed
+86
-1
No files matched your search
@@ -75,7 +75,7 @@ tauri = { version = "2.10.3", features = ["test"] }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = { version = "0.6.4", default-features = false, features = ["std"] }
|
objc2 = { version = "0.6.4", default-features = false, features = ["std"] }
|
||||||
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSApplication", "NSButton", "NSControl", "NSDockTile", "NSImage", "NSResponder", "NSView", "NSWindow"] }
|
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSApplication", "NSButton", "NSControl", "NSDockTile", "NSEvent", "NSImage", "NSResponder", "NSView", "NSWindow"] }
|
||||||
objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSData", "NSString"] }
|
objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSData", "NSString"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ mod data_dir;
|
|||||||
mod db;
|
mod db;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod macos_app_delegate;
|
mod macos_app_delegate;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos_escape_guard;
|
||||||
mod models;
|
mod models;
|
||||||
#[cfg(any(target_os = "windows", test))]
|
#[cfg(any(target_os = "windows", test))]
|
||||||
mod startup_recovery;
|
mod startup_recovery;
|
||||||
@@ -1488,6 +1490,8 @@ pub fn run() {
|
|||||||
commands::ssh_prompt::install_ssh_notice_bridge(app.handle());
|
commands::ssh_prompt::install_ssh_notice_bridge(app.handle());
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
macos_app_delegate::install_dock_quit_handler(app.handle());
|
macos_app_delegate::install_dock_quit_handler(app.handle());
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
macos_escape_guard::install_escape_fullscreen_guard();
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
webview2_recovery::install(app.handle());
|
webview2_recovery::install(app.handle());
|
||||||
let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1));
|
let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1));
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use std::sync::{Once, OnceLock};
|
||||||
|
|
||||||
|
use objc2::{
|
||||||
|
msg_send,
|
||||||
|
runtime::{AnyClass, AnyObject, Imp, Sel},
|
||||||
|
sel,
|
||||||
|
};
|
||||||
|
use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSEventType, NSWindowStyleMask};
|
||||||
|
use objc2_foundation::MainThreadMarker;
|
||||||
|
|
||||||
|
static ORIGINAL_DO_COMMAND: OnceLock<Imp> = OnceLock::new();
|
||||||
|
static INSTALL_ESCAPE_FULLSCREEN_GUARD: Once = Once::new();
|
||||||
|
|
||||||
|
const ESCAPE_KEY_CODE: u16 = 53;
|
||||||
|
|
||||||
|
pub(crate) fn install_escape_fullscreen_guard() {
|
||||||
|
INSTALL_ESCAPE_FULLSCREEN_GUARD.call_once(|| unsafe {
|
||||||
|
let Some(responder) = AnyClass::get(c"NSResponder") else {
|
||||||
|
eprintln!("[WARN] failed to install macOS Escape fullscreen guard: NSResponder class not found");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(do_command) = responder.instance_method(sel!(doCommandBySelector:)) else {
|
||||||
|
eprintln!("[WARN] failed to install macOS Escape fullscreen guard: doCommandBySelector: not found");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = ORIGINAL_DO_COMMAND.set(do_command.implementation());
|
||||||
|
let guard: Imp = std::mem::transmute(do_command_guard as extern "C-unwind" fn(&AnyObject, Sel, Sel));
|
||||||
|
do_command.set_implementation(guard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_event_is_unmodified_escape() -> bool {
|
||||||
|
let Some(mtm) = MainThreadMarker::new() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(event) = NSApplication::sharedApplication(mtm).currentEvent() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if event.r#type() != NSEventType::KeyDown || event.keyCode() != ESCAPE_KEY_CODE {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut modifiers = event.modifierFlags() & NSEventModifierFlags::DeviceIndependentFlagsMask;
|
||||||
|
modifiers.remove(NSEventModifierFlags::CapsLock);
|
||||||
|
modifiers.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the responder's window is in native fullscreen.
|
||||||
|
///
|
||||||
|
/// `isFullscreen` is overridden by tao and returns false while the window is
|
||||||
|
/// fullscreen, so the styleMask bit is the only reliable signal. Responders
|
||||||
|
/// without a window (or without the selector) answer false.
|
||||||
|
fn responder_in_fullscreen(any: &AnyObject) -> bool {
|
||||||
|
unsafe {
|
||||||
|
if !msg_send![any, respondsToSelector: sel!(window)] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(window): Option<&AnyObject> = msg_send![any, window] else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !msg_send![window, respondsToSelector: sel!(styleMask)] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let style: NSWindowStyleMask = msg_send![window, styleMask];
|
||||||
|
style.contains(NSWindowStyleMask::FullScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C-unwind" fn do_command_guard(self_: &AnyObject, sel: Sel, command: Sel) {
|
||||||
|
if command == sel!(cancelOperation:) && current_event_is_unmodified_escape() && responder_in_fullscreen(self_) {
|
||||||
|
// Swallow only an unmodified physical Escape while fullscreen. The
|
||||||
|
// keydown already reached web content, so JS handlers (e.g. closing
|
||||||
|
// in-app dialogs) keep working. Other cancellation commands and
|
||||||
|
// Ctrl+Cmd+F continue through the original responder implementation.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(original) = ORIGINAL_DO_COMMAND.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let original: unsafe extern "C-unwind" fn(&AnyObject, Sel, Sel) = unsafe { std::mem::transmute(*original) };
|
||||||
|
unsafe { original(self_, sel, command) }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user