#[cfg(any(target_os = "macos", test))]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(any(target_os = "macos", test))]
#[derive(Debug, Default)]
pub(crate) struct DisplayChangeGate {
pending: AtomicBool,
}
#[cfg(any(target_os = "macos", test))]
impl DisplayChangeGate {
pub(crate) const fn new() -> Self {
Self {
pending: AtomicBool::new(false),
}
}
pub(crate) fn record(&self) -> bool {
!self.pending.swap(true, Ordering::AcqRel)
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn clear(&self) {
self.pending.store(false, Ordering::Release);
}
#[cfg(test)]
pub(crate) fn is_pending(&self) -> bool {
self.pending.load(Ordering::Acquire)
}
}
#[cfg(target_os = "macos")]
static DISPLAY_CHANGE_GATE: DisplayChangeGate = DisplayChangeGate::new();
#[cfg(target_os = "macos")]
pub(crate) fn display_change_gate() -> &'static DisplayChangeGate {
&DISPLAY_CHANGE_GATE
}
#[cfg(target_os = "macos")]
mod macos {
use std::sync::OnceLock;
use objc2::rc::Retained;
use objc2::runtime::NSObjectProtocol;
use objc2::{define_class, extern_methods};
use objc2_app_kit::NSApplicationDidChangeScreenParametersNotification;
use objc2_foundation::{NSNotification, NSNotificationCenter, NSObject};
use winit::event_loop::EventLoopProxy;
use crate::app::AppEvent;
use super::display_change_gate;
static PROXY: OnceLock<EventLoopProxy<AppEvent>> = OnceLock::new();
define_class!(
#[unsafe(super = NSObject)]
struct DisplayChangeObserver;
unsafe impl NSObjectProtocol for DisplayChangeObserver {}
impl DisplayChangeObserver {
#[unsafe(method(displayParametersChanged:))]
fn display_parameters_changed(&self, _notification: &NSNotification) {
if !display_change_gate().record() {
return;
}
log::info!("macOS display configuration changed; dispatching recovery event");
match PROXY.get() {
Some(proxy) => {
if let Err(e) = proxy.send_event(AppEvent::DisplayConfigurationChanged) {
log::warn!("Failed to send display-change event to event loop: {e}");
display_change_gate().clear();
}
}
None => {
display_change_gate().clear();
}
}
}
}
);
impl DisplayChangeObserver {
extern_methods!(
#[unsafe(method(new))]
fn new() -> Retained<Self>;
);
}
pub(crate) struct DisplayChangeObserverHandle {
observer: Option<Retained<DisplayChangeObserver>>,
}
impl Drop for DisplayChangeObserverHandle {
fn drop(&mut self) {
if let Some(observer) = self.observer.take() {
let center = NSNotificationCenter::defaultCenter();
unsafe { center.removeObserver(&observer) };
}
}
}
pub(crate) fn register_display_change_observer(
proxy: EventLoopProxy<AppEvent>,
) -> DisplayChangeObserverHandle {
let _ = PROXY.set(proxy);
let center = NSNotificationCenter::defaultCenter();
let observer = DisplayChangeObserver::new();
unsafe {
center.addObserver_selector_name_object(
&observer,
objc2::sel!(displayParametersChanged:),
Some(NSApplicationDidChangeScreenParametersNotification),
None,
);
}
log::debug!("Registered NSApplicationDidChangeScreenParametersNotification observer");
DisplayChangeObserverHandle {
observer: Some(observer),
}
}
}
#[cfg(target_os = "macos")]
pub(crate) use macos::register_display_change_observer;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_notification_dispatches() {
let gate = DisplayChangeGate::new();
assert!(!gate.is_pending());
assert!(gate.record(), "first notification must dispatch");
assert!(gate.is_pending());
}
#[test]
fn burst_is_coalesced_to_one_dispatch() {
let gate = DisplayChangeGate::new();
assert!(gate.record());
assert!(
!gate.record(),
"second notification in burst must be suppressed"
);
assert!(
!gate.record(),
"third notification in burst must be suppressed"
);
assert!(gate.is_pending());
}
#[test]
fn clear_re_arms_the_gate() {
let gate = DisplayChangeGate::new();
assert!(gate.record());
gate.clear();
assert!(!gate.is_pending());
assert!(
gate.record(),
"after clear, a new change must dispatch again"
);
}
#[test]
fn interleaved_handling_keeps_edge_trigger_semantics() {
let gate = DisplayChangeGate::new();
assert!(gate.record());
assert!(!gate.record());
assert!(!gate.record());
assert!(!gate.record());
gate.clear();
assert!(gate.record());
assert!(!gate.record());
gate.clear();
assert!(!gate.is_pending());
}
#[test]
fn fresh_gate_is_not_pending() {
let gate = DisplayChangeGate::default();
assert!(!gate.is_pending());
}
}