use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use super::MenuSpec;
use crate::{NativeError, Result, Waker};
static NEXT: AtomicU64 = AtomicU64::new(1);
fn os(e: impl std::fmt::Display) -> NativeError {
NativeError::Os(crate::os_text(e))
}
fn finish(open: impl FnOnce() -> bool, picks: &Receiver<String>, tx: &Sender<Option<String>>, waker: &Waker) {
let chosen = std::panic::catch_unwind(std::panic::AssertUnwindSafe(open)).unwrap_or(false);
let id = if chosen { picks.try_iter().last() } else { None };
if tx.send(id).is_ok() {
waker();
}
}
#[allow(unsafe_code)]
pub fn show(
window: &impl raw_window_handle::HasWindowHandle,
spec: &MenuSpec,
at: Option<(f64, f64)>,
waker: Waker,
) -> Result<Receiver<Option<String>>> {
use muda::ContextMenu as _;
use raw_window_handle::RawWindowHandle;
spec.validate().map_err(|p| NativeError::Invalid(p.join("; ")))?;
let prefix = format!("\u{1}popup/{}/", NEXT.fetch_add(1, Ordering::Relaxed));
let menu = crate::menu_tree::build(&crate::menu_tree::plan(spec, &prefix))?;
let (pick_tx, picks) = channel();
let route = crate::menu_events::subscribe(prefix, pick_tx, crate::no_waker());
let (tx, rx) = channel();
let position = at.map(|(x, y)| muda::dpi::Position::Logical(muda::dpi::LogicalPosition::new(x, y)));
let raw = window.window_handle().map_err(os)?.as_raw();
match raw {
#[cfg(target_os = "macos")]
RawWindowHandle::AppKit(h) => {
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
let mtm = objc2::MainThreadMarker::new().ok_or_else(|| NativeError::Os("open context menus on the main thread".into()))?;
let view: Retained<AnyObject> = unsafe { Retained::retain(h.ns_view.as_ptr().cast::<AnyObject>()) }
.ok_or_else(|| NativeError::Os("the window has no view".into()))?;
let block = block2::RcBlock::new(move || {
let _keep = &route;
finish(|| unsafe { menu.show_context_menu_for_nsview(Retained::as_ptr(&view).cast(), position) }, &picks, &tx, &waker);
});
let _ = mtm;
unsafe { objc2_foundation::NSRunLoop::mainRunLoop().performBlock(&block) };
}
#[cfg(target_os = "windows")]
RawWindowHandle::Win32(h) => {
let hwnd = h.hwnd.get();
finish(|| unsafe { menu.show_context_menu_for_hwnd(hwnd, position) }, &picks, &tx, &waker);
drop(route);
}
_ => {
let _ = (menu, route, picks, tx, position, waker);
return Err(NativeError::Unsupported);
}
}
Ok(rx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_last_pick_wins_and_a_dismissal_is_none() {
let (ptx, picks) = channel();
let (tx, rx) = channel();
ptx.send("a".to_string()).unwrap();
finish(|| true, &picks, &tx, &crate::no_waker());
assert_eq!(rx.try_recv().unwrap().as_deref(), Some("a"));
ptx.send("stale".to_string()).unwrap();
finish(|| false, &picks, &tx, &crate::no_waker());
assert_eq!(rx.try_recv().unwrap(), None);
finish(|| panic!("in the OS binding"), &picks, &tx, &crate::no_waker());
assert_eq!(rx.try_recv().unwrap(), None, "a panic is a dismissal");
}
}