@@ -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,
@@ -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);
@@ -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| {