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] 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]