use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObject};
use objc2::{define_class, msg_send, sel, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSButton, NSView, NSViewFrameDidChangeNotification, NSWindow, NSWindowButton,
NSWindowDidUpdateNotification, NSWindowWillCloseNotification,
};
use objc2_foundation::{
ns_string, MainThreadMarker, NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions,
NSNotification, NSNotificationCenter, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol,
NSPoint, NSRect, NSSize, NSString,
};
use std::cell::RefCell;
use std::collections::HashSet;
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::{Mutex, OnceLock};
#[derive(Clone, Copy)]
struct Inset {
pad_x: f64,
pad_y: f64,
spacing: Option<f64>,
}
static CONFIG: Mutex<Option<Inset>> = Mutex::new(None);
struct DefaultSnapshot {
container_height: f64,
button_origins: [NSPoint; 3],
}
static DEFAULTS: OnceLock<DefaultSnapshot> = OnceLock::new();
fn standard_buttons(window: &NSWindow) -> Option<[Retained<NSButton>; 3]> {
let close = window.standardWindowButton(NSWindowButton::CloseButton)?;
let minimize = window.standardWindowButton(NSWindowButton::MiniaturizeButton)?;
let zoom = window.standardWindowButton(NSWindowButton::ZoomButton)?;
Some([close, minimize, zoom])
}
fn container_of(button: &NSButton) -> Option<Retained<NSView>> {
unsafe { button.superview().and_then(|view| view.superview()) }
}
fn set_origin_if_needed(button: &NSButton, target: NSPoint) {
let current = button.frame().origin;
if (current.x - target.x).abs() > 0.5 || (current.y - target.y).abs() > 0.5 {
button.setFrameOrigin(target);
}
}
fn capture_defaults(buttons: &[Retained<NSButton>; 3], container: &NSView) -> DefaultSnapshot {
DefaultSnapshot {
container_height: container.frame().size.height,
button_origins: [
buttons[0].frame().origin,
buttons[1].frame().origin,
buttons[2].frame().origin,
],
}
}
fn reposition(ns_window: &NSWindow, inset: Inset) {
let Some(buttons) = standard_buttons(ns_window) else {
return;
};
let Some(container) = container_of(&buttons[0]) else {
return;
};
let snapshot = DEFAULTS.get_or_init(|| capture_defaults(&buttons, &container));
let button_height = buttons[0].frame().size.height;
let _ = container;
let button_y = snapshot.container_height - inset.pad_y - button_height;
let spacing = inset
.spacing
.unwrap_or(snapshot.button_origins[1].x - snapshot.button_origins[0].x);
for (i, button) in buttons.iter().enumerate() {
set_origin_if_needed(
button,
NSPoint {
x: inset.pad_x + (i as f64) * spacing,
y: button_y,
},
);
}
}
fn restore_default(ns_window: &NSWindow) {
let Some(snapshot) = DEFAULTS.get() else {
return;
};
let Some(buttons) = standard_buttons(ns_window) else {
return;
};
let Some(container) = container_of(&buttons[0]) else {
return;
};
let window_size = ns_window.frame().size;
container.setFrame(NSRect {
origin: NSPoint {
x: 0.0,
y: window_size.height - snapshot.container_height,
},
size: NSSize {
width: window_size.width,
height: snapshot.container_height,
},
});
for (i, button) in buttons.iter().enumerate() {
set_origin_if_needed(button, snapshot.button_origins[i]);
}
}
thread_local! {
static APPLYING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn is_managed_window(ns_window: &NSWindow) -> bool {
standard_buttons(ns_window).is_some()
}
fn apply_ns(ns_window: &NSWindow) {
if APPLYING.with(|f| f.get()) {
return;
}
APPLYING.with(|f| f.set(true));
let config = *CONFIG.lock().unwrap();
install_observers(ns_window);
match config {
Some(inset) => reposition(ns_window, inset),
None => restore_default(ns_window),
}
APPLYING.with(|f| f.set(false));
}
fn apply_from_object(obj: Option<&AnyObject>) {
let Some(obj) = obj else {
return;
};
if let Some(window) = obj.downcast_ref::<NSWindow>() {
if is_managed_window(window) {
apply_ns(window);
}
return;
}
if let Some(view) = obj.downcast_ref::<NSView>() {
if let Some(window) = view.window() {
if is_managed_window(&window) {
apply_ns(&window);
}
}
}
}
fn forget_window(ns_window: &NSWindow) {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let Some(buttons) = standard_buttons(ns_window) else {
return;
};
let center = NSNotificationCenter::defaultCenter();
let observer = observer(mtm);
for button in &buttons {
let key = Retained::as_ptr(button) as usize;
if !OBSERVED_BUTTONS.with(|set| set.borrow_mut().remove(&key)) {
continue;
}
unsafe {
center.removeObserver_name_object(
&observer,
Some(NSViewFrameDidChangeNotification),
Some(button),
);
button.removeObserver_forKeyPath(&observer, ns_string!("frame"));
}
}
}
static OBSERVER_APP: OnceLock<tauri::AppHandle> = OnceLock::new();
thread_local! {
static RELAYOUT_OBSERVER: RefCell<Option<Retained<TrafficLightObserver>>> =
const { RefCell::new(None) };
static OBSERVED_BUTTONS: RefCell<HashSet<usize>> = RefCell::new(HashSet::new());
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "TishTrafficLightObserver"]
#[thread_kind = MainThreadOnly]
#[ivars = ()]
struct TrafficLightObserver;
unsafe impl NSObjectProtocol for TrafficLightObserver {}
impl TrafficLightObserver {
#[unsafe(method(onWindowRelayout:))]
fn on_window_relayout(&self, note: Option<&NSNotification>) {
guard_objc_callback("onWindowRelayout:", || {
apply_from_object(note.and_then(|n| n.object()).as_deref());
});
}
#[unsafe(method(onWindowWillClose:))]
fn on_window_will_close(&self, note: Option<&NSNotification>) {
guard_objc_callback("onWindowWillClose:", || {
let Some(obj) = note.and_then(|n| n.object()) else {
return;
};
if let Some(window) = obj.downcast_ref::<NSWindow>() {
forget_window(window);
}
});
}
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn observe_value(
&self,
_key_path: Option<&NSString>,
object: Option<&AnyObject>,
_change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
guard_objc_callback("observeValueForKeyPath:", || apply_from_object(object));
}
}
);
pub(crate) fn guard_objc_callback(what: &str, f: impl FnOnce()) {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).is_err() {
eprintln!("[traffic_lights] panic in {what}; skipping this event's chrome update");
}
}
fn apply_all_ns() {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let app = NSApplication::sharedApplication(mtm);
for window in app.windows().iter() {
if is_managed_window(&window) {
apply_ns(&window);
}
}
}
impl TrafficLightObserver {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = mtm.alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
fn observer(mtm: MainThreadMarker) -> Retained<TrafficLightObserver> {
RELAYOUT_OBSERVER.with(|cell| {
cell.borrow_mut()
.get_or_insert_with(|| TrafficLightObserver::new(mtm))
.clone()
})
}
fn install_observers(ns_window: &NSWindow) {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
if OBSERVER_APP.get().is_none() {
return;
}
let center = NSNotificationCenter::defaultCenter();
let observer = observer(mtm);
static WINDOW_OBSERVER_INSTALLED: OnceLock<()> = OnceLock::new();
if WINDOW_OBSERVER_INSTALLED.set(()).is_ok() {
unsafe {
center.addObserver_selector_name_object(
&observer,
sel!(onWindowRelayout:),
Some(NSWindowDidUpdateNotification),
None,
);
center.addObserver_selector_name_object(
&observer,
sel!(onWindowWillClose:),
Some(NSWindowWillCloseNotification),
None,
);
}
}
let Some(buttons) = standard_buttons(ns_window) else {
return;
};
for button in &buttons {
let key = Retained::as_ptr(button) as usize;
if OBSERVED_BUTTONS.with(|set| set.borrow().contains(&key)) {
continue;
}
button.setPostsFrameChangedNotifications(true);
unsafe {
center.addObserver_selector_name_object(
&observer,
sel!(onWindowRelayout:),
Some(NSViewFrameDidChangeNotification),
Some(button),
);
button.addObserver_forKeyPath_options_context(
&observer,
ns_string!("frame"),
NSKeyValueObservingOptions::empty(),
NonNull::dangling().as_ptr(),
);
}
OBSERVED_BUTTONS.with(|set| set.borrow_mut().insert(key));
}
}
pub fn init(app: &tauri::AppHandle) {
let _ = OBSERVER_APP.set(app.clone());
}
fn apply_all_scheduled(app: &tauri::AppHandle) {
for delay_ms in [0u64, 120, 300, 700, 1400] {
let handle = app.clone();
std::thread::spawn(move || {
if delay_ms > 0 {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
let _ = handle.run_on_main_thread(apply_all_ns);
});
}
}
pub fn set_inset(
app: &tauri::AppHandle,
pad_x: Option<f64>,
pad_y: Option<f64>,
spacing: Option<f64>,
) {
let config = match (pad_x, pad_y) {
(Some(x), Some(y)) => Some(Inset {
pad_x: x,
pad_y: y,
spacing,
}),
_ => None,
};
*CONFIG.lock().unwrap() = config;
apply_all_scheduled(app);
}
pub fn reapply(app: &tauri::AppHandle) {
apply_all_scheduled(app);
}