From 9aaee3354da635d70c1a3c59f3665411993893c4 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Fri, 4 Jul 2025 01:16:55 -0400 Subject: [PATCH] Let `Fonts` handle multiple `pixels_per_point` --- crates/egui/src/context.rs | 120 +++-------- crates/egui/src/painter.rs | 6 +- crates/egui/src/ui.rs | 9 +- crates/egui/src/widget_text.rs | 4 +- crates/egui_demo_lib/benches/benchmark.rs | 25 ++- crates/epaint/src/lib.rs | 2 +- crates/epaint/src/shapes/shape.rs | 4 +- crates/epaint/src/shapes/text_shape.rs | 3 +- crates/epaint/src/text/font.rs | 25 +-- crates/epaint/src/text/fonts.rs | 226 +++++++++++--------- crates/epaint/src/text/mod.rs | 2 +- crates/epaint/src/text/text_layout.rs | 51 +++-- crates/epaint/src/text/text_layout_types.rs | 4 +- 13 files changed, 218 insertions(+), 263 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 83ef18b7e..4360f66ce 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2,15 +2,15 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; -use emath::{GuiRounding as _, OrderedFloat}; +use emath::GuiRounding as _; use epaint::{ - ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind, - TessellationOptions, TextureAtlas, TextureId, Vec2, + ClippedPrimitive, ClippedShape, Color32, ImageData, Pos2, Rect, StrokeKind, + TessellationOptions, TextureId, Vec2, emath::{self, TSTransform}, mutex::RwLock, stats::PaintStats, tessellator, - text::{FontInsert, FontPriority, Fonts}, + text::{FontInsert, FontPriority, Fonts, FontsView}, vec2, }; @@ -406,12 +406,7 @@ impl ViewportRepaintInfo { #[derive(Default)] struct ContextImpl { - /// Since we could have multiple viewports across multiple monitors with - /// different `pixels_per_point`, we need a `Fonts` instance for each unique - /// `pixels_per_point`. - /// This is because the `Fonts` depend on `pixels_per_point` for the font atlas - /// as well as kerning, font sizes, etc. - fonts: std::collections::BTreeMap, Fonts>, + fonts: Option, font_definitions: FontDefinitions, memory: Memory, @@ -575,12 +570,11 @@ impl ContextImpl { fn update_fonts_mut(&mut self) { profiling::function_scope!(); let input = &self.viewport().input; - let pixels_per_point = input.pixels_per_point(); let max_texture_side = input.max_texture_side; if let Some(font_definitions) = self.memory.new_font_definitions.take() { // New font definition loaded, so we need to reload all fonts. - self.fonts.clear(); + self.fonts = None; self.font_definitions = font_definitions; #[cfg(feature = "log")] log::trace!("Loading new font definitions"); @@ -589,7 +583,7 @@ impl ContextImpl { if !self.memory.add_fonts.is_empty() { let fonts = self.memory.add_fonts.drain(..); for font in fonts { - self.fonts.clear(); // recreate all the fonts + self.fonts = None; // recreate all the fonts for family in font.families { let fam = self .font_definitions @@ -614,26 +608,22 @@ impl ContextImpl { let mut is_new = false; - let fonts = self - .fonts - .entry(pixels_per_point.into()) - .or_insert_with(|| { - #[cfg(feature = "log")] - log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}"); + let fonts = self.fonts.get_or_insert_with(|| { + #[cfg(feature = "log")] + log::trace!("Creating new Fonts"); - is_new = true; - profiling::scope!("Fonts::new"); - Fonts::new( - pixels_per_point, - max_texture_side, - text_alpha_from_coverage, - self.font_definitions.clone(), - ) - }); + is_new = true; + profiling::scope!("Fonts::new"); + Fonts::new( + max_texture_side, + text_alpha_from_coverage, + self.font_definitions.clone(), + ) + }); { profiling::scope!("Fonts::begin_pass"); - fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage); + fonts.begin_pass(max_texture_side, text_alpha_from_coverage); } if is_new && self.memory.options.preload_font_glyphs { @@ -1052,13 +1042,14 @@ impl Context { /// Not valid until first call to [`Context::run()`]. /// That's because since we don't know the proper `pixels_per_point` until then. #[inline] - pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { + pub fn fonts(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R { self.write(move |ctx| { let pixels_per_point = ctx.pixels_per_point(); reader( - ctx.fonts - .get(&pixels_per_point.into()) - .expect("No fonts available until first call to Context::run()"), + &ctx.fonts + .as_mut() + .expect("No fonts available until first call to Context::run()") + .with_pixels_per_point(pixels_per_point), ) }) } @@ -1068,13 +1059,15 @@ impl Context { /// Not valid until first call to [`Context::run()`]. /// That's because since we don't know the proper `pixels_per_point` until then. #[inline] - pub fn fonts_mut(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R { + pub fn fonts_mut(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R { self.write(move |ctx| { let pixels_per_point = ctx.pixels_per_point(); reader( - ctx.fonts - .get_mut(&pixels_per_point.into()) - .expect("No fonts available until first call to Context::run()"), + &mut ctx + .fonts + .as_mut() + .expect("No fonts available until first call to Context::run()") + .with_pixels_per_point(pixels_per_point), ) }) } @@ -1938,14 +1931,12 @@ impl Context { pub fn set_fonts(&self, font_definitions: FontDefinitions) { profiling::function_scope!(); - let pixels_per_point = self.pixels_per_point(); - let mut update_fonts = true; self.read(|ctx| { - if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) { + if let Some(current_fonts) = ctx.fonts.as_ref() { // NOTE: this comparison is expensive since it checks TTF data for equality - if current_fonts.fonts.definitions() == &font_definitions { + if current_fonts.definitions() == &font_definitions { update_fonts = false; // no need to update } } @@ -1966,14 +1957,11 @@ impl Context { pub fn add_font(&self, new_font: FontInsert) { profiling::function_scope!(); - let pixels_per_point = self.pixels_per_point(); - let mut update_fonts = true; self.read(|ctx| { - if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) { + if let Some(current_fonts) = ctx.fonts.as_ref() { if current_fonts - .fonts .definitions() .font_data .contains_key(&new_font.name) @@ -2466,31 +2454,12 @@ impl ContextImpl { self.memory.end_pass(&viewport.this_pass.used_ids); - let num_font_envs = self.fonts.len(); - if let Some(fonts) = self.fonts.get_mut(&pixels_per_point.into()) { + if let Some(fonts) = self.fonts.as_mut() { let tex_mngr = &mut self.tex_manager.0.write(); if let Some(font_image_delta) = fonts.font_image_delta() { // A partial font atlas update, e.g. a new glyph has been entered. tex_mngr.set(TextureId::default(), font_image_delta); } - - if 1 < num_font_envs { - // We have multiple different `pixels_per_point`, - // e.g. because we have many viewports spread across - // monitors with different DPI scaling. - // All viewports share the same texture namespace and renderer, - // so the all use `TextureId::default()` for the font texture. - // This is a problem. - // We solve this with a hack: we always upload the full font atlas - // every frame, for all viewports. - // This ensures it is up-to-date, solving - // https://github.com/emilk/egui/issues/3664 - // at the cost of a lot of performance. - // (This will override any smaller delta that was uploaded above.) - profiling::scope!("full_font_atlas_update"); - let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options()); - tex_mngr.set(TextureId::default(), full_delta); - } } // Inform the backend of all textures that have been updated (including font atlas). @@ -2633,24 +2602,6 @@ impl ContextImpl { self.memory.set_viewport_id(viewport_id); } - let active_pixels_per_point: std::collections::BTreeSet> = self - .viewports - .values() - .map(|v| v.input.pixels_per_point.into()) - .collect(); - self.fonts.retain(|pixels_per_point, _| { - if active_pixels_per_point.contains(pixels_per_point) { - true - } else { - #[cfg(feature = "log")] - log::trace!( - "Freeing Fonts with pixels_per_point={} because it is no longer needed", - pixels_per_point.into_inner() - ); - false - } - }); - platform_output.num_completed_passes += 1; FullOutput { @@ -2682,7 +2633,7 @@ impl Context { self.write(|ctx| { let tessellation_options = ctx.memory.options.tessellation_options; - let texture_atlas = if let Some(fonts) = ctx.fonts.get(&pixels_per_point.into()) { + let texture_atlas = if let Some(fonts) = ctx.fonts.as_ref() { fonts.texture_atlas() } else { #[cfg(feature = "log")] @@ -2691,7 +2642,6 @@ impl Context { .iter() .next() .expect("No fonts loaded") - .1 .texture_atlas() }; diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index cbb36b09e..814acfa7a 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use emath::GuiRounding as _; use epaint::{ CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind, - text::{Fonts, Galley, LayoutJob}, + text::{FontsView, Galley, LayoutJob}, }; use crate::{ @@ -145,7 +145,7 @@ impl Painter { /// /// See [`Context`] documentation for how locks work. #[inline] - pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { + pub fn fonts(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R { self.ctx.fonts(reader) } @@ -153,7 +153,7 @@ impl Painter { /// /// See [`Context`] documentation for how locks work. #[inline] - pub fn fonts_mut(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R { + pub fn fonts_mut(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R { self.ctx.fonts_mut(reader) } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 532302339..fd08a99f8 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -3,6 +3,7 @@ use emath::GuiRounding as _; use epaint::mutex::RwLock; +use epaint::text::FontsView; use std::{any::Any, hash::Hash, sync::Arc}; use crate::ClosableTag; @@ -16,9 +17,7 @@ use crate::{ WidgetRect, WidgetText, containers::{CollapsingHeader, CollapsingResponse, Frame}, ecolor::Hsva, - emath, epaint, - epaint::text::Fonts, - grid, + emath, epaint, grid, layout::{Direction, Layout}, pass_state, placer::Placer, @@ -849,13 +848,13 @@ impl Ui { /// Read-only access to [`Fonts`]. #[inline] - pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { + pub fn fonts(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R { self.ctx().fonts(reader) } /// Read-write access to [`Fonts`]. #[inline] - pub fn fonts_mut(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R { + pub fn fonts_mut(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R { self.ctx().fonts_mut(reader) } } diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index a7aaf0e07..c66e38d70 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -307,7 +307,7 @@ impl RichText { /// Read the font height of the selected text style. /// /// Returns a value rounded to [`emath::GUI_ROUNDING`]. - pub fn font_height(&self, fonts: &mut epaint::Fonts, style: &Style) -> f32 { + pub fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 { let mut font_id = self.text_style.as_ref().map_or_else( || FontSelection::Default.resolve(style), |text_style| text_style.resolve(style), @@ -676,7 +676,7 @@ impl WidgetText { } /// Returns a value rounded to [`emath::GUI_ROUNDING`]. - pub(crate) fn font_height(&self, fonts: &mut epaint::Fonts, style: &Style) -> f32 { + pub(crate) fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 { match self { Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)), Self::RichText(text) => text.font_height(fonts, style), diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 29b47b915..b101aa94a 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -166,7 +166,6 @@ pub fn criterion_benchmark(c: &mut Criterion) { let font_id = egui::FontId::default(); let text_color = egui::Color32::WHITE; let mut fonts = egui::epaint::text::Fonts::new( - pixels_per_point, max_texture_side, egui::epaint::AlphaFromCoverage::default(), egui::FontDefinitions::default(), @@ -182,13 +181,13 @@ pub fn criterion_benchmark(c: &mut Criterion) { text_color, wrap_width, ); - layout(&mut fonts.fonts, job.into()) + layout(&mut fonts.fonts, job.into(), pixels_per_point) }); }); } c.bench_function("text_layout_cached", |b| { b.iter(|| { - fonts.layout( + fonts.with_pixels_per_point(pixels_per_point).layout( LOREM_IPSUM_LONG.to_owned(), font_id.clone(), text_color, @@ -210,22 +209,28 @@ pub fn criterion_benchmark(c: &mut Criterion) { let mut rng = rand::rng(); b.iter(|| { - fonts.begin_pass( - pixels_per_point, - max_texture_side, - egui::epaint::AlphaFromCoverage::default(), - ); + fonts.begin_pass(max_texture_side, egui::epaint::AlphaFromCoverage::default()); // Delete a random character, simulating a user making an edit in a long file: let mut new_string = string.clone(); let idx = rng.random_range(0..string.len()); new_string.remove(idx); - fonts.layout(new_string, font_id.clone(), text_color, wrap_width); + fonts.with_pixels_per_point(pixels_per_point).layout( + new_string, + font_id.clone(), + text_color, + wrap_width, + ); }); }); - let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width); + let galley = fonts.with_pixels_per_point(pixels_per_point).layout( + LOREM_IPSUM_LONG.to_owned(), + font_id, + text_color, + wrap_width, + ); let font_image_size = fonts.font_image_size(); let prepared_discs = fonts.texture_atlas().prepared_discs(); let mut tessellator = egui::epaint::Tessellator::new( diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 5e6dc27e1..1bf0285bd 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -62,7 +62,7 @@ pub use self::{ stats::PaintStats, stroke::{PathStroke, Stroke, StrokeKind}, tessellator::{TessellationOptions, Tessellator}, - text::{FontFamily, FontId, Fonts, Galley}, + text::{FontFamily, FontId, Fonts, FontsView, Galley}, texture_atlas::TextureAtlas, texture_handle::TextureHandle, textures::TextureManager, diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index c3868f335..8ee852c61 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -7,7 +7,7 @@ use emath::{Align2, Pos2, Rangef, Rect, TSTransform, Vec2, pos2}; use crate::{ Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId, stroke::PathStroke, - text::{FontId, Fonts, Galley}, + text::{FontId, FontsView, Galley}, }; use super::{ @@ -299,7 +299,7 @@ impl Shape { #[expect(clippy::needless_pass_by_value)] pub fn text( - fonts: &mut Fonts, + fonts: &mut FontsView<'_>, pos: Pos2, anchor: Align2, text: impl ToString, diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index d8d059dd2..9fe98f211 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -182,7 +182,6 @@ mod tests { #[test] fn text_bounding_box_under_rotation() { let mut fonts = Fonts::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -190,7 +189,7 @@ mod tests { let font = FontId::monospace(12.0); let mut t = crate::Shape::text( - &mut fonts, + &mut fonts.with_pixels_per_point(1.0), Pos2::ZERO, emath::Align2::CENTER_CENTER, "testing123", diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index f128a6bc8..3d73eaa80 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -155,7 +155,7 @@ impl FontImpl { } /// `\n` will result in `None` - fn glyph_info(&mut self, c: char) -> Option { + pub(super) fn glyph_info(&mut self, c: char) -> Option { { if let Some(glyph_info) = self.glyph_info_cache.get(&c) { return Some(*glyph_info); @@ -424,7 +424,9 @@ impl Font<'_> { return *font_index_glyph_info; } - let font_index_glyph_info = self.glyph_info_no_cache_or_fallback(c); + let font_index_glyph_info = self + .cached_family + .glyph_info_no_cache_or_fallback(c, self.fonts_by_id); let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.cached_family.replacement_glyph); self.cached_family @@ -471,25 +473,6 @@ impl Font<'_> { self.row_height(font_size) } } - - pub(crate) fn glyph_info_no_cache_or_fallback( - &mut self, - c: char, - ) -> Option<(FontFaceKey, GlyphInfo)> { - for font_key in &self.cached_family.fonts { - let font_impl = self - .fonts_by_id - .get_mut(font_key) - .expect("Nonexistent font ID"); - if let Some(glyph_info) = font_impl.glyph_info(c) { - self.cached_family - .glyph_info_cache - .insert(c, (*font_key, glyph_info)); - return Some((*font_key, glyph_info)); - } - } - None - } } /// Code points that will always be invisible (zero width). diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index fefb61c46..3dd52efa0 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -434,7 +434,6 @@ impl CachedFamily { fn new( fonts: Vec, fonts_by_id: &mut nohash_hasher::IntMap, - atlas: &mut TextureAtlas, ) -> Self { if fonts.is_empty() { return Self { @@ -455,15 +454,9 @@ impl CachedFamily { const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback - let mut font = Font { - fonts_by_id, - cached_family: &mut slf, - atlas, - }; - - let replacement_glyph = font - .glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR) - .or_else(|| font.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR)) + let replacement_glyph = slf + .glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR, fonts_by_id) + .or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR, fonts_by_id)) .unwrap_or_else(|| { #[cfg(feature = "log")] log::warn!( @@ -475,6 +468,21 @@ impl CachedFamily { slf } + + pub(crate) fn glyph_info_no_cache_or_fallback( + &mut self, + c: char, + fonts_by_id: &mut nohash_hasher::IntMap, + ) -> Option<(FontFaceKey, GlyphInfo)> { + for font_key in &self.fonts { + let font_impl = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID"); + if let Some(glyph_info) = font_impl.glyph_info(c) { + self.glyph_info_cache.insert(c, (*font_key, glyph_info)); + return Some((*font_key, glyph_info)); + } + } + None + } } // ---------------------------------------------------------------------------- @@ -497,21 +505,14 @@ impl Fonts { /// Create a new [`Fonts`] for text layout. /// This call is expensive, so only create one [`Fonts`] and then reuse it. /// - /// * `pixels_per_point`: how many physical pixels per logical "point". /// * `max_texture_side`: largest supported texture size (one side). pub fn new( - pixels_per_point: f32, max_texture_side: usize, text_alpha_from_coverage: AlphaFromCoverage, definitions: FontDefinitions, ) -> Self { Self { - fonts: FontsImpl::new( - pixels_per_point, - max_texture_side, - text_alpha_from_coverage, - definitions, - ), + fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions), galley_cache: Default::default(), } } @@ -525,30 +526,21 @@ impl Fonts { /// as well as notice when the font atlas is getting full, and handle that. pub fn begin_pass( &mut self, - pixels_per_point: f32, max_texture_side: usize, text_alpha_from_coverage: AlphaFromCoverage, ) { - let pixels_per_point_changed = self.fonts.pixels_per_point != pixels_per_point; let max_texture_side_changed = self.fonts.max_texture_side != max_texture_side; let text_alpha_from_coverage_changed = self.fonts.atlas.text_alpha_from_coverage != text_alpha_from_coverage; let font_atlas_almost_full = self.fonts.atlas.fill_ratio() > 0.8; - let needs_recreate = pixels_per_point_changed - || max_texture_side_changed - || text_alpha_from_coverage_changed - || font_atlas_almost_full; + let needs_recreate = + max_texture_side_changed || text_alpha_from_coverage_changed || font_atlas_almost_full; if needs_recreate { let definitions = self.fonts.definitions.clone(); *self = Self { - fonts: FontsImpl::new( - pixels_per_point, - max_texture_side, - text_alpha_from_coverage, - definitions, - ), + fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions), galley_cache: Default::default(), }; } @@ -562,13 +554,13 @@ impl Fonts { } #[inline] - pub fn pixels_per_point(&self) -> f32 { - self.fonts.pixels_per_point + pub fn max_texture_side(&self) -> usize { + self.fonts.max_texture_side } #[inline] - pub fn max_texture_side(&self) -> usize { - self.fonts.max_texture_side + pub fn definitions(&self) -> &FontDefinitions { + &self.fonts.definitions } /// The font atlas. @@ -589,29 +581,92 @@ impl Fonts { self.fonts.atlas.size() } - /// Width of this character in points. - #[inline] - pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 { - self.fonts.glyph_width(font_id, c) - } - /// Can we display this glyph? - #[inline] pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool { - self.fonts.has_glyph(font_id, c) + self.fonts.font(&font_id.family).has_glyph(c) } /// Can we display all the glyphs in this text? pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool { - self.fonts.has_glyphs(font_id, s) + self.fonts.font(&font_id.family).has_glyphs(s) + } + + pub fn num_galleys_in_cache(&self) -> usize { + self.galley_cache.num_galleys_in_cache() + } + + /// How full is the font atlas? + /// + /// This increases as new fonts and/or glyphs are used, + /// but can also decrease in a call to [`Self::begin_pass`]. + pub fn font_atlas_fill_ratio(&self) -> f32 { + self.fonts.atlas.fill_ratio() + } + + /// Returns a [`FontsView`] with the given `pixels_per_point` that can be used to do text layout. + pub fn with_pixels_per_point(&mut self, pixels_per_point: f32) -> FontsView<'_> { + FontsView { + fonts: &mut self.fonts, + galley_cache: &mut self.galley_cache, + pixels_per_point, + } + } +} + +// ---------------------------------------------------------------------------- + +/// The context's collection of fonts, with this context's `pixels_per_point`. This is what you use to do text layout. +pub struct FontsView<'a> { + pub fonts: &'a mut FontsImpl, + galley_cache: &'a mut GalleyCache, + pixels_per_point: f32, +} + +impl FontsView<'_> { + #[inline] + pub fn max_texture_side(&self) -> usize { + self.fonts.max_texture_side + } + + #[inline] + pub fn definitions(&self) -> &FontDefinitions { + &self.fonts.definitions + } + + /// The full font atlas image. + #[inline] + pub fn image(&self) -> crate::ColorImage { + self.fonts.atlas.image().clone() + } + + /// Current size of the font image. + /// Pass this to [`crate::Tessellator`]. + pub fn font_image_size(&self) -> [usize; 2] { + self.fonts.atlas.size() + } + + /// Width of this character in points. + pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 { + self.fonts + .font(&font_id.family) + .glyph_width(c, font_id.size) + } + + /// Can we display this glyph? + pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool { + self.fonts.font(&font_id.family).has_glyph(c) + } + + /// Can we display all the glyphs in this text? + pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool { + self.fonts.font(&font_id.family).has_glyphs(s) } /// Height of one row of text in points. /// /// Returns a value rounded to [`emath::GUI_ROUNDING`]. - #[inline] pub fn row_height(&mut self, font_id: &FontId) -> f32 { - self.fonts.row_height(font_id) + self.fonts.font(&font_id.family).row_height(font_id.size) } /// List of all known font families. @@ -629,8 +684,12 @@ impl Fonts { #[inline] pub fn layout_job(&mut self, job: LayoutJob) -> Arc { let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs. - self.galley_cache - .layout(&mut self.fonts, job, allow_split_paragraphs) + self.galley_cache.layout( + self.fonts, + job, + self.pixels_per_point, + allow_split_paragraphs, + ) } pub fn num_galleys_in_cache(&self) -> usize { @@ -687,13 +746,10 @@ impl Fonts { // ---------------------------------------------------------------------------- -// ---------------------------------------------------------------------------- - /// The collection of fonts used by `epaint`. /// /// Required in order to paint text. pub struct FontsImpl { - pixels_per_point: f32, max_texture_side: usize, definitions: FontDefinitions, atlas: TextureAtlas, @@ -706,16 +762,10 @@ impl FontsImpl { /// Create a new [`FontsImpl`] for text layout. /// This call is expensive, so only create one [`FontsImpl`] and then reuse it. pub fn new( - pixels_per_point: f32, max_texture_side: usize, text_alpha_from_coverage: AlphaFromCoverage, definitions: FontDefinitions, ) -> Self { - assert!( - 0.0 < pixels_per_point && pixels_per_point < 100.0, - "pixels_per_point out of range: {pixels_per_point}" - ); - let texture_width = max_texture_side.at_most(16 * 1024); let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways. let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage); @@ -732,7 +782,6 @@ impl FontsImpl { } Self { - pixels_per_point, max_texture_side, definitions, atlas, @@ -742,16 +791,6 @@ impl FontsImpl { } } - #[inline(always)] - pub fn pixels_per_point(&self) -> f32 { - self.pixels_per_point - } - - #[inline] - pub fn definitions(&self) -> &FontDefinitions { - &self.definitions - } - /// Get the right font implementation from [`FontFamily`]. pub fn font(&mut self, family: &FontFamily) -> Font<'_> { let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| { @@ -769,7 +808,7 @@ impl FontsImpl { }) .collect(); - CachedFamily::new(fonts, &mut self.fonts_by_id, &mut self.atlas) + CachedFamily::new(fonts, &mut self.fonts_by_id) }); Font { fonts_by_id: &mut self.fonts_by_id, @@ -777,28 +816,6 @@ impl FontsImpl { atlas: &mut self.atlas, } } - - /// Width of this character in points. - fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 { - self.font(&font_id.family).glyph_width(c, font_id.size) - } - - /// Can we display this glyph? - pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool { - self.font(&font_id.family).has_glyph(c) - } - - /// Can we display all the glyphs in this text? - pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool { - self.font(&font_id.family).has_glyphs(s) - } - - /// Height of one row of text in points. - /// - /// Returns a value rounded to [`emath::GUI_ROUNDING`]. - fn row_height(&mut self, font_id: &FontId) -> f32 { - self.font(&font_id.family).row_height(font_id.size) - } } // ---------------------------------------------------------------------------- @@ -827,6 +844,7 @@ impl GalleyCache { &mut self, fonts: &mut FontsImpl, mut job: LayoutJob, + pixels_per_point: f32, allow_split_paragraphs: bool, ) -> (u64, Arc) { if job.wrap.max_width.is_finite() { @@ -882,14 +900,13 @@ impl GalleyCache { let job = Arc::new(job); if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) { let (child_galleys, child_hashes) = - self.layout_each_paragraph_individually(fonts, &job); + self.layout_each_paragraph_individually(fonts, &job, pixels_per_point); debug_assert_eq!( child_hashes.len(), child_galleys.len(), "Bug in `layout_each_paragraph_individuallly`" ); - let galley = - Arc::new(Galley::concat(job, &child_galleys, fonts.pixels_per_point)); + let galley = Arc::new(Galley::concat(job, &child_galleys, pixels_per_point)); self.cache.insert( hash, @@ -901,7 +918,7 @@ impl GalleyCache { ); galley } else { - let galley = super::layout(fonts, job); + let galley = super::layout(fonts, job, pixels_per_point); let galley = Arc::new(galley); entry.insert(CachedGalley { last_used: self.generation, @@ -920,9 +937,11 @@ impl GalleyCache { &mut self, fonts: &mut FontsImpl, job: LayoutJob, + pixels_per_point: f32, allow_split_paragraphs: bool, ) -> Arc { - self.layout_internal(fonts, job, allow_split_paragraphs).1 + self.layout_internal(fonts, job, pixels_per_point, allow_split_paragraphs) + .1 } /// Split on `\n` and lay out (and cache) each paragraph individually. @@ -930,6 +949,7 @@ impl GalleyCache { &mut self, fonts: &mut FontsImpl, job: &LayoutJob, + pixels_per_point: f32, ) -> (Vec>, Vec) { profiling::function_scope!(); @@ -1009,7 +1029,8 @@ impl GalleyCache { } // TODO(emilk): we could lay out each paragraph in parallel to get a nice speedup on multicore machines. - let (hash, galley) = self.layout_internal(fonts, paragraph_job, false); + let (hash, galley) = + self.layout_internal(fonts, paragraph_job, pixels_per_point, false); child_hashes.push(hash); // This will prevent us from invalidating cache entries unnecessarily: @@ -1164,7 +1185,6 @@ mod tests { for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] { let max_texture_side = 4096; let mut fonts = FontsImpl::new( - pixels_per_point, max_texture_side, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -1176,9 +1196,11 @@ mod tests { job.halign = halign; job.justify = justify; - let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false); + let whole = + GalleyCache::default().layout(&mut fonts, job.clone(), 1., false); - let split = GalleyCache::default().layout(&mut fonts, job.clone(), true); + let split = + GalleyCache::default().layout(&mut fonts, job.clone(), 1., true); for (i, row) in whole.rows.iter().enumerate() { println!( @@ -1217,7 +1239,6 @@ mod tests { for pixels_per_point in pixels_per_point { let mut fonts = FontsImpl::new( - pixels_per_point, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -1230,12 +1251,13 @@ mod tests { job.round_output_to_gui = round_output_to_gui; - let galley_wrapped = layout(&mut fonts, job.clone().into()); + let galley_wrapped = + layout(&mut fonts, job.clone().into(), pixels_per_point); job.wrap = TextWrapping::no_max_width(); let text = job.text.clone(); - let galley_unwrapped = layout(&mut fonts, job.into()); + let galley_unwrapped = layout(&mut fonts, job.into(), pixels_per_point); let intrinsic_size = galley_wrapped.intrinsic_size(); let unwrapped_size = galley_unwrapped.size(); diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index cf5c8ebfc..e0f4a3a98 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -12,7 +12,7 @@ pub const TAB_SIZE: usize = 4; pub use { fonts::{ FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts, - FontsImpl, InsertFontFamily, + FontsImpl, FontsView, InsertFontFamily, }, text_layout::*, text_layout_types::*, diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 8b938b56c..a207e7252 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -68,7 +68,7 @@ impl Paragraph { /// /// In most cases you should use [`crate::Fonts::layout_job`] instead /// since that memoizes the input, making subsequent layouting of the same text much faster. -pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { +pub fn layout(fonts: &mut FontsImpl, job: Arc, pixels_per_point: f32) -> Galley { profiling::function_scope!(); if job.wrap.max_rows == 0 { @@ -80,7 +80,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { mesh_bounds: Rect::NOTHING, num_vertices: 0, num_indices: 0, - pixels_per_point: fonts.pixels_per_point(), + pixels_per_point, elided: true, intrinsic_size: Vec2::ZERO, }; @@ -90,10 +90,17 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut paragraphs = vec![Paragraph::from_section_index(0)]; for (section_index, section) in job.sections.iter().enumerate() { - layout_section(fonts, &job, section_index as u32, section, &mut paragraphs); + layout_section( + fonts, + &job, + pixels_per_point, + section_index as u32, + section, + &mut paragraphs, + ); } - let point_scale = PointScale::new(fonts.pixels_per_point()); + let point_scale = PointScale::new(pixels_per_point); let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs); @@ -102,7 +109,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { if elided { if let Some(last_placed) = rows.last_mut() { let last_row = Arc::make_mut(&mut last_placed.row); - replace_last_glyph_with_overflow_character(fonts, &job, last_row); + replace_last_glyph_with_overflow_character(fonts, &job, last_row, pixels_per_point); if let Some(last) = last_row.glyphs.last() { last_row.size.x = last.max_x(); } @@ -134,6 +141,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { fn layout_section( fonts: &mut FontsImpl, job: &LayoutJob, + pixels_per_point: f32, section_index: u32, section: &LayoutSection, out_paragraphs: &mut Vec, @@ -143,7 +151,6 @@ fn layout_section( byte_range, format, } = section; - let pixels_per_point = fonts.pixels_per_point(); let mut font = fonts.font(&format.font_id.family); let font_size = format.font_id.size; let line_height = section @@ -407,6 +414,7 @@ fn replace_last_glyph_with_overflow_character( fonts: &mut FontsImpl, job: &LayoutJob, row: &mut Row, + pixels_per_point: f32, ) { fn row_width(row: &Row) -> f32 { if let (Some(first), Some(last)) = (row.glyphs.first(), row.glyphs.last()) { @@ -427,8 +435,6 @@ fn replace_last_glyph_with_overflow_character( return; }; - let pixels_per_point = fonts.pixels_per_point(); - // We always try to just append the character first: if let Some(last_glyph) = row.glyphs.last() { let section_index = last_glyph.section_index; @@ -514,7 +520,6 @@ fn replace_last_glyph_with_overflow_character( let section = &job.sections[last_glyph.section_index as usize]; let extra_letter_spacing = section.format.extra_letter_spacing; - let pixels_per_point = fonts.pixels_per_point(); let mut font = fonts.font(§ion.format.font_id.family); let font_size = section.format.font_id.size; @@ -568,7 +573,6 @@ fn replace_last_glyph_with_overflow_character( let Some(section) = &job.sections.get(last_glyph.section_index as usize) else { return; }; - let pixels_per_point = fonts.pixels_per_point(); let mut font = fonts.font(§ion.format.font_id.family); let font_size = section.format.font_id.size; // Just replace and be done with it. @@ -1119,14 +1123,13 @@ mod tests { #[test] fn test_zero_max_width() { let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), ); let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default()); layout_job.wrap.max_width = 0.0; - let galley = layout(&mut fonts, layout_job.into()); + let galley = layout(&mut fonts, layout_job.into(), 1.0); assert_eq!(galley.rows.len(), 1); } @@ -1135,7 +1138,6 @@ mod tests { // No matter where we wrap, we should be appending the newline character. let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -1154,7 +1156,7 @@ mod tests { layout_job.wrap.max_rows = 1; layout_job.wrap.break_anywhere = break_anywhere; - let galley = layout(&mut fonts, layout_job.into()); + let galley = layout(&mut fonts, layout_job.into(), 1.0); assert!(galley.elided); assert_eq!(galley.rows.len(), 1); @@ -1173,7 +1175,7 @@ mod tests { layout_job.wrap.max_rows = 1; layout_job.wrap.break_anywhere = false; - let galley = layout(&mut fonts, layout_job.into()); + let galley = layout(&mut fonts, layout_job.into(), 1.0); assert!(galley.elided); assert_eq!(galley.rows.len(), 1); @@ -1185,7 +1187,6 @@ mod tests { #[test] fn test_cjk() { let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -1195,7 +1196,7 @@ mod tests { TextFormat::default(), ); layout_job.wrap.max_width = 90.0; - let galley = layout(&mut fonts, layout_job.into()); + let galley = layout(&mut fonts, layout_job.into(), 1.0); assert_eq!( galley.rows.iter().map(|row| row.text()).collect::>(), vec!["日本語と", "Englishの混在", "した文章"] @@ -1205,7 +1206,6 @@ mod tests { #[test] fn test_pre_cjk() { let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -1215,7 +1215,7 @@ mod tests { TextFormat::default(), ); layout_job.wrap.max_width = 110.0; - let galley = layout(&mut fonts, layout_job.into()); + let galley = layout(&mut fonts, layout_job.into(), 1.0); assert_eq!( galley.rows.iter().map(|row| row.text()).collect::>(), vec!["日本語とEnglish", "の混在した文章"] @@ -1225,7 +1225,6 @@ mod tests { #[test] fn test_truncate_width() { let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), @@ -1235,7 +1234,7 @@ mod tests { layout_job.wrap.max_width = f32::INFINITY; layout_job.wrap.max_rows = 1; layout_job.round_output_to_gui = false; - let galley = layout(&mut fonts, layout_job.into()); + let galley = layout(&mut fonts, layout_job.into(), 1.0); assert!(galley.elided); assert_eq!( galley.rows.iter().map(|row| row.text()).collect::>(), @@ -1249,18 +1248,17 @@ mod tests { #[test] fn test_empty_row() { let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), ); let font_id = FontId::default(); - let font_height = fonts.font(&font_id).row_height(); + let font_height = fonts.font(&font_id.family).row_height(font_id.size); let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY); - let galley = layout(&mut fonts, job.into()); + let galley = layout(&mut fonts, job.into(), 1.0); assert_eq!(galley.rows.len(), 1, "Expected one row"); assert_eq!( @@ -1283,18 +1281,17 @@ mod tests { #[test] fn test_end_with_newline() { let mut fonts = FontsImpl::new( - 1.0, 1024, AlphaFromCoverage::default(), FontDefinitions::default(), ); let font_id = FontId::default(); - let font_height = fonts.font(&font_id).row_height(); + let font_height = fonts.font(&font_id.family).row_height(font_id.size); let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY); - let galley = layout(&mut fonts, job.into()); + let galley = layout(&mut fonts, job.into(), 1.0); assert_eq!(galley.rows.len(), 2, "Expected two rows"); assert_eq!( diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index d82db524c..d88e3f8bf 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -8,7 +8,7 @@ use super::{ cursor::{CCursor, LayoutCursor}, font::UvRect, }; -use crate::{Color32, FontId, Mesh, Stroke}; +use crate::{Color32, FontId, Mesh, Stroke, text::FontsView}; use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2}; /// Describes the task of laying out text. @@ -184,7 +184,7 @@ impl LayoutJob { /// The height of the tallest font used in the job. /// /// Returns a value rounded to [`emath::GUI_ROUNDING`]. - pub fn font_height(&self, fonts: &mut crate::Fonts) -> f32 { + pub fn font_height(&self, fonts: &mut FontsView<'_>) -> f32 { let mut max_height = 0.0_f32; for section in &self.sections { max_height = max_height.max(fonts.row_height(§ion.format.font_id));