fix(macos): minimize instead of hide on macOS 26 to avoid black window (#4890)

On macOS 26 (Tahoe), Apple regressed NSWindow ordering so that
orderOut: (what Tauri's hide() maps to) no longer removes the window
from the screen. The close-to-hide handler left a focused black
phantom window instead of hiding it, and the only recovery was to
quit and relaunch the app.

This is an OS-level regression, not a Readest bug: the same failure
hits native, non-webview apps such as kitty (kovidgoyal/kitty#8952),
and tao 0.34.8 calls a bare orderOut with no Tahoe workaround.

Fix: on macOS 26 or later, minimize() the main window instead of
hide(). Minimize is a different AppKit path that dodges the buggy
orderOut, keeps the app in the dock, and preserves the open book. The
existing Reopen handler already unminimizes on dock reopen, so no
extra restore logic is needed. Older macOS keeps the previous hide()
behavior. Version detection reads NSProcessInfo.operatingSystemVersion.

Closes #4875

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-02 22:52:37 +09:00
committed by GitHub
parent 7a8354d63b
commit a3609731c3
3 changed files with 65 additions and 1 deletions
+11 -1
View File
@@ -574,7 +574,17 @@ pub fn run() {
window.on_window_event(move |event| { window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event { if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close(); 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();
}
} }
}); });
} }
@@ -1,5 +1,6 @@
pub mod apple_auth; pub mod apple_auth;
pub mod menu; pub mod menu;
pub mod os_version;
pub mod safari_auth; pub mod safari_auth;
pub mod system_dictionary; pub mod system_dictionary;
pub mod traffic_light; pub mod traffic_light;
@@ -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
}
}