use core::{ffi::c_void, ptr};
use objc2::{
available, define_class, msg_send,
rc::Retained,
runtime::{AnyObject, Sel},
DefinedClass, MainThreadMarker, MainThreadOnly,
};
use objc2_app_kit::{
NSAppearance, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, NSApplication, NSColor,
NSColorSpace, NSSystemColorsDidChangeNotification, NSWorkspace,
NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification,
};
use objc2_foundation::{
ns_string, NSArray, NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions,
NSNotification, NSNotificationCenter, NSObject, NSObjectNSKeyValueObserverRegistration,
NSString,
};
use std::sync::Arc;
use tokio::sync::Notify;
use crate::{error::Error, ThemeColor, ThemeContrast, ThemeKind, ThemeScheme};
struct Ivars {
application: Retained<NSApplication>,
default_notification: Retained<NSNotificationCenter>,
workspace_notification: Retained<NSNotificationCenter>,
notify: Arc<Notify>,
}
define_class!(
#[unsafe(super = NSObject)]
#[thread_kind = MainThreadOnly]
#[ivars = Ivars]
struct ThemeObserver;
impl ThemeObserver {
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn value_change_callback(
&self,
_key_path: Option<&NSString>,
_object: Option<&AnyObject>,
_change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
self.ivars().notify.notify_waiters();
}
#[unsafe(method(notificationCallback:))]
fn notification_callback(
&self, _notification: &NSNotification
) {
self.ivars().notify.notify_waiters();
}
}
);
impl ThemeObserver {
pub fn new(
mtm: MainThreadMarker,
application: Retained<NSApplication>,
workspace: &NSWorkspace,
notify: Arc<Notify>,
) -> Retained<Self> {
let observer = ThemeObserver::alloc(mtm).set_ivars(Ivars {
application,
default_notification: NSNotificationCenter::defaultCenter(),
workspace_notification: workspace.notificationCenter(),
notify,
});
let observer: Retained<Self> = unsafe { msg_send![super(observer), init] };
unsafe {
observer
.ivars()
.application
.addObserver_forKeyPath_options_context(
&observer,
ns_string!("effectiveAppearance"),
NSKeyValueObservingOptions::New | NSKeyValueObservingOptions::Old,
ptr::null_mut(),
);
}
unsafe {
observer
.ivars()
.default_notification
.addObserver_selector_name_object(
&observer,
Sel::register(c"notificationCallback:"),
Some(NSSystemColorsDidChangeNotification),
None,
);
}
unsafe {
observer
.ivars()
.workspace_notification
.addObserver_selector_name_object(
&observer,
Sel::register(c"notificationCallback:"),
Some(NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification),
None,
);
}
observer
}
}
impl Drop for ThemeObserver {
fn drop(&mut self) {
unsafe {
self.ivars()
.application
.removeObserver_forKeyPath(self, ns_string!("effectiveAppearance"));
self.ivars().default_notification.removeObserver(self);
self.ivars().workspace_notification.removeObserver(self);
}
}
}
fn is_appearance_dark(appearance: Retained<NSAppearance>) -> bool {
let aqua_name = unsafe { NSAppearanceNameAqua };
let dark_aqua_name = unsafe { NSAppearanceNameDarkAqua };
match appearance
.bestMatchFromAppearancesWithNames(&NSArray::from_slice(&[aqua_name, dark_aqua_name]))
{
Some(appearance_name) => *appearance_name == *dark_aqua_name,
None => false,
}
}
pub struct Platform {
workspace: Retained<NSWorkspace>,
application: Retained<NSApplication>,
_observer: Retained<ThemeObserver>,
notify: Arc<Notify>,
}
impl Platform {
pub fn new() -> Result<Self, Error> {
let notify = Arc::new(Notify::new());
let mtm = match MainThreadMarker::new() {
Some(mtm) => mtm,
None => return Err(Error::MainThreadRequired),
};
let application = NSApplication::sharedApplication(mtm);
let workspace = NSWorkspace::sharedWorkspace();
let _observer = ThemeObserver::new(mtm, application.clone(), &workspace, notify.clone());
Ok(Platform {
workspace,
application,
_observer,
notify,
})
}
pub fn theme_kind(&self) -> Result<ThemeKind, Error> {
Ok(ThemeKind::MacOS)
}
pub fn theme_scheme(&self) -> Result<ThemeScheme, Error> {
if !available!(macos = 10.14) {
return Err(Error::Unsupported);
}
if is_appearance_dark(self.application.effectiveAppearance()) {
Ok(ThemeScheme::Dark)
} else {
Ok(ThemeScheme::Light)
}
}
pub fn theme_contrast(&self) -> Result<ThemeContrast, Error> {
if !available!(macos = 10.10) {
return Err(Error::Unsupported);
}
let contrast = if self.workspace.accessibilityDisplayShouldIncreaseContrast() {
ThemeContrast::High
} else {
ThemeContrast::Normal
};
Ok(contrast)
}
pub fn theme_accent(&self) -> Result<ThemeColor, Error> {
if !available!(macos = 10.14) {
return Err(Error::Unsupported);
}
match NSColor::controlAccentColor()
.colorUsingColorSpace(&NSColorSpace::genericRGBColorSpace())
{
Some(color) => Ok(ThemeColor {
red: color.redComponent() as f32,
green: color.greenComponent() as f32,
blue: color.blueComponent() as f32,
}),
None => Err(Error::Unavailable),
}
}
pub fn get_notify(&self) -> Arc<Notify> {
self.notify.clone()
}
}