#[cfg(feature = "runtime-fonts")]
use crate::compat::Box;
use crate::compat::String;
use crate::compat::Vec;
use cocoa::base::{id, nil, BOOL, NO};
#[cfg(device_profile)]
use cocoa::foundation::NSString;
#[cfg(device_profile)]
use objc::declare::ClassDecl;
#[cfg(device_profile)]
use objc::runtime::{Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use std::ffi::{c_char, CStr};
const ENVIRONMENT_TTL: std::time::Duration = std::time::Duration::from_secs(1);
type EnvironmentFactCache<T> =
std::sync::OnceLock<std::sync::Mutex<Option<(std::time::Instant, T)>>>;
fn cached_environment_fact<T: Copy>(
cache: &'static EnvironmentFactCache<T>,
read: impl FnOnce() -> T,
) -> T {
let slot = cache.get_or_init(|| std::sync::Mutex::new(None));
if let Ok(guard) = slot.lock() {
if let Some((sampled_at, value)) = *guard {
if sampled_at.elapsed() < ENVIRONMENT_TTL {
return value;
}
}
}
let value = read();
if let Ok(mut guard) = slot.lock() {
*guard = Some((std::time::Instant::now(), value));
}
value
}
static HIGH_CONTRAST_CACHE: EnvironmentFactCache<Option<bool>> = std::sync::OnceLock::new();
static REDUCE_MOTION_CACHE: EnvironmentFactCache<Option<bool>> = std::sync::OnceLock::new();
static SYSTEM_LOCALE_CACHE: EnvironmentFactCache<Option<&'static str>> = std::sync::OnceLock::new();
unsafe fn workspace_implements(selector: objc::runtime::Sel) -> Option<id> {
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
if workspace == nil {
return None;
}
let responds: BOOL = msg_send![workspace, respondsToSelector: selector];
(responds != NO).then_some(workspace)
}
pub(crate) fn high_contrast() -> Option<bool> {
if !super::types::is_main_thread() {
return None;
}
cached_environment_fact(&HIGH_CONTRAST_CACHE, || {
unsafe {
let workspace = workspace_implements(sel!(accessibilityDisplayShouldIncreaseContrast))?;
let answer: BOOL = msg_send![workspace, accessibilityDisplayShouldIncreaseContrast];
Some(answer != NO)
}
})
}
pub(crate) fn reduce_motion() -> Option<bool> {
if !super::types::is_main_thread() {
return None;
}
cached_environment_fact(&REDUCE_MOTION_CACHE, || {
unsafe {
let workspace = workspace_implements(sel!(accessibilityDisplayShouldReduceMotion))?;
let answer: BOOL = msg_send![workspace, accessibilityDisplayShouldReduceMotion];
Some(answer != NO)
}
})
}
pub(crate) fn reduce_transparency() -> Option<bool> {
if !super::types::is_main_thread() {
return None;
}
unsafe {
let workspace = workspace_implements(sel!(accessibilityDisplayShouldReduceTransparency))?;
let answer: BOOL = msg_send![workspace, accessibilityDisplayShouldReduceTransparency];
Some(answer != NO)
}
}
pub(crate) fn accent_color() -> Option<crate::core::Color> {
if !super::types::is_main_thread() {
return None;
}
unsafe {
let responds: BOOL =
msg_send![class!(NSColor), respondsToSelector: sel!(controlAccentColor)];
if responds == NO {
return None;
}
let color: id = msg_send![class!(NSColor), controlAccentColor];
if color == nil {
return None;
}
let space: id = msg_send![class!(NSColorSpace), sRGBColorSpace];
if space == nil {
return None;
}
let converted: id = msg_send![color, colorUsingColorSpace: space];
if converted == nil {
return None;
}
let to_unit = |component: f64| component.clamp(0.0, 1.0) as f32;
let red: f64 = msg_send![converted, redComponent];
let green: f64 = msg_send![converted, greenComponent];
let blue: f64 = msg_send![converted, blueComponent];
let alpha: f64 = msg_send![converted, alphaComponent];
Some(crate::core::Color::from_f32(
to_unit(red),
to_unit(green),
to_unit(blue),
to_unit(alpha),
))
}
}
#[cfg(device_profile)]
static SYSTEM_COLOR_OBSERVER_INSTALLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[cfg(device_profile)]
pub(crate) fn install_system_color_observer() -> bool {
use std::sync::atomic::Ordering;
if SYSTEM_COLOR_OBSERVER_INSTALLED.load(Ordering::Relaxed) {
return true;
}
if !super::types::is_main_thread() {
return false;
}
let installed = install_system_color_observer_once();
if installed {
SYSTEM_COLOR_OBSERVER_INSTALLED.store(true, Ordering::Relaxed);
}
installed
}
#[cfg(device_profile)]
fn install_system_color_observer_once() -> bool {
unsafe {
let Some(mut declaration) =
ClassDecl::new("RustWidgetsSystemColorObserver", class!(NSObject))
else {
log::debug!(
"[macos] the Objective-C runtime refused RustWidgetsSystemColorObserver; the \
accent reverts to the explicit refresh path"
);
return false;
};
declaration.add_method(
sel!(systemColorsDidChange:),
system_colors_did_change as extern "C" fn(&Object, Sel, id),
);
let observer_class = declaration.register();
let observer: id = msg_send![observer_class, new];
if observer == nil {
return false;
}
let center: id = msg_send![class!(NSNotificationCenter), defaultCenter];
if center == nil {
return false;
}
let name: id = NSString::alloc(nil).init_str("NSSystemColorsDidChangeNotification");
if name == nil {
return false;
}
let _: () = msg_send![
center,
addObserver: observer
selector: sel!(systemColorsDidChange:)
name: name
object: nil
];
true
}
}
#[cfg(device_profile)]
extern "C" fn system_colors_did_change(_this: &Object, _cmd: Sel, _notification: id) {
if !super::types::is_main_thread() {
return;
}
let outcome = std::panic::catch_unwind(|| {
let changed = crate::theme::global_theme_manager().refresh_system_accent();
if changed {
log::debug!("[macos] system colours changed; the theme accent was re-derived");
}
});
if outcome.is_err() {
log::error!("[macos] the system colour observer panicked while refreshing the accent");
}
}
#[cfg(feature = "runtime-fonts")]
pub(crate) const SYSTEM_UI_FAMILY: &str = "SF Pro";
#[cfg(feature = "runtime-fonts")]
pub(crate) fn register_system_ui_face() -> Option<&'static str> {
static BYTES: std::sync::OnceLock<Option<&'static [u8]>> = std::sync::OnceLock::new();
let bytes = *BYTES.get_or_init(read_system_ui_face_bytes);
let bytes = bytes?;
crate::render::text::register_face(SYSTEM_UI_FAMILY, bytes).then_some(SYSTEM_UI_FAMILY)
}
#[cfg(feature = "runtime-fonts")]
fn read_system_ui_face_bytes() -> Option<&'static [u8]> {
const CANDIDATES: [&str; 2] =
["/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/SFNSDisplay.ttf"];
for path in CANDIDATES {
match std::fs::read(path) {
Ok(bytes) if !bytes.is_empty() => return Some(&*Box::leak(bytes.into_boxed_slice())),
Ok(_) => log::debug!("[fonts] {path} is empty; not registering a system UI face"),
Err(error) => {
log::debug!("[fonts] could not read {path}: {error}")
}
}
}
None
}
pub(crate) fn system_fonts() -> Vec<crate::platform::types::SystemFont> {
crate::platform::fonts_apple::system_fonts()
}
pub(crate) fn system_locale() -> Option<&'static str> {
cached_environment_fact(&SYSTEM_LOCALE_CACHE, system_locale_uncached)
}
fn system_locale_uncached() -> Option<&'static str> {
unsafe {
let locale: id = msg_send![class!(NSLocale), currentLocale];
if locale == nil {
return None;
}
let identifier: id = msg_send![locale, localeIdentifier];
if identifier == nil {
return None;
}
let utf8: *const c_char = msg_send![identifier, UTF8String];
if utf8.is_null() {
return None;
}
let raw = CStr::from_ptr(utf8).to_string_lossy();
let tag = if raw.contains('_') {
let mut tag = String::with_capacity(raw.len());
tag.extend(raw.chars().map(|ch| if ch == '_' { '-' } else { ch }));
tag
} else {
raw.into_owned()
};
crate::platform::locale::intern(Some(tag)).or_else(|| {
crate::platform::locale::intern(crate::platform::locale::from_environment())
})
}
}