use objc2::MainThreadMarker;
use objc2::rc::Retained;
use objc2_app_kit::{
NSApplication, NSEventType, NSToolbar, NSView, NSWindow, NSWindowButton, NSWindowToolbarStyle,
};
use winit::window::Window;
use crate::{Rect, Titlebar};
const ASSUMED: Rect = Rect {
x: 0.0,
y: 0.0,
w: 78.0,
h: 28.0,
};
pub fn native_controls(window: &Window) -> Rect {
measure(window).unwrap_or(ASSUMED)
}
pub fn set_titlebar(window: &Window, titlebar: Titlebar) {
let style = match titlebar {
Titlebar::Standard => return,
Titlebar::Medium => NSWindowToolbarStyle::UnifiedCompact,
Titlebar::Tall => NSWindowToolbarStyle::Unified,
};
let Some(ns_window) = ns_window(window) else {
return;
};
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let toolbar = NSToolbar::new(mtm);
ns_window.setToolbar(Some(&toolbar));
ns_window.setToolbarStyle(style);
}
fn ns_window(window: &Window) -> Option<Retained<NSWindow>> {
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
let RawWindowHandle::AppKit(h) = window.window_handle().ok()?.as_raw() else {
return None;
};
let view = unsafe { Retained::retain(h.ns_view.as_ptr().cast::<NSView>()) }?;
view.window()
}
fn measure(window: &Window) -> Option<Rect> {
let ns_window = ns_window(window)?;
let frame = ns_window.frame();
let titlebar = frame.size.height - ns_window.contentLayoutRect().size.height;
if titlebar <= 0.0 {
return None;
}
let in_window = |b: NSWindowButton| {
let button = ns_window.standardWindowButton(b)?;
let superview = unsafe { button.superview() }?;
Some(superview.convertRect_toView(button.frame(), None))
};
let close = in_window(NSWindowButton::CloseButton)?;
let zoom = in_window(NSWindowButton::ZoomButton)?;
let leading = close.origin.x;
let trailing = zoom.origin.x + zoom.size.width;
if leading < 0.0 || trailing <= leading {
return None;
}
let centre = frame.size.height - (close.origin.y + close.size.height / 2.0);
let h = (2.0 * centre).min(titlebar);
if h <= 0.0 {
return None;
}
Some(Rect::new(0.0, 0.0, (trailing + leading) as f32, h as f32))
}
pub fn keep_titlebar_clicks(window: &Window) {
use objc2::runtime::{AnyClass, AnyObject, Bool, Imp, Sel};
use objc2::{ffi, msg_send, sel};
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
unsafe extern "C-unwind" fn cannot(_this: &AnyObject, _sel: Sel) -> Bool {
Bool::NO
}
type CannotFn = unsafe extern "C-unwind" fn(&AnyObject, Sel) -> Bool;
let Ok(handle) = window.window_handle() else {
return;
};
let RawWindowHandle::AppKit(h) = handle.as_raw() else {
return;
};
let view = unsafe { &*h.ns_view.as_ptr().cast::<AnyObject>() };
let cls: &AnyClass = unsafe { msg_send![view, class] };
unsafe {
ffi::class_addMethod(
cls as *const AnyClass as *mut AnyClass,
sel!(mouseDownCanMoveWindow),
std::mem::transmute::<CannotFn, Imp>(cannot),
c"B@:".as_ptr(),
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DoubleClick {
Zoom,
Minimize,
Nothing,
}
fn double_click_of(action: Option<&str>, miniaturize: bool) -> DoubleClick {
match action {
Some("Minimize") => DoubleClick::Minimize,
Some("None") => DoubleClick::Nothing,
Some(_) => DoubleClick::Zoom,
None if miniaturize => DoubleClick::Minimize,
None => DoubleClick::Zoom,
}
}
fn double_click_setting() -> DoubleClick {
use objc2::runtime::{AnyObject, Bool};
use objc2::{class, msg_send};
use objc2_foundation::NSString;
let defaults: Retained<AnyObject> =
unsafe { msg_send![class!(NSUserDefaults), standardUserDefaults] };
let key = NSString::from_str("AppleActionOnDoubleClick");
let action: Option<Retained<NSString>> = unsafe { msg_send![&*defaults, stringForKey: &*key] };
let key = NSString::from_str("AppleMiniaturizeOnDoubleClick");
let miniaturize: Bool = unsafe { msg_send![&*defaults, boolForKey: &*key] };
double_click_of(
action.map(|a| a.to_string()).as_deref(),
miniaturize.as_bool(),
)
}
pub fn titlebar_double_click(window: &Window) -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false;
};
let Some(event) = NSApplication::sharedApplication(mtm).currentEvent() else {
return false;
};
if event.r#type() != NSEventType::LeftMouseDown || event.clickCount() != 2 {
return false;
}
let Some(ns_window) = ns_window(window) else {
return false;
};
match double_click_setting() {
DoubleClick::Zoom => ns_window.zoom(None),
DoubleClick::Minimize => ns_window.miniaturize(None),
DoubleClick::Nothing => {}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_titlebar_double_click_follows_the_setting_and_zooms_unset() {
assert_eq!(double_click_of(None, false), DoubleClick::Zoom);
assert_eq!(double_click_of(None, true), DoubleClick::Minimize);
assert_eq!(double_click_of(Some("Maximize"), true), DoubleClick::Zoom);
assert_eq!(double_click_of(Some("Fill"), false), DoubleClick::Zoom);
assert_eq!(
double_click_of(Some("Minimize"), false),
DoubleClick::Minimize
);
assert_eq!(double_click_of(Some("None"), true), DoubleClick::Nothing);
}
}