Detailed changes
@@ -133,11 +133,12 @@ pub struct Menu<'a> {
}
pub enum MenuItem<'a> {
+ Separator,
+ Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
},
- Separator,
}
#[derive(Clone)]
@@ -40,13 +40,10 @@ impl Element for KeystrokeLabel {
let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) {
Flex::row()
.with_children(keystrokes.iter().map(|keystroke| {
- Label::new(
- keystroke.to_string().to_uppercase(),
- self.text_style.clone(),
- )
- .contained()
- .with_style(self.container_style)
- .boxed()
+ Label::new(keystroke.to_string(), self.text_style.clone())
+ .contained()
+ .with_style(self.container_style)
+ .boxed()
}))
.boxed()
} else {
@@ -4,7 +4,7 @@ use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
collections::{HashMap, HashSet},
- fmt::Debug,
+ fmt::{Debug, Write},
};
use tree_sitter::{Language, Node, Parser};
@@ -318,28 +318,34 @@ impl Keystroke {
impl std::fmt::Display for Keystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.ctrl {
- write!(f, "{}", "^")?;
+ f.write_char('^')?;
}
if self.alt {
- write!(f, "{}", "⎇")?;
+ f.write_char('⎇')?;
}
if self.cmd {
- write!(f, "{}", "⌘")?;
+ f.write_char('⌘')?;
}
if self.shift {
- write!(f, "{}", "⇧")?;
+ f.write_char('⇧')?;
}
let key = match self.key.as_str() {
- "backspace" => "⌫",
- "up" => "↑",
- "down" => "↓",
- "left" => "←",
- "right" => "→",
- "tab" => "⇥",
- "escape" => "⎋",
- key => key,
+ "backspace" => '⌫',
+ "up" => '↑',
+ "down" => '↓',
+ "left" => '←',
+ "right" => '→',
+ "tab" => '⇥',
+ "escape" => '⎋',
+ key => {
+ if key.len() == 1 {
+ key.chars().next().unwrap().to_ascii_uppercase()
+ } else {
+ return f.write_str(key);
+ }
+ }
};
- write!(f, "{}", key)
+ f.write_char(key)
}
}
@@ -127,89 +127,131 @@ impl MacForegroundPlatform {
&self,
menus: Vec<Menu>,
delegate: id,
+ actions: &mut Vec<Box<dyn Action>>,
keystroke_matcher: &keymap::Matcher,
) -> id {
- let menu_bar = NSMenu::new(nil).autorelease();
- menu_bar.setDelegate_(delegate);
- let mut state = self.0.borrow_mut();
-
- state.menu_actions.clear();
+ let application_menu = NSMenu::new(nil).autorelease();
+ application_menu.setDelegate_(delegate);
for menu_config in menus {
- let menu_bar_item = NSMenuItem::new(nil).autorelease();
let menu = NSMenu::new(nil).autorelease();
- let menu_name = menu_config.name;
-
- menu.setTitle_(ns_string(menu_name));
+ menu.setTitle_(ns_string(menu_config.name));
menu.setDelegate_(delegate);
for item_config in menu_config.items {
- let item;
+ menu.addItem_(self.create_menu_item(
+ item_config,
+ delegate,
+ actions,
+ keystroke_matcher,
+ ));
+ }
- match item_config {
- MenuItem::Separator => {
- item = NSMenuItem::separatorItem(nil);
- }
- MenuItem::Action { name, action } => {
- let mut keystroke = None;
- if let Some(binding) = keystroke_matcher
- .bindings_for_action_type(action.as_any().type_id())
- .find(|binding| binding.action().eq(action.as_ref()))
- {
- if binding.keystrokes().len() == 1 {
- keystroke = binding.keystrokes().first()
+ let menu_item = NSMenuItem::new(nil).autorelease();
+ menu_item.setSubmenu_(menu);
+ application_menu.addItem_(menu_item);
+
+ if menu_config.name == "Window" {
+ let app: id = msg_send![APP_CLASS, sharedApplication];
+ app.setWindowsMenu_(menu);
+ }
+ }
+
+ application_menu
+ }
+
+ unsafe fn create_menu_item(
+ &self,
+ item: MenuItem,
+ delegate: id,
+ actions: &mut Vec<Box<dyn Action>>,
+ keystroke_matcher: &keymap::Matcher,
+ ) -> id {
+ match item {
+ MenuItem::Separator => NSMenuItem::separatorItem(nil),
+ MenuItem::Action { name, action } => {
+ let keystrokes = keystroke_matcher
+ .bindings_for_action_type(action.as_any().type_id())
+ .find(|binding| binding.action().eq(action.as_ref()))
+ .map(|binding| binding.keystrokes());
+
+ let item;
+ if let Some(keystrokes) = keystrokes {
+ if keystrokes.len() == 1 {
+ let keystroke = &keystrokes[0];
+ let mut mask = NSEventModifierFlags::empty();
+ for (modifier, flag) in &[
+ (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
+ (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
+ (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
+ ] {
+ if *modifier {
+ mask |= *flag;
}
}
- if let Some(keystroke) = keystroke {
- let mut mask = NSEventModifierFlags::empty();
- for (modifier, flag) in &[
- (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
- (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
- (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
- ] {
- if *modifier {
- mask |= *flag;
- }
+ item = NSMenuItem::alloc(nil)
+ .initWithTitle_action_keyEquivalent_(
+ ns_string(name),
+ selector("handleGPUIMenuItem:"),
+ ns_string(key_to_native(&keystroke.key).as_ref()),
+ )
+ .autorelease();
+ item.setKeyEquivalentModifierMask_(mask);
+ }
+ // For multi-keystroke bindings, render the keystroke as part of the title.
+ else {
+ use std::fmt::Write;
+
+ let mut name = format!("{name} [");
+ for (i, keystroke) in keystrokes.iter().enumerate() {
+ if i > 0 {
+ name.push(' ');
}
-
- item = NSMenuItem::alloc(nil)
- .initWithTitle_action_keyEquivalent_(
- ns_string(name),
- selector("handleGPUIMenuItem:"),
- ns_string(key_to_native(&keystroke.key).as_ref()),
- )
- .autorelease();
- item.setKeyEquivalentModifierMask_(mask);
- } else {
- item = NSMenuItem::alloc(nil)
- .initWithTitle_action_keyEquivalent_(
- ns_string(name),
- selector("handleGPUIMenuItem:"),
- ns_string(""),
- )
- .autorelease();
+ write!(&mut name, "{}", keystroke).unwrap();
}
-
- let tag = state.menu_actions.len() as NSInteger;
- let _: () = msg_send![item, setTag: tag];
- state.menu_actions.push(action);
+ name.push(']');
+
+ item = NSMenuItem::alloc(nil)
+ .initWithTitle_action_keyEquivalent_(
+ ns_string(&name),
+ selector("handleGPUIMenuItem:"),
+ ns_string(""),
+ )
+ .autorelease();
}
+ } else {
+ item = NSMenuItem::alloc(nil)
+ .initWithTitle_action_keyEquivalent_(
+ ns_string(name),
+ selector("handleGPUIMenuItem:"),
+ ns_string(""),
+ )
+ .autorelease();
}
- menu.addItem_(item);
+ let tag = actions.len() as NSInteger;
+ let _: () = msg_send![item, setTag: tag];
+ actions.push(action);
+ item
}
-
- menu_bar_item.setSubmenu_(menu);
- menu_bar.addItem_(menu_bar_item);
-
- if menu_name == "Window" {
- let app: id = msg_send![APP_CLASS, sharedApplication];
- app.setWindowsMenu_(menu);
+ MenuItem::Submenu(Menu { name, items }) => {
+ let item = NSMenuItem::new(nil).autorelease();
+ let submenu = NSMenu::new(nil).autorelease();
+ submenu.setDelegate_(delegate);
+ for item in items {
+ submenu.addItem_(self.create_menu_item(
+ item,
+ delegate,
+ actions,
+ keystroke_matcher,
+ ));
+ }
+ item.setSubmenu_(submenu);
+ item.setTitle_(ns_string(name));
+ item
}
}
-
- menu_bar
}
}
@@ -270,7 +312,14 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
- app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), keystroke_matcher));
+ let mut state = self.0.borrow_mut();
+ let actions = &mut state.menu_actions;
+ app.setMainMenu_(self.create_menu_bar(
+ menus,
+ app.delegate(),
+ actions,
+ keystroke_matcher,
+ ));
}
}
@@ -15,14 +15,23 @@ pub fn menus() -> Vec<Menu<'static>> {
action: Box::new(auto_update::Check),
},
MenuItem::Separator,
- MenuItem::Action {
- name: "Open Settings",
- action: Box::new(super::OpenSettings),
- },
- MenuItem::Action {
- name: "Open Key Bindings",
- action: Box::new(super::OpenKeymap),
- },
+ MenuItem::Submenu(Menu {
+ name: "Preferences",
+ items: vec![
+ MenuItem::Action {
+ name: "Open Settings",
+ action: Box::new(super::OpenSettings),
+ },
+ MenuItem::Action {
+ name: "Open Key Bindings",
+ action: Box::new(super::OpenKeymap),
+ },
+ MenuItem::Action {
+ name: "Select Theme",
+ action: Box::new(theme_selector::Toggle),
+ },
+ ],
+ }),
MenuItem::Action {
name: "Install CLI",
action: Box::new(super::InstallCommandLineInterface),