Skip to main content

nifty_macos/
menus.rs

1//! The app's menu bar: the app menu and the Edit menu `AppKit` apps have, and the app's
2//! own menus, the first before Edit and the rest after it, as File comes before Edit.
3
4use 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
14/// The key equivalents `AppKit` gives the keys that type no character.
15const 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
22/// The Edit menu's commands, and the responder-chain actions `AppKit` implements for them;
23/// they make editing shortcuts work in text fields, which take them through the menu.
24fn 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
35/// Replaces the menu bar with the app menu, the Edit menu and `menus`, whose items tell
36/// `target` when they are chosen.
37pub(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
81/// A context menu of `entries`, which tell `target` when they are chosen.
82pub(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
90/// One of the app's own menus, its items numbered by their tags.
91fn 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            // SAFETY: `MenuTarget` implements the action, and the backend keeps it alive
114            // for as long as these menus show.
115            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    // SAFETY: `action` is a selector the item's target, or the responder chain, implements.
144    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
156/// The key equivalent and modifiers `AppKit` writes `shortcut` as.
157fn 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}