From 7a3c09c05d66d0fcfbcea33d19cd6aaa8a5c3825 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 03:34:00 -0800 Subject: [PATCH] Keyboard Shortcuts: the backtick key's label ignores the next key's text on the web eframe's web build sends each key's text before its press. When several keys arrive in one frame, the editor paired the backtick press with the text typed by the key after it, so a shortcut on that key could show the next key's character. The editor now pairs each text with its press through `shortcuts::typing_presses`, which shortcut dispatch already uses, for both event orders. --- crates/ui-egui/src/shortcut_editor.rs | 30 ++++++++++++++++----------- crates/ui-egui/src/shortcuts.rs | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 3b4671024..50026209e 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -274,19 +274,18 @@ pub fn chord_from_event(key: Key, m: Modifiers) -> Option { ui_mirror!(fn backtick_store() -> Option); -/// Notes what the backtick key (left of 1) typed in `events`: the one-character text whose key, a -/// press of it without modifiers, comes right before it (egui-winit) or, when no press comes -/// before, right after it (eframe's web build with no text field focused). egui reports that key -/// as [`Key::Backtick`] on layouts where it types a character egui has no key for, such as `"` on -/// Turkish Q. +/// Notes what the backtick key (left of 1) typed in `events`: the last one-character text that +/// [`crate::shortcuts::typing_presses`] pairs with a press of that key without modifiers, whether +/// the press comes before its text (egui-winit) or after it (eframe's web build with no text field +/// focused). egui reports that key as [`Key::Backtick`] on layouts where it types a character egui +/// has no key for, such as `"` on Turkish Q. pub(crate) fn note_backtick_text(events: &[egui::Event]) { - let is_press = |e: Option<&egui::Event>| matches!(e, Some(egui::Event::Key { pressed: true, .. })); - let is_backtick = - |e: Option<&egui::Event>| matches!(e, Some(egui::Event::Key { key: Key::Backtick, pressed: true, modifiers, .. }) if modifiers.is_none()); - let typed = events.iter().enumerate().rev().find_map(|(j, e)| match e { - egui::Event::Text(t) if t.chars().count() == 1 && !t.chars().any(char::is_whitespace) => { - let before = j.checked_sub(1).and_then(|j| events.get(j)); - (is_backtick(before) || (!is_press(before) && is_backtick(events.get(j + 1)))).then(|| t.clone()) + let typed_by = crate::shortcuts::typing_presses(events); + let typed = events.iter().zip(typed_by).rev().find_map(|(e, by)| match (e, by.and_then(|k| events.get(k))) { + (egui::Event::Text(t), Some(egui::Event::Key { key: Key::Backtick, modifiers, .. })) + if modifiers.is_none() && t.chars().count() == 1 && !t.chars().any(char::is_whitespace) => + { + Some(t.clone()) } _ => None, }); @@ -1322,6 +1321,13 @@ mod tests { assert_eq!(backtick_text().as_deref(), Some("\"")); note_backtick_text(&[text("`"), press(Key::Backtick)]); assert_eq!(backtick_text(), None); + // Several keys typed in one frame on the web: each text goes with the press after it. + let web = [text("a"), press(Key::A), text("\""), press(Key::Backtick), text("b"), press(Key::B)]; + note_backtick_text(&web); + assert_eq!(backtick_text().as_deref(), Some("\"")); + note_backtick_text(&[text("`"), press(Key::Backtick)]); + note_backtick_text(&[press(Key::A), text("a"), press(Key::Backtick), text("\""), press(Key::B), text("b")]); + assert_eq!(backtick_text().as_deref(), Some("\"")); // Only the unshifted key types that character. assert_eq!(on_layout("Shift+~".into(), Some("\"")), "Shift+~"); assert_eq!(on_layout("Cmd+~".into(), Some("\"")), "Cmd+\""); diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index b551b758d..f8aaf2dd2 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -929,7 +929,7 @@ fn latin(text: &str, physical: Option) -> String { /// types at most one text. A text takes the press after it only when every later text in the same /// run of alternating texts and presses has a press after it too; otherwise the text after that /// press takes it as the press before it, and this text has none. -fn typing_presses(events: &[egui::Event]) -> Vec> { +pub(crate) fn typing_presses(events: &[egui::Event]) -> Vec> { let is_press = |k: usize| matches!(events.get(k), Some(egui::Event::Key { key, pressed: true, .. }) if !is_modifier(*key)); let is_text = |k: usize| matches!(events.get(k), Some(egui::Event::Text(_))); // Whether the run of alternating texts and presses that starts with the text at `j` ends