From cb9522f7cd72ec04397e6fbe22e69b57dadfe5cf Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 15 Oct 2026 11:09:40 -0700 Subject: [PATCH] Type: Cmd/Ctrl+Alt+Q resets tracking to 0 and kerning to Auto The reference app resets tracking and kerning to 0 with Cmd/Ctrl+Alt+Q. VectorCraft had no command for it. type.resetTracking (Reset Tracking and Kerning, default Cmd+Alt+Q) sets tracking to 0 and kerning to Auto, which the Character panel shows as 0, in the text the Type tool has selected, or else in all the text of every selected type object. At a caret it resets only the kerning of the character before it, where Alt+Left/Right kerns. It is one undo step. The range logic of type.step moves into a shared type_ranges. When a call gives ids, type.step and type.resetTracking change all the text of those type objects even while the Type tool edits text, as paint.setFill does. Before, type.step changed the Type tool's selected text instead. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/textedit.rs | 107 +++++++++++++++++++++------- crates/engine/src/tests_prefs.rs | 48 +++++++++++++ crates/mcp/src/tests.rs | 20 ++++++ crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/shortcuts.rs | 34 +++++++++ docs/mcp.md | 11 +-- docs/target-app-parity.md | 1 + 21 files changed, 206 insertions(+), 30 deletions(-) diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index c8542cd8a..c113344c6 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -63,7 +63,7 @@ pub fn specs() -> Vec { "Step Type", [], None, - "{attribute: \"size\"|\"leading\"|\"tracking\"|\"kerning\"|\"baselineShift\", by?: steps (1; negative steps down), id?, start?: byte, end?: byte, ids?: [id…] (instead of id)} step type by the Preferences › Type increments (size and leading: typeSizeIncrement, pt; tracking and kerning: trackingIncrement, 1/1000 em; baseline shift: baselineShiftIncrement, pt), what the Type tool's Alt+arrows do (Cmd/Ctrl too: five steps; ←/→ kerning at the caret or tracking of the selection, ↑/↓ leading, Shift+↑/↓ baseline shift). The range of `id`, else the text the Type tool has selected (at a caret: kerning steps the character before it, leading its paragraph), else every selected type object → {ids}", + "{attribute: \"size\"|\"leading\"|\"tracking\"|\"kerning\"|\"baselineShift\", by?: steps (1; negative steps down), id?, start?: byte, end?: byte, ids?: [id…] (instead of id)} step type by the Preferences › Type increments (size and leading: typeSizeIncrement, pt; tracking and kerning: trackingIncrement, 1/1000 em; baseline shift: baselineShiftIncrement, pt), what the Type tool's Alt+arrows do (Cmd/Ctrl too: five steps; ←/→ kerning at the caret or tracking of the selection, ↑/↓ leading, Shift+↑/↓ baseline shift). The range of `id`, else the type objects in `ids`, else the text the Type tool has selected (at a caret: kerning steps the character before it, leading its paragraph), else every selected type object → {ids}", has_doc, type_step ), @@ -85,6 +85,15 @@ pub fn specs() -> Vec { has_selection, |s, p| type_step(s, &json!({"attribute": "size", "by": -f64_or(p, "by", 1.0)})) ), + cmd!( + "type.resetTracking", + "Reset Tracking and Kerning", + [], + Some("Cmd+Alt+Q"), + "{ids?: [id…]} set tracking to 0 and kerning to Auto (the font's own kerning, which the Character panel shows as 0) in all the text of the type objects in `ids`, else in the text the Type tool has selected (at a caret: only the kerning of the character before it), else in every selected type object, as one undo step → {ids}", + has_selection, + reset_tracking + ), cmd!( "text.discardEmpty", "Discard Empty Type", @@ -598,6 +607,46 @@ pub(crate) fn editing_range(s: &mut Session) -> Result Option<(usize, usize)>, +) -> Result> { + // (text, its range; None: all of it). + let targets: Vec<(NodeId, Option<(usize, usize)>)> = match id_param(p, "id") { + Some(id) => vec![(id, Some(range_of(p, edit::runs_len(&text_ref(s, id)?.runs))))], + None => { + let editing = editing_range(s)?.filter(|_| p.get("ids").filter(|v| !v.is_null()).is_none()); + match editing { + Some((id, a, b)) => vec![(id, Some((a.min(b), a.max(b))))], + None => super::typecmd::text_targets(s, p, c)?.into_iter().map(|id| (id, None)).collect(), + } + } + }; + let mut ranges = vec![]; + for (id, range) in targets { + let text = text_ref(s, id)?.plain_text(); + let (a, b) = range.map_or((0, text.len()), |(a, b)| (a.min(text.len()), b.min(text.len()))); + let (a, b, caret) = if a < b { + (a, b, false) + } else { + match at_caret(&text, a) { + Some((a, b)) => (a, b, true), + None => continue, + } + }; + if a < b { + ranges.push((id, a, b, caret)); + } + } + Ok(ranges) +} + fn type_step(s: &mut Session, p: &Value) -> Result { const C: &str = "type.step"; let step = match str_param(p, "attribute") { @@ -619,36 +668,19 @@ fn type_step(s: &mut Session, p: &Value) -> Result { Step::Tracking | Step::Kerning => prefs.tracking_increment, Step::BaselineShift => prefs.baseline_shift_increment, }; - // (text, its range; None: all of it). - let targets: Vec<(NodeId, Option<(usize, usize)>)> = match id_param(p, "id") { - Some(id) => vec![(id, Some(range_of(p, edit::runs_len(&text_ref(s, id)?.runs))))], - None => match editing_range(s)? { - Some((id, a, b)) => vec![(id, Some((a.min(b), a.max(b))))], - None => super::typecmd::text_targets(s, p, C)?.into_iter().map(|id| (id, None)).collect(), - }, - }; // The ranges stepped. At a caret: kerning is the space after the character before it, leading // the paragraph's; the rest wait for a selection. - let mut ranges = vec![]; - for (id, range) in targets { - let text = text_ref(s, id)?.plain_text(); - let (a, b) = range.map_or((0, text.len()), |(a, b)| (a.min(text.len()), b.min(text.len()))); - let (a, b) = match step { - _ if a < b => (a, b), - Step::Kerning => (edit::prev_char(&text, a), a), - Step::Leading => { - let para = edit::paragraph_at(&text, a); - (para.start, para.end) - } - _ => continue, - }; - if a < b { - ranges.push((id, a, b)); + let ranges = type_ranges(s, p, C, |text, a| match step { + Step::Kerning => Some((edit::prev_char(text, a), a)), + Step::Leading => { + let para = edit::paragraph_at(text, a); + Some((para.start, para.end)) } - } + _ => None, + })?; if !ranges.is_empty() { s.edit("Character", |d, _| { - for &(id, a, b) in &ranges { + for &(id, a, b, _) in &ranges { let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?; let points = if matches!(step, Step::Size | Step::Leading | Step::BaselineShift) { t.style_scale().ok_or_else(|| bad(C, "text transform is collapsed or unrepresentable"))?.points @@ -679,6 +711,29 @@ fn type_step(s: &mut Session, p: &Value) -> Result { Ok(json!({ "ids": ranges.iter().map(|(id, ..)| id.0).collect::>() })) } +/// Cmd/Ctrl+Alt+Q: tracking 0 and kerning Auto (0 in the Character panel) in the type objects in +/// `ids`, else in the Type tool's selected text, else in every selected type object. At a caret, +/// only the kerning of the character before it changes, as Alt+←/→ kerns there. +fn reset_tracking(s: &mut Session, p: &Value) -> Result { + let ranges = type_ranges(s, p, "type.resetTracking", |text, a| Some((edit::prev_char(text, a), a)))?; + if !ranges.is_empty() { + s.edit("Reset Tracking and Kerning", |d, _| { + for &(id, a, b, caret) in &ranges { + let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?; + edit::style_range(&mut t.runs, a, b, |st| { + st.kerning = None; + if !caret { + st.tracking = 0.0; + } + }); + refresh_bounds(t); + } + Ok(()) + })?; + } + Ok(json!({ "ids": ranges.iter().map(|(id, ..)| id.0).collect::>() })) +} + /// Tracking (1/1000 em) that makes the first paragraph of `t` exactly `target` wide, if it fits /// on one line at all. fn headline_tracking(t: &TextObject, target: f64) -> Option { diff --git a/crates/engine/src/tests_prefs.rs b/crates/engine/src/tests_prefs.rs index 74527ae95..f789e8f12 100644 --- a/crates/engine/src/tests_prefs.rs +++ b/crates/engine/src/tests_prefs.rs @@ -1019,6 +1019,54 @@ fn type_increments_step_the_type() { // The font size shortcut steps the selected text while the tool edits. s.execute("type.size.increase", &json!({})).unwrap(); assert_eq!((style_at(&s, id, 0).size, style_at(&s, id, 6).size), (19.0, 22.0)); + // The type objects given in `ids` step in full while the tool edits. + s.execute("type.step", &json!({"attribute": "size", "ids": [id.0]})).unwrap(); + assert_eq!((style_at(&s, id, 0).size, style_at(&s, id, 6).size), (22.0, 25.0)); +} + +/// Cmd/Ctrl+Alt+Q resets tracking to 0 and kerning to Auto (0 in the Character panel) in every +/// selected type object and in the Type tool's selected text. At a caret it resets only the +/// kerning of the character before it. It is one undo step. Type objects given in `ids` are reset +/// in full while the Type tool edits, and the typing in progress ends first. +#[test] +fn reset_tracking_sets_tracking_and_kerning_to_zero() { + use vectorcraft_tools::PointerKind::{Down, Up}; + let mut s = new_doc(); + let id = NodeId(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Hello world", "size": 20})).unwrap()["id"].as_u64().unwrap()); + let spaced = |s: &mut Session| s.execute("text.setRangeStyle", &json!({"id": id.0, "tracking": 200, "kerning": -40})).unwrap(); + let spacing = |s: &Session, byte| { + let st = style_at(s, id, byte); + (st.tracking, st.kerning) + }; + spaced(&mut s); + let other = NodeId(s.execute("text.create", &json!({"x": 100, "y": 300, "text": "Other", "size": 20})).unwrap()["id"].as_u64().unwrap()); + s.execute("text.setRangeStyle", &json!({"id": other.0, "tracking": 200})).unwrap(); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + assert_eq!(find_command("type.resetTracking").unwrap().shortcut, Some("Cmd+Alt+Q")); + assert_eq!(s.execute("type.resetTracking", &json!({})).unwrap()["ids"], json!([id.0])); + assert_eq!((spacing(&s, 0), spacing(&s, 10)), ((0.0, None), (0.0, None))); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(spacing(&s, 0), (200.0, Some(-40.0)), "one undo step"); + // With the Type tool, it resets only the selected text. + let v = ViewInfo::default(); + s.select_tool("type", v).unwrap(); + gesture(&mut s, v, &[(Down, 102.0, 95.0), (Up, 102.0, 95.0)]); + assert_eq!(s.tool_options()["editing"], json!(id.0), "editing the text"); + s.set_tool_option("select", &json!({"start": 0, "end": 5})); + s.execute("type.resetTracking", &json!({})).unwrap(); + assert_eq!((spacing(&s, 4), spacing(&s, 6)), ((0.0, None), (200.0, Some(-40.0)))); + // At a caret it resets only the kerning of the character before it, and the tracking does not change. + s.set_tool_option("select", &json!({"start": 8, "end": 8})); + s.execute("type.resetTracking", &json!({})).unwrap(); + assert_eq!((spacing(&s, 6), spacing(&s, 7), spacing(&s, 8)), ((200.0, Some(-40.0)), (200.0, None), (200.0, Some(-40.0)))); + // While the Type tool edits, the type objects in `ids` are reset in full. + s.execute("type.resetTracking", &json!({"ids": [id.0]})).unwrap(); + assert_eq!((spacing(&s, 6), spacing(&s, 10)), ((0.0, None), (0.0, None))); + // Typing in progress ends first, so the reset of another object stays when typing goes on. + s.tool_text("!", v).unwrap(); + s.execute("type.resetTracking", &json!({"ids": [other.0]})).unwrap(); + s.tool_text("?", v).unwrap(); + assert_eq!(style_at(&s, other, 0).tracking, 0.0, "the reset stays"); } /// Type › Fill New Type Objects With Placeholder Text (#394, on by default): type the Type tool diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 35a768a8a..3a6e73827 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -1038,6 +1038,26 @@ fn type_preferences_apply_to_agents() { assert_eq!(style(&mut s, 6), (size + 4.0, tracking + 50.0)); } +/// Cmd+Alt+Q reaches agents: the placeholder text the Type tool selects goes back to tracking 0 +/// and Auto kerning. +#[test] +fn cmd_alt_q_resets_tracking_and_kerning_for_agents() { + let mut s = server(); + let events = json!([{"kind": "down", "x": 100, "y": 100}, {"kind": "up", "x": 100, "y": 100}]); + let r = call(&mut s, 1, "pointer_gesture", json!({"tool": "type", "events": events})); + assert_eq!(r["isError"], false, "{r}"); + let id = serde_json::from_str::(&text_of(&r)).unwrap()["selection"][0].as_u64().unwrap(); + let r = call(&mut s, 2, "run_command", json!({"command": "text.setRangeStyle", "params": {"id": id, "tracking": 200, "kerning": -40}})); + assert_eq!(r["isError"], false, "{r}"); + let r = call(&mut s, 3, "press_key", json!({"key": "Q", "mods": {"cmd": true, "alt": true}})); + assert_eq!(r["isError"], false, "{r}"); + assert_eq!(serde_json::from_str::(&text_of(&r)).unwrap()["command"], "type.resetTracking", "{r}"); + let r = call(&mut s, 4, "run_command", json!({"command": "text.getRange", "params": {"id": id}})); + let runs = serde_json::from_str::(&text_of(&r)).unwrap()["runs"].clone(); + assert_eq!(runs.as_array().unwrap().len(), 1, "{runs}"); + assert_eq!((&runs[0]["style"]["tracking"], &runs[0]["style"]["kerning"]), (&json!(0.0), &Value::Null), "{runs}"); +} + /// The drawing tools snap to Smart Guides over MCP as with the mouse (#506): a Rectangle drawn /// from near another object's corner starts on it, and a Pen anchor placed beside that object's /// centre lines up with it. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c3a2ed98c..decaa6086 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3456,6 +3456,7 @@ axis Both Obě Area / Path Type Text v oblasti / na cestě Decrease Font Size Zmenšit velikost písma Increase Font Size Zvětšit velikost písma + Reset Tracking and Kerning Obnovit prostrkání a vyrovnání párů Step Type Krokovat text Get Text Range Získat rozsah textu Punctuation Interpunkce diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1c55ba854..2e225e546 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1049,6 +1049,7 @@ Fit Headline Überschrift einpassen Decrease Font Size Schriftgrad verringern Increase Font Size Schriftgrad erhöhen + Reset Tracking and Kerning Laufweite und Kerning zurücksetzen Spacing Abstand Step Type Text schrittweise ändern Fit in View In Ansicht einpassen diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index d17bea834..ab579da6c 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1066,6 +1066,7 @@ Fit Headline Ajustar titular Decrease Font Size Reducir tamaño de fuente Increase Font Size Aumentar tamaño de fuente + Reset Tracking and Kerning Restablecer espaciado e interletraje Spacing Espaciado Step Type Ajustar texto por incrementos Fit in View Encajar en la vista diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 599b7a06b..0a68e83b2 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1065,6 +1065,7 @@ Fit Headline Ajuster le titre Decrease Font Size Réduire la taille de police Increase Font Size Augmenter la taille de police + Reset Tracking and Kerning Réinitialiser l’approche et le crénage Spacing Espacement Step Type Texte par incréments Fit in View Ajuster à la vue diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 2332da341..554c3a99a 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2915,6 +2915,7 @@ Reset to Defaults Alapértékek visszaállítása Reset to White, Black Visszaállítás fehér–fekete átmenetre Reset Tracing Tracing visszaállítása + Reset Tracking and Kerning Betűköz és alávágás visszaállítása Reset Typography Typography visszaállítása Reset Web Web visszaállítása Reset with Mesh Visszaállítás hálóval diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index a8f21c205..dc9ae9e1e 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1065,6 +1065,7 @@ Fit Headline Adatta titolo Decrease Font Size Riduci dimensione font Increase Font Size Aumenta dimensione font + Reset Tracking and Kerning Reimposta spaziatura caratteri e crenatura Spacing Spaziatura Step Type Dimensione testo a incrementi Fit in View Adatta alla vista diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 176fa87d3..310322665 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -329,6 +329,7 @@ Fit Headline ヘッドラインを合わせる Decrease Font Size フォントサイズを小さく Increase Font Size フォントサイズを大きく + Reset Tracking and Kerning トラッキングとカーニングをリセット Step Type 文字を段階的に調整 Find Font… フォントの検索と置換… Change Case 大文字と小文字の変更 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index de1951730..2d19e66fa 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -325,6 +325,7 @@ Fit Headline 제목에 맞추기 Decrease Font Size 글꼴 크기 줄이기 Increase Font Size 글꼴 크기 키우기 + Reset Tracking and Kerning 자간 및 커닝 재설정 Step Type 단계별 문자 Find Font… 글꼴 찾기… Change Case 대소문자 변경 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 72cde3853..510c49576 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3288,6 +3288,7 @@ axis Both Obie Area / Path Type Tekst akapitowy / na ścieżce Decrease Font Size Zmniejsz rozmiar czcionki Increase Font Size Zwiększ rozmiar czcionki + Reset Tracking and Kerning Resetuj światło i kerning Step Type Krok tekstu Get Text Range Pobierz zakres tekstu Punctuation Interpunkcja diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 5c7e5ca75..0571c5dac 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -949,6 +949,7 @@ Fit Headline Ajustar Manchete Decrease Font Size Diminuir Tamanho da Fonte Increase Font Size Aumentar Tamanho da Fonte + Reset Tracking and Kerning Redefinir Rastreamento e Kerning Step Type Ajustar Texto em Incrementos Fit dashes to corners and ends Ajustar tracejados aos cantos e extremidades Fit in Window Ajustar na Janela diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3618173c2..b77b6b6ba 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1063,6 +1063,7 @@ Fit Headline Подогнать заголовок Decrease Font Size Уменьшить размер шрифта Increase Font Size Увеличить размер шрифта + Reset Tracking and Kerning Сбросить трекинг и кернинг Spacing Интервал Step Type Подгонять текст по шагам Fit in View Вписать в вид diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ccb4662c6..ca2980f43 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1059,6 +1059,7 @@ Fit Headline Підігнати заголовок Decrease Font Size Зменшити розмір шрифту Increase Font Size Збільшити розмір шрифту + Reset Tracking and Kerning Скинути трекінг і кернінг Spacing Інтервал Step Type Підганяти текст кроками Fit in View Вписати у вигляд diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index c8b7f261e..fa566505c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2660,6 +2660,7 @@ Reset to Default Đặt lại về mặc định Reset to default Đặt lại về mặc định Reset to White, Black Đặt lại về Trắng, Đen + Reset Tracking and Kerning Đặt lại giãn chữ và kerning Reset with Mesh Đặt lại với lưới Reset with Mesh… Đặt lại với lưới… Reset with Warp Đặt lại với biến dạng diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 37efb628d..ef73b5d98 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1018,6 +1018,7 @@ Fit Headline 适合标题 Decrease Font Size 减小字体大小 Increase Font Size 增大字体大小 + Reset Tracking and Kerning 重置字距和字偶距 Spacing 间距 Step Type 按增量调整文字 Fit in View 适合视图 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 0553f1444..225aa5d58 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1041,6 +1041,7 @@ Fit Headline 符合標題 Decrease Font Size 縮小字體大小 Increase Font Size 放大字體大小 + Reset Tracking and Kerning 重設字距與字距微調 Spacing 間距 Step Type 依增量調整文字 Fit in View 符合檢視 diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 437446f13..580b4fc16 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -662,6 +662,40 @@ mod tests { assert!(!crate::menus::enabled(&app, "type.alignCenter")); } + /// Cmd+Alt+Q resets the tracking and kerning of the selected type (tracking 0, kerning Auto), + /// and of the Type tool's selected text while it edits. + #[test] + fn cmd_alt_q_resets_tracking_and_kerning() { + 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"})).unwrap()["id"].as_u64().unwrap(); + let spaced = + |app: &mut VectorcraftApp| app.session.execute("text.setRangeStyle", &json!({"id": id, "tracking": 200, "kerning": -40})).unwrap(); + let spacing = |app: &mut VectorcraftApp| { + let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap(); + r["runs"] + .as_array() + .unwrap() + .iter() + .map(|run| (run["text"].clone(), run["style"]["tracking"].clone(), run["style"]["kerning"].clone())) + .collect::>() + }; + let chord = + egui::Event::Key { key: Key::Q, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND | Modifiers::ALT }; + spaced(&mut app); + app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); + frame(&mut app, vec![chord.clone()]); + assert_eq!(spacing(&mut app), [(json!("One Two"), json!(0.0), json!(null))]); + // While the Type tool edits: only its selection. + spaced(&mut app); + app.select_tool("type"); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + app.session.set_tool_option("select", &json!({"start": 4, "end": 7})); + assert!(app.session.tool_wants_text()); + frame(&mut app, vec![chord]); + assert_eq!(spacing(&mut app), [(json!("One "), json!(200.0), json!(-40.0)), (json!("Two"), json!(0.0), json!(null))]); + } + /// Tab shows and hides the panels, and in the Type tool it types a tab, without moving the /// keyboard focus on to a field: the keys after it (a tool letter, the next letter typed) still /// work. diff --git a/docs/mcp.md b/docs/mcp.md index 9f68329dd..4c41a0536 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1917,10 +1917,13 @@ Pen's second anchor at (150, 431), in line with the square's centre. - `placeholderText` (on by default): type the Type tools place (`pointer_gesture` with `type`, `areaType`…) starts with placeholder text, selected, so `type_text` replaces it; `text.create` and `text.createInPath` take `placeholder: true`. - `typeSizeIncrement`, `trackingIncrement`, `baselineShiftIncrement`: what `type.step {attribute: "size" | "leading" | - "tracking" | "kerning" | "baselineShift", by?}` steps by, on the text range given, the Type tool's selected text or the - selected type objects. `type.size.increase` / `type.size.decrease` (Cmd+Shift+. and Cmd+Shift+,) step the size; the - Type tool's Alt+arrows (`press_key {key: "Right", mods: {alt: true}}`) step kerning at a caret or tracking of the - selection (←/→), leading (↑/↓) and baseline shift (Shift+↑/↓), five steps with Cmd/Ctrl too. + "tracking" | "kerning" | "baselineShift", by?}` steps by, on the text range given, the type objects in `ids`, the + Type tool's selected text or the selected type objects. `type.size.increase` / `type.size.decrease` (Cmd+Shift+. and + Cmd+Shift+,) step the size; the Type tool's Alt+arrows (`press_key {key: "Right", mods: {alt: true}}`) step kerning at + a caret or tracking of the selection (←/→), leading (↑/↓) and baseline shift (Shift+↑/↓), five steps with Cmd/Ctrl + too. `type.resetTracking` (Cmd+Alt+Q, `press_key {key: "Q", mods: {cmd: true, alt: true}}`) sets tracking to 0 and + kerning to Auto in the type objects in `ids`, the Type tool's selected text or the selected type objects; at a caret + it resets only the kerning of the character before it. - `missingGlyphProtection` (on by default): `text.setStyle` and `text.setRangeStyle` changing the font leave the characters the new font has no glyph for in the font that had one. - `typeSelectionByPathOnly` (off by default): on, the Selection tools (`pointer_gesture` with `selection`, and the diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 50050de22..7220537ae 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -438,6 +438,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields. - Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column. - 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). + - Cmd/Ctrl+Alt+Q (`type.resetTracking`) sets the tracking of the selected type, or of the text the Type tool has selected, to 0 and its kerning to Auto (0 in the Character panel), in one undo step; at a caret it resets the kerning of the character before it. - Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`). - On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one. - **Tools:**