diff --git a/glfw/cocoa_platform.h b/glfw/cocoa_platform.h index b3ac0cc1f..4303d80e7 100644 --- a/glfw/cocoa_platform.h +++ b/glfw/cocoa_platform.h @@ -137,8 +137,10 @@ typedef struct _GLFWwindowNS bool retina; bool in_traditional_fullscreen; bool in_fullscreen_transition; + bool suppress_frame_constraints; bool titlebar_hidden; unsigned long pre_full_screen_style_mask; + CGRect pre_traditional_fullscreen_frame; // Cached window properties to filter out duplicate events int width, height; diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index d2b3c7a92..fd6c53572 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -578,6 +578,8 @@ - (void)request_delayed_cursor_update:(id)sender; @end +static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow*, bool, bool); + @implementation GLFWWindowDelegate - (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow @@ -782,7 +784,47 @@ - (void)windowWillExitFullScreen:(NSNotification *)notification - (void)windowDidExitFullScreen:(NSNotification *)notification { (void)notification; - if (window) window->ns.in_fullscreen_transition = false; + if (window) { + window->ns.in_fullscreen_transition = false; + if (window->ns.in_traditional_fullscreen) { + // macOS finished its Cocoa exit (cleared NSWindowStyleMaskFullScreen). + // Defer restoration to the next run loop iteration because calling + // setStyleMask: inside a delegate callback can leave the window in + // an intermediate state. setStyleMask: also triggers macOS's + // constrainFrameRect:toScreen: and window tiling logic which can + // asynchronously reposition the window, so suppress frame + // constraints during the restoration (#9572). + unsigned long long wid = window->id; + NSWindowStyleMask savedMask = window->ns.pre_full_screen_style_mask; + CGRect savedFrame = window->ns.pre_traditional_fullscreen_frame; + window->ns.in_traditional_fullscreen = false; + window->ns.suppress_frame_constraints = true; + dispatch_async(dispatch_get_main_queue(), ^{ + _GLFWwindow *w = NULL; + for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) { + if (ww->id == wid) { w = ww; break; } + } + if (w) { + NSWindow *nswindow = w->ns.object; + [nswindow setStyleMask: savedMask]; + [nswindow setFrame: savedFrame display:YES]; + update_titlebar_button_visibility_after_fullscreen_transition(w, true, false); + [nswindow makeFirstResponder:w->ns.view]; + NSNotification *resize = [NSNotification notificationWithName:NSWindowDidResizeNotification object:nswindow]; + [w->ns.delegate performSelector:@selector(windowDidResize:) withObject:resize afterDelay:0]; + } + // Lift the constraint guard after a delay, even if the window + // was not found (destroyed), to keep the flag consistent (#9572). + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ + _GLFWwindow *w2 = NULL; + for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) { + if (ww->id == wid) { w2 = ww; break; } + } + if (w2) w2->ns.suppress_frame_constraints = false; + }); + }); + } + } [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3]; } @@ -2079,6 +2121,12 @@ - (void)performMiniaturize:(id)sender else [super performMiniaturize:sender]; } +- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(nullable NSScreen *)screen +{ + if (glfw_window && glfw_window->ns.suppress_frame_constraints) return frameRect; + return [super constrainFrameRect:frameRect toScreen:screen]; +} + - (BOOL)canBecomeKeyWindow { if (!glfw_window) return NO; @@ -2125,6 +2173,12 @@ - (void)toggleFullScreen:(nullable id)sender { if (glfw_window) { if (glfw_window->ns.in_fullscreen_transition) return; + // Capture the windowed frame before any fullscreen transition begins. + // This is more reliable than saving it inside _glfwPlatformToggleFullscreen + // because setStyleMask: calls between cycles can reposition the window (#9572). + if (!glfw_window->ns.in_traditional_fullscreen && !([self styleMask] & NSWindowStyleMaskFullScreen)) { + glfw_window->ns.pre_traditional_fullscreen_frame = [self frame]; + } if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return; glfw_window->ns.in_fullscreen_transition = true; } @@ -3264,16 +3318,38 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) { // As of Big Turd NSWindowStyleMaskFullScreen is no longer usable // Also no longer compatible after a minor release of macOS 10.15.7 if (!w->ns.in_traditional_fullscreen) { + // Apple throws NSGenericException if setStyleMask: clears + // NSWindowStyleMaskFullScreen outside a transition (see #9572). + // Split View sets this flag via the system, so fall back to + // Cocoa fullscreen toggle instead of the traditional path. + if (sm & NSWindowStyleMaskFullScreen) { + [window toggleFullScreen:nil]; + return false; + } w->ns.pre_full_screen_style_mask = sm; + w->ns.pre_traditional_fullscreen_frame = [window frame]; [window setStyleMask: NSWindowStyleMaskBorderless]; [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock]; [window setFrame:[window.screen frame] display:YES]; w->ns.in_traditional_fullscreen = true; } else { made_fullscreen = false; - [window setStyleMask: w->ns.pre_full_screen_style_mask]; - [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; - w->ns.in_traditional_fullscreen = false; + if (sm & NSWindowStyleMaskFullScreen) { + // Split View added NSWindowStyleMaskFullScreen on top of our + // traditional fullscreen. We can't clear that flag directly + // (NSGenericException), so trigger a Cocoa exit and defer the + // traditional fullscreen cleanup to windowDidExitFullScreen: + // which fires after macOS finishes its async transition (#9572). + // Return true to prevent the caller from setting the window + // frame during the Cocoa exit animation. + [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; + [window toggleFullScreen:nil]; + return true; + } else { + [window setStyleMask: w->ns.pre_full_screen_style_mask]; + [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; + w->ns.in_traditional_fullscreen = false; + } } } else { bool in_fullscreen = sm & NSWindowStyleMaskFullScreen; @@ -3828,10 +3904,16 @@ GLFWAPI void glfwCocoaSetWindowChrome(GLFWwindow *w, unsigned int color, bool us // event. See https://github.com/kovidgoyal/kitty/issues/7106 NSWindowStyleMask fsmask = current_style_mask & NSWindowStyleMaskFullScreen; window->ns.pre_full_screen_style_mask = getStyleMask(window); + NSWindowStyleMask desired_mask; if (in_fullscreen && window->ns.in_traditional_fullscreen) { - [nsw setStyleMask:NSWindowStyleMaskBorderless]; + desired_mask = NSWindowStyleMaskBorderless; } else { - [nsw setStyleMask:window->ns.pre_full_screen_style_mask | fsmask]; + desired_mask = window->ns.pre_full_screen_style_mask | fsmask; + } + // Only call setStyleMask: when the mask actually changes. Redundant + // calls can trigger macOS to reposition the window (#9572). + if (desired_mask != current_style_mask) { + [nsw setStyleMask:desired_mask]; } #undef tc apply_titlebar_color_settings(window); diff --git a/kitty/glfw.c b/kitty/glfw.c index 0741a3a48..5f842bc05 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1291,6 +1291,14 @@ intercept_cocoa_fullscreen(GLFWwindow *w) { global_state.callback_os_window); return false; } + // macOS Split View uses Cocoa fullscreen internally, so the window + // can end up with NSWindowStyleMaskFullScreen set even when + // macos_traditional_fullscreen is enabled. Redirecting to traditional + // fullscreen would crash in setStyleMask: (see #9572). + if (glfwIsFullscreen(w, 1)) { + global_state.callback_os_window->background_opacity.os_forces_opaque = false; + return false; + } toggle_fullscreen_for_os_window(global_state.callback_os_window); global_state.callback_os_window = NULL; return true;