From 6d33cea8167f17e03f5e534f2b3692d5e47c3377 Mon Sep 17 00:00:00 2001 From: Andrey Paramonov Date: Sat, 28 Feb 2026 17:56:19 +0300 Subject: [PATCH 1/3] Fix fullscreen handling for macOS Split View to prevent crashes --- glfw/cocoa_window.m | 12 +++++++++++- kitty/glfw.c | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index f3ef8b205..9496c0c40 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -3253,6 +3253,14 @@ 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; [window setStyleMask: NSWindowStyleMaskBorderless]; [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock]; @@ -3260,7 +3268,9 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) { w->ns.in_traditional_fullscreen = true; } else { made_fullscreen = false; - [window setStyleMask: w->ns.pre_full_screen_style_mask]; + // Same NSWindowStyleMaskFullScreen guard as glfwCocoaSetWindowChrome + NSWindowStyleMask fsmask = sm & NSWindowStyleMaskFullScreen; + [window setStyleMask: w->ns.pre_full_screen_style_mask | fsmask]; [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; w->ns.in_traditional_fullscreen = false; } 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; From 72de57b5e5ff3aa27023c796e82e379e119495c4 Mon Sep 17 00:00:00 2001 From: Andrey Paramonov Date: Sat, 28 Feb 2026 19:59:43 +0300 Subject: [PATCH 2/3] Enhance fullscreen transition handling on macOS by adding frame constraint suppression --- glfw/cocoa_platform.h | 2 + glfw/cocoa_window.m | 90 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 8 deletions(-) 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 9496c0c40..ac0b6896b 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -578,6 +578,8 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; @end +static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow*, bool, bool); + @implementation GLFWWindowDelegate - (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow @@ -782,7 +784,49 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; - (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) return; + NSWindow *nswindow = w->ns.object; + @try { + [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]; + } @finally { + // Keep suppressing constraints to block deferred macOS tiling + // repositioning, then lift the guard. The delay is empirical (#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]; } @@ -2068,6 +2112,12 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { 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; @@ -2114,6 +2164,12 @@ update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow* w, bo { 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; } @@ -3262,17 +3318,29 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) { 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; - // Same NSWindowStyleMaskFullScreen guard as glfwCocoaSetWindowChrome - NSWindowStyleMask fsmask = sm & NSWindowStyleMaskFullScreen; - [window setStyleMask: w->ns.pre_full_screen_style_mask | fsmask]; - [[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; @@ -3827,10 +3895,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); From 21c2e585cfda7ac192497f8bf8267955f31d72d2 Mon Sep 17 00:00:00 2001 From: Andrey Paramonov Date: Sat, 28 Feb 2026 20:21:59 +0300 Subject: [PATCH 3/3] Refactor fullscreen exit handling to ensure frame constraint suppression is lifted consistently after a delay --- glfw/cocoa_window.m | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index ac0b6896b..97c2f975f 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -804,26 +804,24 @@ static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWw for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) { if (ww->id == wid) { w = ww; break; } } - if (!w) return; - NSWindow *nswindow = w->ns.object; - @try { + 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]; - } @finally { - // Keep suppressing constraints to block deferred macOS tiling - // repositioning, then lift the guard. The delay is empirical (#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; - }); } + // 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; + }); }); } }