From 0143e0f7065485dfc6db3664ef0fff9d15becc32 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:06:11 -0500 Subject: [PATCH 1/2] Fix #5483: a large font library no longer keeps the window from opening The menu bar builds Type > Font in every frame, the first included, and its family list waited for the scan of every installed font. When that scan took longer than the 20 s first-frame watchdog (thousands of fonts on a slow disk, or each file checked by antivirus), the app took the graphics adapter for hung and started again without it, until none was left and no window ever showed. The menu now lists the fonts already loaded until the scan ends, then all of them (FontDb::menu_family_list_now, installed_fonts_ready). --- crates/text/src/fontdb/db.rs | 26 ++++++++++++++++++++++++ crates/text/src/tests/sysfonts.rs | 30 ++++++++++++++++++++++++++++ crates/ui-egui/src/menus/submenus.rs | 22 ++++++++++++-------- docs/gaps.md | 3 ++- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/crates/text/src/fontdb/db.rs b/crates/text/src/fontdb/db.rs index 0d8eb0e3f..1b14dd6dc 100644 --- a/crates/text/src/fontdb/db.rs +++ b/crates/text/src/fontdb/db.rs @@ -152,6 +152,32 @@ impl FontDb { list } + /// Whether the installed fonts are cataloged, so lookups by family name no longer wait for + /// the scan. Always true on wasm, which has no installed fonts. + pub fn installed_fonts_ready(&self) -> bool { + #[cfg(not(target_arch = "wasm32"))] + { + self.cataloged.get().is_some() + } + #[cfg(target_arch = "wasm32")] + true + } + + /// [`FontDb::menu_family_list`] without waiting for the installed fonts' scan: until it ends, + /// the families already loaded (the bundled fonts and any loaded meanwhile), sorted, without + /// the hidden ones. For what is built every frame, such as the menu bar's Type › Font, so the + /// window's first frame doesn't wait for the scan of a large font library (#5483). + /// [`FontDb::generation`] changes when the scan ends. + pub fn menu_family_list_now(&self) -> Arc<[String]> { + if self.installed_fonts_ready() { + return self.menu_family_list(); + } + let mut v: Vec = self.read_faces().iter().map(|f| f.family.clone()).filter(|f| !f.starts_with('.')).collect(); + v.sort_by_key(|a| a.to_lowercase()); + v.dedup_by(|a, b| a.eq_ignore_ascii_case(b)); + v.into() + } + /// How a font menu labels `family`: the English name when `english` is on (Preferences › Type › /// Show Font Names in English, the default), else the font's native-language name when it has /// one. The document and commands still use the English family ([`Self::canonical`]). diff --git a/crates/text/src/tests/sysfonts.rs b/crates/text/src/tests/sysfonts.rs index 7ab8d9029..d4068eb5d 100644 --- a/crates/text/src/tests/sysfonts.rs +++ b/crates/text/src/tests/sysfonts.rs @@ -308,6 +308,36 @@ fn a_background_scan_serves_the_first_lookup() { assert_eq!(db.load_system_fonts(), 2, "a rescan catalogs the two faces again"); } +/// The menu bar's Type › Font is built in the window's first frame, so its family list doesn't wait +/// for the scan of the installed fonts: a large library would hold the first frame past the +/// first-frame watchdog, and the app would never show its window (#5483). Once the scan ends, the +/// list has the installed fonts. +#[cfg(unix)] +#[test] +fn the_menu_family_list_now_does_not_wait_for_the_scan() { + let dir = font_dir("held-scan"); + // A named pipe with a font's name: the scan opens it and waits there until something writes. + let pipe = dir.join("Held.ttf"); + assert!(std::process::Command::new("mkfifo").arg(&pipe).status().unwrap().success()); + let db: &'static FontDb = Box::leak(Box::new(FontDb::with_font_dirs(vec![dir]))); + let generation = db.generation(); + db.scan_in_background(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let _ = tx.send((db.installed_fonts_ready(), db.menu_family_list_now())); + }); + let listed = rx.recv_timeout(std::time::Duration::from_secs(10)); + // Let the scan go on whatever happened: the pipe is opened and closed with nothing written. + drop(std::fs::OpenOptions::new().write(true).open(&pipe).unwrap()); + let (ready, listed) = listed.expect("the list comes while the scan is held"); + assert!(!ready && !has(&listed, FAMILY), "before the scan ends, only the fonts already loaded"); + // Waits for the scan. + assert!(has(&db.menu_family_list(), FAMILY)); + assert!(db.installed_fonts_ready()); + assert_ne!(db.generation(), generation, "a menu built from the short list is built again"); + assert!(has(&db.menu_family_list_now(), FAMILY)); +} + #[test] fn the_family_list_is_shared_until_fonts_change() { let db = FontDb::with_font_dirs(vec![]); diff --git a/crates/ui-egui/src/menus/submenus.rs b/crates/ui-egui/src/menus/submenus.rs index a86b66c82..a878f7f0c 100644 --- a/crates/ui-egui/src/menus/submenus.rs +++ b/crates/ui-egui/src/menus/submenus.rs @@ -63,21 +63,27 @@ pub(super) 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 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`]). +/// family name ([`vectorcraft_text::FontDb::canonical`]). Until the installed fonts are scanned it +/// lists the fonts already loaded, by their English names, rather than wait for the scan: the menu +/// bar is drawn in the window's first frame, which a large font library would otherwise hold up +/// past the first-frame watchdog (#5483). pub(super) 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, bool, Vec)> = std::sync::Mutex::new((u64::MAX, true, Vec::new())); + static ITEMS: std::sync::Mutex<(u64, bool, bool, Vec)> = std::sync::Mutex::new((u64::MAX, true, false, Vec::new())); let db = vectorcraft_text::FontDb::global(); - // Read first: fonts that load meanwhile make the next frame build the list again. + // Read first: fonts that load meanwhile make the next frame build the list again. The scan + // ends between changing the generation and counting as cataloged, so both are kept. + let cataloged = db.installed_fonts_ready(); let generation = db.generation(); - let fams = db.menu_family_list(); + let fams = db.menu_family_list_now(); let (Ok(mut names), Ok(mut items)) = (NAMES.lock(), ITEMS.lock()) else { return vec![] }; - if items.0 != generation || items.1 != english_names { + if items.0 != generation || items.1 != english_names || items.2 != cataloged { let list = fams .iter() .map(|f| { let english = f.as_str(); - let shown = db.family_display_name(english, english_names); + // A native name may be the catalog's, which isn't there to read yet. + let shown = if cataloged { db.family_display_name(english, english_names) } else { english.to_string() }; let label: &'static str = match names.get(shown.as_str()) { Some(n) => n, None => { @@ -97,9 +103,9 @@ pub(super) fn font_items(english_names: bool) -> Vec { cp(label, "text.setStyle", json!({ "font": font })) }) .collect(); - *items = (generation, english_names, list); + *items = (generation, english_names, cataloged, list); } - items.2.clone() + items.3.clone() } /// The raster effects' submenus of the Effect menu (the Photoshop-style effects, below the diff --git a/docs/gaps.md b/docs/gaps.md index 77acb709a..2f3c5ea61 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (the menu bar no longer waits for the installed fonts' scan, so a large font library doesn't keep the window from opening; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -191,6 +191,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall - **Unconfirmed (2026-10-14):** Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation). - **Done:** RPM required dependencies use 64-bit library capabilities and declare glibc 2.35, libm and libgcc_s (#1275); generated metadata and isolated synthetic-provider checks cover x86_64/aarch64. Real-distribution install/start-up QA is still missing. +- **Done (2026-11-04):** a large font library no longer keeps the window from opening (#5483). The menu bar's Type › Font, drawn in the first frame, waited for the scan of every installed font; when that took longer than the 20 s first-frame watchdog (thousands of fonts on a slow disk, or with each file checked by antivirus) the app took the graphics adapter for hung and started again without it until none was left. The menu now lists the fonts already loaded until the scan ends, then all of them (`the_menu_family_list_now_does_not_wait_for_the_scan`; checked on Linux with 3,923 extra font files, each open delayed 8 ms: the window opened in 1.5 s, where it never did before). Estimate unchanged. - **Done (2026-10-29):** the Linux Flatpak on GNOME no longer reconnects to the compositor and logs a warning every 250 ms when arboard has neither data-control nor X11 (267k warnings in 18 h filled the 16 MB log in 3 h): a clipboard that can't be opened is retried with a wait growing to a minute and logged once. Copy and Paste of text with other apps work there through the core Wayland protocol. - **Missing:** pictures (`image/png`) and SVG as `image/svg+xml` on the clipboard of the Flatpak on GNOME, where only text travels (smithay-clipboard 0.7 carries text only); the app not starting or staying in the background on some Windows 11 machines (#858, #964 fixed 2026-10-10, #620, #818), slow pointer on a Linux AppImage (#575), macOS paste from other apps (#954), a From b8269d7bdf07f7c7b61b6b0787a08c9e684be26b Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:34:44 -0500 Subject: [PATCH 2/2] Native menu: read the menus again when the fonts change On macOS the menu bar is native and is read again only when its state hash changes. Type > Font now lists the loaded fonts until the installed ones are scanned, so the hash takes the font generation and whether the scan has ended. The tests that wait for process-wide generations to hold still wait for the fonts too. --- crates/ui-egui/src/native_menu.rs | 5 ++- crates/ui-egui/src/tests/nativemenu.rs | 51 +++++++++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/native_menu.rs b/crates/ui-egui/src/native_menu.rs index c8d046a68..8d1b7ee08 100644 --- a/crates/ui-egui/src/native_menu.rs +++ b/crates/ui-egui/src/native_menu.rs @@ -590,7 +590,8 @@ pub fn sync(app: &mut VectorcraftApp, ctx: &egui::Context) { /// What the menus' rows depend on, cheaply (no allocation): commands run, the active document, /// its revision (edits, selection and view changes), history and clipboard, the language, -/// shortcuts, plug-ins and an open file dialog. `input` forces a change on frames with a click or +/// shortcuts, plug-ins, an open file dialog and the fonts (Type › Font lists the loaded fonts +/// until the installed ones are scanned, then all of them, #5483). `input` forces a change on frames with a click or /// a key press, which covers what the hash doesn't list (panels shown, a tool picked). While an /// interaction is open (for example a move, scale or shape drag, a dialog's or the font menu's /// preview, or a typing session), the revision, which each of its previews changes, is left out: @@ -609,5 +610,7 @@ fn state_hash(app: &VectorcraftApp, input: Option) -> u64 { app.file_dialog_open().hash(&mut h); (crate::i18n::current().code(), menus::plugin_revision()).hash(&mut h); crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed).hash(&mut h); + let fonts = vectorcraft_text::FontDb::global(); + (fonts.generation(), fonts.installed_fonts_ready()).hash(&mut h); h.finish() } diff --git a/crates/ui-egui/src/tests/nativemenu.rs b/crates/ui-egui/src/tests/nativemenu.rs index cc6e4b346..5d1d1c26f 100644 --- a/crates/ui-egui/src/tests/nativemenu.rs +++ b/crates/ui-egui/src/tests/nativemenu.rs @@ -388,12 +388,20 @@ fn click(at: Pos2, pressed: bool) -> Event { } /// Run `drag` on a new app with the fake native menu, and run it again on another new app until -/// the shortcut and plug-in generations hold still through a run (at most 20 runs). Return that app -/// and what `drag` returned. The generations are process-wide and only grow. A test running -/// alongside can bump them, and the app then reads the menus again, so every step whose menu -/// reads a test counts runs inside `drag`. +/// the shortcut, plug-in and font generations hold still through a run (at most 20 runs). Return +/// that app and what `drag` returned. The generations are process-wide and only grow. A test +/// running alongside can bump them (a font it adds, the installed fonts' scan ending), and the app +/// then reads the menus again, so every step whose menu reads a test counts runs inside `drag`. fn undisturbed(drag: impl Fn(&mut Mac) -> R) -> (Mac, R) { - let generations = || (crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed), crate::menus::plugin_revision()); + let fonts = vectorcraft_text::FontDb::global(); + let generations = || { + ( + crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed), + crate::menus::plugin_revision(), + fonts.generation(), + fonts.installed_fonts_ready(), + ) + }; for _ in 0..20 { let mut m = Mac::new(true); let before = generations(); @@ -420,10 +428,18 @@ fn key_equivalents_run_through_the_normal_shortcut_path_once() { m.key_equivalent("Cmd+Z"); assert_eq!(undo(&m), before - 1, "⌘Z undid one step"); assert!(m.fake.synced() >= 1, "the menus were built"); - // Idle frames don't read the menus again. The shortcut and plug-in generations are - // process-wide, and tests running alongside with other shortcuts bump them (rightly reading - // the menus again), so judge two idle frames in which they held still. - let globals = || (crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed), crate::menus::plugin_revision()); + // Idle frames don't read the menus again. The shortcut, plug-in and font generations are + // process-wide, and tests running alongside with other shortcuts or fonts bump them (rightly + // reading the menus again), so judge two idle frames in which they held still. + let fonts = vectorcraft_text::FontDb::global(); + let globals = || { + ( + crate::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed), + crate::menus::plugin_revision(), + fonts.generation(), + fonts.installed_fonts_ready(), + ) + }; let quiet = (0..50).any(|_| { let (before, synced) = (globals(), m.fake.synced()); m.frame(vec![]); @@ -540,6 +556,23 @@ fn applying_a_dialog_preview_reads_the_menus() { assert_eq!(find(&m.fake.last(), "edit.undo").label, "Undo Rotate"); } +/// Fonts that come in after the menus were read (the installed fonts' scan ending, #5483) make the +/// next frame read them again, with no click or key, so Type › Font lists them. +#[test] +fn fonts_that_load_read_the_menus_again() { + use vectorcraft_text::test_fonts as t; + let db = vectorcraft_text::FontDb::global(); + // A test font no other test loaded yet, so adding it changes the fonts. + let fonts = [t::vertical_font(true), t::two_axis_font(), t::variable_font()]; + let mut m = Mac::new(true); + let synced = m.fake.synced(); + if !fonts.into_iter().flatten().any(|data| db.add_font(data) > 0) { + return; + } + m.frame(vec![]); + assert!(m.fake.synced() > synced, "the menus were read again"); +} + /// The font menu previews each font it highlights on the selected type, in one interaction: the /// menus are read as the preview begins and as it ends, and not for the fonts between. #[test]