From 178102799d03e9a3a31320e44e3998d82a2708e4 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:47:44 +0000 Subject: [PATCH] Resolve variations on fallback faces --- crates/obscura-render/src/inline.rs | 391 +++++++++++++++++++++------- vendor/cosmic-text/src/attrs.rs | 38 +++ vendor/cosmic-text/src/layout.rs | 8 + vendor/cosmic-text/src/shape.rs | 50 +++- 4 files changed, 398 insertions(+), 89 deletions(-) diff --git a/crates/obscura-render/src/inline.rs b/crates/obscura-render/src/inline.rs index 4d47cee..04cc93d 100644 --- a/crates/obscura-render/src/inline.rs +++ b/crates/obscura-render/src/inline.rs @@ -95,20 +95,13 @@ struct LoadedFace { font_id: Option, min_weight: u16, max_weight: u16, - shape_weight: u16, italic: bool, - has_optical_size: bool, } #[derive(Clone)] struct ResolvedFont { family: Arc, font_id: Option, - shape_weight: u16, - /// Authored `wght` coordinate for a ranged `@font-face`, retained while - /// descriptor selection is converted into the canonical axis tuple. - variation_weight: Option, - has_optical_size: bool, } #[derive(Clone)] @@ -155,10 +148,6 @@ fn resolve_loaded_font( return ResolvedFont { family: Arc::clone(&face.name), font_id: face.font_id, - shape_weight: face.shape_weight, - variation_weight: (face.min_weight != face.max_weight) - .then_some(requested_weight), - has_optical_size: face.has_optical_size, }; } let available: Vec<_> = @@ -171,22 +160,15 @@ fn resolve_loaded_font( return ResolvedFont { family: Arc::clone(&face.name), font_id: face.font_id, - shape_weight: face.shape_weight, - variation_weight: None, - has_optical_size: face.has_optical_size, }; } } } } let fallback = resolve_font_family(fam); - let weights: &[u16] = &[400, 700]; ResolvedFont { family: Arc::from(fallback), font_id: None, - shape_weight: match_font_weight(requested_weight, weights), - variation_weight: None, - has_optical_size: false, } } @@ -331,12 +313,25 @@ struct VariableCacheKey { variations: Arc, } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct VariationIntentKey { + font_id: cosmic_text::fontdb::ID, + weight_bits: Option, + optical_size_bits: Option, + italic: bool, + explicit: Option>, +} + /// Swash's ordinary cache key intentionally has no variation coordinates. /// Keep variable instances in a separate cache so different axis tuples can /// never share an outline and repeated paint remains O(1) after first raster. struct VariableSwashCache { context: ScaleContext, images: HashMap>, + /// Canonical coordinates supported by the face actually selected for a + /// glyph. This is deliberately separate from authored intent: unsupported + /// axes and values that clamp to the same endpoint share raster entries. + instances: HashMap>>, } impl VariableSwashCache { @@ -344,9 +339,76 @@ impl VariableSwashCache { Self { context: ScaleContext::new(), images: HashMap::new(), + instances: HashMap::new(), } } + fn effective_variations( + &mut self, + font_system: &mut FontSystem, + font_id: cosmic_text::fontdb::ID, + weight: Option, + optical_size: Option, + italic: bool, + explicit: Option>, + ) -> Option> { + let key = VariationIntentKey { + font_id, + weight_bits: weight + .filter(|value| value.is_finite()) + .map(|value| (value + 0.0).to_bits()), + optical_size_bits: optical_size + .filter(|value| value.is_finite()) + .map(|value| (value + 0.0).to_bits()), + italic, + explicit, + }; + if let Some(cached) = self.instances.get(&key) { + return cached.clone(); + } + + let resolved = font_system.get_font(font_id).and_then(|font| { + let swash_font = font.as_swash(); + let has_ital = swash_font + .variations() + .any(|axis| axis.tag() == swash::tag_from_bytes(b"ital")); + let mut variations = FontVariations::new(); + for axis in swash_font.variations() { + let tag = axis.tag(); + let explicit_value = key.explicit.as_ref().and_then(|settings| { + settings + .iter() + .find(|setting| { + swash::tag_from_bytes(setting.tag.as_bytes()) == tag + }) + .map(|setting| setting.value.0) + }); + let automatic_value = if tag == swash::tag_from_bytes(b"wght") { + key.weight_bits.map(f32::from_bits) + } else if tag == swash::tag_from_bytes(b"opsz") { + key.optical_size_bits.map(f32::from_bits) + } else if italic && tag == swash::tag_from_bytes(b"ital") { + Some(1.0) + } else if italic && !has_ital && tag == swash::tag_from_bytes(b"slnt") { + Some(-14.0) + } else { + None + }; + let Some(value) = explicit_value.or(automatic_value) else { + continue; + }; + if !value.is_finite() { + continue; + } + let value = value.clamp(axis.min_value(), axis.max_value()) + 0.0; + variations.set(VariationTag::new(&tag.to_be_bytes()), value); + } + (!variations.is_empty()).then(|| Arc::new(variations)) + }); + self.instances.insert(key, resolved.clone()); + resolved + } + fn with_pixels( &mut self, font_system: &mut FontSystem, @@ -641,14 +703,6 @@ impl TextEngine { let mut loaded_families = HashMap::new(); for (id, declared_family, declared_weight, declared_italic) in declarations { let Some(face) = db.face(id) else { continue }; - let has_optical_size = db - .with_face_data(id, |data, index| { - swash::FontRef::from_index(data, index as usize).map_or(false, |font| { - font.variations() - .any(|axis| axis.tag() == swash::tag_from_bytes(b"opsz")) - }) - }) - .unwrap_or(false); let names = face.families.clone(); let internal_name = names .first() @@ -670,9 +724,7 @@ impl TextEngine { font_id: Some(id), min_weight: weight.0, max_weight: weight.1, - shape_weight, italic, - has_optical_size, }); } } @@ -813,6 +865,7 @@ impl TextEngine { letter_spacing: context.letter_spacing, letter_spacing_non_normal: context.letter_spacing_non_normal, weight: context.weight, + optical_sizing: context.optical_sizing, font_id: context.font_id, variations: context.variations.clone(), italic: context.italic, @@ -1030,6 +1083,7 @@ struct SpanAttrs { letter_spacing: f32, letter_spacing_non_normal: bool, weight: u16, + optical_sizing: crate::FontOpticalSizing, font_id: Option, variations: Option>, italic: bool, @@ -1051,6 +1105,11 @@ impl SpanAttrs { self.line_height.max(1.0), )); a = a.weight(Weight(self.weight)); + a = a.font_weight_axis(self.weight as f32); + if self.optical_sizing == crate::FontOpticalSizing::Auto { + a = a.font_optical_size(self.font_size); + } + a = a.font_italic_axis(self.italic); if let Some(font_id) = self.font_id { a = a.font_id(font_id); } @@ -1095,10 +1154,9 @@ struct SpanCtx { letter_spacing_non_normal: bool, color: [u8; 4], weight: u16, + optical_sizing: crate::FontOpticalSizing, font_id: Option, variations: Option>, - variable_weight: bool, - has_optical_size: bool, italic: bool, underline: bool, transform: TextTransform, @@ -1126,22 +1184,12 @@ struct Collector { /// transparent (and invisible), unchanged. fn resolved_font_variations( style: &LayoutStyle, - font: &ResolvedFont, + _font: &ResolvedFont, ) -> Option> { let mut variations = FontVariations::new(); - if let Some(weight) = font.variation_weight { - variations.set(VariationTag::new(b"wght"), weight as f32); - } - if font.has_optical_size - && style.font_optical_sizing.unwrap_or(crate::FontOpticalSizing::Auto) - == crate::FontOpticalSizing::Auto - { - let optical_size = style.font_size.unwrap_or(16.0); - if optical_size.is_finite() { - variations.set(VariationTag::new(b"opsz"), optical_size); - } - } - // Low-level CSS axes override the corresponding high-level coordinates. + // Automatic high-level axes travel separately with every glyph so they + // can be resolved against the face actually selected during fallback. + // Only low-level authored settings need an allocated tuple here. for setting in style.font_variation_settings.as_deref().unwrap_or(&[]) { if setting.value.is_finite() { variations.set(VariationTag::new(&setting.tag), setting.value); @@ -1167,11 +1215,10 @@ fn base_span_ctx( letter_spacing: base.letter_spacing.unwrap_or(0.0), letter_spacing_non_normal: base.letter_spacing_non_normal.unwrap_or(false), color: base.color.unwrap_or([0, 0, 0, 255]), - weight: font.shape_weight, + weight: crate::style::used_font_weight(base), + optical_sizing: base.font_optical_sizing.unwrap_or_default(), font_id: font.font_id, variations, - variable_weight: font.variation_weight.is_some(), - has_optical_size: font.has_optical_size, italic: base.font_style_italic.unwrap_or(false), underline: base.underline.unwrap_or(false), transform: base.text_transform.unwrap_or(TextTransform::None), @@ -1224,6 +1271,7 @@ fn collect_node_spans( letter_spacing: ctx.letter_spacing, letter_spacing_non_normal: ctx.letter_spacing_non_normal, weight: ctx.weight, + optical_sizing: ctx.optical_sizing, font_id: ctx.font_id, variations: ctx.variations.clone(), italic: ctx.italic, @@ -1247,6 +1295,7 @@ fn collect_node_spans( letter_spacing: ctx.letter_spacing, letter_spacing_non_normal: ctx.letter_spacing_non_normal, weight: ctx.weight, + optical_sizing: ctx.optical_sizing, font_id: ctx.font_id, variations: ctx.variations.clone(), italic: ctx.italic, @@ -1288,15 +1337,6 @@ fn collect_node_spans( .unwrap_or_else(|| ResolvedFont { family: Arc::clone(&ctx.family), font_id: ctx.font_id, - shape_weight: if ctx.variable_weight { - ctx.weight - } else { - requested_weight - }, - variation_weight: ctx - .variable_weight - .then_some(requested_weight.clamp(1, 1000)), - has_optical_size: ctx.has_optical_size, }); let variations = style .map(|style| resolved_font_variations(style, &font)) @@ -1315,11 +1355,12 @@ fn collect_node_spans( .and_then(|style| style.letter_spacing_non_normal) .unwrap_or(ctx.letter_spacing_non_normal), color, - weight: font.shape_weight, + weight: requested_weight, + optical_sizing: style + .and_then(|style| style.font_optical_sizing) + .unwrap_or(ctx.optical_sizing), font_id: font.font_id, variations, - variable_weight: font.variation_weight.is_some(), - has_optical_size: font.has_optical_size, italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false), // Underline propagates in: an ancestor's underline covers // descendant text; an element only sets its own via CSS. @@ -1709,10 +1750,20 @@ impl TextEngine { pixels[idx] = tiny_skia::PremultipliedColorU8::from_rgba(out_r as u8, out_g as u8, out_b as u8, out_a as u8) .unwrap_or(dst); }; - if let Some(variations) = metadata_variation(glyph.metadata) + let explicit_variations = metadata_variation(glyph.metadata) .and_then(|index| item.variation_sets.get(index)) - .map(Arc::clone) - { + .map(Arc::clone); + let effective_variations = glyph.font_is_variable.then(|| { + variable_swash.effective_variations( + font_system, + physical.cache_key.font_id, + glyph.font_weight_axis_opt, + glyph.font_optical_size_opt, + glyph.font_italic_axis, + explicit_variations, + ) + }).flatten(); + if let Some(variations) = effective_variations { variable_swash.with_pixels( font_system, physical.cache_key, @@ -1900,39 +1951,29 @@ mod tests { font_id: None, min_weight: 400, max_weight: 400, - shape_weight: 400, italic: false, - has_optical_size: false, }, LoadedFace { name: Arc::from("Poppins Medium"), font_id: None, min_weight: 500, max_weight: 500, - shape_weight: 500, italic: false, - has_optical_size: false, }, LoadedFace { name: Arc::from("Poppins"), font_id: None, min_weight: 700, max_weight: 700, - shape_weight: 700, italic: false, - has_optical_size: false, }, ], }; let loaded = HashMap::from([("poppins".to_string(), family)]); let medium = resolve_loaded_font(Some("Poppins, sans-serif"), 500, false, &loaded); assert_eq!(medium.family.as_ref(), "Poppins Medium"); - assert_eq!(medium.shape_weight, 500); - assert_eq!(medium.variation_weight, None); let semibold = resolve_loaded_font(Some("Poppins"), 600, false, &loaded); assert_eq!(semibold.family.as_ref(), "Poppins"); - assert_eq!(semibold.shape_weight, 700); - assert_eq!(semibold.variation_weight, None); } #[test] @@ -1947,17 +1988,26 @@ mod tests { let styles = HashMap::from([(copy, style)]); let mut engine = TextEngine::new(); + let regular_id = engine + .font_system + .db() + .faces() + .find(|face| { + face.weight.0 == 400 + && matches!(face.style, cosmic_text::fontdb::Style::Normal) + && face.families.iter().any(|(name, _)| name == FAMILY) + }) + .map(|face| face.id) + .unwrap(); engine.loaded_families.insert( "test variable".to_string(), LoadedFamily { faces: vec![LoadedFace { name: Arc::from(FAMILY), - font_id: None, + font_id: Some(regular_id), min_weight: 100, max_weight: 900, - shape_weight: 400, italic: false, - has_optical_size: false, }], }, ); @@ -2053,7 +2103,7 @@ mod tests { } #[test] - fn ranged_face_preserves_requested_weight_for_variable_raster() { + fn ranged_face_keeps_requested_weight_as_fallback_axis_intent() { let loaded = HashMap::from([( "inter".to_string(), LoadedFamily { @@ -2062,16 +2112,22 @@ mod tests { font_id: None, min_weight: 100, max_weight: 900, - shape_weight: 400, italic: false, - has_optical_size: true, }], }, )]); let resolved = resolve_loaded_font(Some("Inter, sans-serif"), 725, false, &loaded); assert_eq!(resolved.family.as_ref(), "Inter"); - assert_eq!(resolved.shape_weight, 400); - assert_eq!(resolved.variation_weight, Some(725)); + let style = LayoutStyle { + font_weight: Some("725".to_string()), + ..Default::default() + }; + let mut collector = Collector { + last_was_space: true, + clip_fills: Vec::new(), + }; + let context = base_span_ctx(&style, resolved, &mut collector); + assert_eq!(context.weight, 725); } #[test] @@ -2084,6 +2140,7 @@ mod tests { letter_spacing: 0.0, letter_spacing_non_normal: false, weight: 400, + optical_sizing: crate::FontOpticalSizing::Auto, font_id: None, variations: Some(Arc::new(variations)), italic: false, @@ -2115,9 +2172,6 @@ mod tests { let static_font = ResolvedFont { family: Arc::from(FAMILY), font_id: None, - shape_weight: 400, - variation_weight: None, - has_optical_size: false, }; assert!(resolved_font_variations(&style, &static_font).is_none()); let styles = HashMap::from([(copy, style)]); @@ -2146,9 +2200,6 @@ mod tests { let font = ResolvedFont { family: Arc::from(FAMILY), font_id: None, - shape_weight: 400, - variation_weight: None, - has_optical_size: false, }; assert!(resolved_font_variations(&style, &font).is_none()); } @@ -2161,6 +2212,7 @@ mod tests { letter_spacing: 2.0, letter_spacing_non_normal: true, weight: 400, + optical_sizing: crate::FontOpticalSizing::Auto, font_id: None, variations: None, italic: false, @@ -2194,6 +2246,166 @@ mod tests { wuff::decompress_woff2(&compressed).expect("decompress variable-font fixture") } + fn isolated_fallback_engine() -> ( + TextEngine, + cosmic_text::fontdb::ID, + cosmic_text::fontdb::ID, + ) { + let mut engine = TextEngine::new_with_web_fonts(&[ + WebFont { + data: include_bytes!("../../../vendor/cosmic-text/fonts/NotoSansArabic.ttf") + .to_vec(), + family: Some("Static Primary".to_string()), + weight: Some((400, 400)), + italic: Some(false), + }, + WebFont { + data: variable_font_fixture(), + family: Some("Variable Fallback".to_string()), + weight: Some((100, 900)), + italic: Some(false), + }, + ]); + let static_id = engine.loaded_families["static primary"].faces[0] + .font_id + .unwrap(); + let variable_id = engine.loaded_families["variable fallback"].faces[0] + .font_id + .unwrap(); + let remove = engine + .font_system + .db() + .faces() + .map(|face| face.id) + .filter(|id| *id != static_id && *id != variable_id) + .collect::>(); + let db = engine.font_system.db_mut(); + for id in remove { + db.remove_face(id); + } + (engine, static_id, variable_id) + } + + fn render_variable_fallback( + optical_sizing: crate::FontOpticalSizing, + ) -> ( + cosmic_text::fontdb::ID, + std::collections::HashMap<[u8; 4], std::collections::HashSet>, + ) { + let (mut engine, _, variable_id) = isolated_fallback_engine(); + let tree = obscura_dom::parse_html("

A

"); + let copy = tree.get_element_by_id("copy").unwrap(); + let mut style = LayoutStyle::default(); + style.display = Display::Block; + style.font_family = Some("Static Primary".to_string()); + style.font_weight = Some("900".to_string()); + style.font_size = Some(40.0); + style.font_optical_sizing = Some(optical_sizing); + let item = engine + .try_build(&tree, copy, &HashMap::from([(copy, style)])) + .unwrap(); + engine.measure(item, Some(100.0)); + engine.finalize(item, (0.0, 0.0), 100.0, None); + let shaped_id = engine.items[item] + .buffer + .layout_runs() + .next() + .and_then(|run| run.glyphs.first()) + .map(|glyph| glyph.font_id) + .unwrap(); + assert_eq!(shaped_id, variable_id); + + let mut pixmap = tiny_skia::Pixmap::new(100, 80).unwrap(); + engine.paint_item(item, &mut pixmap, (0.0, 0.0)); + let mut axes = std::collections::HashMap::< + [u8; 4], + std::collections::HashSet, + >::new(); + for key in engine.variable_swash.images.keys() { + for variation in key.variations.iter() { + axes.entry(*variation.tag.as_bytes()) + .or_default() + .insert(variation.value.0.to_bits()); + } + } + (shaped_id, axes) + } + + #[test] + fn automatic_axes_follow_the_actual_variable_fallback_face() { + let (_, auto_axes) = render_variable_fallback(crate::FontOpticalSizing::Auto); + let (_, none_axes) = render_variable_fallback(crate::FontOpticalSizing::None); + + assert_eq!( + auto_axes.get(b"wght"), + Some(&std::collections::HashSet::from([900.0f32.to_bits()])) + ); + assert_eq!( + none_axes.get(b"wght"), + Some(&std::collections::HashSet::from([900.0f32.to_bits()])) + ); + assert_eq!( + auto_axes.get(b"opsz"), + // The fixture's opsz range ends at 32; CSS's automatic 40px + // coordinate must be clamped against the actual fallback face. + Some(&std::collections::HashSet::from([32.0f32.to_bits()])) + ); + assert!(!none_axes.contains_key(b"opsz")); + } + + #[test] + fn unsupported_and_clamped_axes_share_effective_raster_identity() { + let (mut engine, _, variable_id) = isolated_fallback_engine(); + let first = { + let mut settings = FontVariations::new(); + settings + .set(VariationTag::new(b"NOPE"), 1.0) + .set(VariationTag::new(b"wght"), 5_000.0); + Arc::new(settings) + }; + let second = { + let mut settings = FontVariations::new(); + settings + .set(VariationTag::new(b"NOPE"), 999.0) + .set(VariationTag::new(b"wght"), 900.0); + Arc::new(settings) + }; + let first = engine + .variable_swash + .effective_variations( + &mut engine.font_system, + variable_id, + Some(400.0), + Some(40.0), + false, + Some(first), + ) + .unwrap(); + let second = engine + .variable_swash + .effective_variations( + &mut engine.font_system, + variable_id, + Some(400.0), + Some(40.0), + false, + Some(second), + ) + .unwrap(); + + assert_eq!(first, second); + assert!(first + .iter() + .all(|variation| variation.tag != VariationTag::new(b"NOPE"))); + assert_eq!( + first + .iter() + .find(|variation| variation.tag == VariationTag::new(b"wght")) + .map(|variation| variation.value.0), + Some(900.0) + ); + } + fn render_variable_weight(weight: u16) -> ((f32, f32), u64, Vec) { let mut engine = TextEngine::new_with_web_fonts(&[WebFont { data: variable_font_fixture(), @@ -2377,8 +2589,11 @@ mod tests { ); assert_eq!(auto_lines, none_lines); assert_eq!(explicit_lines, none_lines); - assert_eq!(explicit_geometry, none_geometry); - assert_eq!(auto_axes, std::collections::HashSet::from([40.0f32.to_bits()])); + assert_ne!( + explicit_geometry, auto_geometry, + "an explicit low-level opsz coordinate must override automatic sizing" + ); + assert_eq!(auto_axes, std::collections::HashSet::from([32.0f32.to_bits()])); assert!(none_axes.is_empty()); assert_eq!( explicit_axes, diff --git a/vendor/cosmic-text/src/attrs.rs b/vendor/cosmic-text/src/attrs.rs index 20c697a..7e213b7 100644 --- a/vendor/cosmic-text/src/attrs.rs +++ b/vendor/cosmic-text/src/attrs.rs @@ -331,6 +331,14 @@ pub struct Attrs<'a> { /// A specific database face selected by the caller. Font fallback remains /// available when that face does not contain a requested glyph. pub font_id_opt: Option, + /// High-level CSS `font-weight` coordinate, applied only when the actual + /// shaped face provides a `wght` axis. + pub font_weight_axis_opt: Option, + /// Automatic optical-size coordinate derived from the used font size. + /// `None` represents `font-optical-sizing: none`. + pub font_optical_size_opt: Option, + /// Whether high-level font style requests an italic variable instance. + pub font_italic_axis: bool, pub stretch: Stretch, pub style: Style, pub weight: Weight, @@ -352,6 +360,9 @@ impl<'a> Attrs<'a> { color_opt: None, family: Family::SansSerif, font_id_opt: None, + font_weight_axis_opt: None, + font_optical_size_opt: None, + font_italic_axis: false, stretch: Stretch::Normal, style: Style::Normal, weight: Weight::NORMAL, @@ -382,6 +393,21 @@ impl<'a> Attrs<'a> { self } + pub fn font_weight_axis(mut self, value: f32) -> Self { + self.font_weight_axis_opt = Some(VariationValue(value)); + self + } + + pub fn font_optical_size(mut self, value: f32) -> Self { + self.font_optical_size_opt = Some(VariationValue(value)); + self + } + + pub fn font_italic_axis(mut self, italic: bool) -> Self { + self.font_italic_axis = italic; + self + } + /// Set [Stretch] pub fn stretch(mut self, stretch: Stretch) -> Self { self.stretch = stretch; @@ -453,6 +479,9 @@ impl<'a> Attrs<'a> { pub fn compatible(&self, other: &Self) -> bool { self.family == other.family && self.font_id_opt == other.font_id_opt + && self.font_weight_axis_opt == other.font_weight_axis_opt + && self.font_optical_size_opt == other.font_optical_size_opt + && self.font_italic_axis == other.font_italic_axis && self.stretch == other.stretch && self.style == other.style && self.weight == other.weight @@ -487,6 +516,9 @@ pub struct AttrsOwned { pub color_opt: Option, pub family_owned: FamilyOwned, pub font_id_opt: Option, + pub font_weight_axis_opt: Option, + pub font_optical_size_opt: Option, + pub font_italic_axis: bool, pub stretch: Stretch, pub style: Style, pub weight: Weight, @@ -505,6 +537,9 @@ impl AttrsOwned { color_opt: attrs.color_opt, family_owned: FamilyOwned::new(attrs.family), font_id_opt: attrs.font_id_opt, + font_weight_axis_opt: attrs.font_weight_axis_opt, + font_optical_size_opt: attrs.font_optical_size_opt, + font_italic_axis: attrs.font_italic_axis, stretch: attrs.stretch, style: attrs.style, weight: attrs.weight, @@ -522,6 +557,9 @@ impl AttrsOwned { color_opt: self.color_opt, family: self.family_owned.as_family(), font_id_opt: self.font_id_opt, + font_weight_axis_opt: self.font_weight_axis_opt, + font_optical_size_opt: self.font_optical_size_opt, + font_italic_axis: self.font_italic_axis, stretch: self.stretch, style: self.style, weight: self.weight, diff --git a/vendor/cosmic-text/src/layout.rs b/vendor/cosmic-text/src/layout.rs index 5f7287d..a7b1327 100644 --- a/vendor/cosmic-text/src/layout.rs +++ b/vendor/cosmic-text/src/layout.rs @@ -22,6 +22,14 @@ pub struct LayoutGlyph { pub font_id: fontdb::ID, /// Font id of the glyph pub glyph_id: u16, + /// Whether the selected font face has variation axes. + pub font_is_variable: bool, + /// Automatic `wght` axis value requested by the span. + pub font_weight_axis_opt: Option, + /// Automatic `opsz` axis value requested by the span. + pub font_optical_size_opt: Option, + /// Whether the span requests an italic variation axis. + pub font_italic_axis: bool, /// X offset of hitbox pub x: f32, /// Y offset of hitbox diff --git a/vendor/cosmic-text/src/shape.rs b/vendor/cosmic-text/src/shape.rs index 07b6509..7abcd81 100644 --- a/vendor/cosmic-text/src/shape.rs +++ b/vendor/cosmic-text/src/shape.rs @@ -144,10 +144,41 @@ fn shape_fallback( // requests non-default variation coordinates, then use the varied face for // metrics, plan construction, and shaping so advances and glyph selection // are derived from the same instance. - let varied_face = if attrs.font_variations.is_empty() { + let varied_face = if !font.rustybuzz().is_variable() + || (attrs.font_variations.is_empty() + && attrs.font_weight_axis_opt.is_none() + && attrs.font_optical_size_opt.is_none() + && !attrs.font_italic_axis) + { None } else { let mut face = font.rustybuzz().clone(); + if let Some(value) = attrs + .font_weight_axis_opt + .map(|value| value.0) + .filter(|value| value.is_finite()) + { + let _ = face.set_variation(rustybuzz::ttf_parser::Tag::from_bytes(b"wght"), value); + } + if let Some(value) = attrs + .font_optical_size_opt + .map(|value| value.0) + .filter(|value| value.is_finite()) + { + let _ = face.set_variation(rustybuzz::ttf_parser::Tag::from_bytes(b"opsz"), value); + } + if attrs.font_italic_axis { + let italic = rustybuzz::ttf_parser::Tag::from_bytes(b"ital"); + if face + .variation_axes() + .into_iter() + .any(|axis| axis.tag == italic) + { + let _ = face.set_variation(italic, 1.0); + } else { + let _ = face.set_variation(rustybuzz::ttf_parser::Tag::from_bytes(b"slnt"), -14.0); + } + } for variation in attrs .font_variations .iter() @@ -257,6 +288,10 @@ fn shape_fallback( font_monospace_em_width: font.monospace_em_width(), font_id: font.id(), glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"), + font_is_variable: face.is_variable(), + font_weight_axis_opt: attrs.font_weight_axis_opt.map(|value| value.0), + font_optical_size_opt: attrs.font_optical_size_opt.map(|value| value.0), + font_italic_axis: attrs.font_italic_axis, //TODO: color should not be related to shaping color_opt: attrs.color_opt, metadata: attrs.metadata, @@ -539,6 +574,7 @@ fn shape_skip( .expect("no default font found"); let font_id = font.id(); let font_monospace_em_width = font.monospace_em_width(); + let font_is_variable = font.rustybuzz().is_variable(); let font = font.as_swash(); let charmap = font.charmap(); @@ -569,6 +605,10 @@ fn shape_skip( font_monospace_em_width, font_id, glyph_id, + font_is_variable, + font_weight_axis_opt: attrs.font_weight_axis_opt.map(|value| value.0), + font_optical_size_opt: attrs.font_optical_size_opt.map(|value| value.0), + font_italic_axis: attrs.font_italic_axis, color_opt: attrs.color_opt, metadata: attrs.metadata, cache_key_flags: attrs.cache_key_flags, @@ -592,6 +632,10 @@ pub struct ShapeGlyph { pub font_monospace_em_width: Option, pub font_id: fontdb::ID, pub glyph_id: u16, + pub font_is_variable: bool, + pub font_weight_axis_opt: Option, + pub font_optical_size_opt: Option, + pub font_italic_axis: bool, pub color_opt: Option, pub metadata: usize, pub cache_key_flags: CacheKeyFlags, @@ -615,6 +659,10 @@ impl ShapeGlyph { line_height_opt, font_id: self.font_id, glyph_id: self.glyph_id, + font_is_variable: self.font_is_variable, + font_weight_axis_opt: self.font_weight_axis_opt, + font_optical_size_opt: self.font_optical_size_opt, + font_italic_axis: self.font_italic_axis, x, y, w,