#![cfg(all(target_os = "macos", feature = "cocoa-legacy", widgets_unstripped))]
use std::sync::OnceLock;
use cocoa::base::{id, nil};
use cocoa::foundation::NSString;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use crate::platform::display_link as shared;
pub(crate) fn start() -> bool {
let started = shared::start();
if started && !shared::observers_installed() {
if install_lifecycle_observers() {
shared::mark_observers_installed();
}
}
started
}
pub(crate) fn stop() {
shared::stop();
}
pub(crate) fn is_running() -> bool {
shared::is_running()
}
pub(crate) fn next_deadline() -> Option<std::time::Instant> {
shared::next_deadline()
}
fn restart() {
shared::restart();
}
fn observer_class() -> Option<*const Class> {
static CLASS: OnceLock<usize> = OnceLock::new();
let pointer = *CLASS.get_or_init(|| {
let superclass = class!(NSObject);
let Some(mut decl) = ClassDecl::new("RustWidgetsDisplayLinkObserver", superclass) else {
return 0;
};
unsafe {
decl.add_method(
sel!(screenParametersChanged:),
screen_parameters_changed as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(applicationWillTerminate:),
application_will_terminate as extern "C" fn(&Object, Sel, id),
);
}
(decl.register() as *const Class) as usize
});
if pointer == 0 {
None
} else {
Some(pointer as *const Class)
}
}
extern "C" fn screen_parameters_changed(_this: &Object, _cmd: Sel, _notification: id) {
restart();
}
extern "C" fn application_will_terminate(_this: &Object, _cmd: Sel, _notification: id) {
stop();
}
fn install_lifecycle_observers() -> bool {
if !super::types::is_main_thread() {
return false;
}
let Some(class) = observer_class() else {
return false;
};
unsafe {
let center: id = msg_send![class!(NSNotificationCenter), defaultCenter];
if center == nil {
return false;
}
let observer: id = msg_send![class, new];
if observer == nil {
return false;
}
let screen_name: id =
NSString::alloc(nil).init_str("NSApplicationDidChangeScreenParametersNotification");
if screen_name == nil {
return false;
}
let _: () = msg_send![
center,
addObserver: observer
selector: sel!(screenParametersChanged:)
name: screen_name
object: nil
];
let terminate_name: id =
NSString::alloc(nil).init_str("NSApplicationWillTerminateNotification");
if terminate_name == nil {
return false;
}
let _: () = msg_send![
center,
addObserver: observer
selector: sel!(applicationWillTerminate:)
name: terminate_name
object: nil
];
}
true
}
#[cfg(test)]
mod tests {
use super::*;
static LINK_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn no_deadline_is_reported_while_stopped() {
let _guard = LINK_TEST_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
stop();
assert!(!is_running());
assert_eq!(next_deadline(), None);
}
#[test]
fn display_link_start_reports_availability_without_panicking() {
let _guard = LINK_TEST_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let started = start();
assert_eq!(is_running(), started);
stop();
assert!(!is_running());
assert_eq!(next_deadline(), None);
}
}