diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index f167df26..c2acea16 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -574,7 +574,17 @@ pub fn run() { window.on_window_event(move |event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); - let _ = window_for_close.hide(); + // macOS 26 (Tahoe) regressed `NSWindow` ordering: `orderOut:` + // (what `hide()` maps to) can leave a focused black phantom + // window on screen instead of hiding it (#4875). Minimize + // instead on Tahoe — a different AppKit path that still keeps + // the app in the dock and preserves the open book. The Reopen + // handler below already unminimizes on dock reopen. + if macos::os_version::is_macos_tahoe_or_later() { + let _ = window_for_close.minimize(); + } else { + let _ = window_for_close.hide(); + } } }); } diff --git a/apps/readest-app/src-tauri/src/macos/mod.rs b/apps/readest-app/src-tauri/src/macos/mod.rs index 399e2479..0530c7de 100644 --- a/apps/readest-app/src-tauri/src/macos/mod.rs +++ b/apps/readest-app/src-tauri/src/macos/mod.rs @@ -1,5 +1,6 @@ pub mod apple_auth; pub mod menu; +pub mod os_version; pub mod safari_auth; pub mod system_dictionary; pub mod traffic_light; diff --git a/apps/readest-app/src-tauri/src/macos/os_version.rs b/apps/readest-app/src-tauri/src/macos/os_version.rs new file mode 100644 index 00000000..ab65bc15 --- /dev/null +++ b/apps/readest-app/src-tauri/src/macos/os_version.rs @@ -0,0 +1,53 @@ +//! macOS OS-version detection for the Tahoe close-to-hide workaround. +//! +//! macOS 26 (Tahoe) regressed `NSWindow` ordering so that `orderOut:` — +//! which Tauri's `WebviewWindow::hide()` maps to — can leave a focused +//! black phantom window on screen instead of hiding it. See issue #4875. +//! On Tahoe we minimize the window instead, a different AppKit path that +//! still keeps the app in the dock and preserves the open book. + +use objc::{class, msg_send, sel, sel_impl}; + +/// Returns true when `major` is macOS Tahoe (26) or later. +pub(crate) fn is_tahoe_or_later(major: i64) -> bool { + major >= 26 +} + +/// Reads the running macOS major version via `NSProcessInfo`. +fn macos_major_version() -> i64 { + #[repr(C)] + struct NSOperatingSystemVersion { + major: i64, + minor: i64, + patch: i64, + } + + unsafe { + let process_info: *mut objc::runtime::Object = + msg_send![class!(NSProcessInfo), processInfo]; + let version: NSOperatingSystemVersion = msg_send![process_info, operatingSystemVersion]; + version.major + } +} + +/// True when running on macOS Tahoe (26) or later. +pub fn is_macos_tahoe_or_later() -> bool { + is_tahoe_or_later(macos_major_version()) +} + +#[cfg(test)] +mod tests { + use super::is_tahoe_or_later; + + #[test] + fn detects_tahoe_and_later() { + assert!(is_tahoe_or_later(26)); // Tahoe + assert!(is_tahoe_or_later(27)); + } + + #[test] + fn rejects_pre_tahoe() { + assert!(!is_tahoe_or_later(25)); // Sequoia + assert!(!is_tahoe_or_later(15)); // older numbering + } +}