#![cfg(feature = "text-shaping")]
use crate::compat::{RefCell, String, Vec};
use super::font_assets::FaceBytes;
pub const WEIGHT_AXIS_TAG: [u8; 4] = *b"wght";
const WEIGHT_TAG: ttf_parser::Tag = ttf_parser::Tag::from_bytes(b"wght");
pub const MAX_INSTANCES: usize = 8;
#[derive(Debug, Clone, PartialEq)]
pub struct VariableAxis {
pub tag: [u8; 4],
pub name: Option<String>,
pub min: f32,
pub default: f32,
pub max: f32,
pub hidden: bool,
}
impl VariableAxis {
pub fn is_weight(&self) -> bool {
self.tag == WEIGHT_AXIS_TAG
}
pub fn tag_text(&self) -> String {
self.tag
.iter()
.map(|byte| if byte.is_ascii_graphic() { *byte as char } else { '?' })
.collect()
}
}
#[derive(Clone, Copy)]
pub struct VariableFace {
bytes: FaceBytes,
}
impl VariableFace {
pub const fn new(face: FaceBytes) -> Self {
Self { bytes: face }
}
pub const fn face(self) -> FaceBytes {
self.bytes
}
pub fn for_family(family: &str) -> Option<Self> {
super::shaping::face_for_family(family).map(Self::new)
}
pub fn is_variable(self) -> bool {
Self::with_parsed(self.bytes, |face| face.is_variable()).unwrap_or(false)
}
pub fn axes(self) -> Vec<VariableAxis> {
Self::with_parsed(self.bytes, |face| {
face.variation_axes().into_iter().map(|axis| axis_info(face, axis)).collect()
})
.unwrap_or_default()
}
pub fn weight_axis(self) -> Option<VariableAxis> {
Self::with_parsed(self.bytes, |face| {
find_weight_axis(face).map(|axis| axis_info(face, axis))
})
.flatten()
}
pub fn clamp_weight(self, weight: u16) -> f32 {
Self::with_parsed(self.bytes, |face| match find_weight_axis(face) {
Some(axis) => clamp_to_axis(&axis, f32::from(weight)),
None => f32::from(weight),
})
.unwrap_or(f32::from(weight))
}
pub fn instantiate_at_weight(self, weight: u16) -> Option<VariableInstance> {
VariableInstance::with_weight(self.bytes, weight)
}
pub fn instantiate_at(self, coordinates: &[([u8; 4], f32)]) -> Option<VariableInstance> {
VariableInstance::with_coordinates(self.bytes, coordinates)
}
fn with_parsed<R>(face: FaceBytes, f: impl FnOnce(&ttf_parser::Face<'_>) -> R) -> Option<R> {
super::font_assets::with_parsed_face(face, f)
}
}
pub struct VariableInstance {
face: ttf_parser::Face<'static>,
requested_weight: u16,
effective_weight: f32,
weight_axis: Option<VariableAxis>,
}
impl VariableInstance {
fn with_weight(face: FaceBytes, weight: u16) -> Option<Self> {
let mut parsed = ttf_parser::Face::parse(face.bytes, 0).ok()?;
let weight_axis = find_weight_axis(&parsed).map(|axis| axis_info(&parsed, axis));
let effective_weight = match &weight_axis {
Some(axis) => clamp_to_values(axis, f32::from(weight)),
None => f32::from(weight),
};
if weight_axis.is_some() {
let _ = parsed.set_variation(WEIGHT_TAG, effective_weight);
}
Some(Self {
face: parsed,
requested_weight: crate::core::Font::normalize_weight(weight),
effective_weight,
weight_axis,
})
}
fn with_coordinates(face: FaceBytes, coordinates: &[([u8; 4], f32)]) -> Option<Self> {
let mut parsed = ttf_parser::Face::parse(face.bytes, 0).ok()?;
for (tag, value) in coordinates {
let _ = parsed.set_variation(ttf_parser::Tag::from_bytes(tag), *value);
}
let weight_axis = find_weight_axis(&parsed).map(|axis| axis_info(&parsed, axis));
let requested = coordinates
.iter()
.find(|(tag, _)| *tag == WEIGHT_AXIS_TAG)
.map(|(_, value)| *value)
.unwrap_or_else(|| {
weight_axis
.as_ref()
.map(|axis| axis.default)
.unwrap_or_else(|| f32::from(crate::core::Font::default_weight()))
});
let effective_weight = match &weight_axis {
Some(axis) => clamp_to_values(axis, requested),
None => requested,
};
Some(Self {
face: parsed,
requested_weight: crate::core::Font::normalize_weight(
requested.round().clamp(0.0, u16::MAX as f32) as u16,
),
effective_weight,
weight_axis,
})
}
pub fn requested_weight(&self) -> u16 {
self.requested_weight
}
pub fn effective_weight(&self) -> f32 {
self.effective_weight
}
pub fn weight_axis(&self) -> Option<&VariableAxis> {
self.weight_axis.as_ref()
}
pub fn units_per_em(&self) -> u16 {
self.face.units_per_em()
}
pub fn glyph_index(&self, ch: char) -> Option<u16> {
self.face.glyph_index(ch).map(|glyph| glyph.0)
}
pub fn advance(&self, glyph: u16) -> Option<u16> {
self.face.glyph_hor_advance(ttf_parser::GlyphId(glyph))
}
pub fn outline_glyph<B>(&self, glyph: u16, builder: &mut B) -> bool
where
B: ttf_parser::OutlineBuilder,
{
self.face.outline_glyph(ttf_parser::GlyphId(glyph), builder).is_some()
}
}
pub use ttf_parser::OutlineBuilder as VariableOutlineBuilder;
fn find_weight_axis(face: &ttf_parser::Face<'_>) -> Option<ttf_parser::VariationAxis> {
if !face.is_variable() {
return None;
}
face.variation_axes().into_iter().find(|axis| axis.tag == WEIGHT_TAG)
}
fn axis_info(face: &ttf_parser::Face<'_>, axis: ttf_parser::VariationAxis) -> VariableAxis {
VariableAxis {
tag: axis.tag.to_bytes(),
name: axis_name(face, axis.name_id),
min: axis.min_value,
default: axis.def_value,
max: axis.max_value,
hidden: axis.hidden,
}
}
fn axis_name(face: &ttf_parser::Face<'_>, name_id: u16) -> Option<String> {
let record =
face.names().into_iter().find(|name| name.name_id == name_id && name.is_unicode())?;
let mut text = String::new();
let units = record.name.as_chunks::<2>().0.iter().map(|pair| u16::from_be_bytes(*pair));
for scalar in core::char::decode_utf16(units) {
text.push(scalar.ok()?);
}
if text.is_empty() {
None
} else {
Some(text)
}
}
fn clamp_to_axis(axis: &ttf_parser::VariationAxis, value: f32) -> f32 {
value.max(axis.min_value).min(axis.max_value)
}
fn clamp_to_values(axis: &VariableAxis, value: f32) -> f32 {
value.max(axis.min).min(axis.max)
}
struct WeightedTtfFace {
bytes: *const u8,
weight: u16,
face: ttf_parser::Face<'static>,
}
struct WeightedShaperFace {
bytes: *const u8,
weight: u16,
face: rustybuzz::Face<'static>,
}
thread_local! {
static WEIGHTED_TTF_FACES: RefCell<Vec<WeightedTtfFace>> = const { RefCell::new(Vec::new()) };
static WEIGHTED_SHAPER_FACES: RefCell<Vec<WeightedShaperFace>> =
const { RefCell::new(Vec::new()) };
}
fn has_weight_axis(face: FaceBytes) -> bool {
super::font_assets::with_parsed_face(face, |parsed| find_weight_axis(parsed).is_some())
.unwrap_or(false)
}
fn effective_weight_of(face: &ttf_parser::Face<'_>, weight: u16) -> f32 {
match find_weight_axis(face) {
Some(axis) => clamp_to_axis(&axis, f32::from(weight)),
None => f32::from(weight),
}
}
pub(crate) fn with_weighted_face<R>(
face: FaceBytes,
weight: u16,
f: impl FnOnce(&ttf_parser::Face<'_>) -> R,
) -> Option<R> {
if !has_weight_axis(face) {
return super::font_assets::with_parsed_face(face, f);
}
let bytes = face.bytes.as_ptr();
let entry: Option<WeightedTtfFace> = WEIGHTED_TTF_FACES.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(index) =
cache.iter().position(|slot| slot.bytes == bytes && slot.weight == weight)
{
return Some(cache.remove(index));
}
VariableInstance::with_weight(face, weight).map(|instance| WeightedTtfFace {
bytes,
weight,
face: instance.face,
})
});
let answer = entry.as_ref().map(|slot| f(&slot.face));
if let Some(slot) = entry {
WEIGHTED_TTF_FACES.with(|cache| {
let mut cache = cache.borrow_mut();
if !cache.iter().any(|existing| existing.bytes == bytes && existing.weight == weight) {
cache.push(slot);
if cache.len() > MAX_INSTANCES {
cache.remove(0);
}
}
});
}
answer
}
pub(crate) fn with_shaper_face_at_weight<R>(
face: FaceBytes,
weight: u16,
f: impl FnOnce(&rustybuzz::Face<'_>) -> R,
) -> Option<R> {
if !has_weight_axis(face) {
return super::font_assets::with_shaper_face(face, f);
}
let bytes = face.bytes.as_ptr();
let entry: Option<WeightedShaperFace> = WEIGHTED_SHAPER_FACES.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(index) =
cache.iter().position(|slot| slot.bytes == bytes && slot.weight == weight)
{
return Some(cache.remove(index));
}
let mut parsed = rustybuzz::Face::from_slice(face.bytes, 0)?;
let effective = effective_weight_of(&parsed, weight);
let _ = parsed.set_variation(WEIGHT_TAG, effective);
Some(WeightedShaperFace { bytes, weight, face: parsed })
});
let answer = entry.as_ref().map(|slot| f(&slot.face));
if let Some(slot) = entry {
WEIGHTED_SHAPER_FACES.with(|cache| {
let mut cache = cache.borrow_mut();
if !cache.iter().any(|existing| existing.bytes == bytes && existing.weight == weight) {
cache.push(slot);
if cache.len() > MAX_INSTANCES {
cache.remove(0);
}
}
});
}
answer
}
#[cfg(test)]
mod tests {
use super::*;
const WEIGHT_FIXTURE: &[u8] = super::super::font_assets::variable_fixtures::VARIABLE_WEIGHT_TTF;
const WIDTH_FIXTURE: &[u8] = super::super::font_assets::variable_fixtures::VARIABLE_WIDTH_TTF;
const AVAR_FIXTURE: &[u8] = super::super::font_assets::variable_fixtures::VARIABLE_AVAR_TTF;
fn weight_face() -> FaceBytes {
FaceBytes { name: "Variable Test", bytes: WEIGHT_FIXTURE }
}
fn width_face() -> FaceBytes {
FaceBytes { name: "Variable Width Test", bytes: WIDTH_FIXTURE }
}
fn avar_face() -> FaceBytes {
FaceBytes { name: "Variable Avar Test", bytes: AVAR_FIXTURE }
}
#[derive(Default)]
struct Recorder {
points: Vec<(f32, f32)>,
}
impl ttf_parser::OutlineBuilder for Recorder {
fn move_to(&mut self, x: f32, y: f32) {
self.points.push((x, y));
}
fn line_to(&mut self, x: f32, y: f32) {
self.points.push((x, y));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.points.push((x1, y1));
self.points.push((x, y));
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.points.push((x1, y1));
self.points.push((x2, y2));
self.points.push((x, y));
}
fn close(&mut self) {}
}
fn outline_for(face: FaceBytes, weight: u16) -> Vec<(f32, f32)> {
let instance = VariableFace::new(face)
.instantiate_at_weight(weight)
.expect("the fixture is a readable face");
let glyph = instance.glyph_index('A').expect("the fixture maps A");
let mut recorder = Recorder::default();
assert!(instance.outline_glyph(glyph, &mut recorder), "A has an outline");
recorder.points
}
fn outline_at(weight: u16) -> Vec<(f32, f32)> {
outline_for(weight_face(), weight)
}
fn bounds(points: &[(f32, f32)]) -> (f32, f32, f32, f32) {
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for &(x, y) in points {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
(min_x, min_y, max_x, max_y)
}
#[test]
fn the_fixture_reports_its_axes_with_names_and_ranges() {
let face = VariableFace::new(weight_face());
assert!(face.is_variable(), "the fixture declares fvar");
let axes = face.axes();
assert_eq!(axes.len(), 1, "the weight fixture has exactly one axis");
let axis = &axes[0];
assert_eq!(axis.tag, WEIGHT_AXIS_TAG);
assert!(axis.is_weight());
assert_eq!(axis.tag_text(), "wght");
assert_eq!((axis.min, axis.default, axis.max), (100.0, 400.0, 900.0));
assert_eq!(axis.name.as_deref(), Some("Weight"), "the fvar axis name is read");
assert!(!axis.hidden);
assert_eq!(face.weight_axis().map(|axis| axis.tag), Some(WEIGHT_AXIS_TAG));
}
#[test]
fn a_static_face_reports_no_axes_rather_than_guessing() {
for face in super::super::font_assets::active_faces() {
let variable = VariableFace::new(*face);
assert!(!variable.is_variable(), "{} is not a variable face", face.name);
assert!(variable.axes().is_empty(), "{} must not invent an axis", face.name);
assert!(variable.weight_axis().is_none(), "{} has no weight axis", face.name);
}
}
#[test]
fn weight_700_draws_a_different_outline_than_400() {
let regular = outline_at(400);
let bold = outline_at(700);
assert!(!regular.is_empty() && !bold.is_empty());
assert_ne!(regular, bold, "the gvar deltas must move the outline");
let (rx0, ry0, rx1, ry1) = bounds(®ular);
let (bx0, by0, bx1, by1) = bounds(&bold);
assert_eq!((rx0, ry0), (bx0, by0), "the fixture keeps the left and bottom edges fixed");
assert!(bx1 > rx1, "the 700 master widens the right edge: {bx1} > {rx1}");
assert!(by1 > ry1, "the 700 master raises the top edge: {by1} > {ry1}");
}
#[test]
fn the_instance_advance_moves_with_the_weight() {
let face = VariableFace::new(weight_face());
let regular = face.instantiate_at_weight(400).expect("readable");
let bold = face.instantiate_at_weight(700).expect("readable");
let glyph = regular.glyph_index('A').expect("A is mapped");
let regular_advance = regular.advance(glyph).expect("hmtx has an advance");
let bold_advance = bold.advance(glyph).expect("hmtx has an advance");
assert!(
bold_advance > regular_advance,
"the 700 master's wider advance must reach the instance: {bold_advance} > {regular_advance}"
);
assert_eq!(regular.effective_weight(), 400.0);
assert_eq!(bold.effective_weight(), 700.0);
}
#[test]
fn a_weight_beyond_the_axis_is_clamped_to_the_axis_bounds() {
let face = VariableFace::new(weight_face());
assert_eq!(face.clamp_weight(1000), 900.0);
assert_eq!(face.clamp_weight(0), 100.0);
assert_eq!(face.clamp_weight(450), 450.0, "an in-range value is untouched");
let low = face.instantiate_at_weight(0).expect("readable");
let at_min = face.instantiate_at_weight(100).expect("readable");
assert_eq!(low.effective_weight(), 100.0);
assert_eq!(at_min.effective_weight(), 100.0);
assert_eq!(low.requested_weight(), 100, "a requested 0 normalizes to the scale's floor");
assert_eq!(outline_at(0), outline_at(100), "below-range requests land on the minimum");
let high = face.instantiate_at_weight(1000).expect("readable");
let at_max = face.instantiate_at_weight(900).expect("readable");
assert_eq!(high.effective_weight(), 900.0);
assert_eq!(at_max.effective_weight(), 900.0);
assert_eq!(outline_at(1000), outline_at(900), "above-range requests land on the maximum");
}
#[test]
fn avar_remaps_the_weight_coordinate_before_the_deltas_are_applied() {
let face = VariableFace::new(avar_face());
assert!(face.is_variable());
let axis = face.weight_axis().expect("avar does not change the fvar axes");
assert_eq!((axis.min, axis.default, axis.max), (100.0, 400.0, 900.0));
let at_400 = outline_for(avar_face(), 400);
let at_500 = outline_for(avar_face(), 500);
let at_700 = outline_for(avar_face(), 700);
let near = |a: &[(f32, f32)], b: &[(f32, f32)]| {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|(p, q)| (p.0 - q.0).abs() <= 1.0 && (p.1 - q.1).abs() <= 1.0)
};
assert!(
near(&at_500, &at_700),
"avar maps user 500 onto the normalized coordinate that 700 reaches: \
{at_500:?} vs {at_700:?}"
);
assert_ne!(at_500, at_400, "the remapped instance still differs from the default");
let linear_500 = outline_for(weight_face(), 500);
assert_ne!(linear_500, outline_at(700), "linear normalization does not saturate at 500");
assert_ne!(at_500, linear_500, "the avar table must change the drawn shape");
let instance_500 = face.instantiate_at_weight(500).expect("readable");
let instance_700 = face.instantiate_at_weight(700).expect("readable");
let instance_400 = face.instantiate_at_weight(400).expect("readable");
let glyph = instance_500.glyph_index('A').expect("A is mapped");
let advance_500 = instance_500.advance(glyph).expect("hmtx has an advance");
let advance_700 = instance_700.advance(glyph).expect("hmtx has an advance");
assert!(
advance_500.abs_diff(advance_700) <= 1,
"the remapped advance is the 700 instance's: {advance_500} vs {advance_700}"
);
assert_ne!(advance_500, instance_400.advance(glyph).expect("hmtx has an advance"));
assert_ne!(
instance_500.advance(glyph),
face.instantiate_at_weight(400).expect("readable").advance(glyph)
);
}
#[test]
fn a_variable_face_without_a_weight_axis_uses_its_default_instance() {
let face = VariableFace::new(width_face());
assert!(face.is_variable(), "the width fixture is a variable face");
assert!(face.weight_axis().is_none(), "it varies width, not weight");
assert_eq!(face.axes().len(), 1);
assert_eq!(face.axes()[0].tag, *b"wdth");
assert_eq!(face.clamp_weight(700), 700.0, "no axis means nothing to clamp against");
let regular = face.instantiate_at_weight(400).expect("readable");
let bold = face.instantiate_at_weight(700).expect("readable");
assert!(regular.weight_axis().is_none());
assert_eq!(regular.effective_weight(), 400.0);
assert_eq!(bold.effective_weight(), 700.0);
assert_eq!(regular.requested_weight(), 400);
assert_eq!(bold.requested_weight(), 700);
let glyph = regular.glyph_index('A').expect("A is mapped");
assert_eq!(
regular.advance(glyph),
bold.advance(glyph),
"without a weight axis the advance must not move"
);
let mut regular_outline = Recorder::default();
let mut bold_outline = Recorder::default();
assert!(regular.outline_glyph(glyph, &mut regular_outline));
assert!(bold.outline_glyph(glyph, &mut bold_outline));
assert_eq!(
regular_outline.points, bold_outline.points,
"without a weight axis both weights are the face's default instance"
);
}
#[test]
fn unreadable_bytes_answer_none_instead_of_panicking() {
let junk = FaceBytes { name: "junk", bytes: b"not a font" };
let face = VariableFace::new(junk);
assert!(!face.is_variable());
assert!(face.axes().is_empty());
assert!(face.weight_axis().is_none());
assert!(face.instantiate_at_weight(700).is_none());
assert!(face.instantiate_at(&[(WEIGHT_AXIS_TAG, 700.0)]).is_none());
assert!(with_weighted_face(junk, 700, |_| ()).is_none());
assert!(with_shaper_face_at_weight(junk, 700, |_| ()).is_none());
}
#[test]
fn an_explicit_coordinate_set_instantiates_the_same_outline_as_a_weight() {
let face = VariableFace::new(weight_face());
let by_weight = face.instantiate_at_weight(700).expect("readable");
let by_coordinates = face.instantiate_at(&[(WEIGHT_AXIS_TAG, 700.0)]).expect("readable");
assert_eq!(by_coordinates.effective_weight(), 700.0);
assert_eq!(by_weight.effective_weight(), by_coordinates.effective_weight());
let glyph = by_weight.glyph_index('A').expect("A is mapped");
let mut weight_outline = Recorder::default();
let mut coordinate_outline = Recorder::default();
assert!(by_weight.outline_glyph(glyph, &mut weight_outline));
assert!(by_coordinates.outline_glyph(glyph, &mut coordinate_outline));
assert_eq!(weight_outline.points, coordinate_outline.points);
assert_eq!(by_weight.advance(glyph), by_coordinates.advance(glyph));
let untouched = face.instantiate_at(&[(*b"zzzz", 999.0)]).expect("readable");
assert_eq!(untouched.effective_weight(), 400.0);
}
#[test]
fn the_cached_instance_agrees_with_a_fresh_one() {
let face = weight_face();
let fresh = VariableFace::new(face).instantiate_at_weight(700).expect("readable");
let mut fresh_outline = Recorder::default();
let glyph = fresh.glyph_index('A').expect("A is mapped");
assert!(fresh.outline_glyph(glyph, &mut fresh_outline));
let mut cached_outline = Recorder::default();
let answer = with_weighted_face(face, 700, |parsed| {
parsed.outline_glyph(ttf_parser::GlyphId(glyph), &mut cached_outline).is_some()
});
assert_eq!(answer, Some(true));
assert_eq!(
cached_outline.points, fresh_outline.points,
"the cached per-glyph instance must draw the same outline as the public one"
);
let mut again = Recorder::default();
assert_eq!(
with_weighted_face(face, 700, |parsed| {
parsed.outline_glyph(ttf_parser::GlyphId(glyph), &mut again).is_some()
}),
Some(true)
);
assert_eq!(again.points, fresh_outline.points);
}
#[test]
fn the_shaper_measures_a_variable_face_at_the_requested_weight() {
let shaper = super::super::shaping::RustybuzzShaper::new(weight_face())
.expect("the fixture is readable");
let regular = shaper.shape_face_at_weight("AA", 48.0, 1.0, 400).expect("shaped");
let bold = shaper.shape_face_at_weight("AA", 48.0, 1.0, 700).expect("shaped");
assert!(
bold.width > regular.width,
"700 must measure wider than 400: {} > {}",
bold.width,
regular.width
);
assert_eq!(bold.glyph_ids, regular.glyph_ids, "the same characters are mapped");
let own = shaper.shape_face("AA", 48.0, 1.0).expect("shaped");
assert_eq!(own.width, regular.width);
}
#[test]
fn the_shaper_instance_and_the_outline_instance_move_the_axis_together() {
let face = weight_face();
let reference = VariableFace::new(face).instantiate_at_weight(1000).expect("readable");
assert_eq!(reference.effective_weight(), 900.0, "an out-of-range request clamps first");
let glyph = reference.glyph_index('A').expect("A is mapped");
let mut outline_outline = Recorder::default();
assert!(reference.outline_glyph(glyph, &mut outline_outline));
let mut shaper_outline = Recorder::default();
let shaper = with_shaper_face_at_weight(face, 1000, |parsed| {
let _ = parsed.outline_glyph(ttf_parser::GlyphId(glyph), &mut shaper_outline);
parsed.glyph_hor_advance(ttf_parser::GlyphId(glyph))
})
.flatten();
assert_eq!(
shaper_outline.points, outline_outline.points,
"the shaper's instance and the outline instance must be the same face"
);
assert_eq!(
shaper,
reference.advance(glyph),
"the measured advance must come from the same instance as the drawn outline"
);
}
#[cfg(target_os = "macos")]
#[test]
fn the_host_system_ui_face_declares_a_weight_axis() {
const CANDIDATES: [&str; 2] =
["/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/SFNSDisplay.ttf"];
let Some(bytes) = CANDIDATES
.iter()
.find_map(|path| std::fs::read(path).ok())
.filter(|bytes| !bytes.is_empty())
else {
return;
};
let bytes: &'static [u8] = crate::compat::Box::leak(bytes.into_boxed_slice());
let face = VariableFace::new(FaceBytes { name: "System UI", bytes });
assert!(face.is_variable(), "the host system UI face is a variable face");
let axis = face.weight_axis().expect("it declares a wght axis");
assert!(axis.min < axis.max, "the weight axis has a range");
assert!(
(axis.default - 400.0).abs() < 1.0,
"the default instance is regular, not {}",
axis.default
);
let regular = face.instantiate_at_weight(400).expect("readable");
let bold = face.instantiate_at_weight(700).expect("readable");
let glyph = regular.glyph_index('A').expect("the face covers A");
let mut regular_outline = Recorder::default();
let mut bold_outline = Recorder::default();
assert!(regular.outline_glyph(glyph, &mut regular_outline));
assert!(bold.outline_glyph(glyph, &mut bold_outline));
assert_ne!(
regular_outline.points, bold_outline.points,
"the host face must draw a different outline at the two weights"
);
}
#[test]
fn a_static_face_is_unchanged_at_every_weight() {
for face in super::super::font_assets::active_faces() {
let glyph = super::super::font_assets::with_parsed_face(*face, |parsed| {
parsed.glyph_index('A').or_else(|| parsed.glyph_index('中'))
})
.flatten();
let Some(glyph) = glyph else { continue };
let mut regular = Recorder::default();
let mut bold = Recorder::default();
assert_eq!(
with_weighted_face(*face, 400, |parsed| {
parsed.outline_glyph(glyph, &mut regular).is_some()
}),
Some(true)
);
assert_eq!(
with_weighted_face(*face, 700, |parsed| {
parsed.outline_glyph(glyph, &mut bold).is_some()
}),
Some(true)
);
assert_eq!(
regular.points, bold.points,
"{} has no weight axis, so both requests are its one instance",
face.name
);
assert_eq!(
super::super::font_assets::with_parsed_face(*face, |parsed| parsed
.glyph_hor_advance(glyph)),
with_weighted_face(*face, 700, |parsed| parsed.glyph_hor_advance(glyph)),
"{} has no weight axis, so its advance must not move with the weight",
face.name
);
}
}
}