Dispatch actions on focused node

Conrad Irwin created

Allows us to implement context menu matching nicely

Change summary

crates/editor2/src/editor.rs              |  1 
crates/gpui2/src/elements/div.rs          | 31 ++++++-----
crates/gpui2/src/window.rs                | 17 ++++--
crates/ui2/src/components/context_menu.rs | 67 ++++++++++++++++++++----
4 files changed, 85 insertions(+), 31 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -3640,6 +3640,7 @@ impl Editor {
     }
 
     pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
+        dbg!("TOGGLE CODE ACTIONS");
         let mut context_menu = self.context_menu.write();
         if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
             *context_menu = None;

crates/gpui2/src/elements/div.rs 🔗

@@ -221,20 +221,6 @@ pub trait InteractiveElement: Sized + Element {
 
     /// Add a listener for the given action, fires during the bubble event phase
     fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
-        // NOTE: this debug assert has the side-effect of working around
-        // a bug where a crate consisting only of action definitions does
-        // not register the actions in debug builds:
-        //
-        // https://github.com/rust-lang/rust/issues/47384
-        // https://github.com/mmastrac/rust-ctor/issues/280
-        //
-        // if we are relying on this side-effect still, removing the debug_assert!
-        // likely breaks the command_palette tests.
-        // debug_assert!(
-        //     A::is_registered(),
-        //     "{:?} is not registered as an action",
-        //     A::qualified_name()
-        // );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |action, phase, cx| {
@@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element {
         self
     }
 
+    fn on_boxed_action(
+        mut self,
+        action: &Box<dyn Action>,
+        listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
+    ) -> Self {
+        let action = action.boxed_clone();
+        self.interactivity().action_listeners.push((
+            (*action).type_id(),
+            Box::new(move |_, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    (listener)(&action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
     fn on_key_down(
         mut self,
         listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,

crates/gpui2/src/window.rs 🔗

@@ -1348,6 +1348,8 @@ impl<'a> WindowContext<'a> {
                 .dispatch_tree
                 .dispatch_path(node_id);
 
+            let mut actions: Vec<Box<dyn Action>> = Vec::new();
+
             // Capture phase
             let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
             self.propagate_event = true;
@@ -1382,22 +1384,26 @@ impl<'a> WindowContext<'a> {
                 let node = self.window.current_frame.dispatch_tree.node(*node_id);
                 if !node.context.is_empty() {
                     if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
-                        if let Some(action) = self
+                        if let Some(found) = self
                             .window
                             .current_frame
                             .dispatch_tree
                             .dispatch_key(&key_down_event.keystroke, &context_stack)
                         {
-                            self.dispatch_action_on_node(*node_id, action);
-                            if !self.propagate_event {
-                                return;
-                            }
+                            actions.push(found.boxed_clone())
                         }
                     }
 
                     context_stack.pop();
                 }
             }
+
+            for action in actions {
+                self.dispatch_action_on_node(node_id, action);
+                if !self.propagate_event {
+                    return;
+                }
+            }
         }
     }
 
@@ -1425,7 +1431,6 @@ impl<'a> WindowContext<'a> {
                 }
             }
         }
-
         // Bubble phase
         for node_id in dispatch_path.iter().rev() {
             let node = self.window.current_frame.dispatch_tree.node(*node_id);

crates/ui2/src/components/context_menu.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     IntoElement, Render, View, VisualContext,
 };
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
-use std::rc::Rc;
+use std::{rc::Rc, time::Duration};
 
 pub enum ContextMenuItem {
     Separator,
@@ -16,7 +16,7 @@ pub enum ContextMenuItem {
         label: SharedString,
         icon: Option<Icon>,
         handler: Rc<dyn Fn(&mut WindowContext)>,
-        key_binding: Option<KeyBinding>,
+        action: Option<Box<dyn Action>>,
     },
 }
 
@@ -70,8 +70,8 @@ impl ContextMenu {
         self.items.push(ContextMenuItem::Entry {
             label: label.into(),
             handler: Rc::new(on_click),
-            key_binding: None,
             icon: None,
+            action: None,
         });
         self
     }
@@ -84,7 +84,7 @@ impl ContextMenu {
     ) -> Self {
         self.items.push(ContextMenuItem::Entry {
             label: label.into(),
-            key_binding: KeyBinding::for_action(&*action, cx),
+            action: Some(action.boxed_clone()),
             handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
             icon: None,
         });
@@ -99,7 +99,7 @@ impl ContextMenu {
     ) -> Self {
         self.items.push(ContextMenuItem::Entry {
             label: label.into(),
-            key_binding: KeyBinding::for_action(&*action, cx),
+            action: Some(action.boxed_clone()),
             handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
             icon: Some(Icon::Link),
         });
@@ -161,6 +161,36 @@ impl ContextMenu {
             self.select_last(&Default::default(), cx);
         }
     }
+
+    pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.items.iter().position(|item| {
+            if let ContextMenuItem::Entry {
+                action: Some(action),
+                ..
+            } = item
+            {
+                action.partial_eq(&**dispatched)
+            } else {
+                false
+            }
+        }) {
+            self.selected_index = Some(ix);
+            cx.notify();
+            let action = dispatched.boxed_clone();
+            cx.spawn(|this, mut cx| async move {
+                cx.background_executor()
+                    .timer(Duration::from_millis(50))
+                    .await;
+                this.update(&mut cx, |this, cx| {
+                    cx.dispatch_action(action);
+                    this.cancel(&Default::default(), cx)
+                })
+            })
+            .detach_and_log_err(cx);
+        } else {
+            cx.propagate()
+        }
+    }
 }
 
 impl ContextMenuItem {
@@ -185,6 +215,22 @@ impl Render for ContextMenu {
                 .on_action(cx.listener(ContextMenu::select_prev))
                 .on_action(cx.listener(ContextMenu::confirm))
                 .on_action(cx.listener(ContextMenu::cancel))
+                .map(|mut el| {
+                    for item in self.items.iter() {
+                        if let ContextMenuItem::Entry {
+                            action: Some(action),
+                            ..
+                        } = item
+                        {
+                            el = el.on_boxed_action(
+                                action,
+                                cx.listener(ContextMenu::on_action_dispatch),
+                            );
+                        }
+                    }
+                    el
+                })
+                .on_blur(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
                 .flex_none()
                 .child(
                     List::new().children(self.items.iter().enumerate().map(
@@ -196,8 +242,8 @@ impl Render for ContextMenu {
                             ContextMenuItem::Entry {
                                 label,
                                 handler,
-                                key_binding,
                                 icon,
+                                action,
                             } => {
                                 let handler = handler.clone();
                                 let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
@@ -218,11 +264,10 @@ impl Render for ContextMenu {
                                             .w_full()
                                             .justify_between()
                                             .child(label_element)
-                                            .children(
-                                                key_binding
-                                                    .clone()
-                                                    .map(|binding| div().ml_1().child(binding)),
-                                            ),
+                                            .children(action.as_ref().and_then(|action| {
+                                                KeyBinding::for_action(&**action, cx)
+                                                    .map(|binding| div().ml_1().child(binding))
+                                            })),
                                     )
                                     .selected(Some(ix) == self.selected_index)
                                     .on_click(move |event, cx| {