Files
gpui-native-kit/crates/theme/src/source/macos.rs
T

156 lines
5.9 KiB
Rust

//! 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;
/// The rounded-bezel text field's intrinsic height at regular size.
const INPUT_HEIGHT_PTS: f32 = 24.0;
/// The HIG's default body text on macOS is 13 pt (system font size 13 at 1x).
const ROOT_FONT_SIZE_PTS: f32 = 13.0;
#[derive(Default)]
pub(crate) struct MacosSource {
_observers: Vec<Retained<ProtocolObject<dyn NSObjectProtocol>>>,
}
impl MacosSource {
fn color_to_hsla(color: &NSColor) -> Option<gpui::Hsla> {
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_hover: None,
muted_foreground: Self::color_to_hsla(&NSColor::secondaryLabelColor()),
destructive: Self::color_to_hsla(&NSColor::systemRedColor()),
success: None,
warning: None,
info: None,
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()),
overlay: None,
scrollbar_thumb: None,
}
}
}
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)),
input_height: Some(gpui::px(INPUT_HEIGHT_PTS)),
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_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<NSNotification>| {
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()
}
}