diff --git a/egui/src/paint/font.rs b/egui/src/paint/font.rs index c7484445..7d53acbb 100644 --- a/egui/src/paint/font.rs +++ b/egui/src/paint/font.rs @@ -16,9 +16,9 @@ use super::texture_atlas::TextureAtlas; // ---------------------------------------------------------------------------- -// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character. // const REPLACEMENT_CHAR: char = '\u{FFFD}'; // � REPLACEMENT CHARACTER -const REPLACEMENT_CHAR: char = '?'; +// const REPLACEMENT_CHAR: char = '?'; +const REPLACEMENT_CHAR: char = '◻'; // white medium square #[derive(Clone, Copy, Debug)] pub struct UvRect { @@ -44,31 +44,43 @@ pub struct GlyphInfo { pub uv_rect: Option, } +impl Default for GlyphInfo { + fn default() -> Self { + Self { + id: rusttype::GlyphId(0), + advance_width: 0.0, + uv_rect: None, + } + } +} + +// ---------------------------------------------------------------------------- + +/// A specific font with a size. /// The interface uses points as the unit for everything. pub struct FontImpl { - font: rusttype::Font<'static>, + rusttype_font: Arc>, /// Maximum character height scale_in_pixels: f32, pixels_per_point: f32, - glyph_info_cache: RwLock>, + glyph_info_cache: RwLock>, // TODO: standard Mutex atlas: Arc>, } impl FontImpl { pub fn new( atlas: Arc>, - font_data: &'static [u8], - scale_in_points: f32, pixels_per_point: f32, + rusttype_font: Arc>, + scale_in_points: f32, ) -> FontImpl { assert!(scale_in_points > 0.0); assert!(pixels_per_point > 0.0); - let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font"); let scale_in_pixels = pixels_per_point * scale_in_points; let font = Self { - font, + rusttype_font, scale_in_pixels, pixels_per_point, glyph_info_cache: Default::default(), @@ -95,16 +107,19 @@ impl FontImpl { } // Add new character: - let glyph_info = allocate_glyph( - &mut self.atlas.lock(), - c, - &self.font, - self.scale_in_pixels, - self.pixels_per_point, - ); - let glyph_info = glyph_info?; - self.glyph_info_cache.write().insert(c, glyph_info); - Some(glyph_info) + let glyph = self.rusttype_font.glyph(c); + if glyph.id().0 == 0 { + None + } else { + let glyph_info = allocate_glyph( + &mut self.atlas.lock(), + glyph, + self.scale_in_pixels, + self.pixels_per_point, + ); + self.glyph_info_cache.write().insert(c, glyph_info); + Some(glyph_info) + } } pub fn pair_kerning( @@ -113,7 +128,7 @@ impl FontImpl { glyph_id: rusttype::GlyphId, ) -> f32 { let scale_in_pixels = Scale::uniform(self.scale_in_pixels); - self.font + self.rusttype_font .pair_kerning(scale_in_pixels, last_glyph_id, glyph_id) / self.pixels_per_point } @@ -134,7 +149,7 @@ type FontIndex = usize; /// Wrapper over multiple `FontImpl` (commonly two: primary + emoji fallback) pub struct Font { fonts: Vec>, - replacement_font_index_glyph_info: (FontIndex, GlyphInfo), + replacement_glyph: (FontIndex, GlyphInfo), pixels_per_point: f32, row_height: f32, glyph_info_cache: RwLock>, @@ -143,33 +158,25 @@ pub struct Font { impl Font { pub fn new(fonts: Vec>) -> Self { assert!(!fonts.is_empty()); - let replacement_glyph_font_index = 0; + let pixels_per_point = fonts[0].pixels_per_point(); + let row_height = fonts[0].row_height(); - let replacement_glyph_info = fonts[replacement_glyph_font_index] - .glyph_info(REPLACEMENT_CHAR) + let mut slf = Self { + fonts, + replacement_glyph: Default::default(), + pixels_per_point, + row_height, + glyph_info_cache: Default::default(), + }; + let replacement_glyph = slf + .glyph_info_no_cache(REPLACEMENT_CHAR) .unwrap_or_else(|| { panic!( "Failed to find replacement character {:?}", REPLACEMENT_CHAR ) }); - - let replacement_font_index_glyph_info = - (replacement_glyph_font_index, replacement_glyph_info); - - let pixels_per_point = fonts[0].pixels_per_point(); - let row_height = fonts[0].row_height(); - - let slf = Self { - fonts, - replacement_font_index_glyph_info, - pixels_per_point, - row_height, - glyph_info_cache: Default::default(), - }; - slf.glyph_info_cache - .write() - .insert(REPLACEMENT_CHAR, replacement_font_index_glyph_info); + slf.replacement_glyph = replacement_glyph; slf } @@ -193,7 +200,7 @@ impl Font { self.glyph_info(c).1.advance_width } - /// `\n` will (intentionally) show up as '?' (`REPLACEMENT_CHAR`) + /// `\n` will (intentionally) show up as `REPLACEMENT_CHAR` fn glyph_info(&self, c: char) -> (FontIndex, GlyphInfo) { { if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) { @@ -202,8 +209,7 @@ impl Font { } let font_index_glyph_info = self.glyph_info_no_cache(c); - let font_index_glyph_info = - font_index_glyph_info.unwrap_or(self.replacement_font_index_glyph_info); + let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph); self.glyph_info_cache .write() .insert(c, font_index_glyph_info); @@ -439,15 +445,11 @@ impl Font { fn allocate_glyph( atlas: &mut TextureAtlas, - c: char, - font: &rusttype::Font<'static>, + glyph: rusttype::Glyph<'static>, scale_in_pixels: f32, pixels_per_point: f32, -) -> Option { - let glyph = font.glyph(c); - if glyph.id().0 == 0 { - return None; // Failed to find a glyph for the character - } +) -> GlyphInfo { + assert!(glyph.id().0 != 0); let glyph = glyph.scaled(Scale::uniform(scale_in_pixels)); let glyph = glyph.positioned(point(0.0, 0.0)); @@ -492,9 +494,9 @@ fn allocate_glyph( let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point; - Some(GlyphInfo { + GlyphInfo { id: glyph.id(), advance_width: advance_width_in_points, uv_rect, - }) + } } diff --git a/egui/src/paint/fonts.rs b/egui/src/paint/fonts.rs index 33e4e06f..d795fa83 100644 --- a/egui/src/paint/fonts.rs +++ b/egui/src/paint/fonts.rs @@ -23,6 +23,20 @@ pub enum TextStyle { Monospace, } +impl TextStyle { + pub fn all() -> impl Iterator { + [ + TextStyle::Small, + TextStyle::Body, + TextStyle::Button, + TextStyle::Heading, + TextStyle::Monospace, + ] + .iter() + .copied() + } +} + #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] // #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum FontFamily { @@ -108,7 +122,9 @@ impl Fonts { return; } - let mut atlas = TextureAtlas::new(1024, 16); // TODO: better default? + // We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`, + // so we can show the Emoji picker demo window. + let mut atlas = TextureAtlas::new(2048, 64); { // Make the top left pixel fully white: @@ -119,37 +135,29 @@ impl Fonts { let atlas = Arc::new(Mutex::new(atlas)); - self.definitions = definitions.clone(); - let FontDefinitions { - pixels_per_point, - fonts, - ttf_data, - emoji_ttf_data, - } = definitions; + self.definitions = definitions; - self.fonts = fonts - .into_iter() - .map(|(text_style, (family, size))| { - let typeface_data = ttf_data - .get(&family) - .unwrap_or_else(|| panic!("Missing TTF data for {:?}", family)); + let mut font_impl_cache = FontImplCache::new(atlas.clone(), &self.definitions); - let font_impl = Arc::new(FontImpl::new( - atlas.clone(), - typeface_data, - size, - pixels_per_point, - )); + self.fonts = self + .definitions + .fonts + .iter() + .map(|(&text_style, &(family, size))| { + let mut fonts = vec![]; - let mut fonts = vec![font_impl]; + fonts.push(font_impl_cache.font_impl(FontSource::Family(family), size)); - for &emoji_ttf_data in &emoji_ttf_data { - let emoji_font_impl = Arc::new(FontImpl::new( - atlas.clone(), - emoji_ttf_data, - size, - pixels_per_point, - )); + if family == FontFamily::Monospace { + // monospace should have ubuntu as fallback (for √ etc): + fonts.push( + font_impl_cache + .font_impl(FontSource::Family(FontFamily::VariableWidth), size), + ); + } + + for index in 0..self.definitions.emoji_ttf_data.len() { + let emoji_font_impl = font_impl_cache.font_impl(FontSource::Emoji(index), size); fonts.push(emoji_font_impl); } @@ -189,3 +197,79 @@ impl std::ops::Index for Fonts { &self.fonts[&text_style] } } + +// ---------------------------------------------------------------------------- + +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum FontSource { + Family(FontFamily), + /// Emoji fonts are numbered from hight priority (0) and onwards + Emoji(usize), +} + +pub struct FontImplCache { + atlas: Arc>, + pixels_per_point: f32, + font_families: std::collections::BTreeMap>>, + emoji_fonts: Vec>>, + + /// can't have f32 in a HashMap or BTreeMap, + /// so let's do a linear search + cache: Vec<(FontSource, f32, Arc)>, +} + +impl FontImplCache { + pub fn new(atlas: Arc>, definitions: &super::FontDefinitions) -> Self { + let font_families = definitions + .ttf_data + .iter() + .map(|(family, ttf_data)| { + ( + *family, + Arc::new(rusttype::Font::try_from_bytes(ttf_data).expect("Error parsing TTF")), + ) + }) + .collect(); + + let emoji_fonts = definitions + .emoji_ttf_data + .iter() + .map(|ttf_data| { + Arc::new(rusttype::Font::try_from_bytes(ttf_data).expect("Error parsing TTF")) + }) + .collect(); + + Self { + atlas, + pixels_per_point: definitions.pixels_per_point, + font_families, + emoji_fonts, + cache: Default::default(), + } + } + + pub fn rusttype_font(&self, source: FontSource) -> Arc> { + match source { + FontSource::Family(family) => self.font_families.get(&family).unwrap().clone(), + FontSource::Emoji(index) => self.emoji_fonts[index].clone(), + } + } + + pub fn font_impl(&mut self, source: FontSource, scale_in_points: f32) -> Arc { + for entry in &self.cache { + if (entry.0, entry.1) == (source, scale_in_points) { + return entry.2.clone(); + } + } + + let font_impl = Arc::new(FontImpl::new( + self.atlas.clone(), + self.pixels_per_point, + self.rusttype_font(source), + scale_in_points, + )); + self.cache + .push((source, scale_in_points, font_impl.clone())); + font_impl + } +}