Update handling of 'pending' keys

Before this change if you had a matching binding and a pending key,
the matching binding happened unconditionally.

Now we will wait a second before triggering that binding to give you
time to complete the action.
This commit is contained in:
Conrad Irwin 2024-01-21 14:36:59 -07:00
parent b8ed83a452
commit b06e2eb6af
7 changed files with 137 additions and 44 deletions

View file

@ -1,6 +1,6 @@
use crate::{
Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch,
Keymap, Keystroke, KeystrokeMatcher, WindowContext,
Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
};
use collections::FxHashMap;
use parking_lot::Mutex;
@ -276,8 +276,9 @@ impl DispatchTree {
&mut self,
keystroke: &Keystroke,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) -> SmallVec<[KeyBinding; 1]> {
) -> KeymatchResult {
let mut actions = SmallVec::new();
let mut pending = false;
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
for node_id in dispatch_path {
@ -294,12 +295,13 @@ impl DispatchTree {
.entry(context_stack.clone())
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
let mut matches = keystroke_matcher.match_keystroke(keystroke, &context_stack);
actions.append(&mut matches);
let mut result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
pending = result.pending || pending;
actions.append(&mut result.actions);
context_stack.pop();
}
actions
KeymatchResult { actions, pending }
}
pub fn has_pending_keystrokes(&self) -> bool {

View file

@ -1,4 +1,4 @@
use crate::{Action, KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke};
use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::sync::Arc;
@ -9,6 +9,11 @@ pub struct KeystrokeMatcher {
keymap_version: KeymapVersion,
}
pub struct KeymatchResult {
pub actions: SmallVec<[Box<dyn Action>; 1]>,
pub pending: bool,
}
impl KeystrokeMatcher {
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
let keymap_version = keymap.lock().version();
@ -40,7 +45,7 @@ impl KeystrokeMatcher {
&mut self,
keystroke: &Keystroke,
context_stack: &[KeyContext],
) -> SmallVec<[KeyBinding; 1]> {
) -> KeymatchResult {
let keymap = self.keymap.lock();
// Clear pending keystrokes if the keymap has changed since the last matched keystroke.
if keymap.version() != self.keymap_version {
@ -49,7 +54,7 @@ impl KeystrokeMatcher {
}
let mut pending_key = None;
let mut found = SmallVec::new();
let mut actions = SmallVec::new();
for binding in keymap.bindings().rev() {
if !keymap.binding_enabled(binding, context_stack) {
@ -60,7 +65,7 @@ impl KeystrokeMatcher {
self.pending_keystrokes.push(candidate.clone());
match binding.match_keystrokes(&self.pending_keystrokes) {
KeyMatch::Matched => {
found.push(binding.clone());
actions.push(binding.action.boxed_clone());
}
KeyMatch::Pending => {
pending_key.get_or_insert(candidate);
@ -71,15 +76,15 @@ impl KeystrokeMatcher {
}
}
if !found.is_empty() {
self.pending_keystrokes.clear();
} else if let Some(pending_key) = pending_key {
let pending = if let Some(pending_key) = pending_key {
self.pending_keystrokes.push(pending_key);
true
} else {
self.pending_keystrokes.clear();
false
};
found
KeymatchResult { actions, pending }
}
}

View file

@ -359,7 +359,7 @@ impl PlatformInputHandler {
self.cx
.update(|cx| {
self.handler
.replace_text_in_range(replacement_range, text, cx)
.replace_text_in_range(replacement_range, text, cx);
})
.ok();
}
@ -392,6 +392,13 @@ impl PlatformInputHandler {
.ok()
.flatten()
}
pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) {
let Some(range) = self.handler.selected_text_range(cx) else {
return;
};
self.handler.replace_text_in_range(Some(range), &input, cx);
}
}
/// Zed's interface for handling text input from the platform's IME system

View file

@ -30,24 +30,26 @@ impl Keystroke {
pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
let mut possibilities = SmallVec::new();
match self.ime_key.as_ref() {
None => possibilities.push(self.clone()),
Some(ime_key) => {
possibilities.push(Keystroke {
modifiers: Modifiers {
control: self.modifiers.control,
alt: false,
shift: false,
command: false,
function: false,
},
key: ime_key.to_string(),
ime_key: None,
});
if ime_key != &self.key {
possibilities.push(Keystroke {
modifiers: Modifiers {
control: self.modifiers.control,
alt: false,
shift: false,
command: false,
function: false,
},
key: ime_key.to_string(),
ime_key: None,
});
}
possibilities.push(Keystroke {
ime_key: None,
..self.clone()
});
}
None => possibilities.push(self.clone()),
}
possibilities
}

View file

@ -1542,9 +1542,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
replacement_range,
text: text.to_string(),
});
if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key {
pending_key_down.0.keystroke.ime_key = Some(text.to_string());
}
pending_key_down.0.keystroke.ime_key = Some(text.to_string());
window_state.lock().pending_key_down = Some(pending_key_down);
}
}

View file

@ -1,7 +1,8 @@
use crate::{
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds,
WindowOptions,
};
use collections::HashMap;
use parking_lot::Mutex;

View file

@ -4,13 +4,13 @@ use crate::{
DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect,
Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
ImageData, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent,
KeystrokeEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton,
MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel,
Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
Shadow, SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine,
Task, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions,
SUBPIXEL_VARIANTS,
KeymatchResult, KeystrokeEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite,
MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite,
PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
Scene, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, Surface,
TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
@ -38,6 +38,7 @@ use std::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
time::Duration,
};
use util::{post_inc, ResultExt};
@ -282,11 +283,20 @@ pub struct Window {
activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) focus_invalidated: bool,
}
#[derive(Default)]
struct PendingInput {
text: String,
actions: SmallVec<[Box<dyn Action>; 1]>,
focus: Option<FocusId>,
timer: Option<Task<()>>,
}
pub(crate) struct ElementStateBox {
inner: Box<dyn Any>,
parent_view_id: EntityId,
@ -506,6 +516,7 @@ impl Window {
activation_observers: SubscriberSet::new(),
focus: None,
focus_enabled: true,
pending_input: None,
#[cfg(any(test, feature = "test-support"))]
focus_invalidated: false,
@ -1785,21 +1796,56 @@ impl<'a> WindowContext<'a> {
.dispatch_path(node_id);
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
let bindings = self
let KeymatchResult { actions, pending } = self
.window
.rendered_frame
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
if !bindings.is_empty() {
if pending {
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
{
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
}
for action in actions {
currently_pending.actions.push(action);
}
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.replay_pending_input(currently_pending)
})
.log_err();
}));
self.window.pending_input = Some(currently_pending);
self.propagate_event = false;
return;
} else if let Some(currently_pending) = self.window.pending_input.take() {
if actions.is_empty() {
self.replay_pending_input(currently_pending)
}
}
if !actions.is_empty() {
self.clear_pending_keystrokes();
}
self.propagate_event = true;
for binding in bindings {
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
for action in actions {
self.dispatch_action_on_node(node_id, action.boxed_clone());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action));
self.dispatch_keystroke_observers(event, Some(action));
return;
}
}
@ -1840,6 +1886,38 @@ impl<'a> WindowContext<'a> {
.has_pending_keystrokes()
}
fn replay_pending_input(&mut self, currently_pending: PendingInput) {
let node_id = self
.window
.focus
.and_then(|focus_id| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focus_id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
if self.window.focus != currently_pending.focus {
return;
}
self.propagate_event = true;
for action in currently_pending.actions {
self.dispatch_action_on_node(node_id, action);
if !self.propagate_event {
return;
}
}
if !currently_pending.text.is_empty() {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
input_handler.flush_pending_input(&currently_pending.text, self);
self.window.platform_window.set_input_handler(input_handler)
}
}
}
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
let dispatch_path = self
.window