diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs index 632ef818b..08e7960b3 100644 --- a/crates/ui-egui/src/app/commands.rs +++ b/crates/ui-egui/src/app/commands.rs @@ -168,9 +168,21 @@ impl VectorcraftApp { if matches!(id, "dataset.select" | "dataset.next" | "dataset.prev") { self.ui.status.clear(); } - if matches!(id, "text.setStyle" | "text.setRangeStyle") - && let Some(font) = params.get("font").and_then(Value::as_str) - { + // A font applied with Character's font menu or Find Font's Change becomes the newest recent font. + // Find Font's Change records the family the text now carries (any name of it is stored as its + // own name), and only when it changed some text. + let applied = match id { + "text.setStyle" | "text.setRangeStyle" => params.get("font").and_then(Value::as_str).map(str::to_string), + "text.replaceFont" if v["runs"].as_u64().is_some_and(|n| n > 0) => { + let to = ¶ms["to"]; + to["family"].as_str().and_then(|family| { + let style = to["style"].as_str().unwrap_or("Regular"); + vectorcraft_text::FontDb::global().resolve(family, style).map(|(face, _)| face.family.clone()) + }) + } + _ => None, + }; + if let Some(font) = applied.as_deref() { let r = &mut self.ui.recent_fonts; r.retain(|f| !f.eq_ignore_ascii_case(font)); r.insert(0, font.to_string()); diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index 5155542e0..af9a83af0 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -1,6 +1,7 @@ //! Type → Find Font…: the fonts the document uses (missing ones marked), Find (select the text -//! using a font), Change / Change All to another installed font, and Find in Folder… (the missing -//! fonts' files, in the Missing Fonts dialog: `ui.findFontsInFolder`). +//! using a font), Change / Change All to another font from the list Replace With Font From shows +//! (the document's fonts, the recent fonts or every installed font), and Find in Folder… (the +//! missing fonts' files, in the Missing Fonts dialog: `ui.findFontsInFolder`). use egui::Ui; use serde_json::{Value, json}; @@ -22,9 +23,90 @@ pub fn open_on_missing(app: &mut VectorcraftApp) { open_at(app, first.unwrap_or(0)); } -/// Open the dialog with the document's font `selected` (an index into `text.fonts`) chosen. +/// Open the dialog with the document's font `selected` (an index into `text.fonts`) chosen, and +/// System's font ([`system_family`]) to change to. fn open_at(app: &mut VectorcraftApp, selected: usize) { - app.ui.dialog = Some(Dialog::new("findFont", json!({ "selected": selected, "family": "Source Sans 3", "style": "", "selectionOnly": false }))); + app.ui.dialog = Some(Dialog::new( + "findFont", + json!({ "selected": selected, "family": system_family(), "style": "", "selectionOnly": false, "replaceFrom": "system" }), + )); +} + +/// The font System chooses: Source Sans 3 when it is installed, otherwise the font menu's first +/// family (none when the menu is empty). +fn system_family() -> String { + system_family_in(installed, &vectorcraft_text::FontDb::global().menu_family_list()) +} + +/// [`system_family`] for the installed check `is_installed` and the font menu `menu`. +fn system_family_in(is_installed: impl Fn(&str) -> bool, menu: &[String]) -> String { + const PREFERRED: &str = "Source Sans 3"; + if is_installed(PREFERRED) { PREFERRED.to_string() } else { menu.first().cloned().unwrap_or_default() } +} + +/// Replace With Font From's lists (the `replaceFrom` field): the document's fonts, the recent +/// fonts and every installed font. +const SOURCES: [&str; 3] = ["document", "recent", "system"]; +const SOURCE_LABELS: [&str; 3] = ["Document", "Recent", "System"]; + +/// The families Replace With Font From lists. `None` is System: the font menu, every installed +/// family. Document lists the families `text.fonts` names, once each, in its order; Recent lists +/// the font menu's recent families (newest first, Number of Recent Fonts at most) that are +/// installed. +fn replacement_families(app: &VectorcraftApp, source: &str, used: &[Value]) -> Option> { + let names: Vec = match source { + "document" => used.iter().filter_map(|f| f["family"].as_str()).map(str::to_string).collect(), + "recent" => { + let installed = vectorcraft_text::FontDb::global().menu_family_list(); + app.recent_fonts().iter().filter_map(|r| installed.iter().find(|f| f.eq_ignore_ascii_case(r)).cloned()).collect() + } + _ => return None, + }; + let mut out: Vec = vec![]; + for n in names { + if !out.iter().any(|o| o.eq_ignore_ascii_case(&n)) { + out.push(n); + } + } + Some(out) +} + +/// Is `family` installed: it resolves to a face of its own, as `text.replaceFont` requires. +fn installed(family: &str) -> bool { + !family.is_empty() + && vectorcraft_text::FontDb::global().resolve(family, "Regular").is_some_and(|(_, m)| m != vectorcraft_text::FontMatch::Missing) +} + +/// Can Change and Change All replace with the dialog's font: a family is chosen, it is installed, +/// and it is one of the list Replace With Font From shows (System's list is the font menu). +fn can_replace(d: &Dialog, families: Option<&[String]>) -> bool { + let family = d.str("family"); + match families { + Some(list) => can_replace_in(&family, installed, list), + None => can_replace_in(&family, installed, &vectorcraft_text::FontDb::global().menu_family_list()), + } +} + +/// [`can_replace`] for the installed check `is_installed` and the shown list `list`. +fn can_replace_in(family: &str, is_installed: impl Fn(&str) -> bool, list: &[String]) -> bool { + is_installed(family) && list.iter().any(|f| f.eq_ignore_ascii_case(family)) +} + +/// Show list `source` ([`SOURCES`]): the chosen family stays when the list has it and it is +/// installed, otherwise the list's first installed family (none when there is none) is chosen in +/// its closest style. System chooses [`system_family`]. +fn set_source(app: &VectorcraftApp, d: &mut Dialog, source: &str, used: &[Value]) { + d.fields.insert("replaceFrom".into(), json!(source)); + let families = replacement_families(app, source, used); + if can_replace(d, families.as_deref()) { + return; + } + let family = match families { + Some(l) => l.into_iter().find(|f| installed(f)), + None => Some(system_family()), + }; + d.fields.insert("family".into(), json!(family.unwrap_or_default())); + d.fields.insert("style".into(), json!("")); } fn fonts(app: &mut VectorcraftApp) -> Vec { @@ -81,13 +163,31 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { } ui.add_space(10.0); widgets::subheader(ui, tl!("Replace With Font")); + let source = d.str("replaceFrom"); + ui.horizontal(|ui| { + widgets::field_label(ui, tl!("Replace With Font From")); + let at = SOURCES.iter().position(|s| *s == source).unwrap_or(2); + if let Some(i) = widgets::dropdown_named(ui, "ff-from", Some(tl!("Replace With Font From")), SOURCE_LABELS[at], &SOURCE_LABELS, 120.0) { + set_source(app, &mut d, SOURCES[i], &list); + } + }); + let families = replacement_families(app, &d.str("replaceFrom"), &list); let fam = d.str("family"); ui.horizontal(|ui| { - // Picks the font only: nothing is previewed on the document. - let pick = font_menu::font_menu(ui, "ff-family", &fam, 220.0, sample.as_deref(), font_menu::MenuLook::of(app)); - if let Some((f, style)) = font_menu::picked(app, pick) { - d.fields.insert("family".into(), json!(f)); - d.fields.insert("style".into(), json!(style.unwrap_or_default())); + if let Some(families) = &families { + // Font names are shown as they are. + let names: Vec<&str> = families.iter().map(String::as_str).collect(); + if let Some(i) = widgets::dropdown_names_named(ui, "ff-family-list", Some(tl!("Replace With Font")), &fam, &names, 220.0) { + d.fields.insert("family".into(), json!(families[i])); + d.fields.insert("style".into(), json!("")); + } + } else { + // Picks the font only: nothing is previewed on the document. + let pick = font_menu::font_menu(ui, "ff-family", &fam, 220.0, sample.as_deref(), font_menu::MenuLook::of(app)); + if let Some((f, style)) = font_menu::picked(app, pick) { + d.fields.insert("family".into(), json!(f)); + d.fields.insert("style".into(), json!(style.unwrap_or_default())); + } } let styles = vectorcraft_text::FontDb::global().styles(&d.str("family")); // "(closest)" is ours; the font's style names are shown as they are. @@ -101,15 +201,17 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { } }); ui.add_space(14.0); + // An empty list (Recent before any font was applied) leaves nothing to change to. + let replace = sel < list.len() && can_replace(&d, families.as_deref()); ui.horizontal(|ui| { let has = sel < list.len(); if ui.add_enabled(has, egui::Button::new(tl!("Find"))).clicked() { act = Some("find"); } - if ui.add_enabled(has, egui::Button::new(tl!("Change"))).on_hover_text(tl!("In the selected objects")).clicked() { + if ui.add_enabled(replace, egui::Button::new(tl!("Change"))).on_hover_text(tl!("In the selected objects")).clicked() { act = Some("change"); } - if ui.add_enabled(has, egui::Button::new(tl!("Change All"))).clicked() { + if ui.add_enabled(replace, egui::Button::new(tl!("Change All"))).clicked() { act = Some("changeAll"); } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -225,6 +327,194 @@ mod tests { crate::dialogs::cancel(&mut app); } + /// A document set in Inter and Source Serif 4 (Inter twice), with Inter the one recent font. + fn two_font_document() -> VectorcraftApp { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + for (y, font) in [(20, "Inter"), (40, "Source Serif 4"), (60, "Inter")] { + app.session.execute("text.create", &json!({"x": 10, "y": y, "text": "Hi", "font": font})).unwrap(); + } + app.ui.recent_fonts = vec!["Inter".into()]; + app + } + + #[test] + fn replace_with_font_from_lists_the_document_s_recent_or_installed_fonts() { + let mut app = two_font_document(); + let used = fonts(&mut app); + let mut document = replacement_families(&app, "document", &used).unwrap(); + document.sort(); + assert_eq!(document, ["Inter", "Source Serif 4"]); + assert_eq!(replacement_families(&app, "recent", &used).unwrap(), ["Inter"]); + assert_eq!(replacement_families(&app, "system", &used), None, "System is the font menu of every installed family"); + open(&mut app); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("replaceFrom"), "system"); + for source in SOURCES { + let mut d = app.ui.dialog.clone().unwrap(); + set_source(&app, &mut d, source, &used); + let listed = replacement_families(&app, source, &used); + assert!(can_replace(&d, listed.as_deref()), "{source}: {}", d.str("family")); + app.ui.dialog = Some(d); + let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx())); + assert!(text.contains("Replace With Font From"), "{text}"); + } + } + + #[test] + fn change_all_replaces_with_a_font_from_the_document() { + let mut app = two_font_document(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + set_source(&app, &mut d, "document", &used); + d.fields.insert("family".into(), json!("Inter")); + let from = used.iter().find(|f| f["family"] == "Source Serif 4").unwrap().clone(); + change(&mut app, &d, &from, true).unwrap(); + let families: Vec = fonts(&mut app).iter().map(|f| f["family"].clone()).collect(); + assert_eq!(families, [json!("Inter")]); + } + + /// A font applied with Change All becomes Recent's first family. + #[test] + fn change_all_adds_the_new_font_to_recent() { + let mut app = two_font_document(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + d.fields.insert("family".into(), json!("Source Serif 4")); + let from = used.iter().find(|f| f["family"] == "Inter").unwrap().clone(); + change(&mut app, &d, &from, true).unwrap(); + let used = fonts(&mut app); + assert_eq!(replacement_families(&app, "recent", &used).unwrap(), ["Source Serif 4", "Inter"]); + } + + /// A Change All that changes no text (the font's only text is locked) leaves Recent as it was. + #[test] + fn a_change_all_that_changes_nothing_leaves_recent_unchanged() { + let mut app = two_font_document(); + app.run("select.font", json!({"family": "Source Serif 4"})).unwrap(); + app.run("object.lock", json!({})).unwrap(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + d.fields.insert("family".into(), json!("Source Sans 3")); + let from = used.iter().find(|f| f["family"] == "Source Serif 4").unwrap().clone(); + assert_eq!(change(&mut app, &d, &from, true).unwrap()["runs"], 0); + assert_eq!(app.ui.recent_fonts, ["Inter"]); + } + + /// Change to another name of an installed family records the family's own name in Recent. + #[test] + fn change_records_the_family_s_own_name_in_recent() { + let mut app = two_font_document(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + d.fields.insert("family".into(), json!("SourceSerif4-Regular")); + let from = used.iter().find(|f| f["family"] == "Inter").unwrap().clone(); + assert_eq!(change(&mut app, &d, &from, true).unwrap()["runs"], 2); + assert_eq!(app.ui.recent_fonts, ["Source Serif 4", "Inter"]); + } + + #[test] + fn an_empty_recent_list_leaves_nothing_to_change_to() { + let mut app = two_font_document(); + app.ui.recent_fonts.clear(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + set_source(&app, &mut d, "recent", &used); + assert_eq!(replacement_families(&app, "recent", &used).unwrap(), Vec::::new()); + assert_eq!(d.str("family"), ""); + assert!(!can_replace(&d, Some(&[]))); + app.ui.dialog = Some(d); + let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx())); + assert!(text.contains("Recent"), "{text}"); + assert_eq!(app.ui.status, ""); + } + + /// Document lists a missing family, but Change and Change All stay off for it, in every list. + #[test] + fn a_missing_document_font_is_listed_but_not_offered_to_change_to() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + for (y, font) in [(20, "Missing Family"), (40, "Inter")] { + app.session.execute("text.create", &json!({"x": 10, "y": y, "text": "Hi", "font": font})).unwrap(); + } + open(&mut app); + let used = fonts(&mut app); + let document = replacement_families(&app, "document", &used).unwrap(); + assert!(document.iter().any(|f| f == "Missing Family"), "{document:?}"); + let mut d = app.ui.dialog.clone().unwrap(); + d.fields.insert("family".into(), json!("Missing Family")); + set_source(&app, &mut d, "document", &used); + assert_eq!(d.str("family"), "Inter", "the first installed family is chosen"); + assert!(can_replace(&d, Some(&document))); + d.fields.insert("family".into(), json!("Missing Family")); + assert!(!can_replace(&d, Some(&document))); + assert!(!can_replace(&d, None), "System"); + set_source(&app, &mut d, "system", &used); + assert!(can_replace(&d, None), "System chooses an installed family: {}", d.str("family")); + // A document set only in a missing family leaves nothing to change to. + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "Hi", "font": "Missing Family"})).unwrap(); + let used = fonts(&mut app); + set_source(&app, &mut d, "document", &used); + assert_eq!(d.str("family"), ""); + assert!(!can_replace(&d, replacement_families(&app, "document", &used).as_deref())); + } + + /// An empty Recent list clears the font; System then chooses an installed one again. + #[test] + fn system_after_an_empty_recent_list_chooses_an_installed_font() { + let mut app = two_font_document(); + app.ui.recent_fonts.clear(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + set_source(&app, &mut d, "recent", &used); + assert_eq!(d.str("family"), ""); + set_source(&app, &mut d, "system", &used); + assert_eq!((d.str("family").as_str(), d.str("style").as_str()), ("Source Sans 3", "")); + assert!(can_replace(&d, None)); + } + + /// The dialog opens on System's font: Source Sans 3 when it is installed, otherwise the font + /// menu's first family. + #[test] + fn the_dialog_opens_on_an_installed_font() { + let menu = ["Inter".to_string(), "Source Serif 4".to_string()]; + assert_eq!(system_family_in(|_| true, &menu), "Source Sans 3"); + assert_eq!(system_family_in(|f| f != "Source Sans 3", &menu), "Inter"); + assert_eq!(system_family_in(|_| false, &[]), ""); + let mut app = two_font_document(); + open(&mut app); + let d = app.ui.dialog.clone().unwrap(); + assert_eq!(d.str("family"), system_family()); + assert!(can_replace(&d, None), "{}", d.str("family")); + } + + /// A family that is installed but left out of the font menu (a hidden `.` family that + /// Document lists) cannot be changed to from System. + #[test] + fn system_offers_only_the_font_menu_s_families() { + let menu = ["Inter".to_string()]; + assert!(can_replace_in("inter", |_| true, &menu)); + assert!(!can_replace_in(".Hidden", |_| true, &menu)); + assert!(!can_replace_in("Inter", |_| false, &menu)); + let hidden = vectorcraft_text::FontDb::global().family_list().iter().find(|f| f.starts_with('.') && installed(f)).cloned(); + if let Some(hidden) = hidden { + let mut app = two_font_document(); + open(&mut app); + let used = fonts(&mut app); + let mut d = app.ui.dialog.clone().unwrap(); + d.fields.insert("family".into(), json!(hidden)); + assert!(!can_replace(&d, None), "{hidden}"); + set_source(&app, &mut d, "system", &used); + assert_eq!(d.str("family"), system_family()); + } + } + #[test] fn rows_flag_missing_fonts_substituted_styles_and_lacking_characters() { let row = |status: &str, glyphs: u64| json!({"status": status, "resolved": {"style": "W4"}, "missingGlyphs": glyphs}); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index fe8c162df..614054f41 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2640,6 +2640,8 @@ digits Malayalam Малаялам Replace Spine Замяна на оста Replace Symbol Замяна на символ Replace With Font Замяна с шрифт + Replace With Font From Замяна с шрифт от + System Система Replaces the saved preset of that name. Заменя записаната предварителна настройка с това име. Replace… Замяна… Require a password to open the document Изискване на парола за отваряне на документа diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 003a6189a..a0b4e236d 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3918,6 +3918,8 @@ effect Regular Obično In the selected objects U odabranim objektima Find in Folder… Pronađi u folderu… Replace With Font Zamijeni fontom + Replace With Font From Zamijeni fontom iz + System Sistem — substituted by {style} — zamijenjeno stilom {style} — {n} characters from another font — {n} znakova iz drugog fonta All Scripts Sve skripte diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index a28db5502..2a1f0a518 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -3340,6 +3340,8 @@ digits Malayalam Malaiàlam Replace Spine Reemplaça l’espina Replace Symbol Reemplaça el símbol Replace With Font Reemplaça per un tipus de lletra + Replace With Font From Reemplaça per un tipus de lletra de + System Sistema Replace “{name}”, the file this document was opened from? Voleu reemplaçar “{name}”, el fitxer amb què es va obrir aquest document? Replaces the saved preset of that name. Reemplaça el preajust desat amb aquest nom. Replace… Reemplaça… diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index f69545e1b..b9e6fd740 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3960,6 +3960,8 @@ effect Regular Pravidelný In the selected objects Ve vybraných objektech Find in Folder… Najít ve složce… Replace With Font Nahradit písmem + Replace With Font From Nahradit písmem z + System Systém — substituted by {style} — nahrazeno řezem {style} — {n} characters from another font — znaky z jiného písma: {n} All Scripts Všechny systémy písma diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index d78046e91..20a0a4dfe 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -3680,6 +3680,8 @@ effect Regular Regular In the selected objects I de markerede objekter Find in Folder… Find i mappe… Replace With Font Erstat med skrifttype + Replace With Font From Erstat med skrifttype fra + System System — substituted by {style} — erstattet af {style} — {n} characters from another font — {n} tegn fra en anden skrifttype All Scripts Alle scripts diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index cde85a2c0..94430d21e 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2667,6 +2667,8 @@ digits Malayalam Malayalam Replace Spine Achse ersetzen Replace Symbol Symbol ersetzen Replace With Font Durch Schrift ersetzen + Replace With Font From Ersetzende Schrift aus + System System Replaces the saved preset of that name. Ersetzt die gespeicherte Vorgabe dieses Namens. Replace… Ersetzen… Require a password to open the document Kennwort zum Öffnen des Dokuments verlangen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 1dbe39f0f..0a7d2be71 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3263,6 +3263,8 @@ digits Malayalam Μαλαγιάλαμ Replace Spine Αντικατάσταση κεντρικής καμπύλης Replace Symbol Αντικατάσταση συμβόλου Replace With Font Αντικατάσταση με γραμματοσειρά + Replace With Font From Αντικατάσταση με γραμματοσειρά από + System Σύστημα Replaces the saved preset of that name. Αντικαθιστά την αποθηκευμένη προεπιλογή με αυτό το όνομα. Require a password to open the document Απαίτηση κωδικού για το άνοιγμα εγγράφου Reset Bounding Box Επαναφορά πλαισίου οριοθέτησης diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ad2759838..4e5ba28d4 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2710,6 +2710,8 @@ digits Malayalam Malayálam Replace Spine Reemplazar eje Replace Symbol Reemplazar símbolo Replace With Font Reemplazar con fuente + Replace With Font From Reemplazar con fuente de + System Sistema Replaces the saved preset of that name. Reemplaza el preajuste guardado con ese nombre. Replace… Reemplazar… Require a password to open the document Solicitar una contraseña para abrir el documento diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 294c51387..c75f15ba5 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3879,6 +3879,8 @@ effect Regular Normaali In the selected objects Valituissa objekteissa Find in Folder… Etsi kansiosta… Replace With Font Korvaa fontilla + Replace With Font From Korvaavan fontin lähde + System Järjestelmä — substituted by {style} — korvattu tyylillä {style} — {n} characters from another font — {n} merkkiä toisesta fontista All Scripts Kaikki kirjoitusjärjestelmät diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f5e604a25..20b608bb4 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2702,6 +2702,8 @@ digits Malayalam Malayalam Replace Spine Remplacer la dorsale Replace Symbol Remplacer le symbole Replace With Font Remplacer par la police + Replace With Font From Remplacer par une police de + System Système Replaces the saved preset of that name. Remplace le paramètre prédéfini enregistré sous ce nom. Replace… Remplacer… Require a password to open the document Exiger un mot de passe pour ouvrir le document diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 1ce300e5d..12e8b0ec1 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3236,6 +3236,8 @@ mark-style Japanese Japán Replace Spine Gerinc cseréje Replace Symbol Szimbólum cseréje Replace With Font Helyettesítő betűtípus + Replace With Font From Helyettesítő betűtípus forrása + System Rendszer Replace “{name}”, the file this document was opened from? Felülírod ezt a fájlt, amelyből a dokumentumot megnyitottad: „{name}”? Replaces the saved preset of that name. Felülírja az azonos nevű mentett előbeállítást. Replace… Csere… diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 336e64182..8e4aa69f2 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3680,6 +3680,8 @@ effect Regular Reguler In the selected objects Di objek yang dipilih Find in Folder… Temukan di folder… Replace With Font Ganti dengan font + Replace With Font From Ganti dengan font dari + System Sistem — substituted by {style} — digantikan oleh {style} — {n} characters from another font — {n} karakter dari font lain All Scripts Semua skrip diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 72d8ab51f..6fd38b0f7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2743,6 +2743,8 @@ digits Malayalam Malayalam Replace Spine Sostituisci dorsale Replace Symbol Sostituisci simbolo Replace With Font Sostituisci con font + Replace With Font From Sostituisci con font da + System Sistema Replaces the saved preset of that name. Sostituisce il preset salvato con quel nome. Replace… Sostituisci… Require a password to open the document Richiedi una password per aprire il documento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 2fd494bd3..5096fef13 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2728,6 +2728,8 @@ mark-style Japanese 日本式 Replace Font フォントを置換 Replace Symbol シンボルを置き換え Replace With Font 置換するフォント + Replace With Font From 置換するフォントの一覧 + System システム Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 別のファイルに置き換えるか、無視してください。無視した場合、画像にはドキュメントに保存されたプレビューが表示され続けます。 Replaces the saved preset of that name. 同じ名前の保存済みプリセットを置き換えます。 Replace… 置換… diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 3ab0c61b4..5389e1f09 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3463,6 +3463,8 @@ effect Regular ჩვეულებრივი In the selected objects მონიშნულ ობიექტებში Find in Folder… საქაღალდეში ძებნა… Replace With Font ჩანაცვლება შრიფტით + Replace With Font From ჩანაცვლების შრიფტის წყარო + System სისტემა — substituted by {style} — ჩანაცვლებულია: {style} — {n} characters from another font — სხვა შრიფტის სიმბოლოები: {n} All Scripts ყველა დამწერლობა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 7f9542920..8596570c7 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2717,6 +2717,8 @@ mark-style Japanese 일본식 Replace Font 글꼴 바꾸기 Replace Symbol 심볼 바꾸기 Replace With Font 바꿀 글꼴 + Replace With Font From 바꿀 글꼴 목록 + System 시스템 Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 다른 파일로 바꾸거나 무시하세요. 무시하면 해당 이미지는 문서에 저장된 미리 보기로 계속 표시됩니다. Replaces the saved preset of that name. 같은 이름으로 저장된 사전 설정을 대체합니다. Replace… 바꾸기… diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 65a015fd2..4cde5d403 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -3694,6 +3694,8 @@ effect Regular Regelmatig In the selected objects In de geselecteerde objecten Find in Folder… Zoeken in map… Replace With Font Vervangen door lettertype + Replace With Font From Vervangen door lettertype uit + System Systeem — substituted by {style} — vervangen door {style} — {n} characters from another font — {n} tekens van een ander lettertype All Scripts Alle scripts diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 3eb6257e7..52800ade0 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3800,6 +3800,8 @@ effect Regular Normalny In the selected objects W zaznaczonych obiektach Find in Folder… Znajdź w folderze… Replace With Font Zastąp czcionką + Replace With Font From Zastąp czcionką z + System System — substituted by {style} — zastąpiona przez {style} — {n} characters from another font — znaki z innej czcionki: {n} All Scripts Wszystkie skrypty diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 15bccd1f7..8369f4d23 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2258,6 +2258,8 @@ digits Malayalam Malaiala Replace Spine Substituir Coluna Replace Symbol Substituir Símbolo Replace With Font Substituir Com Fonte + Replace With Font From Substituir Com Fonte De + System Sistema Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Substitua por outro arquivo, ou ignore-o: suas imagens continuam mostrando a pré-visualização salva com o documento. Replaces the saved preset of that name. Substitui o predefinido salvo com esse nome. Replace… Substituir… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ccf399ddf..e6bf3f77c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2738,6 +2738,8 @@ digits Malayalam Малаялам Replace Spine Заменить остов Replace Symbol Заменить символ Replace With Font Заменить шрифтом + Replace With Font From Заменить шрифтом из + System Система Replaces the saved preset of that name. Заменяет сохранённый набор с таким именем. Replace… Заменить… Require a password to open the document Требовать пароль для открытия документа diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 4ee641f80..f12351040 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2646,6 +2646,8 @@ digits Malayalam Malayalam Replace Spine Ersätt ryggrad Replace Symbol Ersätt symbol Replace With Font Ersätt med typsnitt + Replace With Font From Ersätt med typsnitt från + System System Replaces the saved preset of that name. Ersätter den sparade förinställningen med samma namn. Replace… Ersätt… Require a password to open the document Kräv lösenord för att öppna dokumentet diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index cbf5bf629..b198e1af1 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2517,6 +2517,8 @@ digits Malayalam Malayalam Replace Spine Omurgayı Değiştir Replace Symbol Sembolü Değiştir Replace With Font Yazı Tipiyle Değiştir + Replace With Font From Değiştirilecek Yazı Tipi Kaynağı + System Sistem Replaces the saved preset of that name. Bu adın kayıtlı ön ayarını değiştirir. Replace… Değiştir… Require a password to open the document Belgeyi açmak için parola gerektir diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 037d98050..d468a63d7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2735,6 +2735,8 @@ digits Malayalam Малаялам Replace Spine Замінити вісь Replace Symbol Замінити символ Replace With Font Замінити шрифтом + Replace With Font From Замінити шрифтом з + System Система Replaces the saved preset of that name. Замінює збережений набір із таким ім’ям. Replace… Замінити… Require a password to open the document Вимагати пароль для відкриття документа diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index c4666f6ca..9d4acddcb 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3004,6 +3004,8 @@ mark-style Japanese Kiểu Nhật Replace Spine Thay thế trục Replace Symbol Thay thế ký hiệu Replace With Font Thay thế bằng phông chữ + Replace With Font From Thay thế bằng phông chữ từ + System Hệ thống Replace “{name}”, the file this document was opened from? Thay thế “{name}”, tệp mà tài liệu này được mở từ đó? Replaces the saved preset of that name. Thay thế thiết lập sẵn đã lưu có tên đó. Replace… Thay thế… diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0a0bee4c1..0426b415f 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2613,6 +2613,8 @@ digits Malayalam 马拉雅拉姆数字 Replace Spine 替换脊线 Replace Symbol 替换符号 Replace With Font 替换为字体 + Replace With Font From 替换字体来源 + System 系统 Replaces the saved preset of that name. 替换同名已存预设。 Replace… 替换… Require a password to open the document 打开文档时需要密码 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b96f2a2a1..92a1a9808 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3595,6 +3595,8 @@ digits Malayalam 馬來亞拉姆數字 Replace Spine 取代脊線 Replace Symbol 取代符號 Replace With Font 取代為字體 + Replace With Font From 取代字體來源 + System 系統 Replaces the saved preset of that name. 取代同名的已儲存預設集。 Replace… 取代… Require a password to open the document 開啟文件時需要密碼 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2bc6f55ab..755c57da5 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -885,6 +885,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Manual kerning in right-to-left text:** a manual kerning value on a Hebrew or Arabic character changes the space between it and the next character in reading order, the one drawn to its left, under Auto and Optical kerning. The character before it keeps its place, and the caret between the pair stays beside the next character. The side the space goes on is VectorCraft's own rule. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html) ("Kerning is the process of adding or subtracting space between specific pairs of characters."). Test: `rtl_manual_kerning_spaces_the_character_from_the_next_in_reading_order`. - **Manual kerning on inline graphics:** a manual kerning value on an inline graphic's character spaces it from the next character in reading order by the same distance as on a letter of its run's size, in left-to-right, right-to-left and vertical type; Auto and Optical kerning still apply no font pairs to it. This is VectorCraft's own rule. Test: `manual_kerning_on_an_inline_graphic_spaces_it_like_a_letter`. - **Recent font pickers (#3515):** The shared Character, Properties, Glyphs and Find Font family picker lists recently applied families first, once each, in most-recent order and limited by Preferences › Type › Number of Recent Fonts. Search and filters apply to both sections; unavailable families are omitted. Section headings cannot be selected or previewed. Confirmed character-range font edits update the same saved history; previews and failed edits do not. Requirement: [issue #3515](https://github.com/storytold/vectorcraft/issues/3515). +- **Find Font: Replace With Font From:** a dropdown under Replace With Font chooses the list the replacement font comes from: Document lists the families the document uses, once each; Recent lists the recently applied families that are installed (fonts applied with Find Font's Change and Change All included), newest first, as many as Preferences › Type › Number of Recent Fonts allows; System, the default, is the font menu of every installed family. The dialog opens on System with Source Sans 3 chosen, or the font menu's first family when Source Sans 3 is not installed. Choosing a list keeps the chosen font when the list has it and it is installed; otherwise Document and Recent choose the list's first installed font, and System chooses Source Sans 3 or the font menu's first family. Change and Change All are unavailable when the list has no installed font (Recent before any font was applied) and when the chosen family is not installed or is not in the shown list. Behavior reference: [Find and replace fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-replace-fonts.html) (public documentation). Tests: `crates/ui-egui/src/find_font.rs`. - **Mouse wheel over the font family field (#5253):** In the Character and Properties panels and the Control bar, turning the mouse wheel over the closed font family field applies the next family to the selected type (the previous one when turned up), keeping the style when the family has it, else its closest. Each notch is one undo step; leaving the field or Escape keeps the family, and Undo takes it back. The steps use the font menu's order at the first turn and keep it until the pointer leaves the field or the text selection changes, so recent-font updates do not reshuffle later turns. That order follows the menu's saved font filters but ignores its search text, puts recent families first, and stops at the first and last family. A turn with a modifier key is left to the panel and the canvas. Behavior reference: the [Illustrator CS6 reference](https://help.adobe.com/archive/en/illustrator/cs6/illustrator_reference.pdf#page=10) describes editing hovered dropdown controls with the mouse wheel; [Find and apply fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-apply-fonts.html) names the family field's places. Applying it to the font field, the order, the ends and the undo steps are VectorCraft's own rules. Tests: `crates/ui-egui/src/tests/font_menu.rs`, `crates/ui-egui/src/font_menu/tests.rs`, `crates/ui-egui/tests/font_wheel.rs`. - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html).