use block2::RcBlock;
use incular_platform::{
PlatformLifecycle, SystemEnvironmentPreferences, canonicalize_system_locales,
};
use objc2::rc::Retained;
use objc2_app_kit::{
NSApplication, NSApplicationDidBecomeActiveNotification,
NSApplicationDidResignActiveNotification, NSWorkspace,
NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification, NSWorkspaceDidWakeNotification,
NSWorkspaceWillSleepNotification,
};
use objc2_foundation::{
MainThreadMarker, NSCurrentLocaleDidChangeNotification, NSNotification, NSNotificationCenter,
NSObject,
};
use std::{
cell::{Cell, RefCell},
collections::VecDeque,
ptr::NonNull,
rc::{Rc, Weak},
sync::Arc,
};
#[derive(Clone)]
pub(crate) struct MacosEnvironmentState {
state: Rc<CallbackState>,
observers: Rc<ObserverLease>,
}
#[derive(Default)]
struct CallbackState {
settings_dirty: Cell<bool>,
lifecycle: RefCell<VecDeque<PlatformLifecycle>>,
active: Cell<bool>,
}
#[derive(Default)]
struct ObserverLease {
state: Rc<CallbackState>,
observer_tokens: RefCell<Vec<(Retained<NSNotificationCenter>, Retained<NSObject>)>>,
watching: Cell<bool>,
}
impl ObserverLease {
fn stop(&mut self) {
self.state.active.set(false);
self.watching.set(false);
let observers = self.observer_tokens.get_mut();
if observers.is_empty() {
return;
}
for (center, observer) in observers.drain(..) {
unsafe { center.removeObserver(&observer) };
}
}
}
impl Drop for ObserverLease {
fn drop(&mut self) {
self.stop();
}
}
impl Default for MacosEnvironmentState {
fn default() -> Self {
let state = Rc::new(CallbackState::default());
let observers = Rc::new(ObserverLease {
state: state.clone(),
observer_tokens: RefCell::default(),
watching: Cell::default(),
});
Self { state, observers }
}
}
impl MacosEnvironmentState {
pub(crate) fn preferences(&self) -> SystemEnvironmentPreferences {
let locales = canonicalize_system_locales(sys_locale::get_locales());
let workspace = unsafe { NSWorkspace::sharedWorkspace() };
SystemEnvironmentPreferences {
locales: (!locales.is_empty()).then_some(locales),
reduced_motion: Some(unsafe { workspace.accessibilityDisplayShouldReduceMotion() }),
high_contrast: Some(unsafe { workspace.accessibilityDisplayShouldIncreaseContrast() }),
..SystemEnvironmentPreferences::default()
}
}
pub(crate) fn application_active(&self) -> Option<bool> {
let mtm = MainThreadMarker::new()?;
let application = NSApplication::sharedApplication(mtm);
Some(unsafe { application.isActive() })
}
pub(crate) fn start_watch(&self, wake: Arc<dyn Fn() + Send + Sync>) {
if self.observers.watching.replace(true) {
return;
}
self.state.active.set(true);
let center = unsafe { NSNotificationCenter::defaultCenter() };
let workspace = unsafe { NSWorkspace::sharedWorkspace() };
let workspace_center = unsafe { workspace.notificationCenter() };
let locale_notification = unsafe { NSCurrentLocaleDidChangeNotification };
{
let dirty = Rc::downgrade(&self.state);
let wake = wake.clone();
let block = RcBlock::new(move |_notification: NonNull<NSNotification>| {
if let Some(state) = active_state(&dirty) {
state.settings_dirty.set(true);
wake();
}
});
let observer = unsafe {
center.addObserverForName_object_queue_usingBlock(
Some(locale_notification),
None,
None,
&block,
)
};
self.observers
.observer_tokens
.borrow_mut()
.push((center.clone(), observer));
}
let accessibility_notification =
unsafe { NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification };
{
let dirty = Rc::downgrade(&self.state);
let wake = wake.clone();
let block = RcBlock::new(move |_notification: NonNull<NSNotification>| {
if let Some(state) = active_state(&dirty) {
state.settings_dirty.set(true);
wake();
}
});
let observer = unsafe {
workspace_center.addObserverForName_object_queue_usingBlock(
Some(accessibility_notification),
Some(&workspace),
None,
&block,
)
};
self.observers
.observer_tokens
.borrow_mut()
.push((workspace_center.clone(), observer));
}
let lifecycle_notifications = unsafe {
[
(
NSApplicationDidBecomeActiveNotification,
PlatformLifecycle::Active,
),
(
NSApplicationDidResignActiveNotification,
PlatformLifecycle::Inactive,
),
]
};
for (name, lifecycle) in lifecycle_notifications {
let state = Rc::downgrade(&self.state);
let wake = wake.clone();
let block = RcBlock::new(move |_notification: NonNull<NSNotification>| {
if let Some(state) = active_state(&state) {
state.lifecycle.borrow_mut().push_back(lifecycle);
wake();
}
});
let observer = unsafe {
center.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block)
};
self.observers
.observer_tokens
.borrow_mut()
.push((center.clone(), observer));
}
let sleep_notifications = unsafe {
[
(
NSWorkspaceWillSleepNotification,
PlatformLifecycle::Suspended,
),
(NSWorkspaceDidWakeNotification, PlatformLifecycle::Resumed),
]
};
for (name, lifecycle) in sleep_notifications {
let state = Rc::downgrade(&self.state);
let wake = wake.clone();
let block = RcBlock::new(move |_notification: NonNull<NSNotification>| {
if let Some(state) = active_state(&state) {
state.lifecycle.borrow_mut().push_back(lifecycle);
wake();
}
});
let observer = unsafe {
workspace_center.addObserverForName_object_queue_usingBlock(
Some(name),
Some(&workspace),
None,
&block,
)
};
self.observers
.observer_tokens
.borrow_mut()
.push((workspace_center.clone(), observer));
}
}
pub(crate) fn take_settings_change(&self) -> bool {
self.state.settings_dirty.replace(false)
}
pub(crate) fn take_lifecycle_events(&self) -> Vec<PlatformLifecycle> {
self.state.lifecycle.borrow_mut().drain(..).collect()
}
}
fn active_state(state: &Weak<CallbackState>) -> Option<Rc<CallbackState>> {
state.upgrade().filter(|state| state.active.get())
}