diff --git a/Cargo.lock b/Cargo.lock index 981e7c475..830536272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1732,6 +1732,14 @@ dependencies = [ "serde", ] +[[package]] +name = "font_variations" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 61f9f9f2f..b1e15371e 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -149,7 +149,11 @@ impl GlyphCacheKey { let StyledMetrics { pixels_per_point, px_scale_factor, - .. + ref location, + scale: _, + y_offset_in_points: _, + ascent: _, + row_height: _, } = *metrics; debug_assert!( 0.0 < pixels_per_point && pixels_per_point.is_finite(), @@ -164,6 +168,7 @@ impl GlyphCacheKey { pixels_per_point.to_bits(), px_scale_factor.to_bits(), bin, + location, ))) } } @@ -175,7 +180,6 @@ struct DependentFontData<'a> { charmap: skrifa::charmap::Charmap<'a>, outline_glyphs: skrifa::outline::OutlineGlyphCollection<'a>, metrics: skrifa::metrics::Metrics, - glyph_metrics: skrifa::metrics::GlyphMetrics<'a>, hinting_instance: Option, } @@ -220,7 +224,9 @@ impl FontCell { if let Some(hinting_instance) = &mut font_data.hinting_instance { let size = skrifa::instance::Size::new(metrics.scale); - if hinting_instance.size() != size { + if hinting_instance.size() != size + || hinting_instance.location().coords() != location.coords() + { hinting_instance .reconfigure( &font_data.outline_glyphs, @@ -337,7 +343,7 @@ pub struct FontFace { font: FontCell, tweak: FontTweak, - glyph_info_cache: ahash::HashMap, + glyph_info_cache: ahash::HashMap<(char, skrifa::instance::Location), GlyphInfo>, glyph_alloc_cache: ahash::HashMap, } @@ -363,11 +369,6 @@ impl FontFace { skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default(), ); - let glyph_metrics = skrifa_font.glyph_metrics( - skrifa::instance::Size::unscaled(), - skrifa::instance::LocationRef::default(), - ); - let hinting_enabled = tweak.hinting_override.unwrap_or(options.font_hinting); let hinting_instance = hinting_enabled .then(|| { @@ -388,7 +389,6 @@ impl FontFace { charmap, outline_glyphs: glyphs, metrics, - glyph_metrics, hinting_instance, }) })?; @@ -432,8 +432,13 @@ impl FontFace { } /// `\n` will result in `None` - pub(super) fn glyph_info(&mut self, c: char) -> Option { - if let Some(glyph_info) = self.glyph_info_cache.get(&c) { + pub(super) fn glyph_info( + &mut self, + c: char, + location: &skrifa::instance::Location, + ) -> Option { + let cache_key = (c, location.clone()); + if let Some(glyph_info) = self.glyph_info_cache.get(&cache_key) { return Some(*glyph_info); } @@ -442,7 +447,7 @@ impl FontFace { } if c == '\t' - && let Some(space) = self.glyph_info(' ') + && let Some(space) = self.glyph_info(' ', location) { let glyph_info = GlyphInfo { advance_width_unscaled: (crate::text::TAB_SIZE as f32 @@ -450,7 +455,7 @@ impl FontFace { .into(), ..space }; - self.glyph_info_cache.insert(c, glyph_info); + self.glyph_info_cache.insert(cache_key, glyph_info); return Some(glyph_info); } @@ -459,21 +464,21 @@ impl FontFace { // https://www.compart.com/en/unicode/U+2009 // https://en.wikipedia.org/wiki/Thin_space - if let Some(space) = self.glyph_info(' ') { + if let Some(space) = self.glyph_info(' ', location) { let em = self.font.borrow_dependent().metrics.units_per_em as f32; let advance_width = f32::min(em / 6.0, space.advance_width_unscaled.0 * 0.5); // TODO(emilk): make configurable let glyph_info = GlyphInfo { advance_width_unscaled: advance_width.into(), ..space }; - self.glyph_info_cache.insert(c, glyph_info); + self.glyph_info_cache.insert(cache_key, glyph_info); return Some(glyph_info); } } if invisible_char(c) { let glyph_info = GlyphInfo::INVISIBLE; - self.glyph_info_cache.insert(c, glyph_info); + self.glyph_info_cache.insert(cache_key, glyph_info); return Some(glyph_info); } @@ -485,15 +490,17 @@ impl FontFace { .map(c) .filter(|id| *id != skrifa::GlyphId::NOTDEF)?; + let glyph_metrics = font_data + .skrifa + .glyph_metrics(skrifa::instance::Size::unscaled(), location); let glyph_info = GlyphInfo { id: Some(glyph_id), - advance_width_unscaled: font_data - .glyph_metrics + advance_width_unscaled: glyph_metrics .advance_width(glyph_id) .unwrap_or_default() .into(), }; - self.glyph_info_cache.insert(c, glyph_info); + self.glyph_info_cache.insert(cache_key, glyph_info); Some(glyph_info) } @@ -626,8 +633,9 @@ pub struct Font<'a> { impl Font<'_> { pub fn preload_characters(&mut self, s: &str) { + let default_location = skrifa::instance::Location::default(); for c in s.chars() { - self.glyph_info(c); + self.glyph_info(c, &default_location); } } @@ -661,7 +669,8 @@ impl Font<'_> { /// Width of this character in points. pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 { - let (key, glyph_info) = self.glyph_info(c); + let default_location = skrifa::instance::Location::default(); + let (key, glyph_info) = self.glyph_info(c, &default_location); if let Some(font) = &self.fonts_by_id.get(&key) { glyph_info.advance_width_unscaled.0 * font.font.px_scale_factor(font_size) } else { @@ -671,7 +680,8 @@ impl Font<'_> { /// Can we display this glyph? pub fn has_glyph(&mut self, c: char) -> bool { - self.glyph_info(c) != self.cached_family.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦‍♂️ + let default_location = skrifa::instance::Location::default(); + self.glyph_info(c, &default_location) != self.cached_family.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦‍♂️ } /// Can we display all the glyphs in this text? @@ -680,19 +690,26 @@ impl Font<'_> { } /// `\n` will (intentionally) show up as the replacement character. - pub(crate) fn glyph_info(&mut self, c: char) -> (FontFaceKey, GlyphInfo) { - if let Some(font_index_glyph_info) = self.cached_family.glyph_info_cache.get(&c) { + pub(crate) fn glyph_info( + &mut self, + c: char, + location: &skrifa::instance::Location, + ) -> (FontFaceKey, GlyphInfo) { + let cache_key = (c, location.clone()); + if let Some(font_index_glyph_info) = + self.cached_family.glyph_info_cache.get(&cache_key) + { return *font_index_glyph_info; } let font_index_glyph_info = self .cached_family - .glyph_info_no_cache_or_fallback(c, self.fonts_by_id); + .glyph_info_no_cache_or_fallback(c, location, self.fonts_by_id); let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.cached_family.replacement_glyph); self.cached_family .glyph_info_cache - .insert(c, font_index_glyph_info); + .insert(cache_key, font_index_glyph_info); font_index_glyph_info } } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 5099e0085..fed933f21 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -438,7 +438,8 @@ pub(super) struct CachedFamily { pub replacement_glyph: (FontFaceKey, GlyphInfo), - pub glyph_info_cache: ahash::HashMap, + pub glyph_info_cache: + ahash::HashMap<(char, skrifa::instance::Location), (FontFaceKey, GlyphInfo)>, } impl CachedFamily { @@ -465,9 +466,10 @@ impl CachedFamily { const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback + let default_location = skrifa::instance::Location::default(); 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)) + .glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR, &default_location, fonts_by_id) + .or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR, &default_location, fonts_by_id)) .unwrap_or_else(|| { log::warn!( "Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph." @@ -482,12 +484,14 @@ impl CachedFamily { pub(crate) fn glyph_info_no_cache_or_fallback( &mut self, c: char, + location: &skrifa::instance::Location, fonts_by_id: &mut nohash_hasher::IntMap, ) -> Option<(FontFaceKey, GlyphInfo)> { for font_key in &self.fonts { let font_face = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID"); - if let Some(glyph_info) = font_face.glyph_info(c) { - self.glyph_info_cache.insert(c, (*font_key, glyph_info)); + if let Some(glyph_info) = font_face.glyph_info(c, location) { + self.glyph_info_cache + .insert((c, location.clone()), (*font_key, glyph_info)); return Some((*font_key, glyph_info)); } } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 0233c1c58..9ce06db32 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -186,7 +186,7 @@ fn layout_section( paragraph = out_paragraphs.last_mut().unwrap(); paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs? } else { - let (font_id, glyph_info) = font.glyph_info(chr); + let (font_id, glyph_info) = font.glyph_info(chr, &font_metrics.location); let mut font_face = font.fonts_by_id.get_mut(&font_id); if current_font != font_id { current_font = font_id; @@ -471,12 +471,9 @@ fn replace_last_glyph_with_overflow_character( let mut font = fonts.font(§ion.format.font_id.family); let font_size = section.format.font_id.size; - let (font_id, glyph_info) = font.glyph_info(overflow_character); + let font_face_metrics = font.styled_metrics(pixels_per_point, font_size, §ion.format.coords); + let (font_id, glyph_info) = font.glyph_info(overflow_character, &font_face_metrics.location); let mut font_face = font.fonts_by_id.get_mut(&font_id); - let font_face_metrics = font_face - .as_mut() - .map(|f| f.styled_metrics(pixels_per_point, font_size, §ion.format.coords)) - .unwrap_or_default(); let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() { // Kern the overflow character properly @@ -484,8 +481,8 @@ fn replace_last_glyph_with_overflow_character( .as_mut() .map(|font_face| { if let (Some(prev_glyph_id), Some(overflow_glyph_id)) = ( - font_face.glyph_info(prev_glyph.chr).and_then(|g| g.id), - font_face.glyph_info(overflow_character).and_then(|g| g.id), + font_face.glyph_info(prev_glyph.chr, &font_face_metrics.location).and_then(|g| g.id), + font_face.glyph_info(overflow_character, &font_face_metrics.location).and_then(|g| g.id), ) { font_face.pair_kerning(&font_face_metrics, prev_glyph_id, overflow_glyph_id) } else { @@ -501,7 +498,7 @@ fn replace_last_glyph_with_overflow_character( let replacement_glyph_width = font_face .as_mut() - .and_then(|f| f.glyph_info(overflow_character)) + .and_then(|f| f.glyph_info(overflow_character, &font_face_metrics.location)) .map(|i| { i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point }) diff --git a/examples/font_variations/Cargo.toml b/examples/font_variations/Cargo.toml new file mode 100644 index 000000000..789625f06 --- /dev/null +++ b/examples/font_variations/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "font_variations" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2024" +rust-version = "1.92" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +env_logger = { workspace = true, features = ["auto-color", "humantime"] } diff --git a/examples/font_variations/data/Recursive-VariableFont.ttf b/examples/font_variations/data/Recursive-VariableFont.ttf new file mode 100644 index 000000000..367e2df5a Binary files /dev/null and b/examples/font_variations/data/Recursive-VariableFont.ttf differ diff --git a/examples/font_variations/src/main.rs b/examples/font_variations/src/main.rs new file mode 100644 index 000000000..af8b9f28b --- /dev/null +++ b/examples/font_variations/src/main.rs @@ -0,0 +1,120 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![expect(rustdoc::missing_crate_level_docs)] // it's an example + +use eframe::egui; +use eframe::epaint::text::{FontInsert, InsertFontFamily}; + +fn main() -> eframe::Result { + env_logger::init(); + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 500.0]), + ..Default::default() + }; + eframe::run_native( + "egui example: font variations", + options, + Box::new(|cc| Ok(Box::new(MyApp::new(cc)))), + ) +} + +struct MyApp { + /// Weight axis (wght): 300..1000 + weight: f32, + /// Casual axis (CASL): 0..1 + casual: f32, + /// Monospace axis (MONO): 0..1 + mono: f32, + /// Slant axis (slnt): -15..0 + slant: f32, + /// Cursive axis (CRSV): 0..1 + cursive: f32, + + preview_text: String, + font_size: f32, +} + +impl MyApp { + fn new(cc: &eframe::CreationContext<'_>) -> Self { + cc.egui_ctx.add_font(FontInsert::new( + "Recursive", + egui::FontData::from_static(include_bytes!("../data/Recursive-VariableFont.ttf")), + vec![ + InsertFontFamily { + family: egui::FontFamily::Proportional, + priority: egui::epaint::text::FontPriority::Highest, + }, + InsertFontFamily { + family: egui::FontFamily::Monospace, + priority: egui::epaint::text::FontPriority::Highest, + }, + ], + )); + + Self { + weight: 400.0, + casual: 0.0, + mono: 0.0, + slant: 0.0, + cursive: 0.5, + preview_text: "The quick brown fox jumps over the lazy dog.\n\ + ABCDEFGHIJKLMNOPQRSTUVWXYZ\n\ + abcdefghijklmnopqrstuvwxyz\n\ + 0123456789 !@#$%^&*()" + .to_owned(), + font_size: 24.0, + } + } +} + +impl eframe::App for MyApp { + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.heading("Font Variations (Recursive)"); + ui.add_space(4.0); + + egui::Grid::new("variation_sliders") + .num_columns(2) + .spacing([16.0, 8.0]) + .show(ui, |ui| { + ui.label("Weight (wght):"); + ui.add(egui::Slider::new(&mut self.weight, 300.0..=1000.0)); + ui.end_row(); + + ui.label("Casual (CASL):"); + ui.add(egui::Slider::new(&mut self.casual, 0.0..=1.0)); + ui.end_row(); + + ui.label("Monospace (MONO):"); + ui.add(egui::Slider::new(&mut self.mono, 0.0..=1.0)); + ui.end_row(); + + ui.label("Slant (slnt):"); + ui.add(egui::Slider::new(&mut self.slant, -15.0..=0.0)); + ui.end_row(); + + ui.label("Cursive (CRSV):"); + ui.add(egui::Slider::new(&mut self.cursive, 0.0..=1.0)); + ui.end_row(); + + ui.label("Font size:"); + ui.add(egui::Slider::new(&mut self.font_size, 8.0..=80.0)); + ui.end_row(); + }); + + ui.separator(); + + let rich = egui::RichText::new(&self.preview_text) + .size(self.font_size) + .variation("wght", self.weight) + .variation("CASL", self.casual) + .variation("MONO", self.mono) + .variation("slnt", self.slant) + .variation("CRSV", self.cursive); + + ui.label(rich); + + ui.add_space(8.0); + ui.text_edit_multiline(&mut self.preview_text); + }); + } +}