Variable font support (#400)

* Variable font support

Here's a pretty naïve solution for variable fonts.

The iterator doesn't use the match keys' weight, but instead tries to
get the requested ideal weight, if the font is variable, otherwise it is
ignored and the actual (non-variable) weight is used. This is because I
didn't implement finding variable weight support for match keys; doing
so would be impossible without parsing TTF files when matching and I
didn't want to add that potentially expensive infrastructure if not
entirely necessary.

This is a breaking change, and I'm open for ideas on how to fix that
if it's an issue.

* cargo fmt

* Add variable font example to rich-text example
This commit is contained in:
Wren [Undefined] 2025-07-07 09:50:40 -05:00 committed by GitHub
parent d15011fba5
commit a03faa654d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 119 additions and 34 deletions

View file

@ -24,6 +24,7 @@ fn set_buffer_text(buffer: &mut BorrowedWithFontSystem<'_, Buffer>) {
let serif_attrs = attrs.clone().family(Family::Serif);
let mono_attrs = attrs.clone().family(Family::Monospace);
let comic_attrs = attrs.clone().family(Family::Name("Comic Neue"));
let inter_attrs = attrs.clone().family(Family::Name("Inter Variable"));
let spans: &[(&str, Attrs)] = &[
(
@ -128,6 +129,21 @@ fn set_buffer_text(buffer: &mut BorrowedWithFontSystem<'_, Buffer>) {
.clone()
.cache_key_flags(CacheKeyFlags::DISABLE_HINTING),
),
(
"Inter Variable: 400 ",
inter_attrs.clone().weight(Weight(400)),
),
("200 ", inter_attrs.clone().weight(Weight(200))),
("250 ", inter_attrs.clone().weight(Weight(250))),
("300\n", inter_attrs.clone().weight(Weight(300))),
(
"Inter Variable Italic: 400 ",
inter_attrs.clone().weight(Weight(400)).style(Style::Italic),
),
(
"800",
inter_attrs.clone().weight(Weight(800)).style(Style::Italic),
),
];
buffer.set_rich_text(
@ -146,6 +162,12 @@ fn main() {
let context = softbuffer::Context::new(window.clone()).unwrap();
let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap();
let mut font_system = FontSystem::new();
let inter_variable = include_bytes!("../../../fonts/InterVariable.ttf");
font_system.db_mut().load_font_data(inter_variable.to_vec());
let inter_variable_italic = include_bytes!("../../../fonts/InterVariable-Italic.ttf");
font_system
.db_mut()
.load_font_data(inter_variable_italic.to_vec());
let mut swash_cache = SwashCache::new();
let mut display_scale = window.scale_factor() as f32;

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6f1f6a172d9e588438db9f986fd5cfad7b30f644374080a8a9d4d91e344586f
size 910252

3
fonts/InterVariable.ttf Normal file
View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4989b125924991b90d05b2d16e0e388c48f7d5bb8b30539bbf9c755278d0ccaf
size 879708

View file

@ -192,6 +192,7 @@ pub struct FontFallbackIter<'a> {
common_i: usize,
other_i: usize,
end: bool,
ideal_weight: fontdb::Weight,
}
impl<'a> FontFallbackIter<'a> {
@ -201,6 +202,7 @@ impl<'a> FontFallbackIter<'a> {
default_families: &'a [&'a Family<'a>],
scripts: &'a [Script],
word: &'a str,
ideal_weight: fontdb::Weight,
) -> Self {
font_system
.fallbacks
@ -217,6 +219,7 @@ impl<'a> FontFallbackIter<'a> {
common_i: 0,
other_i: 0,
end: false,
ideal_weight,
}
}
@ -284,7 +287,10 @@ impl<'a> FontFallbackIter<'a> {
fn next_item(&mut self, fallbacks: &Fallbacks) -> Option<<Self as Iterator>::Item> {
if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
if let Some(font) = self.font_system.get_font(fallback_info.id) {
if let Some(font) = self
.font_system
.get_font(fallback_info.id, self.ideal_weight)
{
return Some(font);
}
}
@ -303,9 +309,12 @@ impl<'a> FontFallbackIter<'a> {
macro_rules! mk_mono_fallback_info {
($m_key:expr) => {{
let supported_cp_count_opt = self
.font_system
.get_font_supported_codepoints_in_word($m_key.id, self.word);
let supported_cp_count_opt =
self.font_system.get_font_supported_codepoints_in_word(
$m_key.id,
self.ideal_weight,
self.word,
);
supported_cp_count_opt.map(|supported_cp_count| {
let codepoint_non_matches = word_chars_count - supported_cp_count;
@ -323,7 +332,7 @@ impl<'a> FontFallbackIter<'a> {
match (is_mono, default_font_match_key.as_ref()) {
(false, None) => break 'DEF_FAM,
(false, Some(m_key)) => {
if let Some(font) = self.font_system.get_font(m_key.id) {
if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
return Some(font);
} else {
break 'DEF_FAM;
@ -338,7 +347,9 @@ impl<'a> FontFallbackIter<'a> {
// Return early if default Monospace font supports all word codepoints.
// Otherewise, add to fallbacks set
if fallback_info.codepoint_non_matches == Some(0) {
if let Some(font) = self.font_system.get_font(m_key.id) {
if let Some(font) =
self.font_system.get_font(m_key.id, self.ideal_weight)
{
return Some(font);
}
} else {
@ -370,9 +381,12 @@ impl<'a> FontFallbackIter<'a> {
};
if is_mono_id {
let supported_cp_count_opt = self
.font_system
.get_font_supported_codepoints_in_word(m_key.id, self.word);
let supported_cp_count_opt =
self.font_system.get_font_supported_codepoints_in_word(
m_key.id,
self.ideal_weight,
self.word,
);
if let Some(supported_cp_count) = supported_cp_count_opt {
let codepoint_non_matches =
self.word.chars().count() - supported_cp_count;
@ -393,7 +407,10 @@ impl<'a> FontFallbackIter<'a> {
}
// If default family is Monospace fallback to first monospaced font
if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
if let Some(font) = self.font_system.get_font(fallback_info.id) {
if let Some(font) = self
.font_system
.get_font(fallback_info.id, self.ideal_weight)
{
return Some(font);
}
}
@ -409,7 +426,7 @@ impl<'a> FontFallbackIter<'a> {
self.script_i.1 += 1;
for m_key in font_match_keys_iter(false) {
if self.face_contains_family(m_key.id, script_family) {
if let Some(font) = self.font_system.get_font(m_key.id) {
if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
return Some(font);
}
}
@ -432,7 +449,7 @@ impl<'a> FontFallbackIter<'a> {
self.common_i += 1;
for m_key in font_match_keys_iter(false) {
if self.face_contains_family(m_key.id, common_family) {
if let Some(font) = self.font_system.get_font(m_key.id) {
if let Some(font) = self.font_system.get_font(m_key.id, self.ideal_weight) {
return Some(font);
}
}
@ -450,7 +467,7 @@ impl<'a> FontFallbackIter<'a> {
.iter()
.all(|family_name| !self.face_contains_family(id, family_name))
{
if let Some(font) = self.font_system.get_font(id) {
if let Some(font) = self.font_system.get_font(id, self.ideal_weight) {
return Some(font);
}
}

View file

@ -110,7 +110,7 @@ impl Font {
}
impl Font {
pub fn new(db: &fontdb::Database, id: fontdb::ID) -> Option<Self> {
pub fn new(db: &fontdb::Database, id: fontdb::ID, weight: fontdb::Weight) -> Option<Self> {
let info = db.face(id)?;
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
@ -186,7 +186,19 @@ impl Font {
(swash.offset, swash.key)
},
rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| {
RustybuzzFace::from_slice((**data).as_ref(), info.index).ok_or(())
RustybuzzFace::from_slice((**data).as_ref(), info.index)
.ok_or(())
.map(|mut face| {
if let Some(axis) = face
.variation_axes()
.into_iter()
.find(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"wght"))
{
let wght = (weight.0 as f32).clamp(axis.min_value, axis.max_value);
let _ = face.set_variation(ttf_parser::Tag::from_bytes(b"wght"), wght);
}
face
})
})
.ok()?,
#[cfg(not(feature = "peniko"))]

View file

@ -89,7 +89,7 @@ pub struct FontSystem {
db: fontdb::Database,
/// Cache for loaded fonts from the database.
font_cache: HashMap<fontdb::ID, Option<Arc<Font>>>,
font_cache: HashMap<(fontdb::ID, fontdb::Weight), Option<Arc<Font>>>,
/// Sorted unique ID's of all Monospace fonts in DB
monospace_font_ids: Vec<fontdb::ID>,
@ -249,16 +249,16 @@ impl FontSystem {
(self.locale, self.db)
}
/// Get a font by its ID.
pub fn get_font(&mut self, id: fontdb::ID) -> Option<Arc<Font>> {
/// Get a font by its ID and weight.
pub fn get_font(&mut self, id: fontdb::ID, weight: fontdb::Weight) -> Option<Arc<Font>> {
self.font_cache
.entry(id)
.entry((id, weight))
.or_insert_with(|| {
#[cfg(feature = "std")]
unsafe {
self.db.make_shared_face_data(id);
}
match Font::new(&self.db, id) {
match Font::new(&self.db, id, weight) {
Some(font) => Some(Arc::new(font)),
None => {
log::warn!(
@ -293,9 +293,10 @@ impl FontSystem {
pub fn get_font_supported_codepoints_in_word(
&mut self,
id: fontdb::ID,
weight: fontdb::Weight,
word: &str,
) -> Option<usize> {
self.get_font(id).map(|font| {
self.get_font(id, weight).map(|font| {
let code_points = font.unicode_codepoints();
let cache = self
.font_codepoint_support_info_cache

View file

@ -25,6 +25,8 @@ pub struct CacheKey {
pub x_bin: SubpixelBin,
/// Binning of fractional Y offset
pub y_bin: SubpixelBin,
/// Font weight
pub font_weight: fontdb::Weight,
/// [`CacheKeyFlags`]
pub flags: CacheKeyFlags,
}
@ -35,6 +37,7 @@ impl CacheKey {
glyph_id: u16,
font_size: f32,
pos: (f32, f32),
weight: fontdb::Weight,
flags: CacheKeyFlags,
) -> (Self, i32, i32) {
let (x, x_bin) = SubpixelBin::new(pos.0);
@ -47,6 +50,7 @@ impl CacheKey {
x_bin,
y_bin,
flags,
font_weight: weight,
},
x,
y,

View file

@ -16,6 +16,8 @@ pub struct LayoutGlyph {
pub end: usize,
/// Font size of the glyph
pub font_size: f32,
/// Font weight of the glyph
pub font_weight: fontdb::Weight,
/// Line height of the glyph, will override buffer setting
pub line_height_opt: Option<f32>,
/// Font id of the glyph
@ -79,6 +81,7 @@ impl LayoutGlyph {
(self.x + x_offset) * scale + offset.0,
math::truncf((self.y - y_offset) * scale + offset.1), // Hinting in Y axis
),
self.font_weight,
self.cache_key_flags,
);

View file

@ -191,6 +191,7 @@ fn shape_fallback(
descent,
font_monospace_em_width: font.monospace_em_width(),
font_id: font.id(),
font_weight: attrs.weight,
glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"),
//TODO: color should not be related to shaping
color_opt: attrs.color_opt,
@ -270,6 +271,7 @@ fn shape_run(
&default_families,
&scripts,
&line[start_run..end_run],
attrs.weight,
);
let font = font_iter.next().expect("no default font found");
@ -447,7 +449,14 @@ fn shape_skip(
let fonts = font_system.get_font_matches(&attrs);
let default_families = [&attrs.family];
let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, &[], "");
let mut font_iter = FontFallbackIter::new(
font_system,
&fonts,
&default_families,
&[],
"",
attrs.weight,
);
let font = font_iter.next().expect("no default font found");
let font_id = font.id();
@ -481,6 +490,7 @@ fn shape_skip(
descent,
font_monospace_em_width,
font_id,
font_weight: attrs.weight,
glyph_id,
color_opt: attrs.color_opt,
metadata: attrs.metadata,
@ -504,6 +514,7 @@ pub struct ShapeGlyph {
pub descent: f32,
pub font_monospace_em_width: Option<f32>,
pub font_id: fontdb::ID,
pub font_weight: fontdb::Weight,
pub glyph_id: u16,
pub color_opt: Option<Color>,
pub metadata: usize,
@ -527,6 +538,7 @@ impl ShapeGlyph {
font_size,
line_height_opt,
font_id: self.font_id,
font_weight: self.font_weight,
glyph_id: self.glyph_id,
x,
y,
@ -1445,13 +1457,7 @@ impl ShapeLine {
}
// Create the LayoutLines using the ranges inside visual lines
let align = align.unwrap_or({
if self.rtl {
Align::Right
} else {
Align::Left
}
});
let align = align.unwrap_or(if self.rtl { Align::Right } else { Align::Left });
let line_width = match width_opt {
Some(width) => width,
@ -1553,7 +1559,9 @@ impl ShapeLine {
.max(1.0)
/ glyph_to_match_factor
* font_size;
log::trace!("Adjusted glyph font size ({font_size} => {glyph_font_size})");
log::trace!(
"Adjusted glyph font size ({font_size} => {glyph_font_size})"
);
glyph_font_size
}
_ => font_size,

View file

@ -17,7 +17,7 @@ fn swash_image(
context: &mut ScaleContext,
cache_key: CacheKey,
) -> Option<SwashImage> {
let font = match font_system.get_font(cache_key.font_id) {
let font = match font_system.get_font(cache_key.font_id, cache_key.font_weight) {
Some(some) => some,
None => {
log::warn!("did not find font {:?}", cache_key.font_id);
@ -25,12 +25,24 @@ fn swash_image(
}
};
let variable_width = font
.as_swash()
.variations()
.find_by_tag(swash::Tag::from_be_bytes(*b"wght"));
// Build the scaler
let mut scaler = context
.builder(font.as_swash())
.size(f32::from_bits(cache_key.font_size_bits))
.hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING))
.build();
.hint(!cache_key.flags.contains(CacheKeyFlags::DISABLE_HINTING));
if let Some(variation) = variable_width {
scaler = scaler.variations(std::iter::once(swash::Setting {
tag: swash::Tag::from_be_bytes(*b"wght"),
value: (cache_key.font_weight.0 as f32)
.clamp(variation.min_value(), variation.max_value()),
}));
}
let mut scaler = scaler.build();
// Compute the fractional offset-- you'll likely want to quantize this
// in a real renderer
@ -68,7 +80,7 @@ fn swash_outline_commands(
) -> Option<Box<[swash::zeno::Command]>> {
use swash::zeno::PathData as _;
let font = match font_system.get_font(cache_key.font_id) {
let font = match font_system.get_font(cache_key.font_id, cache_key.font_weight) {
Some(some) => some,
None => {
log::warn!("did not find font {:?}", cache_key.font_id);