From 89812275d76062406ea320576d5cbb963685eed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Sat, 10 Oct 2026 06:57:01 +0100 Subject: [PATCH] Part of #394: Show Font Names in English labels the font menus. --- ROADMAP.md | 4 +- apps/vectorcraft/src/gpu.rs | 2 + crates/engine/src/cmd/fonts.rs | 2 +- crates/engine/src/tests_prefs.rs | 11 ++++ crates/mcp/src/tests.rs | 18 ++++++ crates/text/src/fontdb.rs | 94 ++++++++++++++++++++++++++++-- crates/text/src/tests_sysfonts.rs | 22 +++++++ crates/ui-egui/src/font_menu.rs | 31 +++++++--- crates/ui-egui/src/menus.rs | 43 +++++++++----- crates/ui-egui/src/native_menu.rs | 2 +- crates/ui-egui/src/prefs_dialog.rs | 15 +++++ docs/mcp.md | 20 ++++--- 12 files changed, 225 insertions(+), 39 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 3944f6d5..c2d0c10d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -115,7 +115,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas. - Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close). - Four brightness themes, persistent preferences. - - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). About 19 stored options still do nothing yet (the rest of #394: Show Font Names in English, Highlight Alternate Glyphs, Display Print Size at 100% Zoom, Identify Objects By, East Asian Type units, Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Appearance of Black, Touch Workspace). + - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). About 18 stored options still do nothing yet (the rest of #394: Highlight Alternate Glyphs, Display Print Size at 100% Zoom, Identify Objects By, East Asian Type units, Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Appearance of Black, Touch Workspace). - Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423). - Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once. - Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off. @@ -234,7 +234,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 | | Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 | -| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | macOS native menu dismissal fixed (M14.8); remaining: Variables, SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas (Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done), about 19 Preferences options stored but not yet acted on (#394: Show Font Names in English, Highlight Alternate Glyphs, Display Print Size, Open Documents As Tabs, Real-time Drawing, hyphenation language, Appearance of Black, devices…) | 14–22 | +| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | macOS native menu dismissal fixed (M14.8); remaining: Variables, SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas (Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done), about 18 Preferences options stored but not yet acted on (#394: Highlight Alternate Glyphs, Display Print Size, Open Documents As Tabs, Real-time Drawing, hyphenation language, Appearance of Black, devices…) | 14–22 | | Libraries, Links, Package | 2 | 80% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles are done (#745); Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 | | **Feature parity** | **103** | **~75%** | | **~210–330** | | Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 | diff --git a/apps/vectorcraft/src/gpu.rs b/apps/vectorcraft/src/gpu.rs index 7cab923c..264583ed 100644 --- a/apps/vectorcraft/src/gpu.rs +++ b/apps/vectorcraft/src/gpu.rs @@ -161,6 +161,7 @@ pub fn display_gpus() -> Vec { } /// Record that the GPU `pci` drives a display, merging it with its other displays. +#[cfg(any(windows, target_os = "linux", test))] fn add_display_gpu(gpus: &mut Vec, pci: (u32, u32), primary: bool) { match gpus.iter_mut().find(|g| g.pci == pci) { Some(gpu) => gpu.primary |= primary, @@ -197,6 +198,7 @@ fn windows_display_gpus() -> Vec { /// The PCI `(vendor, device)` of a Windows device id such as /// `PCI\VEN_10DE&DEV_2204&SUBSYS_40421458&REV_A1`, any case; `None` for anything else. +#[cfg(any(windows, test))] fn pci_ids(device_id: &str) -> Option<(u32, u32)> { let field = |key: &str| { device_id.split(['\\', '&']).find_map(|part| { diff --git a/crates/engine/src/cmd/fonts.rs b/crates/engine/src/cmd/fonts.rs index 71d642c3..e1b6d84d 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -44,7 +44,7 @@ pub fn specs() -> Vec { "Font List", [], None, - "{family?} → {families: [names]} sorted: the bundled fonts, fonts added and the fonts installed on the system (none on the web), as the font menus list them (without the system's hidden families, whose names start with a dot; they still resolve by name); with family: {family, styles: [names]} (upright styles by weight, then italics; an error when the family isn't available)", + "{family?} → {families: [names]} sorted: the bundled fonts, fonts added and the fonts installed on the system (none on the web), each the family's English (canonical) name for `text.setStyle` and documents (without the system's hidden families, whose names start with a dot; they still resolve by name). Preferences › Type › Show Font Names in English only changes the UI menu labels, not this list; with family: {family, styles: [names]} (upright styles by weight, then italics; an error when the family isn't available)", always, font_list ), diff --git a/crates/engine/src/tests_prefs.rs b/crates/engine/src/tests_prefs.rs index 68f73f2f..530cbd3a 100644 --- a/crates/engine/src/tests_prefs.rs +++ b/crates/engine/src/tests_prefs.rs @@ -671,6 +671,17 @@ world", "size": 20}), assert_eq!((t.first_style().size, t.first_style().fill.color().map(|c| c.to_hex())), (20.0, Some("#00ff00".into()))); } +/// Preferences › Type › Show Font Names in English (#394): stored and read by the font menus. +#[test] +fn show_font_names_in_english_preference_is_stored() { + let mut s = Session::new(); + assert!(s.prefs.font_names_in_english, "on by default"); + set_pref(&mut s, "fontNamesInEnglish", json!(false)); + assert!(!s.prefs.font_names_in_english); + set_pref(&mut s, "fontNamesInEnglish", json!(true)); + assert!(s.prefs.font_names_in_english); +} + /// Preferences › Hyphenation › Exceptions (#394): `prefs.set` pushes the list into the hyphenator /// and bumps open documents so type reflows. Parsing of the list is covered in `vectorcraft-text`. #[test] diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 90af6255..25639ebd 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -563,6 +563,24 @@ fn shift_marquee_gesture_toggles_the_selection() { } } +/// Preferences › Type › Show Font Names in English (#394): agents set it with `prefs.set`; +/// `text.fontList` stays the English (canonical) family names used by `text.setStyle`, not the +/// native UI labels. +#[test] +fn show_font_names_in_english_does_not_change_font_list_for_agents() { + let mut s = server(); + let r = call(&mut s, 1, "run_command", json!({"command": "prefs.set", "params": {"key": "fontNamesInEnglish", "value": false}})); + assert_eq!(r["isError"], false, "{r}"); + let r = call(&mut s, 2, "run_command", json!({"command": "prefs.get", "params": {"key": "fontNamesInEnglish"}})); + assert_eq!(r["isError"], false, "{r}"); + assert_eq!(serde_json::from_str::(&text_of(&r)).unwrap(), json!(false)); + let r = call(&mut s, 3, "run_command", json!({"command": "text.fontList", "params": {}})); + assert_eq!(r["isError"], false, "{r}"); + let families = serde_json::from_str::(&text_of(&r)).unwrap()["families"].as_array().unwrap().clone(); + // Canonical English name for commands, whatever the UI labels show with the preference off. + assert!(families.iter().any(|f| f.as_str() == Some("Source Sans 3")), "{families:?}"); +} + /// Type preferences reach agents (#394): type the Type tool places starts with placeholder text, /// selected; Alt+→ tracks it by Tracking and Cmd+Shift+. steps its size by Size/Leading. #[test] diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 8c9695b5..992405bc 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -50,6 +50,9 @@ pub struct FontFace { id: u32, /// Typographic family name (e.g. "Source Sans 3"). pub family: String, + /// Non-English family name from the name table (`ヒラギノ角ゴシック`), when the font has one. + /// Used when Preferences › Type › Show Font Names in English is off. + pub native_family: Option, /// Typographic style name (e.g. "Semibold", "Italic"). pub style: String, /// The font's version string (name ID 5, e.g. "Version 7.200"; empty when it has none). Two @@ -292,6 +295,8 @@ impl FontFace { #[derive(Debug, Default)] struct CatalogFamily { name: String, + /// Non-English family name from the name table, when the scan found one. + native: Option, faces: Vec, /// The family's kind (from its first face found). traits: Option, @@ -428,6 +433,34 @@ fn localized(font: &skrifa::FontRef<'_>, id: StringId) -> Vec<(Option, S font.localized_strings(id).map(|s| (s.language().map(str::to_owned), s.to_string())).filter(|(_, s)| !s.is_empty()).collect() } +/// Whether a name-table language tag is English (Illustrator's "Show Font Names in English"). +fn is_english_lang(lang: Option<&str>) -> bool { + lang.is_some_and(|l| { + let l = l.to_ascii_lowercase(); + l == "en" || l == "eng" || l.starts_with("en-") || l.starts_with("en_") + }) +} + +/// A non-English family label from the name table, for Preferences › Type › Show Font Names in +/// English (off). Prefers a record whose language isn't English; else any label that differs from +/// `english` and isn't plain ASCII. `None` when the font has no other name. +fn pick_native_label(english: &str, pairs: &[(Option, String)]) -> Option { + pairs + .iter() + .find(|(lang, s)| !s.eq_ignore_ascii_case(english) && !is_english_lang(lang.as_deref())) + .or_else(|| pairs.iter().find(|(_, s)| !s.eq_ignore_ascii_case(english) && !s.is_ascii())) + .map(|(_, s)| s.clone()) +} + +/// The family's native-language name from the font (typographic, else legacy), when it has one. +fn native_family_name(font: &skrifa::FontRef<'_>, english: &str) -> Option { + let pairs = { + let t = localized(font, StringId::TYPOGRAPHIC_FAMILY_NAME); + if t.is_empty() { localized(font, StringId::FAMILY_NAME) } else { t } + }; + pick_native_label(english, &pairs) +} + /// Every localized string of `id`. fn all_names(font: &skrifa::FontRef<'_>, id: StringId) -> Vec { localized(font, id).into_iter().map(|(_, s)| s).collect() @@ -692,6 +725,8 @@ fn face_names(f: &skrifa::FontRef<'_>) -> Option<(String, String)> { #[derive(Clone, Debug)] pub(crate) struct FaceStyle { family: String, + /// See [`FontFace::native_family`]. + native_family: Option, style: String, keys: FaceKeys, /// A named instance's axis settings (user units); empty for the face itself. @@ -708,13 +743,14 @@ pub(crate) struct FaceStyle { /// The styles of a face: the face itself, then its named instances when it is a variable font. pub(crate) fn face_styles(f: &skrifa::FontRef<'_>) -> Vec { let Some((family, style)) = face_names(f) else { return vec![] }; + let native_family = native_family_name(f, &family); let mut keys = FaceKeys::of(f, &family, &style); let traits = FontTraits::of(f, &keys); - let mut instances = named_instances(f, &family, &style, &mut keys); + let mut instances = named_instances(f, &family, &style, &mut keys, native_family.clone()); for i in &mut instances { i.traits = traits; } - let face = FaceStyle { family, style, keys, variations: vec![], weight: None, italic: false, traits }; + let face = FaceStyle { family, native_family, style, keys, variations: vec![], weight: None, italic: false, traits }; std::iter::once(face).chain(instances).collect() } @@ -723,7 +759,7 @@ pub(crate) fn face_styles(f: &skrifa::FontRef<'_>) -> Vec { /// names already taken. The default instance's name, when it isn't the face's style name (a face /// `Regular` whose default instance is `Book`), names the face too (`keys`). Damaged tables give /// fewer instances or none; the counts read are capped. -fn named_instances(f: &skrifa::FontRef<'_>, family: &str, style: &str, keys: &mut FaceKeys) -> Vec { +fn named_instances(f: &skrifa::FontRef<'_>, family: &str, style: &str, keys: &mut FaceKeys, native_family: Option) -> Vec { let axes = f.axes(); if axes.is_empty() || axes.len() > MAX_AXES { return vec![]; @@ -766,7 +802,16 @@ fn named_instances(f: &skrifa::FontRef<'_>, family: &str, style: &str, keys: &mu } let keys = FaceKeys { families: keys.families.clone(), styles, legacy: vec![], postscript }; let variations = tags.iter().zip(&coords).map(|(t, v)| (t.to_be_bytes(), *v)).collect(); - out.push(FaceStyle { family: family.to_string(), style, keys, variations, weight, italic, traits: FontTraits::default() }); + out.push(FaceStyle { + family: family.to_string(), + native_family: native_family.clone(), + style, + keys, + variations, + weight, + italic, + traits: FontTraits::default(), + }); } out } @@ -1236,7 +1281,7 @@ fn make_face(bytes: FontBytes, index: u32, spec: FaceStyle, path: Option v.as_slice(), }; let f = skrifa::FontRef::from_index(data, index).ok()?; - let FaceStyle { family, style, keys, variations, weight, italic, traits } = spec; + let FaceStyle { family, native_family, style, keys, variations, weight, italic, traits } = spec; let version = name(&f, &[StringId::VERSION_STRING]).unwrap_or_default(); // A named instance: outlines, metrics and advances at its axis settings. let location = if variations.is_empty() { Location::default() } else { f.axes().location(variations.iter().map(|(t, v)| (Tag::new(t), *v))) }; @@ -1258,6 +1303,7 @@ fn make_face(bytes: FontBytes, index: u32, spec: FaceStyle, path: Option String { + let (family, _) = self.canonical(family, ""); + if english { + return family; + } + if let Some(native) = self.read_faces().iter().find(|f| f.family.eq_ignore_ascii_case(&family)).and_then(|f| f.native_family.clone()) { + return native; + } + if let Some(native) = self.read_catalog().get(&family.to_ascii_lowercase()).and_then(|c| c.native.clone()) { + return native; + } + family + } + /// Style names available for `family`: upright styles by weight, then italics. pub fn styles(&self, family: &str) -> Vec { let (family, _) = self.canonical(family, ""); @@ -1599,7 +1662,7 @@ impl FontDb { if !named && !is_font_file(&p) && !crate::suitcase::is_suitcase(&p) { continue; } - for FaceStyle { family, style, keys, weight, italic, traits, .. } in file_face_names(&p) { + for FaceStyle { family, native_family, style, keys, weight, italic, traits, .. } in file_face_names(&p) { for (k, a) in Alias::of(&family, &style, &keys) { aliases.entry(k).or_default().push(a); } @@ -1612,6 +1675,9 @@ impl FontDb { if entry.name.is_empty() { entry.name = family; } + if entry.native.is_none() { + entry.native = native_family; + } let (weight, italic) = (weight.unwrap_or_else(|| style_weight(&style)), italic || style_italic(&style)); entry.faces.push(CatalogFace { style, path: p.clone(), weight, italic }); n += 1; @@ -2024,6 +2090,22 @@ mod alias_tests { assert!(find("HiraginoSans-W3").iter().any(|a| a.style.as_deref() == Some("W3") && a.paired.is_none()), "the PostScript name"); } + /// #394 Preferences › Type › Show Font Names in English: pick the non-English name-table label. + #[test] + fn pick_native_label_prefers_a_non_english_name_record() { + assert_eq!( + pick_native_label("Hiragino Sans", &[(Some("en".into()), "Hiragino Sans".into()), (Some("ja".into()), "ヒラギノ角ゴシック".into())]) + .as_deref(), + Some("ヒラギノ角ゴシック") + ); + assert!(pick_native_label("Source Sans 3", &[(Some("en".into()), "Source Sans 3".into())]).is_none()); + let font = name_font(HIRAGINO_W3); + let f = skrifa::FontRef::new(&font).unwrap(); + let (family, _) = face_names(&f).unwrap(); + assert_eq!(family, "Hiragino Sans"); + assert_eq!(native_family_name(&f, &family).as_deref(), Some("ヒラギノ角ゴシック")); + } + #[test] fn without_typographic_names_the_legacy_ones_are_the_familys() { let (family, style, k) = keys(&[(3, EN, 1, "Example Serif"), (3, EN, 2, "Bold Italic"), (3, JA, 1, "例セリフ"), (3, JA, 2, "太字斜体")]); diff --git a/crates/text/src/tests_sysfonts.rs b/crates/text/src/tests_sysfonts.rs index faebd104..13a146fa 100644 --- a/crates/text/src/tests_sysfonts.rs +++ b/crates/text/src/tests_sysfonts.rs @@ -547,3 +547,25 @@ fn the_app_font_folder_is_read_by_the_scan() { assert_eq!(dirs.iter().filter(|d| **d == dir).count(), 1, "{dirs:?}"); assert!(!dirs.contains(&dir.join("Other"))); } + +/// #394 Preferences › Type › Show Font Names in English: off, menus label the family in its +/// native language; on (default), the English name. Documents still resolve by either name. +#[test] +fn family_display_name_follows_show_font_names_in_english() { + let bytes = named(&[ + (3, EN, 1, "Hiragino Sans"), + (3, EN, 2, "Regular"), + (3, EN, 6, "HiraginoSans-Regular"), + (3, EN, 16, "Hiragino Sans"), + (3, JA, 16, "ヒラギノ角ゴシック"), + (3, EN, 17, "Regular"), + ]); + let db = FontDb::with_font_dirs(vec![]); + assert!(db.add_font(bytes) > 0); + assert_eq!(db.family_display_name("Hiragino Sans", true), "Hiragino Sans"); + assert_eq!(db.family_display_name("Hiragino Sans", false), "ヒラギノ角ゴシック"); + assert_eq!(db.family_display_name("ヒラギノ角ゴシック", false), "ヒラギノ角ゴシック"); + assert_eq!(db.family_display_name("Source Sans 3", false), "Source Sans 3"); + let face = db.face("ヒラギノ角ゴシック", "Regular").unwrap(); + assert_eq!(face.family, "Hiragino Sans"); +} diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs index 9fed316d..0e1680d2 100644 --- a/crates/ui-egui/src/font_menu.rs +++ b/crates/ui-egui/src/font_menu.rs @@ -38,6 +38,8 @@ pub enum FontPick { pub struct MenuLook<'a> { /// Show each family's sample (Preferences › Type › Enable in-menu font previews). pub samples: bool, + /// Preferences › Type › Show Font Names in English (on by default). + pub english_names: bool, /// Row height in points (Font Preview Size). pub row: f32, /// The starred families. @@ -45,9 +47,9 @@ pub struct MenuLook<'a> { } impl Default for MenuLook<'static> { - /// Samples on, medium rows, no favourites. + /// Samples on, English names, medium rows, no favourites. fn default() -> Self { - Self { samples: true, row: ROW, favorites: &[] } + Self { samples: true, english_names: true, row: ROW, favorites: &[] } } } @@ -58,7 +60,12 @@ impl<'a> MenuLook<'a> { "large" => 32.0, _ => ROW, }; - Self { samples: app.session.prefs.font_preview, row, favorites: &app.ui.favorite_fonts } + Self { + samples: app.session.prefs.font_preview, + english_names: app.session.prefs.font_names_in_english, + row, + favorites: &app.ui.favorite_fonts, + } } } @@ -124,9 +131,9 @@ impl Row { } } -/// What the rows were listed for: the query, the script, kind and favourites filters, the -/// expanded families, the favourites and the font list's generation. -type RowsKey = (String, usize, Option, bool, Vec, Vec, u64); +/// What the rows were listed for: the query, the script, kind and favourites filters, Show Font +/// Names in English, the expanded families, the favourites and the font list's generation. +type RowsKey = (String, usize, Option, bool, bool, Vec, Vec, u64); /// The menu's state between frames. #[derive(Clone, Default)] @@ -254,10 +261,12 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp let current = current.as_str(); let favorite = |f: &str| look.favorites.iter().any(|x| x.eq_ignore_ascii_case(f)); // The generation is read first: fonts that load meanwhile make the next frame list them. - let key: RowsKey = (query.clone(), st.script, st.class, st.favorites_only, st.expanded.clone(), look.favorites.to_vec(), db.generation()); + let key: RowsKey = + (query.clone(), st.script, st.class, st.favorites_only, look.english_names, st.expanded.clone(), look.favorites.to_vec(), db.generation()); if st.rows_key.as_ref() != Some(&key) { let wanted = |f: &str| { - if !(query.is_empty() || f.to_lowercase().contains(&query)) { + let label = db.family_display_name(f, look.english_names); + if !(query.is_empty() || f.to_lowercase().contains(&query) || label.to_lowercase().contains(&query)) { return false; } if st.favorites_only && !favorite(f) { @@ -382,7 +391,11 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp let indent = if row.style().is_some() { DISCLOSURE + 12.0 } else { DISCLOSURE }; let name_w = rect.width() * NAME_SHARE; let name_rect = egui::Rect::from_min_max(rect.min + vec2(indent, 0.0), egui::pos2(rect.min.x + name_w - 4.0, rect.max.y)); - let name = row.style().unwrap_or(row.family()); + // Family rows follow Show Font Names in English; style rows keep the style name (#394). + let name = match row { + Row::Style(_, style) => style.clone(), + Row::Family(f) => db.family_display_name(f, look.english_names), + }; ui.painter_at(name_rect).text(name_rect.left_center(), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(12.0), t.text); let sample_rect = egui::Rect::from_min_max(rect.min + vec2(name_w, 2.0), rect.max - vec2(STAR + 2.0, 2.0)); if !look.samples { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 368c4f51..2db0d999 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1921,7 +1921,13 @@ fn selected_image(app: &VectorcraftApp, keep: impl Fn(&vectorcraft_doc::ImageObj }) } +/// The in-window and native menu tree. `english_names` is Preferences › Type › Show Font Names in +/// English (the Type → Font labels follow it; tests use the default `true`). pub fn menu_tree() -> Vec<(&'static str, Vec)> { + menu_tree_named(true) +} + +pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { let panel = |label: &'static str, id: &'static str| cp(label, "window.panel", json!({ "panel": id })); vec![ ( @@ -2269,7 +2275,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { ( "Type", vec![ - sub("Font", font_items()), + sub("Font", font_items(english_names)), sub("Recent Fonts", RECENT_FONT_IDS.iter().map(|id| c("Recent Font", id)).collect()), sub("Size", TYPE_SIZES.iter().map(|(l, n)| cp(l, "text.setStyle", json!({ "size": n }))).collect()), c("Bold", "type.bold"), @@ -2742,7 +2748,7 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 { ctx.data_mut(|d| d.insert_temp::(field_key, field)); } let mut clicked: Option<(String, Value)> = None; - let tree = menu_tree(); + let tree = menu_tree_named(app.session.prefs.font_names_in_english); let (end, open) = egui::MenuBar::new() .ui(ui, |ui| { let mut titles = Vec::with_capacity(tree.len()); @@ -3147,7 +3153,7 @@ pub struct MenuEntry { /// Flattened menu for `ui.menu.list`. pub fn menu_entries(app: &VectorcraftApp) -> Vec { let mut out = vec![]; - for (title, items) in menu_tree() { + for (title, items) in menu_tree_named(app.session.prefs.font_names_in_english) { flatten(app, vec![title.to_string()], &items, &mut out); } out @@ -3251,34 +3257,45 @@ fn insert_items(list: &[(&'static str, &'static str)]) -> Vec { } /// Type → Font: one item per available family, the installed fonts included. Built again only when -/// the fonts change (the menu tree is built every frame); labels are interned once each, so -/// rebuilding doesn't allocate forever. -fn font_items() -> Vec { +/// the fonts or Show Font Names in English change (the menu tree is built every frame); labels are +/// interned once each, so rebuilding doesn't allocate forever. The command still sets the English +/// family name ([`vectorcraft_text::FontDb::canonical`]). +fn font_items(english_names: bool) -> Vec { static NAMES: std::sync::Mutex> = std::sync::Mutex::new(std::collections::BTreeSet::new()); - static ITEMS: std::sync::Mutex<(u64, Vec)> = std::sync::Mutex::new((u64::MAX, Vec::new())); + static ITEMS: std::sync::Mutex<(u64, bool, Vec)> = std::sync::Mutex::new((u64::MAX, true, Vec::new())); let db = vectorcraft_text::FontDb::global(); // Read first: fonts that load meanwhile make the next frame build the list again. let generation = db.generation(); let fams = db.menu_family_list(); let (Ok(mut names), Ok(mut items)) = (NAMES.lock(), ITEMS.lock()) else { return vec![] }; - if items.0 != generation { + if items.0 != generation || items.1 != english_names { let list = fams .iter() .map(|f| { - let label: &'static str = match names.get(f.as_str()) { + let english = f.as_str(); + let shown = db.family_display_name(english, english_names); + let label: &'static str = match names.get(shown.as_str()) { Some(n) => n, None => { - let n: &'static str = Box::leak(f.clone().into_boxed_str()); + let n: &'static str = Box::leak(shown.into_boxed_str()); names.insert(n); n } }; - cp(label, "text.setStyle", json!({ "font": label })) + let font: &'static str = match names.get(english) { + Some(n) => n, + None => { + let n: &'static str = Box::leak(english.to_string().into_boxed_str()); + names.insert(n); + n + } + }; + cp(label, "text.setStyle", json!({ "font": font })) }) .collect(); - *items = (generation, list); + *items = (generation, english_names, list); } - items.1.clone() + items.2.clone() } /// The raster effects' submenus of the Effect menu (the Photoshop-style effects, below the diff --git a/crates/ui-egui/src/native_menu.rs b/crates/ui-egui/src/native_menu.rs index 2701dc02..aeb12f3c 100644 --- a/crates/ui-egui/src/native_menu.rs +++ b/crates/ui-egui/src/native_menu.rs @@ -413,7 +413,7 @@ fn take_submenu(bar: &mut MenuBar, command: &str) -> Option { /// VectorCraft's menus as a Mac menu bar, as they show now. pub fn layout(app: &VectorcraftApp) -> Layout { - mac_layout(&from_tree(app, &menus::menu_tree()), crate::i18n::current()) + mac_layout(&from_tree(app, &menus::menu_tree_named(app.session.prefs.font_names_in_english)), crate::i18n::current()) } // ----------------------------------------------------------------------------- the app's side diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 743631c4..da7595de 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -578,6 +578,21 @@ mod tests { } } + /// Preferences › Type › Show Font Names in English (#394): OK on the dialog stores the flag the + /// font menus read (Character panel, Type → Font). + #[test] + fn show_font_names_in_english_dialog_ok_stores_the_flag() { + let mut a = app(); + assert!(a.session.prefs.font_names_in_english); + open(&mut a, Some("Type")); + assert_eq!(a.ui.dialog.as_ref().unwrap().str("__category"), "Type"); + a.ui.dialog.as_mut().unwrap().fields.insert("fontNamesInEnglish".into(), json!(false)); + confirm(&mut a).unwrap(); + assert!(a.ui.dialog.is_none()); + assert!(!a.session.prefs.font_names_in_english); + assert!(!crate::font_menu::MenuLook::of(&a).english_names); + } + /// Preferences › Hyphenation › Exceptions (#394): OK on the dialog pushes the list into the /// hyphenator the same way `prefs.set` does (no need to drive the live Preferences window). #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 377e18a1..2ddb47c8 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1503,9 +1503,14 @@ Not read yet: Spacing Guides (the tools draw none). type's frame, type on a path's path — within `selectionTolerance`; a click among the glyphs, between the lines or inside the frame selects nothing. Off, anywhere in the type's bounds selects it. Marquee selection is unchanged, and the Type tools (a click among the characters edits them) and the Eyedropper (`eyedropper`) are not affected. +- `fontNamesInEnglish` (on by default): Preferences › Type › Show Font Names in English. Off, the Character panel and + Type → Font menus label a family by its native-language name when the font has one. `text.fontList` and + `text.setStyle {font}` always use the English (canonical) family name; a document may still name a font by any of + its aliases (`ヒラギノ角ゴシック` resolves to Hiragino Sans). ```json {"name":"run_command","arguments":{"command":"prefs.set","params":{"values":{"typeSizeIncrement":4,"trackingIncrement":50}}}} +{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"fontNamesInEnglish","value":false}}} {"name":"run_command","arguments":{"command":"type.step","params":{"attribute":"tracking","by":-2}}} ``` @@ -2636,13 +2641,14 @@ next to the preferences: `~/Library/Application Support/VectorCraft/Fonts` on ma Windows, and `$XDG_CONFIG_HOME/vectorcraft/Fonts` or `~/.config/vectorcraft/Fonts` on Linux and BSD. `text.addFontFiles` copies fonts into that folder. The installed fonts are cataloged once per session (in the background when the app starts, else on the first lookup by family name), so opening, placing, pasting and importing files find them whatever ran before. `text.fontList` -lists every family available, the installed ones included, as the font menus do: without the system's hidden -families, whose names start with "." (macOS's ".SF NS", ".LastResort"), which still resolve when a document names -them. With `family` it gives that family's styles (upright by weight, then italics) and fails when the family isn't -available. `text.rescanFonts` (Character panel menu › -Refresh Font List) scans the font folders again, for fonts installed or removed since the app started: type set in a -font that became available redraws in it, without editing the document. The app does so by itself when its window -comes to the front and a font folder changed meanwhile. +lists every family available, the installed ones included, each by its English (canonical) name — the same string +`text.setStyle {font}` and documents use — without the system's hidden families, whose names start with "." +(macOS's ".SF NS", ".LastResort"), which still resolve when a document names them. Preferences › Type › Show Font +Names in English (`fontNamesInEnglish`) only changes the UI menu labels, not this list. With `family` it gives that +family's styles (upright by weight, then italics) and fails when the family isn't available. `text.rescanFonts` +(Character panel menu › Refresh Font List) scans the font folders again, for fonts installed or removed since the app +started: type set in a font that became available redraws in it, without editing the document. The app does so by +itself when its window comes to the front and a font folder changed meanwhile. ```json {"name":"run_command","arguments":{"command":"text.fontList","params":{}}}