summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-04-02 21:55:25 +0200
committerLaurenz <laurmaedje@gmail.com>2022-04-03 13:55:58 +0200
commit23d108c8e099798dc4d35ce9cbcd3e37fb50f3b2 (patch)
treeaa068b11b9ac0a4795fb6e86bb8283b1d4718e95 /src/library
parentbeca01c826ee51c9ee6d5eadd7e5ef10f7fb9f58 (diff)
Font fallback
Diffstat (limited to 'src/library')
-rw-r--r--src/library/math/mod.rs28
-rw-r--r--src/library/mod.rs3
-rw-r--r--src/library/structure/heading.rs8
-rw-r--r--src/library/text/deco.rs10
-rw-r--r--src/library/text/mod.rs70
-rw-r--r--src/library/text/raw.rs34
-rw-r--r--src/library/text/shaping.rs408
7 files changed, 274 insertions, 287 deletions
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs
index f20d6543..666e40a7 100644
--- a/src/library/math/mod.rs
+++ b/src/library/math/mod.rs
@@ -1,6 +1,7 @@
//! Mathematical formulas.
use crate::library::prelude::*;
+use crate::library::text::FontFamily;
/// A mathematical formula.
#[derive(Debug, Hash)]
@@ -13,6 +14,10 @@ pub struct MathNode {
#[node(showable)]
impl MathNode {
+ /// The raw text's font family. Just the normal text family if `none`.
+ pub const FAMILY: Smart<FontFamily> =
+ Smart::Custom(FontFamily::new("Latin Modern Math"));
+
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::show(Self {
formula: args.expect("formula")?,
@@ -23,17 +28,24 @@ impl MathNode {
impl Show for MathNode {
fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> {
- Ok(styles
+ let mut content = styles
.show(self, ctx, [
Value::Str(self.formula.clone()),
Value::Bool(self.display),
])?
- .unwrap_or_else(|| {
- let mut content = Content::Text(self.formula.trim().into());
- if self.display {
- content = Content::Block(content.pack());
- }
- content.monospaced()
- }))
+ .unwrap_or_else(|| Content::Text(self.formula.trim().into()));
+
+ let mut map = StyleMap::new();
+ if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) {
+ map.set_family(family, styles);
+ }
+
+ content = content.styled_with_map(map);
+
+ if self.display {
+ content = Content::Block(content.pack());
+ }
+
+ Ok(content)
}
}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 528a2ce7..bba002de 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -119,9 +119,6 @@ pub fn new() -> Scope {
std.def_const("top", Align::Top);
std.def_const("horizon", Align::Horizon);
std.def_const("bottom", Align::Bottom);
- std.def_const("serif", text::FontFamily::Serif);
- std.def_const("sans-serif", text::FontFamily::SansSerif);
- std.def_const("monospace", text::FontFamily::Monospace);
std
}
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs
index f1bc795f..7d3273f5 100644
--- a/src/library/structure/heading.rs
+++ b/src/library/structure/heading.rs
@@ -63,12 +63,7 @@ impl Show for HeadingNode {
map.set(TextNode::SIZE, resolve!(Self::SIZE));
if let Smart::Custom(family) = resolve!(Self::FAMILY) {
- map.set(
- TextNode::FAMILY,
- std::iter::once(family)
- .chain(styles.get_ref(TextNode::FAMILY).iter().cloned())
- .collect(),
- );
+ map.set_family(family, styles);
}
if let Smart::Custom(fill) = resolve!(Self::FILL) {
@@ -101,6 +96,7 @@ impl Show for HeadingNode {
}
let mut content = Content::sequence(seq).styled_with_map(map);
+
if resolve!(Self::BLOCK) {
content = Content::block(content);
}
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs
index 29c04b2d..b98eb0b2 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -94,10 +94,11 @@ pub fn decorate(
width: Length,
) {
let face = fonts.get(text.face_id);
+ let face_metrics = face.metrics();
let metrics = match deco.line {
- STRIKETHROUGH => face.strikethrough,
- OVERLINE => face.overline,
- UNDERLINE | _ => face.underline,
+ STRIKETHROUGH => face_metrics.strikethrough,
+ OVERLINE => face_metrics.overline,
+ UNDERLINE | _ => face_metrics.underline,
};
let evade = deco.evade && deco.line != STRIKETHROUGH;
@@ -146,7 +147,8 @@ pub fn decorate(
for glyph in text.glyphs.iter() {
let dx = glyph.x_offset.resolve(text.size) + x;
- let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw());
+ let mut builder =
+ BezPathBuilder::new(face_metrics.units_per_em, text.size, dx.to_raw());
let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
let path = builder.finish();
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
index eef7f6fb..2c163a59 100644
--- a/src/library/text/mod.rs
+++ b/src/library/text/mod.rs
@@ -29,13 +29,7 @@ pub struct TextNode;
impl TextNode {
/// A prioritized sequence of font families.
#[variadic]
- pub const FAMILY: Vec<FontFamily> = vec![FontFamily::SansSerif];
- /// The serif font family/families.
- pub const SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")];
- /// The sans-serif font family/families.
- pub const SANS_SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")];
- /// The monospace font family/families.
- pub const MONOSPACE: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")];
+ pub const FAMILY: Vec<FontFamily> = vec![FontFamily::new("IBM Plex Sans")];
/// Whether to allow font fallback when the primary font list contains no
/// match.
pub const FALLBACK: bool = true;
@@ -100,9 +94,6 @@ impl TextNode {
#[skip]
#[fold(bool::bitxor)]
pub const EMPH: bool = false;
- /// Whether a monospace font should be preferred.
- #[skip]
- pub const MONOSPACED: bool = false;
/// The case transformation that should be applied to the next.
#[skip]
pub const CASE: Option<Case> = None;
@@ -160,50 +151,11 @@ impl Show for EmphNode {
}
}
-/// A generic or named font family.
-#[derive(Clone, Eq, PartialEq, Hash)]
-pub enum FontFamily {
- /// A family that has "serifs", small strokes attached to letters.
- Serif,
- /// A family in which glyphs do not have "serifs", small attached strokes.
- SansSerif,
- /// A family in which (almost) all glyphs are of equal width.
- Monospace,
- /// A specific font family like "Arial".
- Named(NamedFamily),
-}
-
-impl Debug for FontFamily {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Serif => f.pad("serif"),
- Self::SansSerif => f.pad("sans-serif"),
- Self::Monospace => f.pad("monospace"),
- Self::Named(s) => s.fmt(f),
- }
- }
-}
-
-dynamic! {
- FontFamily: "font family",
- Value::Str(string) => Self::Named(NamedFamily::new(&string)),
-}
-
-castable! {
- Vec<FontFamily>,
- Expected: "string, generic family or array thereof",
- Value::Str(string) => vec![FontFamily::Named(NamedFamily::new(&string))],
- Value::Array(values) => {
- values.into_iter().filter_map(|v| v.cast().ok()).collect()
- },
- @family: FontFamily => vec![family.clone()],
-}
-
-/// A specific font family like "Arial".
+/// A font family like "Arial".
#[derive(Clone, Eq, PartialEq, Hash)]
-pub struct NamedFamily(EcoString);
+pub struct FontFamily(EcoString);
-impl NamedFamily {
+impl FontFamily {
/// Create a named font family variant.
pub fn new(string: &str) -> Self {
Self(string.to_lowercase().into())
@@ -215,20 +167,26 @@ impl NamedFamily {
}
}
-impl Debug for NamedFamily {
+impl Debug for FontFamily {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
castable! {
- Vec<NamedFamily>,
+ FontFamily,
+ Expected: "string",
+ Value::Str(string) => Self::new(&string),
+}
+
+castable! {
+ Vec<FontFamily>,
Expected: "string or array of strings",
- Value::Str(string) => vec![NamedFamily::new(&string)],
+ Value::Str(string) => vec![FontFamily::new(&string)],
Value::Array(values) => values
.into_iter()
.filter_map(|v| v.cast().ok())
- .map(|string: EcoString| NamedFamily::new(&string))
+ .map(|string: EcoString| FontFamily::new(&string))
.collect(),
}
diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs
index e225803f..5c2133c2 100644
--- a/src/library/text/raw.rs
+++ b/src/library/text/raw.rs
@@ -3,8 +3,8 @@ use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
use syntect::parsing::SyntaxSet;
+use super::{FontFamily, TextNode};
use crate::library::prelude::*;
-use crate::library::text::TextNode;
use crate::source::SourceId;
use crate::syntax::{self, RedNode};
@@ -26,6 +26,8 @@ pub struct RawNode {
#[node(showable)]
impl RawNode {
+ /// The raw text's font family. Just the normal text family if `none`.
+ pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
/// The language to syntax-highlight in.
pub const LANG: Option<EcoString> = None;
@@ -40,8 +42,14 @@ impl RawNode {
impl Show for RawNode {
fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> {
let lang = styles.get_ref(Self::LANG).as_ref();
+ let foreground = THEME
+ .settings
+ .foreground
+ .map(Color::from)
+ .unwrap_or(Color::BLACK)
+ .into();
- if let Some(content) = styles.show(self, ctx, [
+ let mut content = if let Some(content) = styles.show(self, ctx, [
Value::Str(self.text.clone()),
match lang {
Some(lang) => Value::Str(lang.clone()),
@@ -49,17 +57,8 @@ impl Show for RawNode {
},
Value::Bool(self.block),
])? {
- return Ok(content);
- }
-
- let foreground = THEME
- .settings
- .foreground
- .map(Color::from)
- .unwrap_or(Color::BLACK)
- .into();
-
- let mut content = if matches!(
+ content
+ } else if matches!(
lang.map(|s| s.to_lowercase()).as_deref(),
Some("typ" | "typst")
) {
@@ -93,11 +92,18 @@ impl Show for RawNode {
Content::Text(self.text.clone())
};
+ let mut map = StyleMap::new();
+ if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) {
+ map.set_family(family, styles);
+ }
+
+ content = content.styled_with_map(map);
+
if self.block {
content = Content::Block(content.pack());
}
- Ok(content.monospaced())
+ Ok(content)
}
}
diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs
index 29141331..6087032f 100644
--- a/src/library/text/shaping.rs
+++ b/src/library/text/shaping.rs
@@ -236,6 +236,18 @@ impl<'a> ShapedText<'a> {
}
}
+/// Holds shaping results and metadata common to all shaped segments.
+struct ShapingContext<'a> {
+ fonts: &'a mut FontStore,
+ glyphs: Vec<ShapedGlyph>,
+ used: Vec<FaceId>,
+ styles: StyleChain<'a>,
+ variant: FontVariant,
+ tags: Vec<rustybuzz::Feature>,
+ fallback: bool,
+ dir: Dir,
+}
+
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
fonts: &mut FontStore,
@@ -248,28 +260,24 @@ pub fn shape<'a>(
None => Cow::Borrowed(text),
};
- let mut glyphs = vec![];
+ let mut ctx = ShapingContext {
+ fonts,
+ glyphs: vec![],
+ used: vec![],
+ styles,
+ variant: variant(styles),
+ tags: tags(styles),
+ fallback: styles.get(TextNode::FALLBACK),
+ dir,
+ };
+
if !text.is_empty() {
- shape_segment(
- fonts,
- &mut glyphs,
- 0,
- &text,
- variant(styles),
- families(styles),
- None,
- dir,
- &tags(styles),
- );
- }
-
- track_and_space(
- &mut glyphs,
- styles.get(TextNode::TRACKING),
- styles.get(TextNode::SPACING),
- );
+ shape_segment(&mut ctx, 0, &text, families(styles));
+ }
+
+ track_and_space(&mut ctx);
- let (size, baseline) = measure(fonts, &glyphs, styles);
+ let (size, baseline) = measure(ctx.fonts, &ctx.glyphs, styles);
ShapedText {
text,
@@ -277,186 +285,62 @@ pub fn shape<'a>(
styles,
size,
baseline,
- glyphs: Cow::Owned(glyphs),
+ glyphs: Cow::Owned(ctx.glyphs),
}
}
-/// Resolve the font variant with `STRONG` and `EMPH` factored in.
-fn variant(styles: StyleChain) -> FontVariant {
- let mut variant = FontVariant::new(
- styles.get(TextNode::STYLE),
- styles.get(TextNode::WEIGHT),
- styles.get(TextNode::STRETCH),
- );
-
- if styles.get(TextNode::STRONG) {
- variant.weight = variant.weight.thicken(300);
- }
-
- if styles.get(TextNode::EMPH) {
- variant.style = match variant.style {
- FontStyle::Normal => FontStyle::Italic,
- FontStyle::Italic => FontStyle::Normal,
- FontStyle::Oblique => FontStyle::Normal,
- }
- }
-
- variant
-}
-
-/// Resolve a prioritized iterator over the font families.
-fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
- let head = if styles.get(TextNode::MONOSPACED) {
- styles.get_ref(TextNode::MONOSPACE).as_slice()
- } else {
- &[]
- };
-
- let core = styles.get_ref(TextNode::FAMILY).iter().flat_map(move |family| {
- match family {
- FontFamily::Named(name) => std::slice::from_ref(name),
- FontFamily::Serif => styles.get_ref(TextNode::SERIF),
- FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF),
- FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE),
- }
- });
-
- let tail: &[&str] = if styles.get(TextNode::FALLBACK) {
- &["ibm plex sans", "latin modern math", "twitter color emoji"]
- } else {
- &[]
- };
-
- head.iter()
- .chain(core)
- .map(|named| named.as_str())
- .chain(tail.iter().copied())
-}
-
-/// Collect the tags of the OpenType features to apply.
-fn tags(styles: StyleChain) -> Vec<Feature> {
- let mut tags = vec![];
- let mut feat = |tag, value| {
- tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
- };
-
- // Features that are on by default in Harfbuzz are only added if disabled.
- if !styles.get(TextNode::KERNING) {
- feat(b"kern", 0);
- }
-
- // Features that are off by default in Harfbuzz are only added if enabled.
- if styles.get(TextNode::SMALLCAPS) {
- feat(b"smcp", 1);
- }
-
- if styles.get(TextNode::ALTERNATES) {
- feat(b"salt", 1);
- }
-
- let storage;
- if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
- storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
- feat(&storage, 1);
- }
-
- if !styles.get(TextNode::LIGATURES) {
- feat(b"liga", 0);
- feat(b"clig", 0);
- }
-
- if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
- feat(b"dlig", 1);
- }
-
- if styles.get(TextNode::HISTORICAL_LIGATURES) {
- feat(b"hilg", 1);
- }
-
- match styles.get(TextNode::NUMBER_TYPE) {
- Smart::Auto => {}
- Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
- Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
- }
-
- match styles.get(TextNode::NUMBER_WIDTH) {
- Smart::Auto => {}
- Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
- Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
- }
-
- match styles.get(TextNode::NUMBER_POSITION) {
- NumberPosition::Normal => {}
- NumberPosition::Subscript => feat(b"subs", 1),
- NumberPosition::Superscript => feat(b"sups", 1),
- }
-
- if styles.get(TextNode::SLASHED_ZERO) {
- feat(b"zero", 1);
- }
-
- if styles.get(TextNode::FRACTIONS) {
- feat(b"frac", 1);
- }
-
- for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
- tags.push(Feature::new(tag, value, ..))
- }
-
- tags
-}
-
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
- fonts: &mut FontStore,
- glyphs: &mut Vec<ShapedGlyph>,
+ ctx: &mut ShapingContext,
base: usize,
text: &str,
- variant: FontVariant,
mut families: impl Iterator<Item = &'a str> + Clone,
- mut first_face: Option<FaceId>,
- dir: Dir,
- tags: &[rustybuzz::Feature],
) {
- // No font has newlines.
- if text.chars().all(|c| c == '\n') {
+ // Fonts dont have newlines and tabs.
+ if text.chars().all(|c| c == '\n' || c == '\t') {
return;
}
- // Select the font family.
- let (face_id, fallback) = loop {
- // Try to load the next available font family.
- match families.next() {
- Some(family) => {
- if let Some(id) = fonts.select(family, variant) {
- break (id, true);
- }
- }
- // We're out of families, so we don't do any more fallback and just
- // shape the tofus with the first face we originally used.
- None => match first_face {
- Some(id) => break (id, false),
- None => return,
- },
+ // Find the next available family.
+ let mut selection = families.find_map(|family| {
+ ctx.fonts
+ .select(family, ctx.variant)
+ .filter(|id| !ctx.used.contains(id))
+ });
+
+ // Do font fallback if the families are exhausted and fallback is enabled.
+ if selection.is_none() && ctx.fallback {
+ let first = ctx.used.first().copied();
+ selection = ctx
+ .fonts
+ .select_fallback(first, ctx.variant, text)
+ .filter(|id| !ctx.used.contains(id));
+ }
+
+ // Extract the face id or shape notdef glyphs if we couldn't find any face.
+ let face_id = if let Some(id) = selection {
+ id
+ } else {
+ if let Some(&face_id) = ctx.used.first() {
+ shape_tofus(ctx, base, text, face_id);
}
+ return;
};
- // Remember the id if this the first available face since we use that one to
- // shape tofus.
- first_face.get_or_insert(face_id);
+ ctx.used.push(face_id);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
- buffer.set_direction(match dir {
+ buffer.set_direction(match ctx.dir {
Dir::LTR => rustybuzz::Direction::LeftToRight,
Dir::RTL => rustybuzz::Direction::RightToLeft,
- _ => unimplemented!(),
+ _ => unimplemented!("vertical text layout"),
});
// Shape!
- let mut face = fonts.get(face_id);
- let buffer = rustybuzz::shape(face.ttf(), tags, buffer);
+ let mut face = ctx.fonts.get(face_id);
+ let buffer = rustybuzz::shape(face.ttf(), &ctx.tags, buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
@@ -467,10 +351,10 @@ fn shape_segment<'a>(
let info = &infos[i];
let cluster = info.cluster as usize;
- if info.glyph_id != 0 || !fallback {
+ if info.glyph_id != 0 {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
- glyphs.push(ShapedGlyph {
+ ctx.glyphs.push(ShapedGlyph {
face_id,
glyph_id: info.glyph_id as u16,
x_advance: face.to_em(pos[i].x_advance),
@@ -506,7 +390,7 @@ fn shape_segment<'a>(
// Glyphs: E C _ _ A
// Clusters: 8 6 4 2 0
// k=2 i=3
- let ltr = dir.is_positive();
+ let ltr = ctx.dir.is_positive();
let first = if ltr { k } else { i };
let start = infos[first].cluster as usize;
let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
@@ -517,33 +401,50 @@ fn shape_segment<'a>(
start .. end
};
+ // Trim half-baked cluster.
+ let remove = base + range.start .. base + range.end;
+ while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.cluster)) {
+ ctx.glyphs.pop();
+ }
+
// Recursively shape the tofu sequence with the next family.
- shape_segment(
- fonts,
- glyphs,
- base + range.start,
- &text[range],
- variant,
- families.clone(),
- first_face,
- dir,
- tags,
- );
-
- face = fonts.get(face_id);
+ shape_segment(ctx, base + range.start, &text[range], families.clone());
+
+ face = ctx.fonts.get(face_id);
}
i += 1;
}
+
+ ctx.used.pop();
+}
+
+/// Shape the text with tofus from the given face.
+fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, face_id: FaceId) {
+ let face = ctx.fonts.get(face_id);
+ let x_advance = face.advance(0).unwrap_or_default();
+ for (cluster, c) in text.char_indices() {
+ ctx.glyphs.push(ShapedGlyph {
+ face_id,
+ glyph_id: 0,
+ x_advance,
+ x_offset: Em::zero(),
+ cluster: base + cluster,
+ safe_to_break: true,
+ c,
+ });
+ }
}
/// Apply tracking and spacing to a slice of shaped glyphs.
-fn track_and_space(glyphs: &mut [ShapedGlyph], tracking: Em, spacing: Relative) {
+fn track_and_space(ctx: &mut ShapingContext) {
+ let tracking = ctx.styles.get(TextNode::TRACKING);
+ let spacing = ctx.styles.get(TextNode::SPACING);
if tracking.is_zero() && spacing.is_one() {
return;
}
- let mut glyphs = glyphs.iter_mut().peekable();
+ let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
if glyph.is_space() {
glyph.x_advance *= spacing.get();
@@ -572,8 +473,9 @@ fn measure(
// Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| {
- top.set_max(face.vertical_metric(top_edge, size));
- bottom.set_max(-face.vertical_metric(bottom_edge, size));
+ let metrics = face.metrics();
+ top.set_max(metrics.vertical(top_edge, size));
+ bottom.set_max(-metrics.vertical(bottom_edge, size));
};
if glyphs.is_empty() {
@@ -599,3 +501,117 @@ fn measure(
(Size::new(width, top + bottom), top)
}
+
+/// Resolve the font variant with `STRONG` and `EMPH` factored in.
+fn variant(styles: StyleChain) -> FontVariant {
+ let mut variant = FontVariant::new(
+ styles.get(TextNode::STYLE),
+ styles.get(TextNode::WEIGHT),
+ styles.get(TextNode::STRETCH),
+ );
+
+ if styles.get(TextNode::STRONG) {
+ variant.weight = variant.weight.thicken(300);
+ }
+
+ if styles.get(TextNode::EMPH) {
+ variant.style = match variant.style {
+ FontStyle::Normal => FontStyle::Italic,
+ FontStyle::Italic => FontStyle::Normal,
+ FontStyle::Oblique => FontStyle::Normal,
+ }
+ }
+
+ variant
+}
+
+/// Resolve a prioritized iterator over the font families.
+fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
+ const FALLBACKS: &[&str] = &[
+ "ibm plex sans",
+ "twitter color emoji",
+ "noto color emoji",
+ "apple color emoji",
+ "segoe ui emoji",
+ ];
+
+ let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] };
+ styles
+ .get_ref(TextNode::FAMILY)
+ .iter()
+ .map(|family| family.as_str())
+ .chain(tail.iter().copied())
+}
+
+/// Collect the tags of the OpenType features to apply.
+fn tags(styles: StyleChain) -> Vec<Feature> {
+ let mut tags = vec![];
+ let mut feat = |tag, value| {
+ tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
+ };
+
+ // Features that are on by default in Harfbuzz are only added if disabled.
+ if !styles.get(TextNode::KERNING) {
+ feat(b"kern", 0);
+ }
+
+ // Features that are off by default in Harfbuzz are only added if enabled.
+ if styles.get(TextNode::SMALLCAPS) {
+ feat(b"smcp", 1);
+ }
+
+ if styles.get(TextNode::ALTERNATES) {
+ feat(b"salt", 1);
+ }
+
+ let storage;
+ if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
+ storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
+ feat(&storage, 1);
+ }
+
+ if !styles.get(TextNode::LIGATURES) {
+ feat(b"liga", 0);
+ feat(b"clig", 0);
+ }
+
+ if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
+ feat(b"dlig", 1);
+ }
+
+ if styles.get(TextNode::HISTORICAL_LIGATURES) {
+ feat(b"hilg", 1);
+ }
+
+ match styles.get(TextNode::NUMBER_TYPE) {
+ Smart::Auto => {}
+ Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
+ Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
+ }
+
+ match styles.get(TextNode::NUMBER_WIDTH) {
+ Smart::Auto => {}
+ Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
+ Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
+ }
+
+ match styles.get(TextNode::NUMBER_POSITION) {
+ NumberPosition::Normal => {}
+ NumberPosition::Subscript => feat(b"subs", 1),
+ NumberPosition::Superscript => feat(b"sups", 1),
+ }
+
+ if styles.get(TextNode::SLASHED_ZERO) {
+ feat(b"zero", 1);
+ }
+
+ if styles.get(TextNode::FRACTIONS) {
+ feat(b"frac", 1);
+ }
+
+ for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
+ tags.push(Feature::new(tag, value, ..))
+ }
+
+ tags
+}