//! macOS Adapter //! //! Reads AppKit's semantic colors in the effective appearance and watches //! the system's color preferences. use objc2::rc::Retained; use objc2::runtime::{NSObjectProtocol, ProtocolObject}; use objc2_app_kit::{ NSAppearance, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, NSColor, NSColorSpace, NSWorkspace, }; use objc2_foundation::{ NSDistributedNotificationCenter, NSNotification, NSNotificationName, NSOperationQueue, }; use std::ptr::NonNull; use super::ThemeSource; use crate::ThemeVariant; use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme}; /// Default control radius for macOS controls. const CONTROL_RADIUS_PTS: f32 = 6.0; #[derive(Default)] pub(crate) struct MacosSource { _observers: Vec>>, } impl MacosSource { fn color_to_hsla(color: &NSColor) -> Option { let srgb = NSColorSpace::sRGBColorSpace(); let converted = color.colorUsingColorSpace(&srgb)?; Some(gpui::Hsla::from(gpui::Rgba { r: converted.redComponent() as f32, g: converted.greenComponent() as f32, b: converted.blueComponent() as f32, a: converted.alphaComponent() as f32, })) } /// Resolves the semantic colors with `variant` as the current drawing appearance. fn snapshot_in_appearance(&self, variant: ThemeVariant) -> PartialPalette { let name = match variant { ThemeVariant::Light => { // SAFETY: extern statics exported by AppKit, always initialized. unsafe { NSAppearanceNameAqua } } ThemeVariant::Dark => { // SAFETY: as above. unsafe { NSAppearanceNameDarkAqua } } }; let Some(appearance) = NSAppearance::appearanceNamed(name) else { return self.read_palette(); }; let palette = std::cell::Cell::new(PartialPalette::default()); appearance.performAsCurrentDrawingAppearance(&block2::RcBlock::new(|| { palette.set(self.read_palette()); })); palette.into_inner() } fn read_palette(&self) -> PartialPalette { PartialPalette { background: Self::color_to_hsla(&NSColor::windowBackgroundColor()), foreground: Self::color_to_hsla(&NSColor::labelColor()), surface: Self::color_to_hsla(&NSColor::textBackgroundColor()), surface_foreground: Self::color_to_hsla(&NSColor::labelColor()), primary_foreground: Self::color_to_hsla(&NSColor::alternateSelectedControlTextColor()), accent_foreground: Self::color_to_hsla(&NSColor::alternateSelectedControlTextColor()), secondary: None, secondary_foreground: Self::color_to_hsla(&NSColor::secondaryLabelColor()), muted: None, muted_foreground: Self::color_to_hsla(&NSColor::secondaryLabelColor()), destructive: Self::color_to_hsla(&NSColor::systemRedColor()), border: Self::color_to_hsla(&NSColor::separatorColor()), input: Self::color_to_hsla(&NSColor::controlBackgroundColor()), ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()), selection: Self::color_to_hsla(&NSColor::selectedTextBackgroundColor()), } } } impl ThemeSource for MacosSource { fn snapshot(&self, variant: ThemeVariant) -> SystemTheme { let workspace = NSWorkspace::sharedWorkspace(); let accessibility = AccessibilityPreferences { high_contrast: workspace.accessibilityDisplayShouldIncreaseContrast(), reduce_motion: workspace.accessibilityDisplayShouldReduceMotion(), reduce_transparency: workspace.accessibilityDisplayShouldReduceTransparency(), }; let palette = self.snapshot_in_appearance(variant); SystemTheme { variant, accent: Self::color_to_hsla(&NSColor::controlAccentColor()), palette, control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)), accessibility, source: SourceKind::Macos, } } /// Fires on accent and appearance changes (System Settings > Appearance) /// and re-runs the resolve pipeline through `async_cx`. fn install_watchers(&mut self, async_cx: gpui::AsyncApp) { let center = NSDistributedNotificationCenter::defaultCenter(); let queue = NSOperationQueue::mainQueue(); for name in [ "AppleColorPreferencesChangedNotification", "AppleInterfaceThemeChangedNotification", ] { let async_cx = async_cx.clone(); let block = block2::RcBlock::new(move |_notification: NonNull| { async_cx.update(crate::reapply); }); let name = NSNotificationName::from_str(name); // SAFETY: the observer token is retained for the source's lifetime, // the block is sendable (AsyncApp is Clone + 'static) // and the main queue runs it outside any App borrow. let observer = unsafe { center.addObserverForName_object_queue_usingBlock( Some(&name), None, Some(&queue), &block, ) }; self._observers.push(observer); } } fn label(&self) -> &'static str { SourceKind::Macos.label() } }