Action release handlers (#8782)

This PR adds support for handling action releases — events that
are fired when the user releases all the modifier keys that were part of
an action-triggering shortcut.

If the user holds modifiers and invokes several actions sequentially via
shortcuts (same or different), only the last action is "released" when
its modifier keys released.

~The following methods were added to `Div`:~
- ~`capture_action_release()`~
- ~`on_action_release()`~
- ~`on_boxed_action_release()`~

~They work similarly to `capture_action()`, `on_action()` and
`on_boxed_action()`.~

See the implementation details in [this
comment](https://github.com/zed-industries/zed/pull/8782#issuecomment-2009154646).

Release Notes:

- Added a fast-switch mode to the file finder: hit `p` or `shift-p`
while holding down `cmd` to select a file immediately. (#8258).

Related Issues:

- Implements #8757 
- Implements #8258
- Part of #7653 

Co-authored-by: @ConradIrwin
This commit is contained in:
Andrew Lygin 2024-03-21 03:43:31 +03:00 committed by GitHub
parent 91ab95ec82
commit 5602c48136
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 260 additions and 31 deletions

View file

@ -18,10 +18,10 @@
use crate::{
point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, Hitbox,
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render,
ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility,
WindowContext,
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
StyleRefinement, Styled, Task, View, Visibility, WindowContext,
};
use collections::HashMap;
use refineable::Refineable;
@ -389,6 +389,18 @@ impl Interactivity {
}));
}
/// Bind the given callback to modifiers changing events.
/// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`]
///
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
pub fn on_modifiers_changed(
&mut self,
listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static,
) {
self.modifiers_changed_listeners
.push(Box::new(move |event, cx| listener(event, cx)));
}
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
/// The imperative API equivalent to [`InteractiveElement::on_drop`]
///
@ -775,6 +787,18 @@ pub trait InteractiveElement: Sized {
self
}
/// Bind the given callback to modifiers changing events.
/// The fluent API equivalent to [`Interactivity::on_modifiers_changed`]
///
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_modifiers_changed(
mut self,
listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().on_modifiers_changed(listener);
self
}
/// Apply the given style when the given data type is dragged over this element
fn drag_over<S: 'static>(
mut self,
@ -999,6 +1023,9 @@ pub(crate) type KeyDownListener =
pub(crate) type KeyUpListener =
Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>;
pub(crate) type ModifiersChangedListener =
Box<dyn Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static>;
pub(crate) type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
/// Construct a new [`Div`] element
@ -1188,6 +1215,7 @@ pub struct Interactivity {
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
pub(crate) key_down_listeners: Vec<KeyDownListener>,
pub(crate) key_up_listeners: Vec<KeyUpListener>,
pub(crate) modifiers_changed_listeners: Vec<ModifiersChangedListener>,
pub(crate) action_listeners: Vec<(TypeId, ActionListener)>,
pub(crate) drop_listeners: Vec<(TypeId, DropListener)>,
pub(crate) can_drop_predicate: Option<CanDropPredicate>,
@ -1873,6 +1901,7 @@ impl Interactivity {
fn paint_keyboard_listeners(&mut self, cx: &mut ElementContext) {
let key_down_listeners = mem::take(&mut self.key_down_listeners);
let key_up_listeners = mem::take(&mut self.key_up_listeners);
let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners);
let action_listeners = mem::take(&mut self.action_listeners);
if let Some(context) = self.key_context.clone() {
cx.set_key_context(context);
@ -1893,6 +1922,12 @@ impl Interactivity {
})
}
for listener in modifiers_changed_listeners {
cx.on_modifiers_changed(move |event: &ModifiersChangedEvent, cx| {
listener(event, cx);
})
}
for (action_type, listener) in action_listeners {
cx.on_action(action_type, listener)
}

View file

@ -51,7 +51,8 @@
///
use crate::{
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent,
WindowContext,
};
use collections::FxHashMap;
use smallvec::SmallVec;
@ -82,6 +83,7 @@ pub(crate) struct DispatchTree {
pub(crate) struct DispatchNode {
pub key_listeners: Vec<KeyListener>,
pub action_listeners: Vec<DispatchActionListener>,
pub modifiers_changed_listeners: Vec<ModifiersChangedListener>,
pub context: Option<KeyContext>,
pub focus_id: Option<FocusId>,
view_id: Option<EntityId>,
@ -106,6 +108,7 @@ impl ReusedSubtree {
}
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut ElementContext)>;
#[derive(Clone)]
pub(crate) struct DispatchActionListener {
@ -241,6 +244,7 @@ impl DispatchTree {
let target = self.active_node();
target.key_listeners = mem::take(&mut source.key_listeners);
target.action_listeners = mem::take(&mut source.action_listeners);
target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners);
}
pub fn reuse_subtree(&mut self, old_range: Range<usize>, source: &mut Self) -> ReusedSubtree {
@ -310,6 +314,12 @@ impl DispatchTree {
self.active_node().key_listeners.push(listener);
}
pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) {
self.active_node()
.modifiers_changed_listeners
.push(listener);
}
pub fn on_action(
&mut self,
action_type: TypeId,

View file

@ -229,4 +229,13 @@ impl Modifiers {
..Default::default()
}
}
/// Checks if this Modifiers is a subset of another Modifiers
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
(other.control || !self.control)
&& (other.alt || !self.alt)
&& (other.shift || !self.shift)
&& (other.command || !self.command)
&& (other.function || !self.function)
}
}

View file

@ -4,10 +4,11 @@ use crate::{
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
FileDropEvent, Flatten, Global, GlobalElementId, GlobalPixels, Hsla, KeyBinding, KeyDownEvent,
KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers,
MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInput, PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, View,
VisualContext, WeakView, WindowAppearance, WindowOptions, WindowParams, WindowTextSystem,
ModifiersChangedEvent, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render, ScaledPixels,
SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
TextStyleRefinement, View, VisualContext, WeakView, WindowAppearance, WindowOptions,
WindowParams, WindowTextSystem,
};
use anyhow::{anyhow, Context as _, Result};
use collections::FxHashSet;
@ -1381,6 +1382,11 @@ impl<'a> WindowContext<'a> {
return;
}
self.dispatch_modifiers_changed_event(event, &dispatch_path);
if !self.propagate_event {
return;
}
self.dispatch_keystroke_observers(event, None);
}
@ -1418,6 +1424,27 @@ impl<'a> WindowContext<'a> {
}
}
fn dispatch_modifiers_changed_event(
&mut self,
event: &dyn Any,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) {
let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() else {
return;
};
for node_id in dispatch_path.iter().rev() {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for listener in node.modifiers_changed_listeners.clone() {
self.with_element_context(|cx| {
listener(event, cx);
});
if !self.propagate_event {
return;
}
}
}
}
/// Determine whether a potential multi-stroke key binding is in progress on this window.
pub fn has_pending_keystrokes(&self) -> bool {
self.window

View file

@ -33,10 +33,11 @@ use crate::{
ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase, DispatchTree,
DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId,
GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId,
LineLayoutIndex, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler,
Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
Shadow, SharedString, Size, StrikethroughStyle, Style, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels,
PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams,
RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext,
SUBPIXEL_VARIANTS,
};
pub(crate) type AnyMouseListener =
@ -1324,4 +1325,22 @@ impl<'a> ElementContext<'a> {
},
));
}
/// Register a modifiers changed event listener on the window for the next frame.
///
/// This is a fairly low-level method, so prefer using event handlers on elements unless you have
/// a specific need to register a global listener.
pub fn on_modifiers_changed(
&mut self,
listener: impl Fn(&ModifiersChangedEvent, &mut ElementContext) + 'static,
) {
self.window
.next_frame
.dispatch_tree
.on_modifiers_changed(Rc::new(
move |event: &ModifiersChangedEvent, cx: &mut ElementContext<'_>| {
listener(event, cx)
},
));
}
}