mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Fix glyph caching on font variations (#8189)
* Closes https://github.com/emilk/egui/pull/8029 --------- Co-authored-by: lucasmerlin <hi@lucasmerlin.me>
This commit is contained in:
@@ -1735,6 +1735,14 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "font_variations"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontconfig-parser"
|
||||
version = "0.5.7"
|
||||
|
||||
@@ -58,6 +58,41 @@ impl GlyphInfo {
|
||||
};
|
||||
}
|
||||
|
||||
/// Result of resolving a `char` to a [`GlyphId`] within a single [`FontFace`].
|
||||
///
|
||||
/// Location-independent: only depends on the font's charmap and `FontTweak`,
|
||||
/// not on variable-font variation coordinates.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(super) enum GlyphIdResolution {
|
||||
/// A real, visible glyph.
|
||||
Glyph(GlyphId),
|
||||
|
||||
/// A valid char, but rendered as zero-width (control chars, joiners, …).
|
||||
Invisible,
|
||||
}
|
||||
|
||||
/// A precomputed hash of a [`skrifa::instance::Location`].
|
||||
///
|
||||
/// Used as a cache key so that we don't have to re-hash the coordinate list
|
||||
/// for every glyph lookup. Compute once per text run and reuse for every glyph
|
||||
/// in the run.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct LocationHash(u64);
|
||||
|
||||
impl nohash_hasher::IsEnabled for LocationHash {}
|
||||
|
||||
impl LocationHash {
|
||||
#[inline]
|
||||
pub fn new(location: &skrifa::instance::Location) -> Self {
|
||||
if location.coords().is_empty() {
|
||||
// Fast path for the (common) default-coords case.
|
||||
Self(0)
|
||||
} else {
|
||||
Self(crate::util::hash(location))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subpixel binning, taken from cosmic-text:
|
||||
// https://github.com/pop-os/cosmic-text/blob/974ddaed96b334f560b606ebe5d2ca2d2f9f23ef/src/glyph_cache.rs
|
||||
|
||||
@@ -131,10 +166,12 @@ struct GlyphCacheKey(u64);
|
||||
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
|
||||
|
||||
impl GlyphCacheKey {
|
||||
#[inline]
|
||||
fn new(glyph_id: GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
|
||||
let StyledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
location_hash,
|
||||
..
|
||||
} = *metrics;
|
||||
debug_assert!(
|
||||
@@ -150,6 +187,7 @@ impl GlyphCacheKey {
|
||||
pixels_per_point.to_bits(),
|
||||
px_scale_factor.to_bits(),
|
||||
bin,
|
||||
location_hash,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -161,7 +199,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>,
|
||||
}
|
||||
|
||||
@@ -204,7 +241,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,
|
||||
@@ -323,7 +362,18 @@ pub struct FontFace {
|
||||
/// `ShaperData` is `Copy` — lives outside the `self_cell`.
|
||||
shaper_data: harfrust::ShaperData,
|
||||
|
||||
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
|
||||
/// Location-independent: `char → GlyphId | Invisible`.
|
||||
///
|
||||
/// Only depends on the font's charmap + `FontTweak`. A miss means the char
|
||||
/// is not in this face's repertoire and the fallback chain should be tried.
|
||||
glyph_id_cache: ahash::HashMap<char, GlyphIdResolution>,
|
||||
|
||||
/// Location-dependent: `(char, LocationHash) → unscaled advance width`.
|
||||
///
|
||||
/// Variable fonts can vary advance widths per axis (HVAR table), so this
|
||||
/// must be re-keyed per resolved [`skrifa::instance::Location`].
|
||||
advance_width_cache: ahash::HashMap<(char, LocationHash), OrderedFloat<f32>>,
|
||||
|
||||
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
|
||||
}
|
||||
|
||||
@@ -345,14 +395,11 @@ impl FontFace {
|
||||
// Note: We use default location here during initialization because
|
||||
// the actual weight will be applied via the stored location during rendering.
|
||||
// The metrics won't be significantly different at this unscaled size.
|
||||
// TODO(emilk): heed location for vertical metrics too (HVAR/MVAR).
|
||||
let metrics = skrifa_font.metrics(
|
||||
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.unwrap_or(options.font_hinting);
|
||||
let hinting_instance = hinting_enabled
|
||||
@@ -374,7 +421,6 @@ impl FontFace {
|
||||
charmap,
|
||||
outline_glyphs: glyphs,
|
||||
metrics,
|
||||
glyph_metrics,
|
||||
hinting_instance,
|
||||
})
|
||||
})?;
|
||||
@@ -389,7 +435,8 @@ impl FontFace {
|
||||
tweak,
|
||||
subpixel_binning,
|
||||
shaper_data,
|
||||
glyph_info_cache: Default::default(),
|
||||
glyph_id_cache: Default::default(),
|
||||
advance_width_cache: Default::default(),
|
||||
glyph_alloc_cache: Default::default(),
|
||||
})
|
||||
}
|
||||
@@ -423,65 +470,86 @@ impl FontFace {
|
||||
.filter_map(|(chr, _)| char::from_u32(chr).filter(|c| !self.ignore_character(*c)))
|
||||
}
|
||||
|
||||
/// `\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) {
|
||||
return Some(*glyph_info);
|
||||
/// Resolve a `char` to a [`GlyphId`] within this face.
|
||||
///
|
||||
/// Location-independent. Returns `None` when this face cannot represent
|
||||
/// the char (the caller should try the fallback chain).
|
||||
///
|
||||
/// `\t` and thin spaces share `' '`s glyph id (they just have a custom advance).
|
||||
pub(super) fn glyph_id_resolution(&mut self, c: char) -> Option<GlyphIdResolution> {
|
||||
if let Some(resolution) = self.glyph_id_cache.get(&c) {
|
||||
return Some(*resolution);
|
||||
}
|
||||
|
||||
if self.ignore_character(c) {
|
||||
return None; // these will result in the replacement character when rendering
|
||||
}
|
||||
|
||||
if c == '\t'
|
||||
&& let Some(space) = self.glyph_info(' ')
|
||||
{
|
||||
let glyph_info = GlyphInfo {
|
||||
advance_width_unscaled: (self.tweak.tab_size * space.advance_width_unscaled.0)
|
||||
.into(),
|
||||
..space
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
|
||||
if (c == '\u{2009}' || c == '\u{202F}')
|
||||
&& let Some(space) = self.glyph_info(' ')
|
||||
{
|
||||
// Thin space (U+2009) and narrow no-break space (U+202F),
|
||||
// often used as thousands separator: 1 234 567 890
|
||||
let advance_width = self.tweak.thin_space_width * space.advance_width_unscaled.0;
|
||||
let glyph_info = GlyphInfo {
|
||||
advance_width_unscaled: advance_width.into(),
|
||||
..space
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
|
||||
if invisible_char(c) {
|
||||
let glyph_info = GlyphInfo::INVISIBLE;
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
|
||||
let font_data = self.font.borrow_dependent();
|
||||
|
||||
// Add new character:
|
||||
let glyph_id = font_data
|
||||
.charmap
|
||||
.map(c)
|
||||
.filter(|id| *id != GlyphId::NOTDEF)?;
|
||||
|
||||
let glyph_info = GlyphInfo {
|
||||
id: Some(glyph_id),
|
||||
advance_width_unscaled: font_data
|
||||
.glyph_metrics
|
||||
.advance_width(glyph_id)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
let resolution = if c == '\t' || c == '\u{2009}' || c == '\u{202F}' {
|
||||
// `\t` and thin spaces are rendered as a space glyph with a custom advance.
|
||||
self.glyph_id_resolution(' ')?
|
||||
} else if invisible_char(c) {
|
||||
GlyphIdResolution::Invisible
|
||||
} else {
|
||||
let glyph_id = self
|
||||
.font
|
||||
.borrow_dependent()
|
||||
.charmap
|
||||
.map(c)
|
||||
.filter(|id| *id != GlyphId::NOTDEF)?;
|
||||
GlyphIdResolution::Glyph(glyph_id)
|
||||
};
|
||||
|
||||
self.glyph_id_cache.insert(c, resolution);
|
||||
Some(resolution)
|
||||
}
|
||||
|
||||
/// Unscaled advance width for `c` at the given variation location.
|
||||
///
|
||||
/// Location-dependent (variable fonts can vary advances via HVAR).
|
||||
/// Cached per `(char, LocationHash)`.
|
||||
fn advance_width_unscaled(&mut self, c: char, metrics: &StyledMetrics) -> f32 {
|
||||
let cache_key = (c, metrics.location_hash);
|
||||
if let Some(advance) = self.advance_width_cache.get(&cache_key) {
|
||||
return advance.0;
|
||||
}
|
||||
|
||||
let advance = match c {
|
||||
'\t' => self.tweak.tab_size * self.advance_width_unscaled(' ', metrics),
|
||||
'\u{2009}' | '\u{202F}' => {
|
||||
// Thin space (U+2009) and narrow no-break space (U+202F),
|
||||
// often used as thousands separator.
|
||||
self.tweak.thin_space_width * self.advance_width_unscaled(' ', metrics)
|
||||
}
|
||||
_ => {
|
||||
let Some(GlyphIdResolution::Glyph(glyph_id)) = self.glyph_id_resolution(c) else {
|
||||
return 0.0;
|
||||
};
|
||||
let font_data = self.font.borrow_dependent();
|
||||
let glyph_metrics = font_data
|
||||
.skrifa
|
||||
.glyph_metrics(skrifa::instance::Size::unscaled(), &metrics.location);
|
||||
glyph_metrics.advance_width(glyph_id).unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
self.advance_width_cache.insert(cache_key, advance.into());
|
||||
advance
|
||||
}
|
||||
|
||||
/// `\n` will result in `None`.
|
||||
///
|
||||
/// Caller must pass [`StyledMetrics`] resolved against *this* face so that
|
||||
/// variable-font advance widths are looked up at the correct location.
|
||||
pub(super) fn glyph_info(&mut self, c: char, metrics: &StyledMetrics) -> Option<GlyphInfo> {
|
||||
let resolution = self.glyph_id_resolution(c)?;
|
||||
let glyph_info = match resolution {
|
||||
GlyphIdResolution::Invisible => GlyphInfo::INVISIBLE,
|
||||
GlyphIdResolution::Glyph(glyph_id) => GlyphInfo {
|
||||
id: Some(glyph_id),
|
||||
advance_width_unscaled: self.advance_width_unscaled(c, metrics).into(),
|
||||
},
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
Some(glyph_info)
|
||||
}
|
||||
|
||||
@@ -510,6 +578,7 @@ impl FontFace {
|
||||
// argument (probably from TextFormat).
|
||||
let settings = std::iter::chain(self.tweak.coords.as_ref(), coords.as_ref());
|
||||
let location = axes.location(settings);
|
||||
let location_hash = LocationHash::new(&location);
|
||||
|
||||
StyledMetrics {
|
||||
pixels_per_point,
|
||||
@@ -519,6 +588,7 @@ impl FontFace {
|
||||
ascent,
|
||||
row_height: ascent - descent + line_gap,
|
||||
location,
|
||||
location_hash,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +664,7 @@ pub struct Font<'a> {
|
||||
impl Font<'_> {
|
||||
pub fn preload_characters(&mut self, s: &str) {
|
||||
for c in s.chars() {
|
||||
self.glyph_info(c);
|
||||
self.resolve_face(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,19 +696,23 @@ impl Font<'_> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Width of this character in points.
|
||||
/// Width of this character in points, at the font's default variation location.
|
||||
pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 {
|
||||
let (key, glyph_info) = self.glyph_info(c);
|
||||
if let Some(font) = &self.fonts_by_id.get(&key) {
|
||||
glyph_info.advance_width_unscaled.0 * font.font.px_scale_factor(font_size)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
let face_key = self.resolve_face(c);
|
||||
let Some(font_face) = self.fonts_by_id.get_mut(&face_key) else {
|
||||
return 0.0;
|
||||
};
|
||||
let metrics = font_face.styled_metrics(1.0, font_size, &VariationCoords::default());
|
||||
let Some(glyph_info) = font_face.glyph_info(c, &metrics) else {
|
||||
return 0.0;
|
||||
};
|
||||
glyph_info.advance_width_unscaled.0 * font_face.font.px_scale_factor(font_size)
|
||||
}
|
||||
|
||||
/// 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 🤦♂️
|
||||
// TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦♂️
|
||||
self.resolve_face(c) != self.cached_family.replacement_face_key
|
||||
}
|
||||
|
||||
/// Can we display all the glyphs in this text?
|
||||
@@ -646,21 +720,52 @@ impl Font<'_> {
|
||||
s.chars().all(|c| self.has_glyph(c))
|
||||
}
|
||||
|
||||
/// `\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) {
|
||||
return *font_index_glyph_info;
|
||||
/// Find which face in the fallback chain owns `c`.
|
||||
///
|
||||
/// Location-independent — fallback choice depends only on charmap support.
|
||||
/// Falls back to the replacement-glyph face when no fallback face has `c`.
|
||||
#[inline]
|
||||
pub(crate) fn resolve_face(&mut self, c: char) -> FontFaceKey {
|
||||
if let Some(font_key) = self.cached_family.face_cache.get(&c) {
|
||||
return *font_key;
|
||||
}
|
||||
self.resolve_face_slow(c)
|
||||
}
|
||||
|
||||
let font_index_glyph_info = self
|
||||
#[cold]
|
||||
fn resolve_face_slow(&mut self, c: char) -> FontFaceKey {
|
||||
let font_key = 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
|
||||
.glyph_info_cache
|
||||
.insert(c, font_index_glyph_info);
|
||||
font_index_glyph_info
|
||||
.find_face_for_char(c, self.fonts_by_id)
|
||||
.unwrap_or(self.cached_family.replacement_face_key);
|
||||
self.cached_family.face_cache.insert(c, font_key);
|
||||
font_key
|
||||
}
|
||||
|
||||
/// Resolve `c` to its (face, [`GlyphInfo`]) at the given face's location.
|
||||
///
|
||||
/// `\n` will (intentionally) show up as the replacement character.
|
||||
///
|
||||
/// `metrics` must be the resolved [`StyledMetrics`] for the face that ends
|
||||
/// up owning `c`. Most callers pass the metrics of their text run's primary
|
||||
/// face — that is correct as long as `c` is in that face. For correct
|
||||
/// fallback-face advances, resolve the face first with [`Self::resolve_face`]
|
||||
/// and build metrics for that face.
|
||||
pub(crate) fn glyph_info(
|
||||
&mut self,
|
||||
c: char,
|
||||
metrics: &StyledMetrics,
|
||||
) -> (FontFaceKey, GlyphInfo) {
|
||||
let face_key = self.resolve_face(c);
|
||||
let Some(face) = self.fonts_by_id.get_mut(&face_key) else {
|
||||
return (face_key, GlyphInfo::INVISIBLE);
|
||||
};
|
||||
let glyph_info = face.glyph_info(c, metrics).unwrap_or_else(|| {
|
||||
// `c` is in no face — render the replacement character instead.
|
||||
face.glyph_info(self.cached_family.replacement_char, metrics)
|
||||
.unwrap_or(GlyphInfo::INVISIBLE)
|
||||
});
|
||||
(face_key, glyph_info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,6 +798,12 @@ pub struct StyledMetrics {
|
||||
|
||||
/// Resolved variation coordinates.
|
||||
pub location: skrifa::instance::Location,
|
||||
|
||||
/// Precomputed hash of [`Self::location`].
|
||||
///
|
||||
/// Hashed once per run of text so per-glyph cache lookups don't have to
|
||||
/// re-hash the full coordinate list.
|
||||
pub(crate) location_hash: LocationHash,
|
||||
}
|
||||
|
||||
/// Code points that will always be invisible (zero width).
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
TextureAtlas,
|
||||
text::{
|
||||
Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
|
||||
font::{Font, FontFace, GlyphInfo},
|
||||
font::{Font, FontFace},
|
||||
},
|
||||
};
|
||||
use emath::{NumExt as _, OrderedFloat};
|
||||
@@ -457,9 +457,20 @@ pub(super) struct CachedFamily {
|
||||
/// Lazily calculated.
|
||||
pub characters: Option<BTreeMap<char, Vec<String>>>,
|
||||
|
||||
pub replacement_glyph: (FontFaceKey, GlyphInfo),
|
||||
/// The face used when no face in [`Self::fonts`] supports a char.
|
||||
pub replacement_face_key: FontFaceKey,
|
||||
|
||||
pub glyph_info_cache: ahash::HashMap<char, (FontFaceKey, GlyphInfo)>,
|
||||
/// The char that [`Self::replacement_face_key`] actually contains.
|
||||
///
|
||||
/// When the user asks about a char that no fallback face supports we
|
||||
/// render this char in its place.
|
||||
pub replacement_char: char,
|
||||
|
||||
/// Cache: `char → which face in the fallback chain owns this char`.
|
||||
///
|
||||
/// Location-independent (fallback choice depends only on charmap support,
|
||||
/// not on variation coordinates).
|
||||
pub face_cache: ahash::HashMap<char, FontFaceKey>,
|
||||
}
|
||||
|
||||
impl CachedFamily {
|
||||
@@ -467,49 +478,59 @@ impl CachedFamily {
|
||||
fonts: Vec<FontFaceKey>,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
) -> Self {
|
||||
const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
|
||||
const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
|
||||
|
||||
if fonts.is_empty() {
|
||||
return Self {
|
||||
fonts,
|
||||
characters: None,
|
||||
replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
|
||||
glyph_info_cache: Default::default(),
|
||||
replacement_face_key: FontFaceKey::INVALID,
|
||||
replacement_char: PRIMARY_REPLACEMENT_CHAR,
|
||||
face_cache: Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut slf = Self {
|
||||
fonts,
|
||||
characters: None,
|
||||
replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
|
||||
glyph_info_cache: Default::default(),
|
||||
replacement_face_key: FontFaceKey::INVALID,
|
||||
replacement_char: PRIMARY_REPLACEMENT_CHAR,
|
||||
face_cache: Default::default(),
|
||||
};
|
||||
|
||||
const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
|
||||
const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
|
||||
|
||||
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))
|
||||
let (replacement_face_key, replacement_char) = slf
|
||||
.find_face_for_char(PRIMARY_REPLACEMENT_CHAR, fonts_by_id)
|
||||
.map(|key| (key, PRIMARY_REPLACEMENT_CHAR))
|
||||
.or_else(|| {
|
||||
slf.find_face_for_char(FALLBACK_REPLACEMENT_CHAR, fonts_by_id)
|
||||
.map(|key| (key, FALLBACK_REPLACEMENT_CHAR))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!(
|
||||
"Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
|
||||
);
|
||||
(FontFaceKey::INVALID, GlyphInfo::INVISIBLE)
|
||||
(FontFaceKey::INVALID, PRIMARY_REPLACEMENT_CHAR)
|
||||
});
|
||||
slf.replacement_glyph = replacement_glyph;
|
||||
slf.replacement_face_key = replacement_face_key;
|
||||
slf.replacement_char = replacement_char;
|
||||
|
||||
slf
|
||||
}
|
||||
|
||||
pub(crate) fn glyph_info_no_cache_or_fallback(
|
||||
&mut self,
|
||||
/// Walk the fallback chain and return the first face whose charmap supports `c`.
|
||||
///
|
||||
/// Pure — does not touch any cache. Callers that want memoisation should
|
||||
/// insert into [`Self::face_cache`] themselves.
|
||||
pub(crate) fn find_face_for_char(
|
||||
&self,
|
||||
c: char,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
) -> Option<(FontFaceKey, GlyphInfo)> {
|
||||
) -> Option<FontFaceKey> {
|
||||
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));
|
||||
return Some((*font_key, glyph_info));
|
||||
if font_face.glyph_id_resolution(c).is_some() {
|
||||
return Some(*font_key);
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
@@ -261,7 +261,7 @@ fn layout_shaped_run(
|
||||
if chr == '\t' {
|
||||
let tweak = font.fonts_by_id.get(&run.font_key).map(|ff| ff.tweak());
|
||||
let tab_size = tweak.map_or(4.0, |t| t.tab_size);
|
||||
let (_, space_info) = font.glyph_info(' ');
|
||||
let (_, space_info) = font.glyph_info(' ', face_metrics);
|
||||
let space_width_px = space_info.advance_width_unscaled.0 * px_scale;
|
||||
advance_width_px = tab_size * space_width_px;
|
||||
}
|
||||
@@ -271,7 +271,7 @@ fn layout_shaped_run(
|
||||
if chr == '\u{2009}' || chr == '\u{202F}' {
|
||||
let tweak = font.fonts_by_id.get(&run.font_key).map(|ff| ff.tweak());
|
||||
let thin_space_width = tweak.map_or(0.5, |t| t.thin_space_width);
|
||||
let (_, space_info) = font.glyph_info(' ');
|
||||
let (_, space_info) = font.glyph_info(' ', face_metrics);
|
||||
let space_width_px = space_info.advance_width_unscaled.0 * px_scale;
|
||||
advance_width_px = thin_space_width * space_width_px;
|
||||
}
|
||||
@@ -308,7 +308,7 @@ fn layout_shaped_run(
|
||||
}
|
||||
|
||||
// Use the fallback font face (not run.font_key which returned NOTDEF).
|
||||
let (fallback_key, glyph_info) = font.glyph_info(chr);
|
||||
let fallback_key = font.resolve_face(chr);
|
||||
let fallback_metrics = font
|
||||
.fonts_by_id
|
||||
.get(&fallback_key)
|
||||
@@ -316,6 +316,7 @@ fn layout_shaped_run(
|
||||
ff.styled_metrics(ctx.pixels_per_point, ctx.font_size, &Default::default())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let (_, glyph_info) = font.glyph_info(chr, &fallback_metrics);
|
||||
let advance_width_px =
|
||||
glyph_info.advance_width_unscaled.0 * fallback_metrics.px_scale_factor;
|
||||
let (glyph_alloc, physical_x) =
|
||||
@@ -774,12 +775,14 @@ 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 mut font_face = font.fonts_by_id.get_mut(&font_id);
|
||||
let font_face_metrics = font_face
|
||||
.as_mut()
|
||||
let font_id = font.resolve_face(overflow_character);
|
||||
let font_face_metrics = font
|
||||
.fonts_by_id
|
||||
.get(&font_id)
|
||||
.map(|f| f.styled_metrics(pixels_per_point, font_size, §ion.format.coords))
|
||||
.unwrap_or_default();
|
||||
let (_, glyph_info) = font.glyph_info(overflow_character, &font_face_metrics);
|
||||
let mut font_face = font.fonts_by_id.get_mut(&font_id);
|
||||
|
||||
let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
|
||||
prev_glyph.max_x() + extra_letter_spacing
|
||||
@@ -1371,7 +1374,7 @@ fn segment_into_runs(font: &mut Font<'_>, text: &str, out: &mut Vec<TextRun>) {
|
||||
let byte_end = byte_offset + grapheme_str.len();
|
||||
|
||||
let base_char = grapheme_str.chars().next().unwrap_or(' ');
|
||||
let (font_key, _) = font.glyph_info(base_char);
|
||||
let font_key = font.resolve_face(base_char);
|
||||
|
||||
if let Some(last_run) = out.last_mut()
|
||||
&& last_run.font_key == font_key
|
||||
|
||||
19
examples/font_variations/Cargo.toml
Normal file
19
examples/font_variations/Cargo.toml
Normal 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"] }
|
||||
BIN
examples/font_variations/data/Recursive-VariableFont.ttf
Normal file
BIN
examples/font_variations/data/Recursive-VariableFont.ttf
Normal file
Binary file not shown.
129
examples/font_variations/src/main.rs
Normal file
129
examples/font_variations/src/main.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
#![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({
|
||||
#[expect(clippy::large_include_file, reason = "intentional for the example")]
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user