#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::{NSObject, NSObjectProtocol, Sel};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{
NSApplication, NSControlStateValue, NSControlStateValueOff, NSControlStateValueOn,
NSEventModifierFlags, NSMenu, NSMenuItem,
};
use objc2_foundation::NSString;
use std::cell::RefCell;
use super::spec::{self, Entry, ItemKind, ItemSpec, KeyEquivalent, MenuCommand, PanelMarks};
struct Chosen {
queue: RefCell<Vec<MenuCommand>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "ConcinnityEditorMenuTarget"]
#[ivars = Chosen]
struct MenuTarget;
unsafe impl NSObjectProtocol for MenuTarget {}
impl MenuTarget {
#[unsafe(method(menuItemChosen:))]
fn menu_item_chosen(&self, sender: &NSMenuItem) {
if let Some(command) = MenuCommand::from_tag(sender.tag()) {
self.ivars().queue.borrow_mut().push(command);
}
}
}
);
impl MenuTarget {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(Chosen {
queue: RefCell::new(Vec::new()),
});
unsafe { msg_send![super(this), init] }
}
}
thread_local! {
static BAR: RefCell<Option<AppMenu>> = const { RefCell::new(None) };
}
pub(crate) fn install(state: PanelMarks) {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
BAR.with(|bar| *bar.borrow_mut() = Some(AppMenu::build(mtm, state)));
}
pub(crate) fn take_chosen() -> Vec<MenuCommand> {
BAR.with(|bar| {
bar.borrow()
.as_ref()
.map(AppMenu::take_chosen)
.unwrap_or_default()
})
}
pub(crate) fn sync(state: PanelMarks) {
BAR.with(|bar| {
if let Some(menu) = bar.borrow_mut().as_mut() {
menu.sync(state);
}
});
}
struct AppMenu {
target: Retained<MenuTarget>,
view_items: Vec<Retained<NSMenuItem>>,
synced: PanelMarks,
}
impl AppMenu {
fn build(mtm: MainThreadMarker, state: PanelMarks) -> Self {
let target = MenuTarget::new(mtm);
let bar = NSMenu::new(mtm);
let (app, _) = submenu(mtm, "", &spec::app_entries(), &target);
bar.addItem(&app);
let (view, view_items) = submenu(mtm, "View", &spec::view_entries(state), &target);
bar.addItem(&view);
NSApplication::sharedApplication(mtm).setMainMenu(Some(&bar));
Self {
target,
view_items,
synced: state,
}
}
fn take_chosen(&self) -> Vec<MenuCommand> {
std::mem::take(&mut self.target.ivars().queue.borrow_mut())
}
fn sync(&mut self, state: PanelMarks) {
if self.synced == state {
return;
}
self.synced = state;
let entries = spec::view_entries(state);
let marks = entries.iter().filter_map(Entry::item);
for (item, spec) in self.view_items.iter().zip(marks) {
item.setState(check_state(spec.checked));
}
}
}
fn submenu(
mtm: MainThreadMarker,
title: &str,
entries: &[Entry],
target: &MenuTarget,
) -> (Retained<NSMenuItem>, Vec<Retained<NSMenuItem>>) {
let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str(title));
menu.setAutoenablesItems(false);
let mut items = Vec::new();
for entry in entries {
match entry {
Entry::Separator => menu.addItem(&NSMenuItem::separatorItem(mtm)),
Entry::Item(spec) => {
let item = build_item(mtm, spec, target);
menu.addItem(&item);
items.push(item);
}
}
}
let holder = NSMenuItem::new(mtm);
holder.setTitle(&NSString::from_str(title));
holder.setSubmenu(Some(&menu));
(holder, items)
}
fn build_item(mtm: MainThreadMarker, spec: &ItemSpec, target: &MenuTarget) -> Retained<NSMenuItem> {
let item = NSMenuItem::new(mtm);
item.setTitle(&NSString::from_str(&spec.title));
item.setState(check_state(spec.checked));
match spec.kind {
ItemKind::Standard(selector) => {
unsafe { item.setAction(Some(standard_selector(selector))) };
}
ItemKind::Command(command) => {
item.setTag(command.tag());
unsafe {
item.setAction(Some(sel!(menuItemChosen:)));
item.setTarget(Some(target));
}
}
}
if let Some(key) = spec.key {
item.setKeyEquivalent(&NSString::from_str(key.key));
item.setKeyEquivalentModifierMask(modifiers(key));
}
item
}
fn standard_selector(name: &str) -> Sel {
match name {
"orderFrontStandardAboutPanel:" => sel!(orderFrontStandardAboutPanel:),
"hide:" => sel!(hide:),
"hideOtherApplications:" => sel!(hideOtherApplications:),
"unhideAllApplications:" => sel!(unhideAllApplications:),
other => unreachable!("{other} is not one of the spec's standard selectors"),
}
}
fn modifiers(key: KeyEquivalent) -> NSEventModifierFlags {
let mut flags = NSEventModifierFlags::Command;
if key.option {
flags |= NSEventModifierFlags::Option;
}
flags
}
fn check_state(checked: bool) -> NSControlStateValue {
if checked {
NSControlStateValueOn
} else {
NSControlStateValueOff
}
}