From 2ebc607ed2e1e353399d5586932e9ba6991e1b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Thu, 15 Oct 2026 16:39:38 +0100 Subject: [PATCH 1/2] Fix #2094: Cmd+Shift+O Create Outlines while typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep Type tool from inserting Text when Cmd/Ctrl/Alt is held so the chord outlines instead of typing, and cover Selection and Type-tool paths so File › Open and the Artboard tool cannot steal it. Co-authored-by: Cursor --- crates/ui-egui/src/menus.rs | 3 +-- crates/ui-egui/src/shortcuts.rs | 46 ++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 7f32ab483..81bcbf5a9 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -4137,8 +4137,7 @@ mod tests { Item::Cmd(_, id, _) if vectorcraft_engine::find_command(id).is_none() && !UI_COMMANDS.iter().any(|c| c.0 == *id) - && !id.starts_with("object.path.") - && *id != "type.createOutlines" => + && !id.starts_with("object.path.") => { bad.push(id.to_string()); } diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 11b1115c1..0c078d6e6 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -178,13 +178,16 @@ fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) { let view = app.view_info(); // Switching apps mid-composition needs nothing: the macOS IME keeps its marked text and goes // on with it when the window comes back (measured with Kotoeri), as the tool does. + // Cmd/Ctrl/Alt chords are shortcuts (Create Outlines, Bold, …): never type their letter into + // the text if a host still emits `Text` for them (#2094). + let shortcut_mods = ctx.input(|i| i.modifiers.command || i.modifiers.ctrl || i.modifiers.alt); let events: Vec = ctx.input(|i| i.events.iter().filter(|e| matches!(e, egui::Event::Text(_) | egui::Event::Ime(_))).cloned().collect()); for e in events { // Errors are already reported by the engine's interaction (a failed preview keeps it). let _ = match e { // Keys the IME passed through come as text only when nothing is marked. - egui::Event::Text(t) if !app.session.tool_composing() => app.session.tool_text(&t, view), + egui::Event::Text(t) if !app.session.tool_composing() && !shortcut_mods => app.session.tool_text(&t, view), egui::Event::Ime(egui::ImeEvent::Preedit { text, active_range_chars }) => { app.ime_marked = Some(text.clone()).filter(|t| !t.is_empty()); app.session.tool_preedit(&text, active_range_chars, view) @@ -532,6 +535,47 @@ mod tests { out } + /// #2094: Cmd+Shift+O is Type › Create Outlines (not File › Open's Cmd+O, not the Artboard + /// tool's Shift+O), with the Selection tool or while the Type tool edits the text. + #[test] + fn cmd_shift_o_creates_outlines() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + assert_eq!(crate::menus::shortcut_of("type.createOutlines"), Some("Cmd+Shift+O")); + let open = parse("Cmd+O").unwrap(); + let outlines = parse("Cmd+Shift+O").unwrap(); + let pos = |want: egui::KeyboardShortcut, id: &str| super::all_shortcuts().iter().position(|(sc, i, _)| *i == id && *sc == want); + assert!(pos(outlines, "type.createOutlines").unwrap() < pos(open, "file.open").unwrap(), "more modifiers win so Open does not eat the chord"); + let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT; + let chord = egui::Event::Key { key: Key::O, physical_key: None, pressed: true, repeat: false, modifiers: cmd_shift }; + let is_text = |app: &VectorcraftApp, id: u64| { + matches!(app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Text(_))) + }; + let group_kids = |app: &VectorcraftApp| -> usize { + let id = app.session.active().unwrap().selection.objects.first().copied().unwrap(); + match &app.session.doc().unwrap().doc.node(id).unwrap().kind { + vectorcraft_doc::NodeKind::Group { children, .. } => children.len(), + _ => 0, + } + }; + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); + assert!(is_text(&app, id)); + frame(&mut app, vec![chord.clone()]); + assert!(!is_text(&app, id), "Selection tool: the chord outlines the text"); + assert_eq!(group_kids(&app), 2, "H and i"); + assert_ne!(app.session.tool_id(), "artboard", "not the Artboard tool's Shift+O"); + assert!(app.ui.dialog.is_none(), "not File › Open"); + // While the Type tool edits, the same chord outlines; a Text event with Cmd held must not type. + let id = app.session.execute("text.create", &json!({"x": 20, "y": 80, "text": "Yo"})).unwrap()["id"].as_u64().unwrap(); + app.select_tool("type"); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + assert!(app.session.tool_wants_text()); + frame(&mut app, vec![egui::Event::ModifiersChanged(cmd_shift), chord, egui::Event::Text("o".into())]); + assert!(!is_text(&app, id), "Type tool: the chord outlines the text being edited"); + assert_eq!(group_kids(&app), 2, "Yo only: the stray Text with Cmd was not typed"); + assert!(app.ui.dialog.is_none(), "not File › Open while typing"); + } + /// Cmd+Shift+B while the Type tool edits text is Type › Bold, not Hide Bounding Box (#724). #[test] fn cmd_shift_b_while_typing_is_bold_not_the_bounding_box() { From f5fcb30745e4dc0425dce0762df86b92f59cdecb Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 16 Oct 2026 02:06:32 +0700 Subject: [PATCH 2/2] Type: Option and AltGr still type while Cmd/Ctrl chords don't (#2094) --- crates/ui-egui/src/shortcuts.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 0c078d6e6..9cd6de9a7 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -173,14 +173,19 @@ pub(crate) fn keep_marked_text(app: &mut VectorcraftApp) { } } +/// Cmd/Ctrl chords are shortcuts (Create Outlines, Bold, …): their letter is never typed into the +/// text if a host still emits `Text` for them (#2094). Alt is left to type: Option+key on macOS +/// and AltGr (Ctrl+Alt) on Windows keyboards type characters such as é, ø and @. +fn is_shortcut_chord(m: egui::Modifiers) -> bool { + (m.command || m.ctrl) && !m.alt +} + /// Typed text and IME events, in the order they came, to the Type tool. fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) { let view = app.view_info(); // Switching apps mid-composition needs nothing: the macOS IME keeps its marked text and goes // on with it when the window comes back (measured with Kotoeri), as the tool does. - // Cmd/Ctrl/Alt chords are shortcuts (Create Outlines, Bold, …): never type their letter into - // the text if a host still emits `Text` for them (#2094). - let shortcut_mods = ctx.input(|i| i.modifiers.command || i.modifiers.ctrl || i.modifiers.alt); + let shortcut_mods = ctx.input(|i| is_shortcut_chord(i.modifiers)); let events: Vec = ctx.input(|i| i.events.iter().filter(|e| matches!(e, egui::Event::Text(_) | egui::Event::Ime(_))).cloned().collect()); for e in events { @@ -535,6 +540,17 @@ mod tests { out } + #[test] + fn option_and_altgr_still_type_but_cmd_and_ctrl_chords_do_not() { + use egui::Modifiers as M; + assert!(is_shortcut_chord(M::COMMAND | M::SHIFT)); + assert!(is_shortcut_chord(M::CTRL)); + assert!(!is_shortcut_chord(M::NONE)); + assert!(!is_shortcut_chord(M::SHIFT)); + assert!(!is_shortcut_chord(M::ALT), "Option+key types é, ø on macOS"); + assert!(!is_shortcut_chord(M::CTRL | M::ALT), "AltGr types @ on a German keyboard"); + } + /// #2094: Cmd+Shift+O is Type › Create Outlines (not File › Open's Cmd+O, not the Artboard /// tool's Shift+O), with the Selection tool or while the Type tool edits the text. #[test]