@@ -1,3 +1,54 @@
+/// KeyDispatch is where GPUI deals with binding actions to key events.
+///
+/// The key pieces to making a key binding work are to define an action,
+/// implement a method that takes that action as a type paramater,
+/// and then to register the action during render on a focused node
+/// with a keymap context:
+///
+/// ```rust
+/// actions!(editor,[Undo, Redo]);;
+///
+/// impl Editor {
+/// fn undo(&mut self, _: &Undo, _cx: &mut ViewContext<Self>) { ... }
+/// fn redo(&mut self, _: &Redo, _cx: &mut ViewContext<Self>) { ... }
+/// }
+///
+/// impl Render for Editor {
+/// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+/// div()
+/// .track_focus(&self.focus_handle)
+/// .keymap_context("Editor")
+/// .on_action(cx.listener(Editor::undo))
+/// .on_action(cx.listener(Editor::redo))
+/// ...
+/// }
+/// }
+///```
+///
+/// The keybindings themselves are managed independently by calling cx.bind_keys().
+/// (Though mostly when developing Zed itself, you just need to add a new line to
+/// assets/keymaps/default.json).
+///
+/// ```rust
+/// cx.bind_keys([
+/// KeyBinding::new("cmd-z", Editor::undo, Some("Editor")),
+/// KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")),
+/// ])
+/// ```
+///
+/// With all of this in place, GPUI will ensure that if you have an Editor that contains
+/// the focus, hitting cmd-z will Undo.
+///
+/// In real apps, it is a little more complicated than this, because typically you have
+/// several nested views that each register keyboard handlers. In this case action matching
+/// bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined
+/// then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace.
+///
+/// In GPUI, keybindings are not limited to just single keystrokes, you can define
+/// sequences by separating the keys with a space:
+///
+/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
+///
use crate::{
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
@@ -2,11 +2,11 @@ use crate::{
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
- GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeymatchResult, KeystrokeEvent,
- Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels,
- PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render,
- ScaledPixels, SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task, View,
- VisualContext, WeakView, WindowBounds, WindowOptions,
+ GlobalElementId, Hsla, KeyBinding, KeyContext, 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, View, VisualContext, WeakView, WindowBounds, WindowOptions,
};
use anyhow::{anyhow, Context as _, Result};
use collections::FxHashSet;
@@ -289,12 +289,39 @@ pub struct Window {
#[derive(Default, Debug)]
struct PendingInput {
- text: String,
+ keystrokes: SmallVec<[Keystroke; 1]>,
bindings: SmallVec<[KeyBinding; 1]>,
focus: Option<FocusId>,
timer: Option<Task<()>>,
}
+impl PendingInput {
+ fn is_noop(&self) -> bool {
+ self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none()))
+ }
+
+ fn input(&self) -> String {
+ self.keystrokes
+ .iter()
+ .flat_map(|k| k.ime_key.clone())
+ .collect::<Vec<String>>()
+ .join("")
+ }
+
+ fn used_by_binding(&self, binding: &KeyBinding) -> bool {
+ if self.keystrokes.is_empty() {
+ return true;
+ }
+ let keystroke = &self.keystrokes[0];
+ for candidate in keystroke.match_candidates() {
+ if binding.match_keystrokes(&[candidate]) == KeyMatch::Pending {
+ return true;
+ }
+ }
+ false
+ }
+}
+
pub(crate) struct ElementStateBox {
pub(crate) inner: Box<dyn Any>,
pub(crate) parent_view_id: EntityId,
@@ -1179,15 +1206,15 @@ impl<'a> WindowContext<'a> {
currently_pending = PendingInput::default();
}
currently_pending.focus = self.window.focus;
- if let Some(new_text) = &key_down_event.keystroke.ime_key.as_ref() {
- currently_pending.text += new_text
- }
+ currently_pending
+ .keystrokes
+ .push(key_down_event.keystroke.clone());
for binding in bindings {
currently_pending.bindings.push(binding);
}
// for vim compatibility, we also should check "is input handler enabled"
- if !currently_pending.text.is_empty() || !currently_pending.bindings.is_empty() {
+ if !currently_pending.is_noop() {
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
@@ -1199,24 +1226,18 @@ impl<'a> WindowContext<'a> {
})
.log_err();
}));
- self.window.pending_input = Some(currently_pending);
+ } else {
+ currently_pending.timer = None;
}
+ self.window.pending_input = Some(currently_pending);
self.propagate_event = false;
return;
} else if let Some(currently_pending) = self.window.pending_input.take() {
- // if you have bound , to one thing, and ,w to another.
- // then typing ,i should trigger the comma actions, then the i actions.
- // in that scenario "binding.keystrokes" is "i" and "pending.keystrokes" is ",".
- // on the other hand if you type ,, it should not trigger the , action.
- // in that scenario "binding.keystrokes" is ",w" and "pending.keystrokes" is ",".
-
- if bindings.iter().all(|binding| {
- currently_pending
- .bindings
- .iter()
- .all(|pending| binding.keystrokes().starts_with(&pending.keystrokes))
- }) {
+ if bindings
+ .iter()
+ .all(|binding| !currently_pending.used_by_binding(&binding))
+ {
self.replay_pending_input(currently_pending)
}
}
@@ -1290,6 +1311,8 @@ impl<'a> WindowContext<'a> {
return;
}
+ let input = currently_pending.input();
+
self.propagate_event = true;
for binding in currently_pending.bindings {
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
@@ -1298,9 +1321,9 @@ impl<'a> WindowContext<'a> {
}
}
- if !currently_pending.text.is_empty() {
+ if !input.is_empty() {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
- input_handler.flush_pending_input(¤tly_pending.text, self);
+ input_handler.flush_pending_input(&input, self);
self.window.platform_window.set_input_handler(input_handler)
}
}