use crate::state::AppState;
use crate::windows;
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject, Imp, ProtocolObject, Sel};
use objc2::{define_class, msg_send, sel, MainThreadOnly};
use objc2_app_kit::{NSApplication, NSControlStateValueOff, NSControlStateValueOn, NSMenu, NSMenuItem};
use objc2_foundation::{MainThreadMarker, NSObject, NSObjectProtocol, NSString};
use std::cell::RefCell;
use std::ffi::CStr;
use std::sync::OnceLock;
use tauri::{AppHandle, Manager};
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
thread_local! {
static DOCK_MENU_TARGET: RefCell<Option<Retained<DockMenuTarget>>> = const { RefCell::new(None) };
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "TishDesktopDockMenuTarget"]
#[thread_kind = MainThreadOnly]
#[ivars = ()]
struct DockMenuTarget;
unsafe impl NSObjectProtocol for DockMenuTarget {}
impl DockMenuTarget {
#[unsafe(method(dockNewWindow:))]
fn dock_new_window(&self, _sender: Option<&AnyObject>) {
dispatch(DockAction::NewWindow);
}
#[unsafe(method(dockFocusWindow:))]
fn dock_focus_window(&self, sender: Option<&AnyObject>) {
let Some(sender) = sender else { return };
let Some(item) = sender.downcast_ref::<NSMenuItem>() else { return };
let Some(repr) = item.representedObject() else { return };
let Some(key) = repr.downcast_ref::<NSString>() else { return };
dispatch(DockAction::Focus(key.to_string()));
}
}
);
impl DockMenuTarget {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = mtm.alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
enum DockAction {
NewWindow,
Focus(String),
}
fn dispatch(action: DockAction) {
let Some(app) = APP_HANDLE.get() else { return };
let app = app.clone();
std::thread::spawn(move || {
let handle = app.clone();
let _ = app.run_on_main_thread(move || match action {
DockAction::NewWindow => {
let spec = handle
.try_state::<AppState>()
.and_then(|s| s.config.lock().windows.first().cloned());
if let Some(mut spec) = spec {
spec.label = String::new();
let _ = windows::create_from_spec(&handle, &spec);
}
}
DockAction::Focus(label) => {
let _ = windows::focus(&handle, &label);
}
});
});
}
fn dock_menu_target(mtm: MainThreadMarker) -> Retained<DockMenuTarget> {
DOCK_MENU_TARGET.with(|cell| {
let mut slot = cell.borrow_mut();
if let Some(target) = slot.as_ref() {
return target.clone();
}
let target = DockMenuTarget::new(mtm);
*slot = Some(target.clone());
target
})
}
fn build_ns_dock_menu(rows: &[(String, String, bool)]) -> Retained<NSMenu> {
let mtm = MainThreadMarker::new().expect("dock menu requires main thread");
let menu = NSMenu::new(mtm);
let target = dock_menu_target(mtm);
let new_item = NSMenuItem::new(mtm);
new_item.setTitle(&NSString::from_str("New Window"));
new_item.setEnabled(true);
unsafe {
new_item.setTarget(Some(&target));
new_item.setAction(Some(sel!(dockNewWindow:)));
}
menu.addItem(&new_item);
if !rows.is_empty() {
menu.addItem(&NSMenuItem::separatorItem(mtm));
for (label, title, focused) in rows {
let item = NSMenuItem::new(mtm);
item.setTitle(&NSString::from_str(title));
item.setEnabled(true);
unsafe {
item.setTarget(Some(&target));
item.setAction(Some(sel!(dockFocusWindow:)));
item.setRepresentedObject(Some(&NSString::from_str(label)));
}
item.setState(if *focused {
NSControlStateValueOn
} else {
NSControlStateValueOff
});
menu.addItem(&item);
}
}
menu
}
extern "C" fn application_dock_menu(
_this: *mut AnyObject,
_sel: Sel,
_sender: *mut AnyObject,
) -> *mut AnyObject {
let rows = windows::window_rows();
let menu = build_ns_dock_menu(&rows);
Retained::into_raw(menu) as *mut AnyObject
}
pub fn install(app: &AppHandle) -> Result<(), String> {
let _ = APP_HANDLE.set(app.clone());
let mtm =
MainThreadMarker::new().ok_or_else(|| "dock delegate requires main thread".to_string())?;
let _ = dock_menu_target(mtm);
let ns_app = NSApplication::sharedApplication(mtm);
let delegate = ns_app
.delegate()
.ok_or_else(|| "NSApplication has no delegate".to_string())?;
let delegate_obj: &AnyObject = ProtocolObject::as_ref(&*delegate);
let delegate_cls: *const AnyClass = delegate_obj.class();
let sel = sel!(applicationDockMenu:);
let types = CStr::from_bytes_with_nul(b"@@:@\0").map_err(|e| e.to_string())?;
let imp: Imp = unsafe {
std::mem::transmute::<
unsafe extern "C" fn(*mut AnyObject, Sel, *mut AnyObject) -> *mut AnyObject,
Imp,
>(application_dock_menu as unsafe extern "C" fn(_, _, _) -> _)
};
let ok = unsafe { objc2::ffi::class_addMethod(delegate_cls.cast_mut(), sel, imp, types.as_ptr()) };
if ok == objc2::runtime::Bool::NO {
}
Ok(())
}