Resolve variations on fallback faces
This commit is contained in:
@@ -95,20 +95,13 @@ struct LoadedFace {
|
||||
font_id: Option<cosmic_text::fontdb::ID>,
|
||||
min_weight: u16,
|
||||
max_weight: u16,
|
||||
shape_weight: u16,
|
||||
italic: bool,
|
||||
has_optical_size: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ResolvedFont {
|
||||
family: Arc<str>,
|
||||
font_id: Option<cosmic_text::fontdb::ID>,
|
||||
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<u16>,
|
||||
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<FontVariations>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct VariationIntentKey {
|
||||
font_id: cosmic_text::fontdb::ID,
|
||||
weight_bits: Option<u32>,
|
||||
optical_size_bits: Option<u32>,
|
||||
italic: bool,
|
||||
explicit: Option<Arc<FontVariations>>,
|
||||
}
|
||||
|
||||
/// 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<VariableCacheKey, Option<SwashImage>>,
|
||||
/// 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<VariationIntentKey, Option<Arc<FontVariations>>>,
|
||||
}
|
||||
|
||||
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<f32>,
|
||||
optical_size: Option<f32>,
|
||||
italic: bool,
|
||||
explicit: Option<Arc<FontVariations>>,
|
||||
) -> Option<Arc<FontVariations>> {
|
||||
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<F: FnMut(i32, i32, Color)>(
|
||||
&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<cosmic_text::fontdb::ID>,
|
||||
variations: Option<Arc<FontVariations>>,
|
||||
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<cosmic_text::fontdb::ID>,
|
||||
variations: Option<Arc<FontVariations>>,
|
||||
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<Arc<FontVariations>> {
|
||||
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::<Vec<_>>();
|
||||
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<u32>>,
|
||||
) {
|
||||
let (mut engine, _, variable_id) = isolated_fallback_engine();
|
||||
let tree = obscura_dom::parse_html("<p id='copy'>A</p>");
|
||||
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<u32>,
|
||||
>::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<u16>) {
|
||||
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,
|
||||
|
||||
38
vendor/cosmic-text/src/attrs.rs
vendored
38
vendor/cosmic-text/src/attrs.rs
vendored
@@ -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<fontdb::ID>,
|
||||
/// High-level CSS `font-weight` coordinate, applied only when the actual
|
||||
/// shaped face provides a `wght` axis.
|
||||
pub font_weight_axis_opt: Option<VariationValue>,
|
||||
/// Automatic optical-size coordinate derived from the used font size.
|
||||
/// `None` represents `font-optical-sizing: none`.
|
||||
pub font_optical_size_opt: Option<VariationValue>,
|
||||
/// 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<Color>,
|
||||
pub family_owned: FamilyOwned,
|
||||
pub font_id_opt: Option<fontdb::ID>,
|
||||
pub font_weight_axis_opt: Option<VariationValue>,
|
||||
pub font_optical_size_opt: Option<VariationValue>,
|
||||
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,
|
||||
|
||||
8
vendor/cosmic-text/src/layout.rs
vendored
8
vendor/cosmic-text/src/layout.rs
vendored
@@ -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<f32>,
|
||||
/// Automatic `opsz` axis value requested by the span.
|
||||
pub font_optical_size_opt: Option<f32>,
|
||||
/// Whether the span requests an italic variation axis.
|
||||
pub font_italic_axis: bool,
|
||||
/// X offset of hitbox
|
||||
pub x: f32,
|
||||
/// Y offset of hitbox
|
||||
|
||||
50
vendor/cosmic-text/src/shape.rs
vendored
50
vendor/cosmic-text/src/shape.rs
vendored
@@ -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<f32>,
|
||||
pub font_id: fontdb::ID,
|
||||
pub glyph_id: u16,
|
||||
pub font_is_variable: bool,
|
||||
pub font_weight_axis_opt: Option<f32>,
|
||||
pub font_optical_size_opt: Option<f32>,
|
||||
pub font_italic_axis: bool,
|
||||
pub color_opt: Option<Color>,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user