Dispatch actions on focused node
Allows us to implement context menu matching nicely
This commit is contained in:
parent
79773178c8
commit
c82fea375d
4 changed files with 85 additions and 31 deletions
|
@ -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;
|
||||
|
|
|
@ -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| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue