diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index 21fc1a92a4..433baee074 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -109,6 +109,16 @@ pub fn generation() -> u64 { GENERATION.load(Ordering::Relaxed) } +/// Bumped only by the [`FontDb`] of [`crate::shared`], so caches of the shared engine's fonts +/// aren't invalidated by unrelated databases (a document's own engine, a test's throwaway one). +static SHARED_GENERATION: AtomicU64 = AtomicU64::new(0); + +/// Changes whenever font data is registered in the shared engine's [`FontDb`] ([`crate::shared`]). +/// Font menus key their caches on this, not [`generation`]. +pub fn shared_generation() -> u64 { + SHARED_GENERATION.load(Ordering::Relaxed) +} + /// One face in the database (for font menus). #[derive(Clone, Debug, PartialEq)] pub struct FaceInfo { @@ -145,6 +155,8 @@ pub struct FontDb { missing_glyph_protection: bool, face_cache: HashMap>, face_aliases: HashMap<(String, usize), String>, + /// This is the shared engine's database: registrations also move [`shared_generation`]. + shared: bool, } impl Default for FontDb { @@ -165,6 +177,7 @@ impl FontDb { missing_glyph_protection: true, face_cache: HashMap::new(), face_aliases: HashMap::new(), + shared: false, }; for (_, bytes) in BUNDLED { db.register_static_font(bytes.as_slice()); @@ -246,9 +259,19 @@ impl FontDb { self.face_cache.clear(); self.refresh_generics(); GENERATION.fetch_add(1, Ordering::Relaxed); + if self.shared { + SHARED_GENERATION.fetch_add(1, Ordering::Relaxed); + } names } + /// Marks this as the shared engine's database ([`crate::shared`]) and moves + /// [`shared_generation`], since its fonts replace whatever caches saw before. + pub(crate) fn mark_shared(&mut self) { + self.shared = true; + SHARED_GENERATION.fetch_add(1, Ordering::Relaxed); + } + fn refresh_generics(&mut self) { let c = &mut self.fcx.collection; if let Some(inter) = c.family_id(DEFAULT_FAMILY) { diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index da323b5a22..35e7dccbdf 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -147,7 +147,9 @@ fn uses_system_fonts() -> bool { } fn new_shared() -> std::sync::Mutex { - std::sync::Mutex::new(if uses_system_fonts() { TextEngine::with_system_fonts() } else { TextEngine::new() }) + let mut engine = if uses_system_fonts() { TextEngine::with_system_fonts() } else { TextEngine::new() }; + engine.fonts.mark_shared(); + std::sync::Mutex::new(engine) } /// A process-wide engine for callers without their own (bundled fonts only on the web; bundled diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index ae3092ca46..d502385f07 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -47,6 +47,21 @@ fn registering_fonts_moves_the_generation() { assert!(fonts::generation() > g); } +/// Font menus cache on the shared engine's generation: other engines registering fonts (a test's +/// throwaway one, in parallel) must not invalidate them, or the menus re-render every frame. +#[test] +fn only_the_shared_database_moves_the_shared_generation() { + let _ = crate::shared(); + let g = fonts::shared_generation(); + TextEngine::new().fonts.register_font_data(fonts::INTER_REGULAR.to_vec()); + assert_eq!(fonts::shared_generation(), g, "an unrelated engine leaves it alone"); + let mut e = TextEngine::new(); + e.fonts.mark_shared(); + let g = fonts::shared_generation(); + e.fonts.register_font_data(fonts::INTER_REGULAR.to_vec()); + assert!(fonts::shared_generation() > g); +} + #[test] fn literal_psd_tabs_shape_as_whitespace_without_shifting_text_offsets() { let mut e = TextEngine::new(); diff --git a/crates/ui-egui/src/font_preview.rs b/crates/ui-egui/src/font_preview.rs index 1568abf655..74c334d682 100644 --- a/crates/ui-egui/src/font_preview.rs +++ b/crates/ui-egui/src/font_preview.rs @@ -49,9 +49,12 @@ pub(crate) fn sample(preview: FontPreview) -> Option<(f32, [f32; 2])> { Some((size, box_for(size))) } -/// Family, display scale, the font database generations (so a family installed or fetched -/// (served fonts) after its first miss gets a sample once it is available) and the point size. -type Key = (String, u32, (u64, u64), u32); +/// Family, display scale, the shared font database's generation (so a family installed or +/// fetched (served fonts) after its first miss gets a sample once it is available) and the point +/// size. Not the served catalog's generation: a listed family has no sample until its fonts are +/// registered, which moves the database's generation, and keying on the catalog re-rendered +/// every sample whenever it changed. +type Key = (String, u32, u64, u32); #[derive(Clone)] struct Samples { latin: TextureHandle, @@ -111,7 +114,7 @@ fn images(engine: &mut photocraft_text::TextEngine, family: &str, scale: u32, si fn key(ctx: &egui::Context, family: &str, size: f32) -> Key { let scale = ctx.pixels_per_point().ceil().clamp(1.0, 4.0) as u32; - (family.to_owned(), scale, (photocraft_text::fonts::generation(), photocraft_text::served::generation()), size as u32) + (family.to_owned(), scale, photocraft_text::fonts::shared_generation(), size as u32) } /// Width a row reserves for its samples: wider once a painted row showed the family covers diff --git a/crates/ui-egui/src/type_tool.rs b/crates/ui-egui/src/type_tool.rs index 4bfa31227c..6a801b3385 100644 --- a/crates/ui-egui/src/type_tool.rs +++ b/crates/ui-egui/src/type_tool.rs @@ -985,11 +985,11 @@ fn draw_mask_overlay(app: &PhotocraftApp, painter: &egui::Painter, xf: &ViewXfor } /// Font families: bundled, system, and those the host serves (`photocraft_text::served`, fetched -/// when picked). Cached until fonts are registered or the served catalog changes. +/// when picked). Cached until the shared engine registers fonts or the served catalog changes. pub fn families() -> Arc> { type Cached = Option<((u64, u64), Arc>)>; static FAMILIES: std::sync::Mutex = std::sync::Mutex::new(None); - let key = (photocraft_text::fonts::generation(), photocraft_text::served::generation()); + let key = (photocraft_text::fonts::shared_generation(), photocraft_text::served::generation()); let mut cached = FAMILIES.lock().unwrap_or_else(PoisonError::into_inner); if let Some((k, v)) = cached.as_ref() && *k == key @@ -1027,11 +1027,11 @@ pub fn sync_font_names(ctx: &egui::Context, show_in_english: bool) { }); } -/// The fonts' localized family names, keyed by family. Cached until fonts are registered. +/// The fonts' localized family names, keyed by family. Cached until the shared engine registers fonts. fn localized_families() -> Arc> { type Cached = Option<(u64, Arc>)>; static LOCALIZED: std::sync::Mutex = std::sync::Mutex::new(None); - let key = photocraft_text::fonts::generation(); + let key = photocraft_text::fonts::shared_generation(); let mut cached = LOCALIZED.lock().unwrap_or_else(PoisonError::into_inner); if let Some((k, v)) = cached.as_ref() && *k == key diff --git a/crates/ui-egui/src/type_tool_tests.rs b/crates/ui-egui/src/type_tool_tests.rs index 25011745e2..046fd00f6f 100644 --- a/crates/ui-egui/src/type_tool_tests.rs +++ b/crates/ui-egui/src/type_tool_tests.rs @@ -1428,6 +1428,33 @@ mod multi_layer_formatting { assert_ne!(text(h.state(), ids[2]).antialias, AntiAlias::Sharp); } + /// Other engines registering fonts and the served catalog growing (tests running in parallel) + /// used to move the key of the font menu's preview cache, so its samples re-rendered and asked + /// for a repaint every pass and `Harness::run` never settled (a flaky `exceeded max_steps`). + /// Here both happen on every pass. + #[test] + fn the_font_menu_settles_while_other_engines_register_fonts() { + let (app, _) = fixture(); + let mut pass = 0u32; + let mut h = Harness::builder().with_size(vec2(1100.0, 800.0)).build_ui_state( + move |ui, app| { + photocraft_text::TextEngine::new(); + // The served catalog is process-wide: names that sort last. + photocraft_text::served::add_families([format!("zz Churn Test {pass:03}")]); + pass = pass.saturating_add(1); + if ui.ctx().fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) { + ui.horizontal(|ui| super::super::options_bar(app, ui)); + } + }, + app, + ); + PhotocraftApp::setup_context(&h.ctx, Default::default()); + h.run_steps(4); + h.get_all_by_role(Role::ComboBox).next().unwrap().click(); + h.run(); + h.run(); + } + #[test] fn properties_edit_shown_metrics_and_paragraphs_with_a_non_type_primary() { let (mut app, ids) = fixture();