diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index bb0cb06e0..086b32bd5 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -358,15 +358,18 @@ fn single_key_shortcuts(app: &mut VectorcraftApp, ctx: &egui::Context) { // Single-key tool shortcuts (no Cmd/Ctrl/Alt). With a layout without Latin letters (Persian, // Arabic, Russian, Greek…) the key's position stands in for what it types (#793). let events: Vec<(String, Modifiers)> = ctx.input(|i| { - let mut physical = None; + let typed_by = typing_presses(&i.events); i.events .iter() - .filter_map(|e| match e { - egui::Event::Key { physical_key, pressed: true, .. } => { - physical = *physical_key; - None + .zip(typed_by) + .filter_map(|(e, by)| match e { + egui::Event::Text(t) => { + let physical = match by.and_then(|k| i.events.get(k)) { + Some(egui::Event::Key { physical_key, .. }) => *physical_key, + _ => None, + }; + Some((latin(t, physical), i.modifiers)) } - egui::Event::Text(t) => Some((latin(t, physical), i.modifiers)), _ => None, }) .collect() @@ -843,18 +846,39 @@ fn latin(text: &str, physical: Option) -> String { } } +/// For each event, the index of the key press that typed it when it is text: the press right +/// before it (egui-winit) unless an earlier text took that press, or else the press right after it +/// (eframe's web build with no text field focused). Each press types at most one text. +fn typing_presses(events: &[egui::Event]) -> Vec> { + let is_press = |k: usize| matches!(events.get(k), Some(egui::Event::Key { pressed: true, .. })); + let mut taken = None; + events + .iter() + .enumerate() + .map(|(j, e)| { + if !matches!(e, egui::Event::Text(_)) { + return None; + } + let before = j.checked_sub(1).filter(|&k| is_press(k) && taken != Some(k)); + let by = before.or_else(|| Some(j + 1).filter(|&k| is_press(k))); + taken = by.or(taken); + by + }) + .collect() +} + /// Each new press's shortcut names, first match first, and the letter it types, without Cmd, Ctrl /// or Alt. A text-only press counts as well (Shift+~ arrives as the text `~` with Shift), unless -/// the key that typed it repeated. A text's key is the press right before it (egui-winit) or, -/// failing that, right after it (eframe's web build with no text field focused). Text reads its -/// key's modifiers, or the latest modifier change (`held` before any), and tries its key's own -/// name first. +/// the key that typed it repeated. A text's key is the one `typing_presses` pairs it with. Text +/// reads its key's modifiers, or the latest modifier change (`held` before any), and tries its +/// key's own name first. fn shortcut_presses(events: &[egui::Event], held: Modifiers) -> Vec<(Vec, Option)> { let key_name = |key: &Key, m: &Modifiers| if m.shift { format!("Shift+{}", key.name()) } else { key.name().to_string() }; let press_at = |j: Option| match j.and_then(|j| events.get(j)) { Some(egui::Event::Key { key, pressed: true, repeat, modifiers, .. }) => Some((*repeat, *modifiers, key_name(key, modifiers))), _ => None, }; + let typed_by = typing_presses(events); let mut latest = held; events .iter() @@ -868,8 +892,8 @@ fn shortcut_presses(events: &[egui::Event], held: Modifiers) -> Vec<(Vec Some((vec![key_name(key, modifiers)], letter_of(*key))) } egui::Event::Text(t) => { - let typed_by = press_at(j.checked_sub(1)).or_else(|| press_at(Some(j + 1))); - let (repeated, mods, key_name) = typed_by.map_or((false, latest, None), |(r, m, n)| (r, m, Some(n))); + let (repeated, mods, key_name) = + press_at(typed_by.get(j).copied().flatten()).map_or((false, latest, None), |(r, m, n)| (r, m, Some(n))); if repeated || mods.command || mods.alt || mods.ctrl { return None; } diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 553c57df4..49c41536e 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -1470,6 +1470,31 @@ fn one_key_shortcuts_follow_the_key_on_layouts_without_latin_letters() { assert_eq!(app.session.tool_id(), "directSelection", "AZERTY: the letter typed"); } +/// The web build sends a keystroke's text before its key press: the one-key shortcut on a layout +/// without Latin letters still goes by the key of the same keystroke, and two keystrokes in one +/// frame each use their own key, in either order. +#[test] +fn one_key_shortcuts_use_the_key_of_the_same_keystroke_in_either_order() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let press = |key| egui::Event::Key { key, physical_key: Some(key), pressed: true, repeat: false, modifiers: Modifiers::NONE }; + let text = |t: &str| egui::Event::Text(t.into()); + frame(&mut app, vec![text("з"), press(Key::P)]); + assert_eq!(app.session.tool_id(), "pen", "text before its key"); + frame(&mut app, vec![text("м"), press(Key::V), text("з"), press(Key::P)]); + assert_eq!(app.session.tool_id(), "pen", "the second text uses the second key"); + app.select_tool("pen"); + frame(&mut app, vec![press(Key::P), text("з"), press(Key::V), text("м")]); + assert_eq!(app.session.tool_id(), "selection", "key before its text"); + let names = |events: &[egui::Event]| -> Vec { + shortcut_presses(events, Modifiers::NONE).into_iter().filter_map(|(n, _)| n.into_iter().next()).collect() + }; + let web = names(&[text("м"), press(Key::V), text("з"), press(Key::P)]); + assert_eq!(web, ["V", "V", "P", "P"], "each text tries its own key's name first"); + let desktop = names(&[press(Key::V), text("м"), press(Key::P), text("з")]); + assert_eq!(desktop, ["V", "V", "P", "P"]); +} + #[test] fn plus_typed_any_way_zooms_in() { assert_eq!(parse("Cmd++"), parse("Cmd+="), "`+` and `=` are one chord key"); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index aef1c3790..a4207f302 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -617,6 +617,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols activate a background tab on the first press, hide its expanded dock or floating group when that tab is in front, and restore the group with all its tabs together on the next press; in an icon column they open and close the flyout while keeping the icons. A reassigned chord keeps that behavior, and Window > a hidden sibling restores the group. - Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected). - While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also open Layer Options for a new layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command. + - On a layout without Latin letters, a one-key shortcut goes by the position of the key that typed the character, in the web build too, where the character arrives before its key press: on a Russian layout the P key (`з`) chooses the Pen, and two keys pressed in one frame each run their own shortcut. - With the Artboard tool, Cmd/Ctrl+A after a click on a row of the Artboards panel selects every artboard, as the macOS menu bar's Select All does then; the active artboard stays active, and the art's selection does not change. After a press outside the panel's list, Cmd/Ctrl+A selects every object again. It used to select every object after a click in the panel too. Public reference: [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html). - Cmd/Ctrl+/ (`appearance.addFill`) and Cmd/Ctrl+Alt+/ (`appearance.addStroke`) add a new fill and a new stroke to the selected objects, as the Appearance panel's Add New Fill and Add New Stroke buttons do, on the keys the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for adding a new fill and a new stroke. Keyboard Shortcuts lists both under Window › Appearance. - Cmd/Ctrl+Alt+Shift+. and , (`type.size.increaseByFive`, `type.size.decreaseByFive`) step the font size of the selected type, or of the text the Type tool has selected, by five times the Size/Leading increment (Preferences › Type); Cmd/Ctrl+Shift+. and , still step it by one increment.