Improve logic for reused bindings, add docs.

This commit is contained in:
Conrad Irwin
2024-01-22 09:53:04 -07:00
parent 6c4d024237
commit 0903d29ab3
3 changed files with 100 additions and 26 deletions
+51
View File
@@ -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,
+1 -1
View File
@@ -94,7 +94,7 @@ impl KeystrokeMatcher {
/// - KeyMatch::None => No match is valid for this key given any pending keystrokes.
/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys.
/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses.
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum KeyMatch {
None,
Pending,
+48 -25
View File
@@ -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(&currently_pending.text, self);
input_handler.flush_pending_input(&input, self);
self.window.platform_window.set_input_handler(input_handler)
}
}