From 6053c398cda7237b897f48f4c4f6fe585936eff5 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 19:35:26 +0100 Subject: [PATCH 01/14] Make font matching generic over input type Signed-off-by: Nico Burns --- fontique/src/matching.rs | 68 +++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index bd276599..a180cae7 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -3,44 +3,70 @@ //! Implementation of the CSS font matching algorithm. +use core::ops::Deref; + use super::attributes::{FontStyle, FontWeight, FontWidth}; use super::font::FontInfo; use smallvec::SmallVec; const DEFAULT_OBLIQUE_ANGLE: f32 = 14.0; +#[derive(Copy, Clone)] +pub struct FontMatchingInfo { + width: i32, + style: FontStyle, + weight: f32, + has_slnt: bool, +} + +impl From<&FontInfo> for FontMatchingInfo { + fn from(info: &FontInfo) -> Self { + Self { + width: (info.width().ratio() * 100.0) as i32, + style: info.style(), + weight: info.weight().value(), + has_slnt: info.has_slant_axis(), + } + } +} + pub fn match_font( - set: &[FontInfo], + set: impl IntoIterator>, width: FontWidth, style: FontStyle, weight: FontWeight, synthesize_style: bool, ) -> Option { const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; - match set.len() { - 0 => return None, - 1 => return Some(0), - _ => {} - } + #[derive(Copy, Clone)] - struct Candidate { + struct CandidateFont { index: usize, - width: i32, - style: FontStyle, - weight: f32, - has_slnt: bool, + info: FontMatchingInfo, + } + impl Deref for CandidateFont { + type Target = FontMatchingInfo; + fn deref(&self) -> &Self::Target { + &self.info + } } - let mut set: SmallVec<[Candidate; 16]> = set - .iter() + + let mut set: SmallVec<[CandidateFont; 16]> = set + .into_iter() .enumerate() - .map(|(i, font)| Candidate { - index: i, - width: (font.width().ratio() * 100.0) as i32, - style: font.style(), - weight: font.weight().value(), - has_slnt: font.has_slant_axis(), + .map(|(index, info)| CandidateFont { + index, + info: info.into(), }) .collect(); + + // Early return for case of 0 or 1 fonts where matching is trivial + match set.len() { + 0 => return None, + 1 => return Some(0), + _ => {} + } + let width = (width.ratio() * 100.0) as i32; let weight = weight.value(); // font-width is tried first: @@ -373,8 +399,8 @@ pub fn match_font( } set.retain(|f| f.style == use_style); // font-weight is matched next: - if let Some(f) = set.iter().find(|f| f.weight == weight) { - return Some(f.index); + if let Some(index) = set.iter().position(|f| f.weight == weight) { + return Some(index); } else { // If the desired weight is inclusively between 400 and 500... if (400.0..=500.0).contains(&weight) { From 8598c59beb035ecd6931b5044117c99cf5b9d035 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 19:52:05 +0100 Subject: [PATCH 02/14] Refactor width matching --- fontique/src/matching.rs | 115 +++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index a180cae7..a647ac55 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -3,7 +3,7 @@ //! Implementation of the CSS font matching algorithm. -use core::ops::Deref; +use core::ops::{Deref, DerefMut}; use super::attributes::{FontStyle, FontWeight, FontWidth}; use super::font::FontInfo; @@ -37,6 +37,7 @@ pub fn match_font( weight: FontWeight, synthesize_style: bool, ) -> Option { + use core::cmp::Ordering::Less; const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; #[derive(Copy, Clone)] @@ -51,14 +52,50 @@ pub fn match_font( } } - let mut set: SmallVec<[CandidateFont; 16]> = set - .into_iter() - .enumerate() - .map(|(index, info)| CandidateFont { - index, - info: info.into(), - }) - .collect(); + struct CandidateFontSet(SmallVec<[CandidateFont; 16]>); + impl Deref for CandidateFontSet { + type Target = SmallVec<[CandidateFont; 16]>; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl DerefMut for CandidateFontSet { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + impl CandidateFontSet { + fn has_width(&self, width: i32) -> bool { + self.0.iter().any(|f| f.width == width) + } + + fn max_width_below(&self, width: i32) -> Option { + self.0 + .iter() + .filter(|f| f.width < width) + .max_by_key(|f| f.width) + .map(|f| f.width) + } + + fn min_width_above(&self, width: i32) -> Option { + self.0 + .iter() + .filter(|f| f.width > width) + .min_by_key(|f| f.width) + .map(|f| f.width) + } + } + + let mut set = CandidateFontSet( + set.into_iter() + .enumerate() + .map(|(index, info)| CandidateFont { + index, + info: info.into(), + }) + .collect(), + ); // Early return for case of 0 or 1 fonts where matching is trivial match set.len() { @@ -69,56 +106,30 @@ pub fn match_font( let width = (width.ratio() * 100.0) as i32; let weight = weight.value(); + // font-width is tried first: - let mut use_width = set[0].width; - if !set.iter().any(|f| f.width == width) { - // If the desired width value is less than or equal to 100%... + let use_width = if !set.has_width(width) { + // If the desired width value is less than or equal to 100% then... if width <= 100 { - // width values below the desired width value are checked in - // descending order... - if let Some(found) = set - .iter() - .filter(|f| f.width < width) - .max_by_key(|f| f.width) - { - use_width = found.width; - } - // followed by width values above the desired width value in - // ascending order until a match is found. - else if let Some(found) = set - .iter() - .filter(|f| f.width > width) - .min_by_key(|f| f.width) - { - use_width = found.width; - } + // Width values below the desired width value are checked in descending order followed by + // width values above the desired width value in ascending order until a match is found. + set.max_width_below(width) + .or_else(|| set.min_width_above(width)) + .unwrap_or(width) } - // Otherwise, ... + // Otherwise... else { - // width values above the desired width value are checked in - // ascending order... - if let Some(found) = set - .iter() - .filter(|f| f.width > width) - .min_by_key(|f| f.width) - { - use_width = found.width; - } - // followed by width values below the desired width value in - // descending order until a match is found. - else if let Some(found) = set - .iter() - .filter(|f| f.width < width) - .max_by_key(|f| f.width) - { - use_width = found.width; - } + // Width values above the desired width value are checked in ascending order followed by + // width values below the desired width value in descending order until a match is found. + set.min_width_above(width) + .or_else(|| set.max_width_below(width)) + .unwrap_or(width) } } else { - use_width = width; - } + width + }; set.retain(|f| f.width == use_width); - use core::cmp::Ordering::*; + let oblique_fonts = set.iter().filter_map(|f| oblique_style(f.style)); // font-style is tried next: // NOTE: this code uses an oblique threshold of 14deg rather than From 3d2704966314a22bf04e012565b0d83b5f43e633 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 20:13:20 +0100 Subject: [PATCH 03/14] Refactor weight matching Signed-off-by: Nico Burns --- fontique/src/matching.rs | 95 ++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 61 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index a647ac55..444a8d09 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -85,6 +85,22 @@ pub fn match_font( .min_by_key(|f| f.width) .map(|f| f.width) } + + fn max_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { + use core::cmp::Ordering::Less; + self.0 + .iter() + .filter(|f| predicate(f.weight)) + .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) + } + + fn min_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { + use core::cmp::Ordering::Less; + self.0 + .iter() + .filter(|f| predicate(f.weight)) + .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) + } } let mut set = CandidateFontSet( @@ -409,81 +425,38 @@ pub fn match_font( } } set.retain(|f| f.style == use_style); + // font-weight is matched next: if let Some(index) = set.iter().position(|f| f.weight == weight) { - return Some(index); + Some(index) } else { // If the desired weight is inclusively between 400 and 500... if (400.0..=500.0).contains(&weight) { - // weights greater than or equal to the target weight are checked in ascending - // order until 500 is hit and checked - if let Some(found) = set - .iter() - .enumerate() - .filter(|f| f.1.weight >= weight && f.1.weight <= 500.0) - .min_by(|x, y| x.1.weight.partial_cmp(&y.1.weight).unwrap_or(Less)) - { - return Some(found.1.index); - } - // followed by weights less than the target weight in descending order - if let Some(found) = set - .iter() - .enumerate() - .filter(|f| f.1.weight < weight) - .max_by(|x, y| x.1.weight.partial_cmp(&y.1.weight).unwrap_or(Less)) - { - return Some(found.1.index); - } - // followed by weights greater than 500, until a match is found. - if let Some(found) = set - .iter() - .enumerate() - .filter(|f| f.1.weight > 500.0) - .min_by(|x, y| x.1.weight.partial_cmp(&y.1.weight).unwrap_or(Less)) - { - return Some(found.1.index); - } + set + // weights greater than or equal to the target weight are checked in ascending + // order until 500 is hit and checked + .min_weight_matching(|w| w >= weight && w <= 500.0) + // followed by weights less than the target weight in descending order + .or_else(|| set.max_weight_matching(|w| w < weight)) + // followed by weights greater than 500, until a match is found. + .or_else(|| set.min_weight_matching(|w| w > 500.0)) } // If the desired weight is less than 400... else if weight < 400.0 { - // weights less than or equal to the target weight are checked in descending - if let Some(found) = set - .iter() - .filter(|f| f.weight <= weight) - .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) - { - return Some(found.index); - } - // followed by weights greater than the target weight in ascending order - if let Some(found) = set - .iter() - .filter(|f| f.weight > weight) - .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) - { - return Some(found.index); - } + // weights less than or equal to the target weight are checked in descending order + set.max_weight_matching(|w| w < weight) + // followed by weights greater than the target weight in ascending order + .or_else(|| set.min_weight_matching(|w| w > weight)) } // If the desired weight is greater than 500... else { // weights greater than or equal to the target weight are checked in ascending - if let Some(found) = set - .iter() - .filter(|f| f.weight >= weight) - .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) - { - return Some(found.index); - } - // followed by weights less than the target weight in descending order - if let Some(found) = set - .iter() - .filter(|f| f.weight < weight) - .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) - { - return Some(found.index); - } + set.min_weight_matching(|w| w >= weight) + // followed by weights less than the target weight in descending order + .or_else(|| set.max_weight_matching(|w| w < weight)) } + .map(|found| found.index) } - None } fn oblique_angle(style: FontStyle) -> Option { From 596b1a2dc04f022799955ef1c99ae7ad483d06f8 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 20:33:49 +0100 Subject: [PATCH 04/14] Add CandidateFontSet::new method --- fontique/src/matching.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 444a8d09..621e276d 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -31,7 +31,7 @@ impl From<&FontInfo> for FontMatchingInfo { } pub fn match_font( - set: impl IntoIterator>, + fonts: impl IntoIterator>, width: FontWidth, style: FontStyle, weight: FontWeight, @@ -66,6 +66,18 @@ pub fn match_font( } impl CandidateFontSet { + fn new(fonts: impl IntoIterator>) -> Self { + let inner = fonts + .into_iter() + .enumerate() + .map(|(index, info)| CandidateFont { + index, + info: info.into(), + }) + .collect(); + Self(inner) + } + fn has_width(&self, width: i32) -> bool { self.0.iter().any(|f| f.width == width) } @@ -103,15 +115,7 @@ pub fn match_font( } } - let mut set = CandidateFontSet( - set.into_iter() - .enumerate() - .map(|(index, info)| CandidateFont { - index, - info: info.into(), - }) - .collect(), - ); + let mut set = CandidateFontSet::new(fonts); // Early return for case of 0 or 1 fonts where matching is trivial match set.len() { From f4f4ace109906f71c9c9fcb1de6da58eaaf6aeef Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 20:37:00 +0100 Subject: [PATCH 05/14] Make oblique_angle a method Signed-off-by: Nico Burns --- fontique/src/attributes.rs | 11 ++++++++++- fontique/src/matching.rs | 13 ++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/fontique/src/attributes.rs b/fontique/src/attributes.rs index 8d7dc7bc..40988087 100644 --- a/fontique/src/attributes.rs +++ b/fontique/src/attributes.rs @@ -9,6 +9,8 @@ use core_maths::CoreFloat; use core::fmt; +pub(crate) const DEFAULT_OBLIQUE_ANGLE: f32 = 14.0; + /// Primary attributes for font matching: [`FontWidth`], [`FontStyle`] and [`FontWeight`]. /// /// These are used to [configure] a [`Query`]. @@ -492,6 +494,13 @@ impl FontStyle { _ => Self::Normal, } } + + pub(crate) fn oblique_angle(&self) -> Option { + match self { + Self::Oblique(angle) => Some(angle.unwrap_or(DEFAULT_OBLIQUE_ANGLE)), + _ => None, + } + } } impl fmt::Display for FontStyle { @@ -500,7 +509,7 @@ impl fmt::Display for FontStyle { Self::Normal => "normal", Self::Italic => "italic", Self::Oblique(None) => "oblique", - Self::Oblique(Some(degrees)) if *degrees == 14.0 => "oblique", + Self::Oblique(Some(degrees)) if *degrees == DEFAULT_OBLIQUE_ANGLE => "oblique", Self::Oblique(Some(degrees)) => { return write!(f, "oblique({degrees}deg)"); } diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 621e276d..0f1a6d59 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -5,12 +5,10 @@ use core::ops::{Deref, DerefMut}; -use super::attributes::{FontStyle, FontWeight, FontWidth}; +use super::attributes::{DEFAULT_OBLIQUE_ANGLE, FontStyle, FontWeight, FontWidth}; use super::font::FontInfo; use smallvec::SmallVec; -const DEFAULT_OBLIQUE_ANGLE: f32 = 14.0; - #[derive(Copy, Clone)] pub struct FontMatchingInfo { width: i32, @@ -190,7 +188,7 @@ pub fn match_font( } } // If the value of font-style is oblique... - else if let Some(angle) = oblique_angle(style) { + else if let Some(angle) = style.oblique_angle() { // and the requested angle is greater than or equal to 14deg if angle >= OBLIQUE_THRESHOLD { // oblique values greater than or equal to angle are checked in @@ -463,13 +461,6 @@ pub fn match_font( } } -fn oblique_angle(style: FontStyle) -> Option { - match style { - FontStyle::Oblique(angle) => Some(angle.unwrap_or(DEFAULT_OBLIQUE_ANGLE)), - _ => None, - } -} - fn oblique_style(style: FontStyle) -> Option<(FontStyle, f32)> { match style { FontStyle::Oblique(angle) => Some((style, angle.unwrap_or(DEFAULT_OBLIQUE_ANGLE))), From 71076647ebe991b71bd1da3e6785fd8ad368aafa Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 21:14:54 +0100 Subject: [PATCH 06/14] Refactor style matching Signed-off-by: Nico Burns --- fontique/src/matching.rs | 368 ++++++++++++++------------------------- 1 file changed, 131 insertions(+), 237 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 0f1a6d59..b0f67793 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -35,7 +35,6 @@ pub fn match_font( weight: FontWeight, synthesize_style: bool, ) -> Option { - use core::cmp::Ordering::Less; const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; #[derive(Copy, Clone)] @@ -80,6 +79,14 @@ pub fn match_font( self.0.iter().any(|f| f.width == width) } + fn has_style(&self, style: FontStyle) -> bool { + self.0.iter().any(|f| f.style == style) + } + + fn has_variable_font_with_slnt_axis(&self) -> bool { + self.iter().any(|f| f.has_slnt) + } + fn max_width_below(&self, width: i32) -> Option { self.0 .iter() @@ -111,6 +118,53 @@ pub fn match_font( .filter(|f| predicate(f.weight)) .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) } + + fn min_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { + use core::cmp::Ordering::Less; + self.0 + .iter() + .filter_map(|f| match f.style.oblique_angle() { + Some(a) if predicate(a) => Some((f.style, a)), + _ => None, + }) + .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) + .map(|x| x.0) + } + + fn max_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { + use core::cmp::Ordering::Less; + self.0 + .iter() + .filter_map(|f| match f.style.oblique_angle() { + Some(a) if predicate(a) => Some((f.style, a)), + _ => None, + }) + .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) + .map(|x| x.0) + } + + fn fallback_style( + &self, + synthesize_style: bool, + predicate: impl Fn(f32) -> bool, + ) -> FontStyle { + if synthesize_style { + if self.has_style(FontStyle::Normal) { + FontStyle::Normal + } else { + self[0].style + } + } else { + // Choose an italic style + if self.has_style(FontStyle::Italic) { + FontStyle::Italic + } else { + // oblique values less than or equal to 0deg are checked in descending order + self.max_oblique_angle_matching(predicate) + .unwrap_or(self[0].style) + } + } + } } let mut set = CandidateFontSet::new(fonts); @@ -148,44 +202,25 @@ pub fn match_font( }; set.retain(|f| f.width == use_width); - let oblique_fonts = set.iter().filter_map(|f| oblique_style(f.style)); // font-style is tried next: // NOTE: this code uses an oblique threshold of 14deg rather than // the current value of 20deg in the spec. // See: https://github.com/w3c/csswg-drafts/issues/2295 - let mut use_style = style; let mut _use_slnt = false; - if !set.iter().any(|f| f.style == use_style) { + let use_style = if set.has_style(style) { + style + } else { // If the value of font-style is italic: if style == FontStyle::Italic { // oblique values greater than or equal to 14deg are checked in // ascending order - if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a >= OBLIQUE_THRESHOLD) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // followed by positive oblique values below 14deg in descending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a > 0. && *a < OBLIQUE_THRESHOLD) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // If no match is found, oblique values less than or equal to 0deg - // are checked in descending order until a match is found. - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a < 0.) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - use_style = set[0].style; - } + set.min_oblique_angle_matching(|a| a >= OBLIQUE_THRESHOLD) + // followed by positive oblique values below 14deg in descending order + .or_else(|| set.max_oblique_angle_matching(|a| a > 0.0 && a < OBLIQUE_THRESHOLD)) + // If no match is found, oblique values less than or equal to 0deg + // are checked in descending order until a match is found. + .or_else(|| set.max_oblique_angle_matching(|a| a < 0.0)) + .unwrap_or(set[0].style) } // If the value of font-style is oblique... else if let Some(angle) = style.oblique_angle() { @@ -193,239 +228,105 @@ pub fn match_font( if angle >= OBLIQUE_THRESHOLD { // oblique values greater than or equal to angle are checked in // ascending order - if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a >= angle) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // followed by positive oblique values below angle in descending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a > 0. && *a < angle) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style { - if set.iter().any(|f| f.has_slnt) { + set.min_oblique_angle_matching(|a| a >= angle) + // followed by positive oblique values below angle in descending order + .or_else(|| set.max_oblique_angle_matching(|a| a > 0.0 && a < angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by setting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && set.has_variable_font_with_slnt_axis() { _use_slnt = true; + style } else { - use_style = if set.iter().any(|f| f.style == FontStyle::Normal) { - FontStyle::Normal - } else { - set[0].style - }; - } - } else { - // Choose an italic style - if set.iter().any(|f| f.style == FontStyle::Italic) { - use_style = FontStyle::Italic; + set.fallback_style(synthesize_style, |a| a <= 0.0) } - // oblique values less than or equal to 0deg are checked in descending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a <= 0.) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - use_style = set[0].style; - } - } - } + }) } // if the requested angle is greater than or equal to 0deg // and less than 14deg else if angle >= 0. { // positive oblique values below angle in descending order - if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a > 0. && *a < angle) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // followed by oblique values greater than or equal to angle in - // ascending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a >= angle) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style { - if set.iter().any(|f| f.has_slnt) { + set.max_oblique_angle_matching(|a| a > 0.0 && a < angle) + // followed by oblique values greater than or equal to angle in + // ascending order + .or_else(|| set.min_oblique_angle_matching(|a| a >= angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by setting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && set.has_variable_font_with_slnt_axis() { _use_slnt = true; + style } else { - use_style = if set.iter().any(|f| f.style == FontStyle::Normal) { - FontStyle::Normal - } else { - set[0].style - }; - } - } else { - // Choose an italic style - if set.iter().any(|f| f.style == FontStyle::Italic) { - use_style = FontStyle::Italic; - } - // oblique values less than or equal to 0deg are checked in descending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a <= 0.) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - use_style = set[0].style; + set.fallback_style(synthesize_style, |a| a <= 0.0) } - } - } + }) } // -14deg < angle < 0deg else if angle > -OBLIQUE_THRESHOLD { // negative oblique values above angle in ascending order - if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a < 0. && *a > angle) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // followed by oblique values less than or equal to angle in - // descending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a <= angle) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style { - if set.iter().any(|f| f.has_slnt) { + set.min_oblique_angle_matching(|a| a < 0. && a > angle) + // followed by oblique values less than or equal to angle in + // descending order + .or_else(|| set.max_oblique_angle_matching(|a| a <= angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by setting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && set.has_variable_font_with_slnt_axis() { _use_slnt = true; + style } else { - use_style = if set.iter().any(|f| f.style == FontStyle::Normal) { - FontStyle::Normal - } else { - set[0].style - }; - } - } else { - // Choose an italic style - if set.iter().any(|f| f.style == FontStyle::Italic) { - use_style = FontStyle::Italic; + set.fallback_style(synthesize_style, |a| a >= 0.0) } - // oblique values greater than or equal to 0deg are checked in ascending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a >= 0.) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - use_style = set[0].style; - } - } - } + }) } // angle < -14 deg else { // oblique values less than or equal to angle are checked in // descending order - if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a <= angle) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // followed by negative oblique values above angle in ascending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a < 0. && *a > angle) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style { - if set.iter().any(|f| f.has_slnt) { + set.max_oblique_angle_matching(|a| a >= angle) + // followed by negative oblique values above angle in ascending order + .or_else(|| set.min_oblique_angle_matching(|a| a < 0.0 && a > angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by setting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && set.has_variable_font_with_slnt_axis() { _use_slnt = true; + style } else { - use_style = if set.iter().any(|f| f.style == FontStyle::Normal) { - FontStyle::Normal - } else { - set[0].style - }; - } - } else { - // Choose an italic style - if set.iter().any(|f| f.style == FontStyle::Italic) { - use_style = FontStyle::Italic; + set.fallback_style(synthesize_style, |a| a >= 0.0) } - // oblique values greater than or equal to 0deg are checked in ascending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a >= 0.) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - use_style = set[0].style; - } - } - } + }) } } // If the value of font-style is normal... else { // oblique values greater than or equal to 0deg are checked in // ascending order - if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a >= 0.) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } - // followed by italic fonts - else if let Some(found) = set.iter().find(|f| f.style == FontStyle::Italic) { - use_style = found.style; - } - // followed by oblique values less than 0deg in descending order - else if let Some(found) = oblique_fonts - .clone() - .filter(|(_, a)| *a < 0.) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - { - use_style = found.0; - } else { - use_style = set[0].style; - } + set.min_oblique_angle_matching(|a| a >= 0.0) + // followed by italic fonts + .or_else(|| { + set.iter() + .find(|f| f.style == FontStyle::Italic) + .map(|f| f.style) + }) + // followed by oblique values less than 0deg in descending order + .or_else(|| set.max_oblique_angle_matching(|a| a < 0.0)) + .unwrap_or(set[0].style) } - } + }; + set.retain(|f| f.style == use_style); // font-weight is matched next: @@ -460,10 +361,3 @@ pub fn match_font( .map(|found| found.index) } } - -fn oblique_style(style: FontStyle) -> Option<(FontStyle, f32)> { - match style { - FontStyle::Oblique(angle) => Some((style, angle.unwrap_or(DEFAULT_OBLIQUE_ANGLE))), - _ => None, - } -} From eb25e32efd1bd363581787dfd86e43852a478fe5 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 21:20:32 +0100 Subject: [PATCH 07/14] Update current spec value --- fontique/src/matching.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index b0f67793..ba609c04 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -204,7 +204,7 @@ pub fn match_font( // font-style is tried next: // NOTE: this code uses an oblique threshold of 14deg rather than - // the current value of 20deg in the spec. + // the current value of 11deg in the spec. // See: https://github.com/w3c/csswg-drafts/issues/2295 let mut _use_slnt = false; let use_style = if set.has_style(style) { From bb65edd2f6e5a9bce624c3a7647109f401c6771c Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 21:27:25 +0100 Subject: [PATCH 08/14] More code reuse --- fontique/src/matching.rs | 52 +++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index ba609c04..0e4a91e9 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -7,6 +7,7 @@ use core::ops::{Deref, DerefMut}; use super::attributes::{DEFAULT_OBLIQUE_ANGLE, FontStyle, FontWeight, FontWidth}; use super::font::FontInfo; +use core::cmp::Ordering; use smallvec::SmallVec; #[derive(Copy, Clone)] @@ -103,44 +104,45 @@ pub fn match_font( .map(|f| f.width) } + fn fonts_matching_weight( + &self, + predicate: impl Fn(f32) -> bool, + ) -> impl Iterator { + self.0.iter().filter(move |f| predicate(f.weight)) + } + fn max_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { - use core::cmp::Ordering::Less; - self.0 - .iter() - .filter(|f| predicate(f.weight)) - .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) + self.fonts_matching_weight(predicate) + .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) } fn min_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { - use core::cmp::Ordering::Less; - self.0 - .iter() - .filter(|f| predicate(f.weight)) - .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Less)) + self.fonts_matching_weight(predicate) + .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) } - fn min_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { - use core::cmp::Ordering::Less; + fn fonts_matching_oblique_angle( + &self, + predicate: impl Fn(f32) -> bool, + ) -> impl Iterator { self.0 .iter() - .filter_map(|f| match f.style.oblique_angle() { - Some(a) if predicate(a) => Some((f.style, a)), + .filter_map(move |f| match f.style.oblique_angle() { + Some(a) if predicate(a) => Some((f, a)), _ => None, }) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - .map(|x| x.0) + } + + fn min_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { + self.fonts_matching_oblique_angle(predicate) + .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Ordering::Less)) + .map(|f| f.0.style) } fn max_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { - use core::cmp::Ordering::Less; - self.0 - .iter() - .filter_map(|f| match f.style.oblique_angle() { - Some(a) if predicate(a) => Some((f.style, a)), - _ => None, - }) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Less)) - .map(|x| x.0) + self.fonts_matching_oblique_angle(predicate) + .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Ordering::Less)) + .map(|f| f.0.style) } fn fallback_style( From 2a4599f46ccaef6488034183c0c5362acb5fdeff Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 21:29:32 +0100 Subject: [PATCH 09/14] Move OBLIQUE_THRESHOLD back down Signed-off-by: Nico Burns --- fontique/src/matching.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 0e4a91e9..683b8621 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -36,8 +36,6 @@ pub fn match_font( weight: FontWeight, synthesize_style: bool, ) -> Option { - const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; - #[derive(Copy, Clone)] struct CandidateFont { index: usize, @@ -204,10 +202,12 @@ pub fn match_font( }; set.retain(|f| f.width == use_width); - // font-style is tried next: // NOTE: this code uses an oblique threshold of 14deg rather than // the current value of 11deg in the spec. // See: https://github.com/w3c/csswg-drafts/issues/2295 + const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; + + // font-style is tried next: let mut _use_slnt = false; let use_style = if set.has_style(style) { style From 9625125b1dca9116153514d9635ca0a8bf8796b5 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 22:07:17 +0100 Subject: [PATCH 10/14] Remove Deref impls for CandidateFontSet Signed-off-by: Nico Burns --- fontique/src/matching.rs | 593 ++++++++++++++++++++------------------- 1 file changed, 297 insertions(+), 296 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 683b8621..4b38b96f 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -3,7 +3,7 @@ //! Implementation of the CSS font matching algorithm. -use core::ops::{Deref, DerefMut}; +use core::ops::Deref; use super::attributes::{DEFAULT_OBLIQUE_ANGLE, FontStyle, FontWeight, FontWidth}; use super::font::FontInfo; @@ -18,6 +18,19 @@ pub struct FontMatchingInfo { has_slnt: bool, } +pub fn match_font( + fonts: impl IntoIterator>, + width: FontWidth, + style: FontStyle, + weight: FontWeight, + synthesize_style: bool, +) -> Option { + let set = CandidateFontSet::new(fonts); + set.match_font_impl(width, style, weight, synthesize_style) +} + +// Private implementation details + impl From<&FontInfo> for FontMatchingInfo { fn from(info: &FontInfo) -> Self { Self { @@ -29,337 +42,325 @@ impl From<&FontInfo> for FontMatchingInfo { } } -pub fn match_font( - fonts: impl IntoIterator>, - width: FontWidth, - style: FontStyle, - weight: FontWeight, - synthesize_style: bool, -) -> Option { - #[derive(Copy, Clone)] - struct CandidateFont { - index: usize, - info: FontMatchingInfo, - } - impl Deref for CandidateFont { - type Target = FontMatchingInfo; - fn deref(&self) -> &Self::Target { - &self.info - } +#[derive(Copy, Clone)] +struct CandidateFont { + index: usize, + info: FontMatchingInfo, +} +impl Deref for CandidateFont { + type Target = FontMatchingInfo; + fn deref(&self) -> &Self::Target { + &self.info } +} - struct CandidateFontSet(SmallVec<[CandidateFont; 16]>); - impl Deref for CandidateFontSet { - type Target = SmallVec<[CandidateFont; 16]>; - fn deref(&self) -> &Self::Target { - &self.0 - } - } - impl DerefMut for CandidateFontSet { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } +struct CandidateFontSet(SmallVec<[CandidateFont; 16]>); - impl CandidateFontSet { - fn new(fonts: impl IntoIterator>) -> Self { - let inner = fonts - .into_iter() - .enumerate() - .map(|(index, info)| CandidateFont { - index, - info: info.into(), - }) - .collect(); - Self(inner) - } +impl CandidateFontSet { + fn new(fonts: impl IntoIterator>) -> Self { + let inner = fonts + .into_iter() + .enumerate() + .map(|(index, info)| CandidateFont { + index, + info: info.into(), + }) + .collect(); + Self(inner) + } - fn has_width(&self, width: i32) -> bool { - self.0.iter().any(|f| f.width == width) - } + fn has_width(&self, width: i32) -> bool { + self.0.iter().any(|f| f.width == width) + } - fn has_style(&self, style: FontStyle) -> bool { - self.0.iter().any(|f| f.style == style) - } + fn has_style(&self, style: FontStyle) -> bool { + self.0.iter().any(|f| f.style == style) + } - fn has_variable_font_with_slnt_axis(&self) -> bool { - self.iter().any(|f| f.has_slnt) - } + fn has_variable_font_with_slnt_axis(&self) -> bool { + self.0.iter().any(|f| f.has_slnt) + } - fn max_width_below(&self, width: i32) -> Option { - self.0 - .iter() - .filter(|f| f.width < width) - .max_by_key(|f| f.width) - .map(|f| f.width) - } + fn max_width_below(&self, width: i32) -> Option { + self.0 + .iter() + .filter(|f| f.width < width) + .max_by_key(|f| f.width) + .map(|f| f.width) + } - fn min_width_above(&self, width: i32) -> Option { - self.0 - .iter() - .filter(|f| f.width > width) - .min_by_key(|f| f.width) - .map(|f| f.width) - } + fn min_width_above(&self, width: i32) -> Option { + self.0 + .iter() + .filter(|f| f.width > width) + .min_by_key(|f| f.width) + .map(|f| f.width) + } - fn fonts_matching_weight( - &self, - predicate: impl Fn(f32) -> bool, - ) -> impl Iterator { - self.0.iter().filter(move |f| predicate(f.weight)) - } + fn fonts_matching_weight( + &self, + predicate: impl Fn(f32) -> bool, + ) -> impl Iterator { + self.0.iter().filter(move |f| predicate(f.weight)) + } - fn max_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { - self.fonts_matching_weight(predicate) - .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) - } + fn max_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { + self.fonts_matching_weight(predicate) + .max_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) + } - fn min_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { - self.fonts_matching_weight(predicate) - .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) - } + fn min_weight_matching(&self, predicate: impl Fn(f32) -> bool) -> Option<&CandidateFont> { + self.fonts_matching_weight(predicate) + .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) + } - fn fonts_matching_oblique_angle( - &self, - predicate: impl Fn(f32) -> bool, - ) -> impl Iterator { - self.0 - .iter() - .filter_map(move |f| match f.style.oblique_angle() { - Some(a) if predicate(a) => Some((f, a)), - _ => None, - }) - } + fn fonts_matching_oblique_angle( + &self, + predicate: impl Fn(f32) -> bool, + ) -> impl Iterator { + self.0 + .iter() + .filter_map(move |f| match f.style.oblique_angle() { + Some(a) if predicate(a) => Some((f, a)), + _ => None, + }) + } - fn min_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { - self.fonts_matching_oblique_angle(predicate) - .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Ordering::Less)) - .map(|f| f.0.style) - } + fn min_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { + self.fonts_matching_oblique_angle(predicate) + .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Ordering::Less)) + .map(|f| f.0.style) + } - fn max_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { - self.fonts_matching_oblique_angle(predicate) - .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Ordering::Less)) - .map(|f| f.0.style) - } + fn max_oblique_angle_matching(&self, predicate: impl Fn(f32) -> bool) -> Option { + self.fonts_matching_oblique_angle(predicate) + .max_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(Ordering::Less)) + .map(|f| f.0.style) + } - fn fallback_style( - &self, - synthesize_style: bool, - predicate: impl Fn(f32) -> bool, - ) -> FontStyle { - if synthesize_style { - if self.has_style(FontStyle::Normal) { - FontStyle::Normal - } else { - self[0].style - } + fn fallback_style(&self, synthesize_style: bool, predicate: impl Fn(f32) -> bool) -> FontStyle { + if synthesize_style { + if self.has_style(FontStyle::Normal) { + FontStyle::Normal } else { - // Choose an italic style - if self.has_style(FontStyle::Italic) { - FontStyle::Italic - } else { - // oblique values less than or equal to 0deg are checked in descending order - self.max_oblique_angle_matching(predicate) - .unwrap_or(self[0].style) - } + self.0[0].style + } + } else { + // Choose an italic style + if self.has_style(FontStyle::Italic) { + FontStyle::Italic + } else { + // oblique values less than or equal to 0deg are checked in descending order + self.max_oblique_angle_matching(predicate) + .unwrap_or(self.0[0].style) } } } - let mut set = CandidateFontSet::new(fonts); - - // Early return for case of 0 or 1 fonts where matching is trivial - match set.len() { - 0 => return None, - 1 => return Some(0), - _ => {} - } + // Method consumes self because it mutates it's containing collection + // which means that calling this twice could not work + fn match_font_impl( + mut self, + width: FontWidth, + style: FontStyle, + weight: FontWeight, + synthesize_style: bool, + ) -> Option { + // Early return for case of 0 or 1 fonts where matching is trivial + match self.0.len() { + 0 => return None, + 1 => return Some(0), + _ => {} + } - let width = (width.ratio() * 100.0) as i32; - let weight = weight.value(); + let width = (width.ratio() * 100.0) as i32; + let weight = weight.value(); - // font-width is tried first: - let use_width = if !set.has_width(width) { - // If the desired width value is less than or equal to 100% then... - if width <= 100 { - // Width values below the desired width value are checked in descending order followed by - // width values above the desired width value in ascending order until a match is found. - set.max_width_below(width) - .or_else(|| set.min_width_above(width)) - .unwrap_or(width) - } - // Otherwise... - else { - // Width values above the desired width value are checked in ascending order followed by - // width values below the desired width value in descending order until a match is found. - set.min_width_above(width) - .or_else(|| set.max_width_below(width)) - .unwrap_or(width) - } - } else { - width - }; - set.retain(|f| f.width == use_width); + // font-width is tried first: + let use_width = if !self.has_width(width) { + // If the desired width value is less than or equal to 100% then... + if width <= 100 { + // Width values below the desired width value are checked in descending order followed by + // width values above the desired width value in ascending order until a match is found. + self.max_width_below(width) + .or_else(|| self.min_width_above(width)) + .unwrap_or(width) + } + // Otherwise... + else { + // Width values above the desired width value are checked in ascending order followed by + // width values below the desired width value in descending order until a match is found. + self.min_width_above(width) + .or_else(|| self.max_width_below(width)) + .unwrap_or(width) + } + } else { + width + }; + self.0.retain(|f| f.width == use_width); - // NOTE: this code uses an oblique threshold of 14deg rather than - // the current value of 11deg in the spec. - // See: https://github.com/w3c/csswg-drafts/issues/2295 - const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; + // NOTE: this code uses an oblique threshold of 14deg rather than + // the current value of 11deg in the spec. + // See: https://github.com/w3c/csswg-drafts/issues/2295 + const OBLIQUE_THRESHOLD: f32 = DEFAULT_OBLIQUE_ANGLE; - // font-style is tried next: - let mut _use_slnt = false; - let use_style = if set.has_style(style) { - style - } else { - // If the value of font-style is italic: - if style == FontStyle::Italic { - // oblique values greater than or equal to 14deg are checked in - // ascending order - set.min_oblique_angle_matching(|a| a >= OBLIQUE_THRESHOLD) - // followed by positive oblique values below 14deg in descending order - .or_else(|| set.max_oblique_angle_matching(|a| a > 0.0 && a < OBLIQUE_THRESHOLD)) - // If no match is found, oblique values less than or equal to 0deg - // are checked in descending order until a match is found. - .or_else(|| set.max_oblique_angle_matching(|a| a < 0.0)) - .unwrap_or(set[0].style) - } - // If the value of font-style is oblique... - else if let Some(angle) = style.oblique_angle() { - // and the requested angle is greater than or equal to 14deg - if angle >= OBLIQUE_THRESHOLD { - // oblique values greater than or equal to angle are checked in + // font-style is tried next: + let mut _use_slnt = false; + let use_style = if self.has_style(style) { + style + } else { + // If the value of font-style is italic: + if style == FontStyle::Italic { + // oblique values greater than or equal to 14deg are checked in // ascending order - set.min_oblique_angle_matching(|a| a >= angle) - // followed by positive oblique values below angle in descending order - .or_else(|| set.max_oblique_angle_matching(|a| a > 0.0 && a < angle)) - .unwrap_or_else(|| { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style && set.has_variable_font_with_slnt_axis() { - _use_slnt = true; - style - } else { - set.fallback_style(synthesize_style, |a| a <= 0.0) - } + self.min_oblique_angle_matching(|a| a >= OBLIQUE_THRESHOLD) + // followed by positive oblique values below 14deg in descending order + .or_else(|| { + self.max_oblique_angle_matching(|a| a > 0.0 && a < OBLIQUE_THRESHOLD) }) + // If no match is found, oblique values less than or equal to 0deg + // are checked in descending order until a match is found. + .or_else(|| self.max_oblique_angle_matching(|a| a < 0.0)) + .unwrap_or(self.0[0].style) } - // if the requested angle is greater than or equal to 0deg - // and less than 14deg - else if angle >= 0. { - // positive oblique values below angle in descending order - set.max_oblique_angle_matching(|a| a > 0.0 && a < angle) - // followed by oblique values greater than or equal to angle in + // If the value of font-style is oblique... + else if let Some(angle) = style.oblique_angle() { + // and the requested angle is greater than or equal to 14deg + if angle >= OBLIQUE_THRESHOLD { + // oblique values greater than or equal to angle are checked in // ascending order - .or_else(|| set.min_oblique_angle_matching(|a| a >= angle)) - .unwrap_or_else(|| { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style && set.has_variable_font_with_slnt_axis() { - _use_slnt = true; - style - } else { - set.fallback_style(synthesize_style, |a| a <= 0.0) - } - }) - } - // -14deg < angle < 0deg - else if angle > -OBLIQUE_THRESHOLD { - // negative oblique values above angle in ascending order - set.min_oblique_angle_matching(|a| a < 0. && a > angle) - // followed by oblique values less than or equal to angle in + self.min_oblique_angle_matching(|a| a >= angle) + // followed by positive oblique values below angle in descending order + .or_else(|| self.max_oblique_angle_matching(|a| a > 0.0 && a < angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by selfting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && self.has_variable_font_with_slnt_axis() { + _use_slnt = true; + style + } else { + self.fallback_style(synthesize_style, |a| a <= 0.0) + } + }) + } + // if the requested angle is greater than or equal to 0deg + // and less than 14deg + else if angle >= 0. { + // positive oblique values below angle in descending order + self.max_oblique_angle_matching(|a| a > 0.0 && a < angle) + // followed by oblique values greater than or equal to angle in + // ascending order + .or_else(|| self.min_oblique_angle_matching(|a| a >= angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by selfting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && self.has_variable_font_with_slnt_axis() { + _use_slnt = true; + style + } else { + self.fallback_style(synthesize_style, |a| a <= 0.0) + } + }) + } + // -14deg < angle < 0deg + else if angle > -OBLIQUE_THRESHOLD { + // negative oblique values above angle in ascending order + self.min_oblique_angle_matching(|a| a < 0. && a > angle) + // followed by oblique values less than or equal to angle in + // descending order + .or_else(|| self.max_oblique_angle_matching(|a| a <= angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by selfting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && self.has_variable_font_with_slnt_axis() { + _use_slnt = true; + style + } else { + self.fallback_style(synthesize_style, |a| a >= 0.0) + } + }) + } + // angle < -14 deg + else { + // oblique values less than or equal to angle are checked in // descending order - .or_else(|| set.max_oblique_angle_matching(|a| a <= angle)) - .unwrap_or_else(|| { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style && set.has_variable_font_with_slnt_axis() { - _use_slnt = true; - style - } else { - set.fallback_style(synthesize_style, |a| a >= 0.0) - } - }) + self.max_oblique_angle_matching(|a| a >= angle) + // followed by negative oblique values above angle in ascending order + .or_else(|| self.min_oblique_angle_matching(|a| a < 0.0 && a > angle)) + .unwrap_or_else(|| { + // If font-synthesis-style has the value auto, then for variable + // fonts with a slnt axis a match is created by selfting the slnt + // value with the specified oblique value; otherwise, a fallback + // match is produced by geometric shearing to the specified + // oblique value. + if synthesize_style && self.has_variable_font_with_slnt_axis() { + _use_slnt = true; + style + } else { + self.fallback_style(synthesize_style, |a| a >= 0.0) + } + }) + } } - // angle < -14 deg + // If the value of font-style is normal... else { - // oblique values less than or equal to angle are checked in - // descending order - set.max_oblique_angle_matching(|a| a >= angle) - // followed by negative oblique values above angle in ascending order - .or_else(|| set.min_oblique_angle_matching(|a| a < 0.0 && a > angle)) - .unwrap_or_else(|| { - // If font-synthesis-style has the value auto, then for variable - // fonts with a slnt axis a match is created by setting the slnt - // value with the specified oblique value; otherwise, a fallback - // match is produced by geometric shearing to the specified - // oblique value. - if synthesize_style && set.has_variable_font_with_slnt_axis() { - _use_slnt = true; - style - } else { - set.fallback_style(synthesize_style, |a| a >= 0.0) - } + // oblique values greater than or equal to 0deg are checked in + // ascending order + self.min_oblique_angle_matching(|a| a >= 0.0) + // followed by italic fonts + .or_else(|| { + self.0 + .iter() + .find(|f| f.style == FontStyle::Italic) + .map(|f| f.style) }) + // followed by oblique values less than 0deg in descending order + .or_else(|| self.max_oblique_angle_matching(|a| a < 0.0)) + .unwrap_or(self.0[0].style) } - } - // If the value of font-style is normal... - else { - // oblique values greater than or equal to 0deg are checked in - // ascending order - set.min_oblique_angle_matching(|a| a >= 0.0) - // followed by italic fonts - .or_else(|| { - set.iter() - .find(|f| f.style == FontStyle::Italic) - .map(|f| f.style) - }) - // followed by oblique values less than 0deg in descending order - .or_else(|| set.max_oblique_angle_matching(|a| a < 0.0)) - .unwrap_or(set[0].style) - } - }; + }; - set.retain(|f| f.style == use_style); + self.0.retain(|f| f.style == use_style); - // font-weight is matched next: - if let Some(index) = set.iter().position(|f| f.weight == weight) { - Some(index) - } else { - // If the desired weight is inclusively between 400 and 500... - if (400.0..=500.0).contains(&weight) { - set + // font-weight is matched next: + if let Some(index) = self.0.iter().position(|f| f.weight == weight) { + Some(index) + } else { + // If the desired weight is inclusively between 400 and 500... + if (400.0..=500.0).contains(&weight) { + self + // weights greater than or equal to the target weight are checked in ascending + // order until 500 is hit and checked + .min_weight_matching(|w| w >= weight && w <= 500.0) + // followed by weights less than the target weight in descending order + .or_else(|| self.max_weight_matching(|w| w < weight)) + // followed by weights greater than 500, until a match is found. + .or_else(|| self.min_weight_matching(|w| w > 500.0)) + } + // If the desired weight is less than 400... + else if weight < 400.0 { + // weights less than or equal to the target weight are checked in descending order + self.max_weight_matching(|w| w < weight) + // followed by weights greater than the target weight in ascending order + .or_else(|| self.min_weight_matching(|w| w > weight)) + } + // If the desired weight is greater than 500... + else { // weights greater than or equal to the target weight are checked in ascending - // order until 500 is hit and checked - .min_weight_matching(|w| w >= weight && w <= 500.0) - // followed by weights less than the target weight in descending order - .or_else(|| set.max_weight_matching(|w| w < weight)) - // followed by weights greater than 500, until a match is found. - .or_else(|| set.min_weight_matching(|w| w > 500.0)) - } - // If the desired weight is less than 400... - else if weight < 400.0 { - // weights less than or equal to the target weight are checked in descending order - set.max_weight_matching(|w| w < weight) - // followed by weights greater than the target weight in ascending order - .or_else(|| set.min_weight_matching(|w| w > weight)) - } - // If the desired weight is greater than 500... - else { - // weights greater than or equal to the target weight are checked in ascending - set.min_weight_matching(|w| w >= weight) - // followed by weights less than the target weight in descending order - .or_else(|| set.max_weight_matching(|w| w < weight)) + self.min_weight_matching(|w| w >= weight) + // followed by weights less than the target weight in descending order + .or_else(|| self.max_weight_matching(|w| w < weight)) + } + .map(|found| found.index) } - .map(|found| found.index) } } From 89817017f16ebd4310a2b5ca3de68f8052403092 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 22:09:32 +0100 Subject: [PATCH 11/14] Reorder methods Signed-off-by: Nico Burns --- fontique/src/matching.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 4b38b96f..f4aaa550 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -69,18 +69,12 @@ impl CandidateFontSet { Self(inner) } + // Width methods + fn has_width(&self, width: i32) -> bool { self.0.iter().any(|f| f.width == width) } - fn has_style(&self, style: FontStyle) -> bool { - self.0.iter().any(|f| f.style == style) - } - - fn has_variable_font_with_slnt_axis(&self) -> bool { - self.0.iter().any(|f| f.has_slnt) - } - fn max_width_below(&self, width: i32) -> Option { self.0 .iter() @@ -97,6 +91,8 @@ impl CandidateFontSet { .map(|f| f.width) } + // Weight methods + fn fonts_matching_weight( &self, predicate: impl Fn(f32) -> bool, @@ -114,6 +110,16 @@ impl CandidateFontSet { .min_by(|x, y| x.weight.partial_cmp(&y.weight).unwrap_or(Ordering::Less)) } + // Style methods + + fn has_style(&self, style: FontStyle) -> bool { + self.0.iter().any(|f| f.style == style) + } + + fn has_variable_font_with_slnt_axis(&self) -> bool { + self.0.iter().any(|f| f.has_slnt) + } + fn fonts_matching_oblique_angle( &self, predicate: impl Fn(f32) -> bool, @@ -157,6 +163,8 @@ impl CandidateFontSet { } } + // Matching algorithm + // Method consumes self because it mutates it's containing collection // which means that calling this twice could not work fn match_font_impl( From 47f2445685c2f60e0a0de9c5e9bbb9b50f79792d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 22:13:05 +0100 Subject: [PATCH 12/14] Add public API comment Signed-off-by: Nico Burns --- fontique/src/matching.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index f4aaa550..1748470d 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -10,6 +10,8 @@ use super::font::FontInfo; use core::cmp::Ordering; use smallvec::SmallVec; +// Public API + #[derive(Copy, Clone)] pub struct FontMatchingInfo { width: i32, From 6880f01992cad3181bb83f5dd092e8e2fc713b0f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 23:10:53 +0100 Subject: [PATCH 13/14] Move conversion into font module Signed-off-by: Nico Burns --- fontique/src/font.rs | 12 ++++++++++++ fontique/src/matching.rs | 20 ++++---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/fontique/src/font.rs b/fontique/src/font.rs index 599739af..009e6281 100644 --- a/fontique/src/font.rs +++ b/fontique/src/font.rs @@ -4,6 +4,7 @@ //! Model for a font. use crate::CharmapIndex; +use crate::matching::FontMatchingInfo; use super::attributes::{FontStyle, FontWeight, FontWidth}; use super::source::{SourceInfo, SourceKind}; @@ -27,6 +28,17 @@ pub struct FontInfo { charmap_index: CharmapIndex, } +impl Into for &FontInfo { + fn into(self) -> FontMatchingInfo { + FontMatchingInfo { + width: (self.width().ratio() * 100.0) as i32, + style: self.style(), + weight: self.weight().value(), + has_slnt: self.has_slant_axis(), + } + } +} + impl FontInfo { /// Creates a new font object from the given source and index. pub fn from_source(source: SourceInfo, index: u32) -> Option { diff --git a/fontique/src/matching.rs b/fontique/src/matching.rs index 1748470d..ea443d25 100644 --- a/fontique/src/matching.rs +++ b/fontique/src/matching.rs @@ -6,7 +6,6 @@ use core::ops::Deref; use super::attributes::{DEFAULT_OBLIQUE_ANGLE, FontStyle, FontWeight, FontWidth}; -use super::font::FontInfo; use core::cmp::Ordering; use smallvec::SmallVec; @@ -14,10 +13,10 @@ use smallvec::SmallVec; #[derive(Copy, Clone)] pub struct FontMatchingInfo { - width: i32, - style: FontStyle, - weight: f32, - has_slnt: bool, + pub width: i32, + pub style: FontStyle, + pub weight: f32, + pub has_slnt: bool, } pub fn match_font( @@ -33,17 +32,6 @@ pub fn match_font( // Private implementation details -impl From<&FontInfo> for FontMatchingInfo { - fn from(info: &FontInfo) -> Self { - Self { - width: (info.width().ratio() * 100.0) as i32, - style: info.style(), - weight: info.weight().value(), - has_slnt: info.has_slant_axis(), - } - } -} - #[derive(Copy, Clone)] struct CandidateFont { index: usize, From 1688fd7b9c5e0193dc8c59c3ca1daad12ab448b9 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 15 Sep 2025 23:31:46 +0100 Subject: [PATCH 14/14] Fix clippy Signed-off-by: Nico Burns --- fontique/src/font.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/fontique/src/font.rs b/fontique/src/font.rs index 009e6281..af7ff246 100644 --- a/fontique/src/font.rs +++ b/fontique/src/font.rs @@ -28,6 +28,7 @@ pub struct FontInfo { charmap_index: CharmapIndex, } +#[allow(clippy::from_over_into)] // In future From won't be possible impl Into for &FontInfo { fn into(self) -> FontMatchingInfo { FontMatchingInfo {