Repository navigation
macOS: no way to pin an app to the light appearance (windows are forced to match the system) #11092
Description
Summary
A macOS Perry app can't pin itself to the light appearance. When a window is created, app.rs asks perry_system_is_dark_mode() and calls window.setAppearance(DarkAqua | Aqua) to match the system. A window-level appearance overrides the app-level one, so an app with only a light palette still gets DarkAqua windows on a Mac set to Dark. AppKit then draws the parts the app doesn't style (title bar, NSTextField placeholder text, selection) in dark colors over the app's light surfaces. Placeholders come out near-white on a white field.
perry/crates/perry-ui-macos/src/app.rs
Lines 103 to 118 in e27f0a0
| // Match window appearance to the system setting so native controls | |
| // (NSTextField, NSPopUpButton, etc.) use the correct light/dark theme. | |
| // isDarkMode() is called here (after NSApp exists) to get the correct value. | |
| let is_dark = super::perry_system_is_dark_mode() != 0; | |
| let appearance_name = if is_dark { | |
| NSString::from_str("NSAppearanceNameDarkAqua") | |
| } else { | |
| NSString::from_str("NSAppearanceNameAqua") | |
| }; | |
| let appearance_cls = objc2::runtime::AnyClass::get(c"NSAppearance").unwrap(); | |
| let appearance: *mut objc2::runtime::AnyObject = objc2::msg_send![ | |
| appearance_cls, appearanceNamed: &*appearance_name | |
| ]; | |
| if !appearance.is_null() { | |
| let _: () = objc2::msg_send![&*window, setAppearance: appearance]; | |
| } |
Why the usual AppKit opt-outs don't work
NSRequiresAquaSystemAppearance = truein the bundle's Info.plist sets the app appearance to Aqua.perry_system_is_dark_mode()never reaches that check, because its first check reads theAppleInterfaceStyleuser default, which falls through to the globalDark, and returns dark.- Passing
-AppleInterfaceStyle Light -NSRequiresAquaSystemAppearance YESon the command line does make the window Aqua. It can't ship, though: the args need a launcher script asCFBundleExecutable, and with a launcher the bundle'sATSApplicationFontsPathfonts no longer load.
perry/crates/perry-ui-macos/src/lib_ffi/system.rs
Lines 226 to 259 in e27f0a0
| pub extern "C" fn perry_system_is_dark_mode() -> i64 { | |
| unsafe { | |
| // Method 1: NSUserDefaults — works before the window exists and for | |
| // explicit Dark mode. Returns nil for Auto mode. | |
| let defaults_cls = objc2::runtime::AnyClass::get(c"NSUserDefaults").unwrap(); | |
| let defaults: *mut objc2::runtime::AnyObject = | |
| objc2::msg_send![defaults_cls, standardUserDefaults]; | |
| let key = objc2_foundation::NSString::from_str("AppleInterfaceStyle"); | |
| let style: *mut objc2::runtime::AnyObject = objc2::msg_send![defaults, stringForKey: &*key]; | |
| if !style.is_null() { | |
| let dark_str = objc2_foundation::NSString::from_str("Dark"); | |
| let is_dark: bool = objc2::msg_send![style, isEqualToString: &*dark_str]; | |
| if is_dark { | |
| return 1; | |
| } | |
| } | |
| // Method 2: NSApp.effectiveAppearance — works once the app is initialized. | |
| let app_cls = objc2::runtime::AnyClass::get(c"NSApplication").unwrap(); | |
| let app: *mut objc2::runtime::AnyObject = objc2::msg_send![app_cls, sharedApplication]; | |
| let appearance: *mut objc2::runtime::AnyObject = objc2::msg_send![app, effectiveAppearance]; | |
| if !appearance.is_null() { | |
| let name: *mut objc2::runtime::AnyObject = objc2::msg_send![appearance, name]; | |
| if !name.is_null() { | |
| let dark_name = objc2_foundation::NSString::from_str("NSAppearanceNameDarkAqua"); | |
| let is_dark: bool = objc2::msg_send![name, isEqualToString: &*dark_name]; | |
| if is_dark { | |
| return 1; | |
| } | |
| } | |
| } | |
| 0 | |
| } | |
| } |
Request
Let an app choose the window appearance. Either of these would do:
- An
App({ appearance: "light" | "dark" | "system" })option, default"system", that setsNSApp.appearanceand skips the per-window override. - Or leave
window.appearanceunset so windows inheritNSApp.effectiveAppearance. AppKit resolves that from the system setting by default, and fromNSRequiresAquaSystemAppearancewhen the bundle sets it.
Related
#10861 asks for appearance-adaptive colors, so an app can support both appearances. This issue is the other half: an app that supports only one appearance needs a way to say so.
Environment
Perry 0.5.1638 (local build), macOS 26.6.2 arm64, system appearance Dark.
Activity