1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Add font variations example and cache bug in variations api

This commit is contained in:
lucasmerlin
2026-03-26 11:29:00 +01:00
parent 1c9f74b8bd
commit cb3b146067
7 changed files with 206 additions and 41 deletions

View File

@@ -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"

View File

@@ -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<skrifa::outline::HintingInstance>,
}
@@ -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<char, GlyphInfo>,
glyph_info_cache: ahash::HashMap<(char, skrifa::instance::Location), GlyphInfo>,
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
}
@@ -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<GlyphInfo> {
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<GlyphInfo> {
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
}
}

View File

@@ -438,7 +438,8 @@ pub(super) struct CachedFamily {
pub replacement_glyph: (FontFaceKey, GlyphInfo),
pub glyph_info_cache: ahash::HashMap<char, (FontFaceKey, GlyphInfo)>,
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<FontFaceKey, FontFace>,
) -> 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));
}
}

View File

@@ -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(&section.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, &section.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, &section.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
})

View File

@@ -0,0 +1,19 @@
[package]
name = "font_variations"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
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"] }

View File

@@ -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);
});
}
}