Start on app menus

Nathan Sobo created

Change summary

crates/gpui2/src/app.rs                    |  40 ++++
crates/gpui2/src/key_dispatch.rs           |  20 +
crates/gpui2/src/platform.rs               |   4 
crates/gpui2/src/platform/app_menu.rs      |  96 ++++++++++
crates/gpui2/src/platform/mac/platform.rs  | 230 ++++++++++++-----------
crates/gpui2/src/platform/mac/window.rs    |   2 
crates/gpui2/src/platform/test/platform.rs |   2 
crates/gpui2/src/window.rs                 |   4 
8 files changed, 277 insertions(+), 121 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -425,6 +425,10 @@ impl AppContext {
             .collect()
     }
 
+    pub fn active_window(&self) -> Option<AnyWindowHandle> {
+        self.platform.active_window()
+    }
+
     /// Opens a new window with the given option and the root view returned by the given function.
     /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
     /// functionality.
@@ -1015,6 +1019,42 @@ impl AppContext {
         activate();
         subscription
     }
+
+    pub(crate) fn clear_pending_keystrokes(&mut self) {
+        for window in self.windows() {
+            window
+                .update(self, |_, cx| {
+                    cx.window
+                        .current_frame
+                        .dispatch_tree
+                        .clear_pending_keystrokes()
+                })
+                .ok();
+        }
+    }
+
+    pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
+        if let Some(window) = self.active_window() {
+            let window_action_available = window
+                .update(self, |_, cx| {
+                    if let Some(focus_id) = cx.window.focus {
+                        cx.window
+                            .current_frame
+                            .dispatch_tree
+                            .is_action_available(action, focus_id)
+                    } else {
+                        false
+                    }
+                })
+                .unwrap_or(false);
+            if window_action_available {
+                return true;
+            }
+        }
+
+        self.global_action_listeners
+            .contains_key(&action.as_any().type_id())
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/key_dispatch.rs 🔗

@@ -82,13 +82,13 @@ impl DispatchTree {
         }
     }
 
-    pub fn clear_keystroke_matchers(&mut self) {
+    pub fn clear_pending_keystrokes(&mut self) {
         self.keystroke_matchers.clear();
     }
 
     /// Preserve keystroke matchers from previous frames to support multi-stroke
     /// bindings across multiple frames.
-    pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
+    pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
         if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
             let dispatch_path = self.dispatch_path(node_id);
 
@@ -163,6 +163,22 @@ impl DispatchTree {
         actions
     }
 
+    pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool {
+        if let Some(node) = self.focusable_node_ids.get(&target) {
+            for node_id in self.dispatch_path(*node) {
+                let node = &self.nodes[node_id.0];
+                if node
+                    .action_listeners
+                    .iter()
+                    .any(|listener| listener.action_type == action.as_any().type_id())
+                {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
     pub fn bindings_for_action(
         &self,
         action: &dyn Action,

crates/gpui2/src/platform.rs 🔗

@@ -1,3 +1,4 @@
+mod app_menu;
 mod keystroke;
 #[cfg(target_os = "macos")]
 mod mac;
@@ -32,6 +33,7 @@ use std::{
 };
 use uuid::Uuid;
 
+pub use app_menu::*;
 pub use keystroke::*;
 #[cfg(target_os = "macos")]
 pub use mac::*;
@@ -59,7 +61,7 @@ pub trait Platform: 'static {
 
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
     fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
-    fn main_window(&self) -> Option<AnyWindowHandle>;
+    fn active_window(&self) -> Option<AnyWindowHandle>;
     fn open_window(
         &self,
         handle: AnyWindowHandle,

crates/gpui2/src/platform/app_menu.rs 🔗

@@ -0,0 +1,96 @@
+use crate::{Action, AppContext, Platform};
+use util::ResultExt;
+
+pub struct Menu<'a> {
+    pub name: &'a str,
+    pub items: Vec<MenuItem<'a>>,
+}
+
+pub enum MenuItem<'a> {
+    Separator,
+    Submenu(Menu<'a>),
+    Action {
+        name: &'a str,
+        action: Box<dyn Action>,
+        os_action: Option<OsAction>,
+    },
+}
+
+impl<'a> MenuItem<'a> {
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+
+    pub fn submenu(menu: Menu<'a>) -> Self {
+        Self::Submenu(menu)
+    }
+
+    pub fn action(name: &'a str, action: impl Action) -> Self {
+        Self::Action {
+            name,
+            action: Box::new(action),
+            os_action: None,
+        }
+    }
+
+    pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
+        Self::Action {
+            name,
+            action: Box::new(action),
+            os_action: Some(os_action),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum OsAction {
+    Cut,
+    Copy,
+    Paste,
+    SelectAll,
+    Undo,
+    Redo,
+}
+
+pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) {
+    platform.on_will_open_menu(Box::new({
+        let cx = cx.to_async();
+        move || {
+            cx.update(|cx| cx.clear_pending_keystrokes()).ok();
+        }
+    }));
+
+    platform.on_validate_menu_command(Box::new({
+        let cx = cx.to_async();
+        move |action| {
+            cx.update(|cx| cx.is_action_available(action))
+                .unwrap_or(false)
+        }
+    }));
+
+    platform.on_menu_command(Box::new({
+        let cx = cx.to_async();
+        move |action| {
+            cx.update(|cx| {
+                // if let Some(main_window) = cx.active_window() {
+                //     let dispatched = main_window
+                //         .update(&mut *cx, |cx| {
+                //             if let Some(view_id) = cx.focused_view_id() {
+                //                 cx.dispatch_action(Some(view_id), action);
+                //                 true
+                //             } else {
+                //                 false
+                //             }
+                //         })
+                //         .unwrap_or(false);
+
+                //     if dispatched {
+                //         return;
+                //     }
+                // }
+                // cx.dispatch_global_action_any(action);
+            })
+            .log_err();
+        }
+    }));
+}

crates/gpui2/src/platform/mac/platform.rs 🔗

@@ -1,16 +1,17 @@
 use super::BoolExt;
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
-    ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
-    MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow,
-    Result, SemanticVersion, VideoTimestamp, WindowOptions,
+    ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker,
+    MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
 use cocoa::{
     appkit::{
         NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
-        NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow,
+        NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString,
+        NSSavePanel, NSWindow,
     },
     base::{id, nil, BOOL, YES},
     foundation::{
@@ -237,114 +238,115 @@ impl MacPlatform {
     //     application_menu
     // }
 
-    // unsafe fn create_menu_item(
-    //     &self,
-    //     item: MenuItem,
-    //     delegate: id,
-    //     actions: &mut Vec<Box<dyn Action>>,
-    //     keystroke_matcher: &KeymapMatcher,
-    // ) -> id {
-    //     match item {
-    //         MenuItem::Separator => NSMenuItem::separatorItem(nil),
-    //         MenuItem::Action {
-    //             name,
-    //             action,
-    //             os_action,
-    //         } => {
-    //             // TODO
-    //             let keystrokes = keystroke_matcher
-    //                 .bindings_for_action(action.id())
-    //                 .find(|binding| binding.action().eq(action.as_ref()))
-    //                 .map(|binding| binding.keystrokes());
-    //             let selector = match os_action {
-    //                 Some(crate::OsAction::Cut) => selector("cut:"),
-    //                 Some(crate::OsAction::Copy) => selector("copy:"),
-    //                 Some(crate::OsAction::Paste) => selector("paste:"),
-    //                 Some(crate::OsAction::SelectAll) => selector("selectAll:"),
-    //                 Some(crate::OsAction::Undo) => selector("undo:"),
-    //                 Some(crate::OsAction::Redo) => selector("redo:"),
-    //                 None => selector("handleGPUIMenuItem:"),
-    //             };
-
-    //             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),
-    //                         (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
-    //                     ] {
-    //                         if *modifier {
-    //                             mask |= *flag;
-    //                         }
-    //                     }
-
-    //                     item = NSMenuItem::alloc(nil)
-    //                         .initWithTitle_action_keyEquivalent_(
-    //                             ns_string(name),
-    //                             selector,
-    //                             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(' ');
-    //                         }
-    //                         write!(&mut name, "{}", keystroke).unwrap();
-    //                     }
-    //                     name.push(']');
-
-    //                     item = NSMenuItem::alloc(nil)
-    //                         .initWithTitle_action_keyEquivalent_(
-    //                             ns_string(&name),
-    //                             selector,
-    //                             ns_string(""),
-    //                         )
-    //                         .autorelease();
-    //                 }
-    //             } else {
-    //                 item = NSMenuItem::alloc(nil)
-    //                     .initWithTitle_action_keyEquivalent_(
-    //                         ns_string(name),
-    //                         selector,
-    //                         ns_string(""),
-    //                     )
-    //                     .autorelease();
-    //             }
-
-    //             let tag = actions.len() as NSInteger;
-    //             let _: () = msg_send![item, setTag: tag];
-    //             actions.push(action);
-    //             item
-    //         }
-    //         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
-    //         }
-    //     }
-    // }
+    unsafe fn create_menu_item(
+        &self,
+        item: MenuItem,
+        delegate: id,
+        actions: &mut Vec<Box<dyn Action>>,
+        keystroke_matcher: &KeystrokeMatcher,
+    ) -> id {
+        todo!()
+        // match item {
+        //     MenuItem::Separator => NSMenuItem::separatorItem(nil),
+        //     MenuItem::Action {
+        //         name,
+        //         action,
+        //         os_action,
+        //     } => {
+        //         // TODO
+        //         let keystrokes = keystroke_matcher
+        //             .bindings_for_action(action.id())
+        //             .find(|binding| binding.action().eq(action.as_ref()))
+        //             .map(|binding| binding.keystrokes());
+        //         let selector = match os_action {
+        //             Some(crate::OsAction::Cut) => selector("cut:"),
+        //             Some(crate::OsAction::Copy) => selector("copy:"),
+        //             Some(crate::OsAction::Paste) => selector("paste:"),
+        //             Some(crate::OsAction::SelectAll) => selector("selectAll:"),
+        //             Some(crate::OsAction::Undo) => selector("undo:"),
+        //             Some(crate::OsAction::Redo) => selector("redo:"),
+        //             None => selector("handleGPUIMenuItem:"),
+        //         };
+
+        //         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),
+        //                     (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
+        //                 ] {
+        //                     if *modifier {
+        //                         mask |= *flag;
+        //                     }
+        //                 }
+
+        //                 item = NSMenuItem::alloc(nil)
+        //                     .initWithTitle_action_keyEquivalent_(
+        //                         ns_string(name),
+        //                         selector,
+        //                         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(' ');
+        //                     }
+        //                     write!(&mut name, "{}", keystroke).unwrap();
+        //                 }
+        //                 name.push(']');
+
+        //                 item = NSMenuItem::alloc(nil)
+        //                     .initWithTitle_action_keyEquivalent_(
+        //                         ns_string(&name),
+        //                         selector,
+        //                         ns_string(""),
+        //                     )
+        //                     .autorelease();
+        //             }
+        //         } else {
+        //             item = NSMenuItem::alloc(nil)
+        //                 .initWithTitle_action_keyEquivalent_(
+        //                     ns_string(name),
+        //                     selector,
+        //                     ns_string(""),
+        //                 )
+        //                 .autorelease();
+        //         }
+
+        //         let tag = actions.len() as NSInteger;
+        //         let _: () = msg_send![item, setTag: tag];
+        //         actions.push(action);
+        //         item
+        //     }
+        //     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
+        //     }
+        // }
+    }
 }
 
 impl Platform for MacPlatform {
@@ -479,8 +481,8 @@ impl Platform for MacPlatform {
         MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
     }
 
-    fn main_window(&self) -> Option<AnyWindowHandle> {
-        MacWindow::main_window()
+    fn active_window(&self) -> Option<AnyWindowHandle> {
+        MacWindow::active_window()
     }
 
     fn open_window(

crates/gpui2/src/platform/mac/window.rs 🔗

@@ -662,7 +662,7 @@ impl MacWindow {
         }
     }
 
-    pub fn main_window() -> Option<AnyWindowHandle> {
+    pub fn active_window() -> Option<AnyWindowHandle> {
         unsafe {
             let app = NSApplication::sharedApplication(nil);
             let main_window: id = msg_send![app, mainWindow];

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -127,7 +127,7 @@ impl Platform for TestPlatform {
         self.displays().iter().find(|d| d.id() == id).cloned()
     }
 
-    fn main_window(&self) -> Option<crate::AnyWindowHandle> {
+    fn active_window(&self) -> Option<crate::AnyWindowHandle> {
         unimplemented!()
     }
 

crates/gpui2/src/window.rs 🔗

@@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> {
         self.window
             .current_frame
             .dispatch_tree
-            .clear_keystroke_matchers();
+            .clear_pending_keystrokes();
         self.app.push_effect(Effect::FocusChanged {
             window_handle: self.window.handle,
             focused: Some(focus_id),
@@ -1177,7 +1177,7 @@ impl<'a> WindowContext<'a> {
         self.window
             .current_frame
             .dispatch_tree
-            .preserve_keystroke_matchers(
+            .preserve_pending_keystrokes(
                 &mut self.window.previous_frame.dispatch_tree,
                 self.window.focus,
             );