forked from reya/gpui-native-kit
144 lines
5.4 KiB
Rust
144 lines
5.4 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;
|
|
|
|
#[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_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<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()
|
|
}
|
|
}
|