use cocoa::appkit::{NSControl, NSMenuItem};
use cocoa::base::{id, nil, selector};
use cocoa::foundation::{NSPoint, NSRect, NSSize, NSString};
use objc::declare::ClassDecl;
use objc::runtime::{Object, Sel, NO, YES};
use objc::{class, msg_send, sel, sel_impl};
use std::sync::Arc;
use tauri::{Runtime, Window};
use crate::keymap::{get_key_map, get_modifier_map};
use crate::macos_window_holder::CURRENT_WINDOW;
use crate::theme::Theme;
use crate::{MenuItem, Position};
extern "C" {
fn NSPointInRect(aPoint: NSPoint, aRect: NSRect) -> bool;
}
extern "C" fn menu_item_action<R: Runtime>(_self: &Object, _cmd: Sel, _item: id) {
let window_arc: Arc<tauri::Window<R>> = match CURRENT_WINDOW.get_window() {
Some(window_arc) => window_arc,
None => return println!("No window found"),
};
let nsstring_obj: id = unsafe { msg_send![_item, representedObject] };
let combined_str: String = unsafe {
let cstr: *const std::os::raw::c_char = msg_send![nsstring_obj, UTF8String];
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
};
let parts: Vec<&str> = combined_str.split(":::").collect();
let event_name = parts.get(0).unwrap_or(&"").to_string();
let payload = parts.get(1).cloned();
let window = &*window_arc;
window.emit(&event_name, payload).unwrap();
}
extern "C" fn menu_did_close<R: Runtime>(_self: &Object, _cmd: Sel, _menu: id) {
if let Some(window) = CURRENT_WINDOW.get_window::<R>() {
window.emit("menu-did-close", ()).unwrap();
} else {
println!("Menu did close, but no window was found.");
}
}
fn register_menu_item_action<R: Runtime>() -> Sel {
let selector_name = "menuAction:";
let exists: bool;
let class = objc::runtime::Class::get("MenuItemDelegate");
exists = class.is_some();
if !exists {
let superclass = objc::runtime::Class::get("NSObject").unwrap();
let mut decl = ClassDecl::new("MenuItemDelegate", superclass).unwrap();
unsafe {
decl.add_method(
selector(selector_name),
menu_item_action::<R> as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
selector("menuDidClose:"),
menu_did_close::<R> as extern "C" fn(&Object, Sel, id),
);
decl.register();
}
}
selector(selector_name)
}
fn create_custom_menu_item<R: Runtime>(option: &MenuItem) -> id {
if option.is_separator.unwrap_or(false) {
let separator: id = unsafe { msg_send![class!(NSMenuItem), separatorItem] };
return separator;
}
let sel = register_menu_item_action::<R>();
let menu_item: id = unsafe {
let title = match &option.label {
Some(label) => NSString::alloc(nil).init_str(label),
None => NSString::alloc(nil).init_str(""),
};
let (key, mask) = match &option.shortcut {
Some(shortcut) => {
let parts: Vec<&str> = shortcut.split('+').collect();
let key_map = get_key_map();
let modifier_map = get_modifier_map();
let mut key_str = "";
let mut mask = cocoa::appkit::NSEventModifierFlags::empty();
for part in parts.iter() {
if let Some(k) = key_map.get(*part) {
key_str = k;
} else if let Some(m) = modifier_map.get(*part) {
mask.insert(*m);
} else {
key_str = *part; }
}
(NSString::alloc(nil).init_str(key_str), mask)
}
None => (
NSString::alloc(nil).init_str(""),
cocoa::appkit::NSEventModifierFlags::empty(),
),
};
let item = cocoa::appkit::NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(title, sel, key);
item.setKeyEquivalentModifierMask_(mask);
item.setEnabled_(match option.disabled {
Some(true) => NO,
_ => YES,
});
let string_payload = match &option.payload {
Some(payload) => format!(
"{}:::{}",
&option.event.as_ref().unwrap_or(&"".to_string()),
payload
),
None => option.event.as_ref().unwrap_or(&"".to_string()).clone(),
};
let ns_string_payload = NSString::alloc(nil).init_str(&string_payload);
let _: () = msg_send![item, setRepresentedObject:ns_string_payload];
if let Some(icon) = &option.icon {
let ns_string_path: id = NSString::alloc(nil).init_str(&icon.path);
let image: *mut Object = msg_send![class!(NSImage), alloc];
let image: *mut Object = msg_send![image, initWithContentsOfFile:ns_string_path];
if image.is_null() {
println!("Failed to load image from path: {}", icon.path);
} else {
let width = icon.width.unwrap_or(16);
let height = icon.height.unwrap_or(16);
let size = NSSize::new(width as f64, height as f64);
let _: () = msg_send![image, setSize:size];
let _: () = msg_send![item, setImage:image];
}
}
let delegate_class_name = "MenuItemDelegate";
let delegate_class: &'static objc::runtime::Class =
objc::runtime::Class::get(delegate_class_name).expect("Class should exist");
let delegate_instance: id = msg_send![delegate_class, new];
item.setTarget_(delegate_instance);
if let Some(subitems) = &option.subitems {
let submenu: id = msg_send![class!(NSMenu), new];
let _: () = msg_send![submenu, setAutoenablesItems:NO];
for subitem in subitems.iter() {
let sub_menu_item: id = create_custom_menu_item::<R>(subitem);
let _: () = msg_send![submenu, addItem:sub_menu_item];
}
let _: () = msg_send![item, setSubmenu:submenu];
}
let state = match option.checked {
Some(true) => 1,
_ => 0,
};
let _: () = msg_send![item, setState:state];
item
};
menu_item
}
fn create_context_menu<R: Runtime>(
options: &[MenuItem],
window: &Window<R>,
theme: Option<Theme>,
) -> id {
let _: () = CURRENT_WINDOW.set_window(window.clone());
unsafe {
let title = NSString::alloc(nil).init_str("Menu");
let menu: id = msg_send![class!(NSMenu), alloc];
let menu: id = msg_send![menu, initWithTitle: title];
if let Some(theme) = theme {
let appearance_name: id = match theme {
Theme::Dark => NSString::alloc(nil).init_str("NSAppearanceNameDarkAqua"),
Theme::Light => NSString::alloc(nil).init_str("NSAppearanceNameAqua"),
};
let appearance: id = msg_send![class!(NSAppearance), appearanceNamed:appearance_name];
let _: () = msg_send![menu, setAppearance: appearance];
}
let _: () = msg_send![menu, setAutoenablesItems:NO];
for option in options.iter().cloned() {
let item: id = create_custom_menu_item::<R>(&option);
let _: () = msg_send![menu, addItem:item];
}
let delegate_class_name = "MenuItemDelegate";
let delegate_class: &'static objc::runtime::Class =
objc::runtime::Class::get(delegate_class_name).expect("Class should exist");
let delegate_instance: id = msg_send![delegate_class, new];
let _: () = msg_send![menu, setDelegate:delegate_instance];
menu
}
}
pub fn show_context_menu<R: Runtime>(
window: Window<R>,
pos: Option<Position>,
items: Option<Vec<MenuItem>>,
theme: Option<Theme>,
) {
let main_queue = dispatch::Queue::main();
main_queue.exec_async(move || {
let items_slice = items.as_ref().map(|v| v.as_slice()).unwrap_or(&[]);
let menu = create_context_menu(items_slice, &window, theme);
let location = match pos {
Some(pos) if pos.x != 0.0 || pos.y != 0.0 => unsafe {
let window_position = window.outer_position().unwrap();
let screens: id = msg_send![class!(NSScreen), screens];
let screen_count: usize = msg_send![screens, count];
let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation];
let mut target_screen: id = nil;
let mut target_screen_frame: NSRect =
NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0));
for i in 0..screen_count {
let screen: id = msg_send![screens, objectAtIndex:i];
let frame: NSRect = msg_send![screen, frame];
if NSPointInRect(mouse_location, frame) {
target_screen = screen;
target_screen_frame = frame;
break;
}
}
if target_screen == nil {
target_screen = msg_send![class!(NSScreen), mainScreen];
target_screen_frame = msg_send![target_screen, frame];
}
let screen_height = target_screen_frame.size.height;
let screen_origin_y = target_screen_frame.origin.y;
let scale_factor = match window.scale_factor() {
Ok(factor) => factor,
Err(_) => 1.0, };
if pos.is_absolute.unwrap_or(false) {
let x = pos.x;
let y = screen_origin_y + screen_height - pos.y;
NSPoint::new(x, y)
} else {
let x = pos.x + (window_position.x as f64 / scale_factor);
let y = screen_origin_y + screen_height
- (window_position.y as f64 / scale_factor)
- pos.y;
NSPoint::new(x, y)
}
},
_ => unsafe {
let event: NSPoint = msg_send![class!(NSEvent), mouseLocation];
NSPoint::new(event.x, event.y)
},
};
unsafe {
let _: () =
msg_send![menu, popUpMenuPositioningItem:nil atLocation:location inView:nil];
}
});
}