From 32bcf8900cbcc93a59b1381b55b3758a9a356185 Mon Sep 17 00:00:00 2001 From: Mirko Kerezovic Date: Fri, 23 Oct 2026 14:23:03 +0200 Subject: [PATCH 1/3] Fix text spacing shortcuts for selected type --- crates/ui-egui/src/shortcuts.rs | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index fde2d6751..c041cd54e 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -628,6 +628,34 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { } return; } + // Alt+arrows on selected type objects use the same spacing increments as the Type tool: + // ←/→ tracking, ↑/↓ leading, Shift+↑/↓ baseline shift; Cmd/Ctrl multiplies by five. Keep + // Alt+arrow's normal copy-and-nudge behaviour for every other kind of selection. + let selected_type = app.session.active().is_some_and(|st| { + st.selection.guides.is_empty() + && !st.selection.objects.is_empty() + && st.selection.objects.iter().all(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Text(_)))) + }); + if selected_type { + let m = ctx.input(|i| i.modifiers); + if m.alt { + let steps = [ + (Key::ArrowLeft, (!m.shift).then_some(("tracking", -1.0))), + (Key::ArrowRight, (!m.shift).then_some(("tracking", 1.0))), + (Key::ArrowUp, Some((if m.shift { "baselineShift" } else { "leading" }, if m.shift { 1.0 } else { -1.0 }))), + (Key::ArrowDown, Some((if m.shift { "baselineShift" } else { "leading" }, if m.shift { -1.0 } else { 1.0 }))), + ]; + for (key, step) in steps { + if let Some((attribute, by)) = step + && ctx.input_mut(|i| i.consume_key(m, key)) + { + let by = if m.command { by * 5.0 } else { by }; + let _ = app.run("type.step", json!({"attribute": attribute, "by": by})); + return; + } + } + } + } // Arrow nudges (Shift = ×10, Alt = copy). let arrows = [(Key::ArrowLeft, -1.0, 0.0), (Key::ArrowRight, 1.0, 0.0), (Key::ArrowUp, 0.0, -1.0), (Key::ArrowDown, 0.0, 1.0)]; for (k, dx, dy) in arrows { @@ -1624,6 +1652,39 @@ mod tests { assert_eq!(sizes(&mut app), [(json!("One "), json!(14.0)), (json!("Two"), json!(24.0))]); } + /// #3786: Alt+arrows step a selected text object's spacing instead of copying and nudging it. + #[test] + fn alt_arrows_step_selected_type_object() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "One Two", "size": 20})).unwrap()["id"].as_u64().unwrap(); + app.select_tool("selection"); + app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); + + let press = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }; + frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::ALT), press(Key::ArrowRight, Modifiers::ALT)]); + let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); + assert_eq!(r["runs"][0]["style"]["tracking"], json!(20.0)); + assert_eq!(app.session.active().unwrap().selection.objects, [vectorcraft_doc::NodeId(id)]); + + frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::ALT), press(Key::ArrowDown, Modifiers::ALT)]); + let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); + assert_eq!(r["runs"][0]["style"]["leading"], json!(26.0)); + + let alt_shift = Modifiers::ALT | Modifiers::SHIFT; + frame(&mut app, vec![egui::Event::ModifiersChanged(alt_shift), press(Key::ArrowUp, alt_shift)]); + let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); + assert_eq!(r["runs"][0]["style"]["baseline_shift"], json!(2.0)); + + // Other objects keep Alt+arrow's copy-and-nudge shortcut. + let rect = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("select.set", &json!({"ids": [rect]})).unwrap(); + frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::ALT), press(Key::ArrowRight, Modifiers::ALT)]); + let selected = &app.session.active().unwrap().selection.objects; + assert_eq!(selected.len(), 1); + assert_ne!(selected[0], vectorcraft_doc::NodeId(rect), "a non-type object is still copied by Alt+arrow"); + } + /// #793: with a layout without Latin letters the one-key shortcuts go by the key's position: /// the P key typing Persian `ح` chooses the Pen, the V key typing Russian `м` the Selection /// tool. A Latin layout keeps the letter typed: AZERTY's A (the Q key's place) is A. From a79bf0f42e9a063f68d4965823063f698cc9e62b Mon Sep 17 00:00:00 2001 From: Mirko Kerezovic Date: Fri, 23 Oct 2026 14:49:05 +0200 Subject: [PATCH 2/3] Handle Shift Alt arrows on selected type --- crates/ui-egui/src/shortcuts.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index c041cd54e..7493f362e 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -646,11 +646,11 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { (Key::ArrowDown, Some((if m.shift { "baselineShift" } else { "leading" }, if m.shift { -1.0 } else { 1.0 }))), ]; for (key, step) in steps { - if let Some((attribute, by)) = step - && ctx.input_mut(|i| i.consume_key(m, key)) - { - let by = if m.command { by * 5.0 } else { by }; - let _ = app.run("type.step", json!({"attribute": attribute, "by": by})); + if ctx.input_mut(|i| i.consume_key(m, key)) { + if let Some((attribute, by)) = step { + let by = if m.command { by * 5.0 } else { by }; + let _ = app.run("type.step", json!({"attribute": attribute, "by": by})); + } return; } } @@ -1676,6 +1676,13 @@ mod tests { let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); assert_eq!(r["runs"][0]["style"]["baseline_shift"], json!(2.0)); + // Shift+Alt+Left/Right have no text-spacing action; they must still be consumed instead of + // falling through to Alt+arrow's copy-and-nudge behaviour. + for key in [Key::ArrowLeft, Key::ArrowRight] { + frame(&mut app, vec![egui::Event::ModifiersChanged(alt_shift), press(key, alt_shift)]); + assert_eq!(app.session.active().unwrap().selection.objects, [vectorcraft_doc::NodeId(id)]); + } + // Other objects keep Alt+arrow's copy-and-nudge shortcut. let rect = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap(); app.session.execute("select.set", &json!({"ids": [rect]})).unwrap(); From 50c801b36fb7a0cf9f3e04e98283630b2a627c05 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 01:50:46 +0700 Subject: [PATCH 3/3] Alt+arrow type spacing: reuse the Type tool's step_key, check Alt before the selection walk --- crates/tools/src/text.rs | 2 +- crates/ui-egui/src/shortcuts.rs | 41 ++++++++++++++------------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index a6b1f3d0c..894f087c3 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -427,7 +427,7 @@ pub fn overflow_markers(marks: &mut OverflowMarks, cx: &ToolContext) -> Vec Option<(&'static str, f64)> { +pub fn step_key(key: ToolKey, shift: bool, caret: bool) -> Option<(&'static str, f64)> { let sign = |up: bool| if up { 1.0 } else { -1.0 }; Some(match (key, shift) { (ToolKey::Left | ToolKey::Right, false) => (if caret { "kerning" } else { "tracking" }, sign(key == ToolKey::Right)), diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 7493f362e..47450aef5 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -628,31 +628,24 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { } return; } - // Alt+arrows on selected type objects use the same spacing increments as the Type tool: - // ←/→ tracking, ↑/↓ leading, Shift+↑/↓ baseline shift; Cmd/Ctrl multiplies by five. Keep - // Alt+arrow's normal copy-and-nudge behaviour for every other kind of selection. - let selected_type = app.session.active().is_some_and(|st| { - st.selection.guides.is_empty() - && !st.selection.objects.is_empty() - && st.selection.objects.iter().all(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Text(_)))) - }); - if selected_type { - let m = ctx.input(|i| i.modifiers); - if m.alt { - let steps = [ - (Key::ArrowLeft, (!m.shift).then_some(("tracking", -1.0))), - (Key::ArrowRight, (!m.shift).then_some(("tracking", 1.0))), - (Key::ArrowUp, Some((if m.shift { "baselineShift" } else { "leading" }, if m.shift { 1.0 } else { -1.0 }))), - (Key::ArrowDown, Some((if m.shift { "baselineShift" } else { "leading" }, if m.shift { -1.0 } else { 1.0 }))), - ]; - for (key, step) in steps { - if ctx.input_mut(|i| i.consume_key(m, key)) { - if let Some((attribute, by)) = step { - let by = if m.command { by * 5.0 } else { by }; - let _ = app.run("type.step", json!({"attribute": attribute, "by": by})); - } - return; + // Alt+arrows on selected type objects step their spacing as the Type tool does (Cmd/Ctrl: + // five steps); Alt+arrow copies and nudges every other selection. + let m = ctx.input(|i| i.modifiers); + if m.alt + && app.session.active().is_some_and(|st| { + st.selection.guides.is_empty() + && !st.selection.objects.is_empty() + && st.selection.objects.iter().all(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(vectorcraft_doc::NodeKind::Text(_)))) + }) + { + let keys = [(Key::ArrowLeft, ToolKey::Left), (Key::ArrowRight, ToolKey::Right), (Key::ArrowUp, ToolKey::Up), (Key::ArrowDown, ToolKey::Down)]; + for (key, tool_key) in keys { + if ctx.input_mut(|i| i.consume_key(m, key)) { + if let Some((attribute, by)) = vectorcraft_tools::text::step_key(tool_key, m.shift, false) { + let by = if m.command { by * 5.0 } else { by }; + let _ = app.run("type.step", json!({"attribute": attribute, "by": by})); } + return; } } }