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

Add font variations API (#7859)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes N/A
* [x] I have followed the instructions in the PR template

This was mostly from last month, but I never got around to submitting
it.

This PR adds font variation coordinates to the `TextFormat` struct, and
uses them when rendering text. The coordinates are stored in a
`SmallVec`; I've chosen to store up to 2 inline, which makes it take up
24 bytes (the minimum possible for a `SmallVec`). The variation axis
tags are stored as the `font_types::Tag` type, which I've chosen to
re-export from `epaint::text`.

The variation coordinates are resolved to a `skrifa::Location` during
font rendering/scaling, and are cached in the same way as all the other
scaled metrics. I've renamed the `ScaledMetrics` struct to
`StyledMetrics`, since it now also contains the resolved variation
coordinates. I haven't benchmarked the performance of text layout with
variation coordinates, but the existing text layout performance is
unchanged.

I've replaced the API for manually overriding a font's weight
(https://github.com/emilk/egui/pull/7790) with an API for manually
overriding any variation coordinates via `FontTweak`. This should
support the same use case as #7790 while being substantially more
flexible.

I have *not* yet added any higher-level API for mapping style attributes
(weight, width, slant, etc) to variation coordinates or to different
font faces within a single family. That's a pretty huge can of worms,
and it'd involve rethinking the split between `FontId` and `TextFormat`
(and whether `FontId` is so big that we should provide a way to reuse
it). This API is intentionally pretty low-level for now.

Likewise, I've intentionally not used variation coordinates when
computing a font's row height. I can't think of any fonts that change
their vertical metrics depending on variation axes, so this should be
fine for now.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
valadaptive
2026-03-03 16:58:42 -05:00
committed by GitHub
parent ee3e73bdf9
commit 699fc7e887
11 changed files with 258 additions and 110 deletions

View File

@@ -48,7 +48,7 @@ mint = ["emath/mint"]
rayon = ["dep:rayon"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"]
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"]
## Change Vertex layout to be compatible with unity
unity = []
@@ -62,12 +62,14 @@ emath.workspace = true
ecolor.workspace = true
ahash.workspace = true
font-types.workspace = true
log.workspace = true
nohash-hasher.workspace = true
parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
profiling.workspace = true
self_cell.workspace = true
skrifa.workspace = true
smallvec.workspace = true
vello_cpu.workspace = true
#! ### Optional dependencies

View File

@@ -12,7 +12,7 @@ use vello_cpu::{color, kurbo};
use crate::{
TextOptions, TextureAtlas,
text::{
FontTweak,
FontTweak, VariationCoords,
fonts::{Blob, CachedFamily, FontFaceKey},
},
};
@@ -145,8 +145,8 @@ struct GlyphCacheKey(u64);
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
impl GlyphCacheKey {
fn new(glyph_id: skrifa::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
let ScaledMetrics {
fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
let StyledMetrics {
pixels_per_point,
px_scale_factor,
..
@@ -197,10 +197,10 @@ impl FontCell {
fn allocate_glyph_uncached(
&mut self,
atlas: &mut TextureAtlas,
metrics: &ScaledMetrics,
metrics: &StyledMetrics,
glyph_info: &GlyphInfo,
bin: SubpixelBin,
location: &skrifa::instance::Location,
location: skrifa::instance::LocationRef<'_>,
) -> Option<GlyphAllocation> {
let glyph_id = glyph_info.id?;
@@ -337,8 +337,6 @@ pub struct FontFace {
font: FontCell,
tweak: FontTweak,
/// Variable font location (for weight axis, etc.)
location: skrifa::instance::Location,
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
}
@@ -350,7 +348,6 @@ impl FontFace {
font_data: Blob,
index: u32,
tweak: FontTweak,
preferred_weight: Option<u16>,
) -> Result<Self, Box<dyn std::error::Error>> {
let font = FontCell::try_new(font_data, |font_data| {
let skrifa_font =
@@ -396,44 +393,10 @@ impl FontFace {
})
})?;
// Use preferred_weight if provided, otherwise try to read from the OS/2 table or fvar default
let weight = preferred_weight.or_else(|| {
// First try OS/2 table
if let Some(w) = font
.borrow_dependent()
.skrifa
.os2()
.ok()
.map(|os2| os2.us_weight_class())
{
return Some(w);
}
// If no OS/2 or preferred_weight, try to get default from variable font's fvar table
font.borrow_dependent()
.skrifa
.axes()
.iter()
.find(|axis| axis.tag() == skrifa::raw::types::Tag::new(b"wght"))
.map(|axis| axis.default_value() as u16)
});
// Create location for variable font with weight axis
// If weight is provided (either from preferred_weight, OS/2, or fvar default), use it
// Otherwise fall back to Location::default() which uses all axis defaults
let location = if let Some(w) = weight {
font.borrow_dependent()
.skrifa
.axes()
.location([("wght", w as f32)])
} else {
skrifa::instance::Location::default()
};
Ok(Self {
name,
font,
tweak,
location,
glyph_info_cache: Default::default(),
glyph_alloc_cache: Default::default(),
})
@@ -537,7 +500,7 @@ impl FontFace {
#[inline]
pub(super) fn pair_kerning_pixels(
&self,
metrics: &ScaledMetrics,
metrics: &StyledMetrics,
last_glyph_id: skrifa::GlyphId,
glyph_id: skrifa::GlyphId,
) -> f32 {
@@ -559,7 +522,7 @@ impl FontFace {
#[inline]
pub fn pair_kerning(
&self,
metrics: &ScaledMetrics,
metrics: &StyledMetrics,
last_glyph_id: skrifa::GlyphId,
glyph_id: skrifa::GlyphId,
) -> f32 {
@@ -567,7 +530,12 @@ impl FontFace {
}
#[inline(always)]
pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
pub fn styled_metrics(
&self,
pixels_per_point: f32,
font_size: f32,
coords: &VariationCoords,
) -> StyledMetrics {
let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
let font_data = self.font.borrow_dependent();
let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
@@ -581,20 +549,32 @@ impl FontFace {
+ self.tweak.y_offset)
.round_ui();
ScaledMetrics {
let axes = font_data.skrifa.axes();
// Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the
// argument (probably from TextFormat).
let settings = self
.tweak
.coords
.as_ref()
.iter()
.chain(coords.as_ref().iter());
let location = axes.location(settings);
StyledMetrics {
pixels_per_point,
px_scale_factor,
scale,
y_offset_in_points,
ascent,
row_height: ascent - descent + line_gap,
location,
}
}
pub fn allocate_glyph(
&mut self,
atlas: &mut TextureAtlas,
metrics: &ScaledMetrics,
metrics: &StyledMetrics,
glyph_info: GlyphInfo,
chr: char,
h_pos: f32,
@@ -628,7 +608,7 @@ impl FontFace {
let allocation = self
.font
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, &self.location)
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into())
.unwrap_or_default();
entry.insert(allocation);
@@ -665,12 +645,17 @@ impl Font<'_> {
})
}
pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
pub fn styled_metrics(
&self,
pixels_per_point: f32,
font_size: f32,
coords: &VariationCoords,
) -> StyledMetrics {
self.cached_family
.fonts
.first()
.and_then(|key| self.fonts_by_id.get(key))
.map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
.map(|font_face| font_face.styled_metrics(pixels_per_point, font_size, coords))
.unwrap_or_default()
}
@@ -713,8 +698,8 @@ impl Font<'_> {
}
/// Metrics for a font at a specific screen-space scale.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct ScaledMetrics {
#[derive(Clone, Debug, PartialEq, Default)]
pub struct StyledMetrics {
/// The DPI part of the screen-space scale.
pub pixels_per_point: f32,
@@ -738,6 +723,9 @@ pub struct ScaledMetrics {
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub row_height: f32,
/// Resolved variation coordinates.
pub location: skrifa::instance::Location,
}
/// Code points that will always be invisible (zero width).

View File

@@ -10,7 +10,7 @@ use std::{
use crate::{
TextureAtlas,
text::{
Galley, LayoutJob, LayoutSection, TextOptions,
Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
font::{Font, FontFace, GlyphInfo},
},
};
@@ -125,12 +125,6 @@ pub struct FontData {
/// Extra scale and vertical tweak to apply to all text of this font.
pub tweak: FontTweak,
/// The font weight (100-900), if available.
/// Standard values: 100 (Thin), 200 (Extra Light), 300 (Light), 400 (Regular),
/// 500 (Medium), 600 (Semi Bold), 700 (Bold), 800 (Extra Bold), 900 (Black).
/// `None` if the weight could not be determined.
pub weight: Option<u16>,
}
impl FontData {
@@ -139,7 +133,6 @@ impl FontData {
font: Cow::Borrowed(font),
index: 0,
tweak: Default::default(),
weight: None,
}
}
@@ -148,43 +141,12 @@ impl FontData {
font: Cow::Owned(font),
index: 0,
tweak: Default::default(),
weight: None,
}
}
pub fn tweak(self, tweak: FontTweak) -> Self {
Self { tweak, ..self }
}
/// Set the font weight (100-900).
///
/// This is typically read automatically from the font file when loaded,
/// but can be overridden manually if needed.
///
/// Standard weight values:
/// - 100: Thin
/// - 200: Extra Light
/// - 300: Light
/// - 400: Regular/Normal
/// - 500: Medium
/// - 600: Semi Bold
/// - 700: Bold
/// - 800: Extra Bold
/// - 900: Black
///
/// # Example
/// ```
/// # use epaint::text::FontData;
/// let font_data = FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf"))
/// .weight(300); // Override to Light weight
/// assert_eq!(font_data.weight, Some(300));
/// ```
pub fn weight(self, weight: u16) -> Self {
Self {
weight: Some(weight),
..self
}
}
}
impl AsRef<[u8]> for FontData {
@@ -196,7 +158,7 @@ impl AsRef<[u8]> for FontData {
// ----------------------------------------------------------------------------
/// Extra scale and vertical tweak to apply to all text of a certain font.
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FontTweak {
/// Scale the font's glyphs by this much.
@@ -228,6 +190,9 @@ pub struct FontTweak {
///
/// `None` means use the global setting.
pub hinting_override: Option<bool>,
/// Override the font's default variation coordinates.
pub coords: VariationCoords,
}
impl Default for FontTweak {
@@ -237,6 +202,7 @@ impl Default for FontTweak {
y_offset_factor: 0.0,
y_offset: 0.0,
hinting_override: None,
coords: VariationCoords::default(),
}
}
}
@@ -701,7 +667,12 @@ impl FontsView<'_> {
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
self.fonts
.font(&font_id.family)
.scaled_metrics(self.pixels_per_point, font_id.size)
.styled_metrics(
self.pixels_per_point,
font_id.size,
// TODO(valadaptive): use font variation coords when calculating row height
&VariationCoords::default(),
)
.row_height
}
@@ -807,15 +778,13 @@ impl FontsImpl {
let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace> = Default::default();
let mut fonts_by_name: ahash::HashMap<String, FontFaceKey> = Default::default();
for (name, font_data) in &definitions.font_data {
let tweak = font_data.tweak;
let blob = blob_from_font_data(font_data);
let font_face = FontFace::new(
options,
name.clone(),
blob,
font_data.index,
tweak,
font_data.weight,
font_data.tweak.clone(),
)
.unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"));
let key = FontFaceKey::new();

View File

@@ -8,7 +8,7 @@ use crate::{
Color32, Mesh, Stroke, Vertex,
stroke::PathStroke,
text::{
font::{ScaledMetrics, is_cjk, is_cjk_break_allowed},
font::{StyledMetrics, is_cjk, is_cjk_break_allowed},
fonts::FontFaceKey,
},
};
@@ -160,7 +160,7 @@ fn layout_section(
} = section;
let mut font = fonts.font(&format.font_id.family);
let font_size = format.font_id.size;
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords);
let line_height = section
.format
.line_height
@@ -178,7 +178,7 @@ fn layout_section(
// Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
let mut current_font = FontFaceKey::INVALID;
let mut current_font_face_metrics = ScaledMetrics::default();
let mut current_font_face_metrics = StyledMetrics::default();
for chr in job.text[byte_range.clone()].chars() {
if job.break_on_newline && chr == '\n' {
@@ -192,7 +192,9 @@ fn layout_section(
current_font = font_id;
current_font_face_metrics = font_face
.as_ref()
.map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
.map(|font_face| {
font_face.styled_metrics(pixels_per_point, font_size, &format.coords)
})
.unwrap_or_default();
}
@@ -468,7 +470,7 @@ fn replace_last_glyph_with_overflow_character(
let mut font_face = font.fonts_by_id.get_mut(&font_id);
let font_face_metrics = font_face
.as_mut()
.map(|f| f.scaled_metrics(pixels_per_point, font_size))
.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() {
@@ -519,7 +521,8 @@ fn replace_last_glyph_with_overflow_character(
})
.unwrap_or_default();
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
let font_metrics =
font.styled_metrics(pixels_per_point, font_size, &section.format.coords);
let line_height = section
.format
.line_height
@@ -1212,7 +1215,7 @@ mod tests {
let font_id = FontId::default();
let font_height = fonts
.font(&font_id.family)
.scaled_metrics(pixels_per_point, font_id.size)
.styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
.row_height;
let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
@@ -1245,7 +1248,7 @@ mod tests {
let font_id = FontId::default();
let font_height = fonts
.font(&font_id.family)
.scaled_metrics(pixels_per_point, font_id.size)
.styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
.row_height;
let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);

View File

@@ -1,5 +1,5 @@
use std::ops::Range;
use std::sync::Arc;
use std::{ops::Range, str::FromStr as _};
use super::{
cursor::{CCursor, LayoutCursor},
@@ -7,6 +7,8 @@ use super::{
};
use crate::{Color32, FontId, Mesh, Stroke, text::FontsView};
use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2};
pub use font_types::Tag;
use smallvec::SmallVec;
/// Describes the task of laying out text.
///
@@ -257,6 +259,107 @@ impl std::hash::Hash for LayoutSection {
// ----------------------------------------------------------------------------
/// Helper trait for all types that can be parsed as a [`font_types::Tag`].
pub trait IntoTag {
fn into_tag(self) -> font_types::Tag;
}
impl IntoTag for font_types::Tag {
#[inline(always)]
fn into_tag(self) -> font_types::Tag {
self
}
}
impl IntoTag for u32 {
#[inline(always)]
fn into_tag(self) -> font_types::Tag {
font_types::Tag::from_u32(self)
}
}
impl IntoTag for [u8; 4] {
#[inline(always)]
fn into_tag(self) -> font_types::Tag {
font_types::Tag::new_checked(&self).expect("Invalid variation axis tag")
}
}
impl IntoTag for &[u8; 4] {
#[inline(always)]
fn into_tag(self) -> font_types::Tag {
font_types::Tag::new_checked(self).expect("Invalid variation axis tag")
}
}
impl IntoTag for &str {
#[inline(always)]
fn into_tag(self) -> font_types::Tag {
font_types::Tag::from_str(self).expect("Invalid variation axis tag")
}
}
/// List of font variation coordinates by axis tag. If more than one coordinate for a given axis is provided, the last
/// one added is used.
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct VariationCoords(SmallVec<[(font_types::Tag, f32); 2]>);
impl VariationCoords {
/// Create a list of variation coordinates from a sequence of (tag, value) pairs.
///
/// ## Example:
/// ```
/// use epaint::text::VariationCoords;
///
/// let coords = VariationCoords::new([
/// (b"wght", 500.0),
/// (b"wdth", 75.0),
/// ]);
/// ```
pub fn new<T: IntoTag>(values: impl IntoIterator<Item = (T, f32)>) -> Self {
Self(values.into_iter().map(|(t, c)| (t.into_tag(), c)).collect())
}
/// Add a variation coordinate to the list.
#[inline(always)]
pub fn push(&mut self, tag: impl IntoTag, coord: f32) {
self.0.push((tag.into_tag(), coord));
}
/// Remove the coordinate at the given index.
pub fn remove(&mut self, index: usize) {
self.0.remove(index);
}
pub fn clear(&mut self) {
self.0.clear();
}
}
impl AsRef<[(font_types::Tag, f32)]> for VariationCoords {
#[inline(always)]
fn as_ref(&self) -> &[(font_types::Tag, f32)] {
&self.0
}
}
impl AsMut<[(font_types::Tag, f32)]> for VariationCoords {
fn as_mut(&mut self) -> &mut [(font_types::Tag, f32)] {
&mut self.0
}
}
impl std::hash::Hash for VariationCoords {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.len().hash(state);
for (tag, coord) in &self.0 {
tag.hash(state);
OrderedFloat(*coord).hash(state);
}
}
}
/// Formatting option for a section of text.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -287,6 +390,8 @@ pub struct TextFormat {
/// Default: 1.0
pub expand_bg: f32,
pub coords: VariationCoords,
pub italics: bool,
pub underline: Stroke,
@@ -315,6 +420,7 @@ impl Default for TextFormat {
color: Color32::GRAY,
background: Color32::TRANSPARENT,
expand_bg: 1.0,
coords: VariationCoords::default(),
italics: false,
underline: Stroke::NONE,
strikethrough: Stroke::NONE,
@@ -333,6 +439,7 @@ impl std::hash::Hash for TextFormat {
color,
background,
expand_bg,
coords,
italics,
underline,
strikethrough,
@@ -346,6 +453,7 @@ impl std::hash::Hash for TextFormat {
color.hash(state);
background.hash(state);
emath::OrderedFloat(*expand_bg).hash(state);
coords.hash(state);
italics.hash(state);
underline.hash(state);
strikethrough.hash(state);