Dispatch actions on focused node

Allows us to implement context menu matching nicely
This commit is contained in:
Conrad Irwin 2023-12-04 22:54:48 +00:00
parent 79773178c8
commit c82fea375d
4 changed files with 85 additions and 31 deletions

View file

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

View file

@ -221,20 +221,6 @@ pub trait InteractiveElement: Sized + Element {
/// Add a listener for the given action, fires during the bubble event phase /// 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 { 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(( self.interactivity().action_listeners.push((
TypeId::of::<A>(), TypeId::of::<A>(),
Box::new(move |action, phase, cx| { Box::new(move |action, phase, cx| {
@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element {
self 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( fn on_key_down(
mut self, mut self,
listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,

View file

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

View file

@ -7,7 +7,7 @@ use gpui::{
IntoElement, Render, View, VisualContext, IntoElement, Render, View, VisualContext,
}; };
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use std::rc::Rc; use std::{rc::Rc, time::Duration};
pub enum ContextMenuItem { pub enum ContextMenuItem {
Separator, Separator,
@ -16,7 +16,7 @@ pub enum ContextMenuItem {
label: SharedString, label: SharedString,
icon: Option<Icon>, icon: Option<Icon>,
handler: Rc<dyn Fn(&mut WindowContext)>, 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 { self.items.push(ContextMenuItem::Entry {
label: label.into(), label: label.into(),
handler: Rc::new(on_click), handler: Rc::new(on_click),
key_binding: None,
icon: None, icon: None,
action: None,
}); });
self self
} }
@ -84,7 +84,7 @@ impl ContextMenu {
) -> Self { ) -> Self {
self.items.push(ContextMenuItem::Entry { self.items.push(ContextMenuItem::Entry {
label: label.into(), 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())), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
icon: None, icon: None,
}); });
@ -99,7 +99,7 @@ impl ContextMenu {
) -> Self { ) -> Self {
self.items.push(ContextMenuItem::Entry { self.items.push(ContextMenuItem::Entry {
label: label.into(), 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())), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
icon: Some(Icon::Link), icon: Some(Icon::Link),
}); });
@ -161,6 +161,36 @@ impl ContextMenu {
self.select_last(&Default::default(), cx); 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 { impl ContextMenuItem {
@ -185,6 +215,22 @@ impl Render for ContextMenu {
.on_action(cx.listener(ContextMenu::select_prev)) .on_action(cx.listener(ContextMenu::select_prev))
.on_action(cx.listener(ContextMenu::confirm)) .on_action(cx.listener(ContextMenu::confirm))
.on_action(cx.listener(ContextMenu::cancel)) .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() .flex_none()
.child( .child(
List::new().children(self.items.iter().enumerate().map( List::new().children(self.items.iter().enumerate().map(
@ -196,8 +242,8 @@ impl Render for ContextMenu {
ContextMenuItem::Entry { ContextMenuItem::Entry {
label, label,
handler, handler,
key_binding,
icon, icon,
action,
} => { } => {
let handler = handler.clone(); let handler = handler.clone();
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
@ -218,11 +264,10 @@ impl Render for ContextMenu {
.w_full() .w_full()
.justify_between() .justify_between()
.child(label_element) .child(label_element)
.children( .children(action.as_ref().and_then(|action| {
key_binding KeyBinding::for_action(&**action, cx)
.clone() .map(|binding| div().ml_1().child(binding))
.map(|binding| div().ml_1().child(binding)), })),
),
) )
.selected(Some(ix) == self.selected_index) .selected(Some(ix) == self.selected_index)
.on_click(move |event, cx| { .on_click(move |event, cx| {