From 7ad02da722635121ec1f77a316a052792745f198 Mon Sep 17 00:00:00 2001 From: kjct0s_ Date: Fri, 6 Nov 2026 01:33:46 +0700 Subject: [PATCH] fix(window): cover the whole screen with a full-screen project window group on notched Macs (#1608) A project tab group in full screen uses simple fullscreen and keeps its title bar for the window buttons. AppKit's constrainFrameRect:toScreen: keeps a titled window below the camera housing whenever it is placed, shown or resized, so on a notched display the group sat 32 pt low with the status bar off-screen. Windows in simple fullscreen now keep the frame they ask for; every other window still goes through AppKit's constraint. Co-authored-by: KJ-AIML <202620766+KJ-AIML@users.noreply.github.com> --- src-tauri/src/macos.rs | 53 +++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index dcb8bc5f5..fd1cd74b4 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -8,7 +8,7 @@ use objc2::{ runtime::{AnyClass, AnyObject, Imp, ProtocolObject, Sel}, }; use objc2_app_kit::{NSWindow, NSWindowUserTabbingPreference}; -use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; +use objc2_foundation::{NSNotificationCenter, NSObjectProtocol, NSRect}; use tauri::{AppHandle, Manager, WebviewWindow}; fn tabbing_preference(app: &AppHandle) -> IdeResult { @@ -136,30 +136,35 @@ pub fn set_simple_fullscreen(window: &WebviewWindow, on: bool) -> tauri::Result< }) } +/// A window in simple fullscreen: its `NSWindow` and the minimum size restored when it leaves. +struct Covering { + native: usize, + min_size: objc2_foundation::NSSize, +} + thread_local! { - /// Minimum sizes of windows in simple fullscreen, restored when they leave it. - static MIN_SIZES: std::cell::RefCell> = - std::cell::RefCell::default(); + static COVERING: std::cell::RefCell> = std::cell::RefCell::default(); } /// Simple fullscreen drops the title bar and resizing; ours is transparent over the web toolbar, /// so keep it and its buttons, the green one (enabled only for a resizable window) leading back /// out of fullscreen. A minimum size of the whole screen keeps the edges from being dragged. +/// A titled window is kept below the menu bar or camera housing; see `constrain_frame_rect`. fn keep_window_buttons(window: &WebviewWindow, on: bool) { use objc2_app_kit::NSWindowStyleMask; let Ok(pointer) = window.ns_window() else { return }; let native = unsafe { &*pointer.cast::() }; let label = window.label().to_owned(); if !on { - if let Some(size) = MIN_SIZES.with_borrow_mut(|sizes| sizes.remove(&label)) { - native.setMinSize(size); + if let Some(covering) = COVERING.with_borrow_mut(|windows| windows.remove(&label)) { + native.setMinSize(covering.min_size); } return; } let Some(screen) = native.screen() else { return }; let frame = screen.frame(); - MIN_SIZES.with_borrow_mut(|sizes| { - sizes.entry(label).or_insert_with(|| native.minSize()); + COVERING.with_borrow_mut(|windows| { + windows.entry(label).or_insert_with(|| Covering { native: pointer as usize, min_size: native.minSize() }); }); native.setMinSize(frame.size); native.setStyleMask(native.styleMask() | NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable); @@ -177,7 +182,7 @@ pub fn forget_simple_fullscreen(app: &AppHandle, label: &str) -> bool { if was { let label = label.to_owned(); let _ = app.run_on_main_thread(move || { - MIN_SIZES.with_borrow_mut(|sizes| sizes.remove(&label)); + COVERING.with_borrow_mut(|windows| windows.remove(&label)); present(any); }); } @@ -218,9 +223,39 @@ pub fn intercept_toggle_fullscreen(window: &WebviewWindow) { let _ = NATIVE_TOGGLE_FULLSCREEN.set(native); let replacement: Imp = std::mem::transmute(toggle_fullscreen as ToggleFullScreen); objc2::ffi::class_replaceMethod(class, selector, replacement, types); + + let selector = objc2::sel!(constrainFrameRect:toScreen:); + let Some(method) = (*class).instance_method(selector) else { return }; + let native: ConstrainFrameRect = std::mem::transmute(method.implementation()); + let types = objc2::ffi::method_getTypeEncoding(method); + let _ = NATIVE_CONSTRAIN_FRAME_RECT.set(native); + let replacement: Imp = std::mem::transmute(constrain_frame_rect as ConstrainFrameRect); + objc2::ffi::class_replaceMethod(class, selector, replacement, types); }); } +type ConstrainFrameRect = unsafe extern "C-unwind" fn(*mut AnyObject, Sel, NSRect, *mut AnyObject) -> NSRect; + +static NATIVE_CONSTRAIN_FRAME_RECT: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// AppKit keeps a titled window's top edge below the menu bar, and on a notched display below the +/// camera housing, whenever the window is placed, shown or resized. A window in simple fullscreen +/// is titled for its buttons but must still cover the whole screen. +unsafe extern "C-unwind" fn constrain_frame_rect( + this: *mut AnyObject, + selector: Sel, + frame: NSRect, + screen: *mut AnyObject, +) -> NSRect { + if COVERING.with_borrow(|windows| windows.values().any(|covering| covering.native == this as usize)) { + return frame; + } + match NATIVE_CONSTRAIN_FRAME_RECT.get() { + Some(native) => unsafe { native(this, selector, frame, screen) }, + None => frame, + } +} + unsafe extern "C-unwind" fn toggle_fullscreen(this: *mut AnyObject, selector: Sel, sender: *mut AnyObject) { if let Some(window) = APP.get().and_then(|app| simple_fullscreen_window(app, this)) { if let Err(error) = crate::window_groups::toggle_fullscreen(&window) {