From d2cc22e7c61bed62b5b68ea5a2147ff617942c83 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 29 Sep 2025 17:17:46 +0530 Subject: [PATCH] macOS: React to changes in effective appearance of the NSApplication not the content view for each window This is nicer now every OS Windows doesnt cause a notification. Also fixes #9034 which was caused by us setting an explicit appearance on the window when the titlebar is set to a specific color thereby preventing the views in the window from getting appearance change notifications. --- docs/changelog.rst | 4 ++++ glfw/cocoa_init.m | 56 ++++++++++++++++++++++++++++++++++++++++++++- glfw/cocoa_window.m | 24 ------------------- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 06581d16e..5aea1db83 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -147,6 +147,10 @@ Detailed list of changes - macOS: Workaround for bug in macOS Tahoe that caused closed OS Windows to remain as invisible rectangles that intercept mouse events (:iss:`8952`) +- macOS: Fix a regression in the previous release that broke automatic + switching of dark/light mode when setting :opt:`macos_titlebar_color` to a + arbitrary color (:iss:`9034`) + - goto_session: Add ``--sort-by=alphabetical`` to have the interactive session picker list the sessions in a fixed order rather than by most recent (:disc:`9033`) diff --git a/glfw/cocoa_init.m b/glfw/cocoa_init.m index d40e7210a..b03c770b9 100644 --- a/glfw/cocoa_init.m +++ b/glfw/cocoa_init.m @@ -297,6 +297,7 @@ static NSDictionary *global_shortcuts = nil; // Delegate for application related notifications {{{ @interface GLFWApplicationDelegate : NSObject + - (void)handleAppearanceChange; @end @implementation GLFWApplicationDelegate @@ -415,20 +416,73 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL; } } +static void *AppearanceObservationContext = &AppearanceObservationContext; + - (void)applicationDidFinishLaunching:(NSNotification *)notification { - if (finish_launching_callback) finish_launching_callback(true); (void)notification; + [[NSApplication sharedApplication] addObserver:self + forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial + context:AppearanceObservationContext]; + + if (finish_launching_callback) finish_launching_callback(true); [NSApp stop:nil]; CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL); _glfwCocoaPostEmptyEvent(); } +GLFWAPI GLFWColorScheme glfwGetCurrentSystemColorTheme(bool query_if_unintialized) { + (void)query_if_unintialized; + int theme_type = GLFW_COLOR_SCHEME_NO_PREFERENCE; + NSAppearance *changedAppearance = NSApp.effectiveAppearance; + NSAppearanceName newAppearance = [changedAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]]; + if([newAppearance isEqualToString:NSAppearanceNameDarkAqua]){ + theme_type = GLFW_COLOR_SCHEME_DARK; + } else { + theme_type = GLFW_COLOR_SCHEME_LIGHT; + } + return theme_type; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (context == AppearanceObservationContext) { + if ([keyPath isEqualToString:@"effectiveAppearance"]) { + // The initial call (from NSKeyValueObservingOptionInitial) might happen on a background thread. + // Dispatch to the main thread to be safe, especially if updating UI. + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAppearanceChange]; + }); + } + } else { + // If the context doesn't match, pass the notification to the superclass. + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)handleAppearanceChange { + static GLFWColorScheme previously_reported_appearance = GLFW_COLOR_SCHEME_NO_PREFERENCE; + GLFWColorScheme new_appearance = glfwGetCurrentSystemColorTheme(true); + if (new_appearance != previously_reported_appearance) { + previously_reported_appearance = new_appearance; + _glfwInputColorScheme(new_appearance, false); + } +} + - (void)applicationWillTerminate:(NSNotification *)aNotification { (void)aNotification; CGDisplayRemoveReconfigurationCallback(display_reconfigured, NULL); + @try { + [[NSApplication sharedApplication] removeObserver:self + forKeyPath:@"effectiveAppearance" + context:AppearanceObservationContext]; + } @catch (NSException * __unused exception) { + // Ignore exceptions, which can happen if the observer was never added. + } } - (void)applicationDidHide:(NSNotification *)notification diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 78f67ba35..137f8ab5c 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -981,16 +981,6 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; updateCursorImage(window); } -- (void)viewDidChangeEffectiveAppearance -{ - static GLFWColorScheme appearance = GLFW_COLOR_SCHEME_NO_PREFERENCE; - GLFWColorScheme new_appearance = glfwGetCurrentSystemColorTheme(true); - if (new_appearance != appearance) { - appearance = new_appearance; - _glfwInputColorScheme(appearance, false); - } -} - - (void)viewDidChangeBackingProperties { if (!window) return; @@ -3434,20 +3424,6 @@ GLFWAPI void glfwCocoaSetWindowChrome(GLFWwindow *w, unsigned int color, bool us [window->ns.object makeFirstResponder:window->ns.view]; }} -GLFWAPI GLFWColorScheme glfwGetCurrentSystemColorTheme(bool query_if_unintialized) { - (void)query_if_unintialized; - int theme_type = 0; - NSAppearance *changedAppearance = NSApp.effectiveAppearance; - NSAppearanceName newAppearance = [changedAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]]; - if([newAppearance isEqualToString:NSAppearanceNameDarkAqua]){ - theme_type = 1; - } else { - theme_type = 2; - } - return theme_type; -} - - GLFWAPI uint32_t glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) { *cocoa_mods = 0;