1use objc2::rc::Retained;
5use objc2::runtime::Sel;
6use objc2::{MainThreadMarker, MainThreadOnly, sel};
7use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem};
8use objc2_foundation::NSString;
9
10use nifty_runtime::{Key, Menu, MenuEntry, Shortcut, SystemCommand};
11
12use crate::classes::MenuTarget;
13
14const RETURN: &str = "\r";
16const BACKSPACE: &str = "\u{8}";
17const UP_ARROW: &str = "\u{F700}";
18const DOWN_ARROW: &str = "\u{F701}";
19const LEFT_ARROW: &str = "\u{F702}";
20const RIGHT_ARROW: &str = "\u{F703}";
21
22fn edit_commands() -> [(SystemCommand, Sel); 6] {
25 [
26 (SystemCommand::Undo, sel!(undo:)),
27 (SystemCommand::Redo, sel!(redo:)),
28 (SystemCommand::Cut, sel!(cut:)),
29 (SystemCommand::Copy, sel!(copy:)),
30 (SystemCommand::Paste, sel!(paste:)),
31 (SystemCommand::SelectAll, sel!(selectAll:)),
32 ]
33}
34
35pub(crate) fn install(
38 app: &NSApplication,
39 mtm: MainThreadMarker,
40 menus: &[Menu<usize>],
41 target: Option<&MenuTarget>,
42) {
43 let menu_bar = NSMenu::new(mtm);
44 let quit = system_item(mtm, SystemCommand::Quit, sel!(terminate:));
45 add_submenu(&menu_bar, &submenu(mtm, "", [quit]), mtm);
46 let (first, rest) = match menus.split_first() {
47 Some((first, rest)) => (Some(first), rest),
48 None => (None, menus),
49 };
50 if let Some(first) = first {
51 add_submenu(&menu_bar, &app_menu(mtm, first, target), mtm);
52 }
53 let edit = edit_commands()
54 .into_iter()
55 .map(|(command, action)| system_item(mtm, command, action));
56 add_submenu(&menu_bar, &submenu(mtm, "Edit", edit), mtm);
57 for menu in rest {
58 add_submenu(&menu_bar, &app_menu(mtm, menu, target), mtm);
59 }
60 app.setMainMenu(Some(&menu_bar));
61}
62
63fn submenu(
64 mtm: MainThreadMarker,
65 title: &str,
66 items: impl IntoIterator<Item = Retained<NSMenuItem>>,
67) -> Retained<NSMenu> {
68 let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str(title));
69 for item in items {
70 menu.addItem(&item);
71 }
72 menu
73}
74
75fn add_submenu(menu_bar: &NSMenu, menu: &NSMenu, mtm: MainThreadMarker) {
76 let item = NSMenuItem::new(mtm);
77 item.setSubmenu(Some(menu));
78 menu_bar.addItem(&item);
79}
80
81pub(crate) fn context_menu(
83 mtm: MainThreadMarker,
84 entries: &[MenuEntry<usize>],
85 target: &MenuTarget,
86) -> Retained<NSMenu> {
87 entries_menu(mtm, "", entries, Some(target))
88}
89
90fn app_menu(
92 mtm: MainThreadMarker,
93 menu: &Menu<usize>,
94 target: Option<&MenuTarget>,
95) -> Retained<NSMenu> {
96 entries_menu(mtm, &menu.title, &menu.entries, target)
97}
98
99fn entries_menu(
100 mtm: MainThreadMarker,
101 title: &str,
102 entries: &[MenuEntry<usize>],
103 target: Option<&MenuTarget>,
104) -> Retained<NSMenu> {
105 let items = entries.iter().map(|entry| match entry {
106 MenuEntry::Item {
107 title,
108 shortcut,
109 action,
110 } => {
111 let item = item(mtm, title, MenuTarget::action(), *shortcut);
112 item.setTag(isize::try_from(*action).expect("menus have far fewer items than isize"));
113 unsafe { item.setTarget(target.map(AsRef::as_ref)) };
116 item
117 }
118 MenuEntry::Separator => NSMenuItem::separatorItem(mtm),
119 MenuEntry::Submenu(submenu) => {
120 let item = NSMenuItem::new(mtm);
121 item.setTitle(&NSString::from_str(&submenu.title));
122 item.setSubmenu(Some(&app_menu(mtm, submenu, target)));
123 item
124 }
125 });
126 submenu(mtm, title, items)
127}
128
129fn system_item(mtm: MainThreadMarker, command: SystemCommand, action: Sel) -> Retained<NSMenuItem> {
130 item(mtm, command.title(), action, Some(command.shortcut()))
131}
132
133fn item(
134 mtm: MainThreadMarker,
135 title: &str,
136 action: Sel,
137 shortcut: Option<Shortcut>,
138) -> Retained<NSMenuItem> {
139 let (key, modifiers) = shortcut.map_or(
140 (String::new(), NSEventModifierFlags::empty()),
141 key_equivalent,
142 );
143 let item = unsafe {
145 NSMenuItem::initWithTitle_action_keyEquivalent(
146 NSMenuItem::alloc(mtm),
147 &NSString::from_str(title),
148 Some(action),
149 &NSString::from_str(&key),
150 )
151 };
152 item.setKeyEquivalentModifierMask(modifiers);
153 item
154}
155
156fn key_equivalent(shortcut: Shortcut) -> (String, NSEventModifierFlags) {
158 let key = match shortcut.key {
159 Key::Return => RETURN.to_owned(),
160 Key::Delete => BACKSPACE.to_owned(),
161 Key::Up => UP_ARROW.to_owned(),
162 Key::Down => DOWN_ARROW.to_owned(),
163 Key::Left => LEFT_ARROW.to_owned(),
164 Key::Right => RIGHT_ARROW.to_owned(),
165 typed => typed
166 .character()
167 .map(String::from)
168 .expect("every other key types a character"),
169 };
170 let mut modifiers = NSEventModifierFlags::Command;
171 if shortcut.shift {
172 modifiers |= NSEventModifierFlags::Shift;
173 }
174 if shortcut.alt {
175 modifiers |= NSEventModifierFlags::Option;
176 }
177 (key, modifiers)
178}