Improve logic for reused bindings, add docs.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user