render: preserve numeric font weights
This commit is contained in:
1 parent
d6be828d17
commit
20be62bca9
6 files changed
+266
-47
No files matched your search
@@ -488,7 +488,7 @@ pub fn layout_dom_with_resources(
|
|||||||
struct Inherited {
|
struct Inherited {
|
||||||
color: Option<[u8; 4]>,
|
color: Option<[u8; 4]>,
|
||||||
font_size: Option<f32>,
|
font_size: Option<f32>,
|
||||||
font_weight: Option<String>,
|
font_weight: u16,
|
||||||
font_family: Option<String>,
|
font_family: Option<String>,
|
||||||
text_align: Option<taffy::AlignItems>,
|
text_align: Option<taffy::AlignItems>,
|
||||||
legacy_center: bool,
|
legacy_center: bool,
|
||||||
@@ -518,7 +518,7 @@ pub fn layout_dom_with_resources(
|
|||||||
Inherited {
|
Inherited {
|
||||||
color: None,
|
color: None,
|
||||||
font_size: None,
|
font_size: None,
|
||||||
font_weight: None,
|
font_weight: 400,
|
||||||
font_family: None,
|
font_family: None,
|
||||||
text_align: None,
|
text_align: None,
|
||||||
legacy_center: false,
|
legacy_center: false,
|
||||||
@@ -776,7 +776,12 @@ pub fn layout_dom_with_resources(
|
|||||||
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
match &style.font_weight { Some(w) => inh.font_weight = Some(w.clone()), None => style.font_weight = inh.font_weight.clone() }
|
let computed_weight = crate::style::computed_font_weight(
|
||||||
|
style.font_weight.as_deref(),
|
||||||
|
inh.font_weight,
|
||||||
|
);
|
||||||
|
style.font_weight = Some(computed_weight.to_string());
|
||||||
|
inh.font_weight = computed_weight;
|
||||||
match &style.font_family { Some(f) => inh.font_family = Some(f.clone()), None => style.font_family = inh.font_family.clone() }
|
match &style.font_family { Some(f) => inh.font_family = Some(f.clone()), None => style.font_family = inh.font_family.clone() }
|
||||||
let is_table = tree
|
let is_table = tree
|
||||||
.get_node(id)
|
.get_node(id)
|
||||||
@@ -2740,7 +2745,7 @@ fn build_text_words(
|
|||||||
if let Some(parent_id) = node.parent {
|
if let Some(parent_id) = node.parent {
|
||||||
if let Some(p_style) = styles.get(&parent_id) {
|
if let Some(p_style) = styles.get(&parent_id) {
|
||||||
fsize = p_style.font_size.unwrap_or(16.0);
|
fsize = p_style.font_size.unwrap_or(16.0);
|
||||||
is_bold = p_style.font_weight.as_deref() == Some("bold");
|
is_bold = crate::style::used_font_weight(p_style) >= 600;
|
||||||
family = p_style.font_family.as_deref();
|
family = p_style.font_family.as_deref();
|
||||||
line_height = crate::inline::used_line_height(p_style);
|
line_height = crate::inline::used_line_height(p_style);
|
||||||
transform = p_style
|
transform = p_style
|
||||||
@@ -2839,7 +2844,7 @@ fn build_pseudo_content(
|
|||||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||||
) -> Vec<taffy::NodeId> {
|
) -> Vec<taffy::NodeId> {
|
||||||
let fsize = style.font_size.unwrap_or(16.0);
|
let fsize = style.font_size.unwrap_or(16.0);
|
||||||
let is_bold = style.font_weight.as_deref() == Some("bold");
|
let is_bold = crate::style::used_font_weight(style) >= 600;
|
||||||
build_word_leaves(
|
build_word_leaves(
|
||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -78,10 +78,18 @@ fn resolve_font_family(fam: Option<&str>) -> &'static str {
|
|||||||
FAMILY
|
FAMILY
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_loaded_font_family(
|
#[derive(Clone)]
|
||||||
|
struct LoadedFamily {
|
||||||
|
name: Arc<str>,
|
||||||
|
weights: Vec<u16>,
|
||||||
|
variable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_loaded_font(
|
||||||
fam: Option<&str>,
|
fam: Option<&str>,
|
||||||
loaded: &HashMap<String, Arc<str>>,
|
requested_weight: u16,
|
||||||
) -> Arc<str> {
|
loaded: &HashMap<String, LoadedFamily>,
|
||||||
|
) -> (Arc<str>, u16) {
|
||||||
if let Some(stack) = fam {
|
if let Some(stack) = fam {
|
||||||
for token in stack.split(',') {
|
for token in stack.split(',') {
|
||||||
let name = token
|
let name = token
|
||||||
@@ -89,11 +97,81 @@ fn resolve_loaded_font_family(
|
|||||||
.trim_matches(|c| c == '"' || c == '\'')
|
.trim_matches(|c| c == '"' || c == '\'')
|
||||||
.trim();
|
.trim();
|
||||||
if let Some(family) = loaded.get(&name.to_ascii_lowercase()) {
|
if let Some(family) = loaded.get(&name.to_ascii_lowercase()) {
|
||||||
return Arc::clone(family);
|
let weight = if family.variable {
|
||||||
|
requested_weight
|
||||||
|
} else {
|
||||||
|
match_font_weight(requested_weight, &family.weights)
|
||||||
|
};
|
||||||
|
return (Arc::clone(&family.name), weight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Arc::from(resolve_font_family(fam))
|
let fallback = resolve_font_family(fam);
|
||||||
|
let weights: &[u16] = &[400, 700];
|
||||||
|
(Arc::from(fallback), match_font_weight(requested_weight, weights))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CSS Fonts' asymmetric missing-weight search. In particular, 600 selects
|
||||||
|
/// 700 (not 400) when a family only provides regular and bold faces.
|
||||||
|
fn match_font_weight(requested: u16, available: &[u16]) -> u16 {
|
||||||
|
if available.contains(&requested) {
|
||||||
|
return requested;
|
||||||
|
}
|
||||||
|
let mut weights = available.to_vec();
|
||||||
|
weights.sort_unstable();
|
||||||
|
weights.dedup();
|
||||||
|
if weights.is_empty() {
|
||||||
|
return requested;
|
||||||
|
}
|
||||||
|
if (400..=500).contains(&requested) {
|
||||||
|
weights
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|weight| *weight >= requested && *weight <= 500)
|
||||||
|
.min()
|
||||||
|
.or_else(|| weights.iter().copied().filter(|weight| *weight < requested).max())
|
||||||
|
.or_else(|| weights.iter().copied().filter(|weight| *weight > 500).min())
|
||||||
|
.unwrap_or(requested)
|
||||||
|
} else if requested < 400 {
|
||||||
|
weights
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|weight| *weight <= requested)
|
||||||
|
.max()
|
||||||
|
.or_else(|| weights.iter().copied().filter(|weight| *weight > requested).min())
|
||||||
|
.unwrap_or(requested)
|
||||||
|
} else {
|
||||||
|
weights
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|weight| *weight >= requested)
|
||||||
|
.min()
|
||||||
|
.or_else(|| weights.iter().copied().filter(|weight| *weight < requested).max())
|
||||||
|
.unwrap_or(requested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_is_variable(data: &[u8], face_index: u32) -> bool {
|
||||||
|
let base = if data.get(..4) == Some(b"ttcf") {
|
||||||
|
let offset = 12usize.saturating_add(face_index as usize * 4);
|
||||||
|
data.get(offset..offset + 4)
|
||||||
|
.and_then(|bytes| bytes.try_into().ok())
|
||||||
|
.map(u32::from_be_bytes)
|
||||||
|
.unwrap_or(0) as usize
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let Some(count) = data
|
||||||
|
.get(base + 4..base + 6)
|
||||||
|
.and_then(|bytes| bytes.try_into().ok())
|
||||||
|
.map(u16::from_be_bytes)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
(0..count as usize).any(|index| {
|
||||||
|
let start = base + 12 + index * 16;
|
||||||
|
data.get(start..start + 4) == Some(b"fvar")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/// Resolve `line-height: normal` from the selected face's horizontal header.
|
/// Resolve `line-height: normal` from the selected face's horizontal header.
|
||||||
///
|
///
|
||||||
@@ -166,7 +244,7 @@ pub struct InlineItem {
|
|||||||
/// [`crate::DomLayout`] so paint can rasterize the shaped glyphs.
|
/// [`crate::DomLayout`] so paint can rasterize the shaped glyphs.
|
||||||
pub struct TextEngine {
|
pub struct TextEngine {
|
||||||
font_system: FontSystem,
|
font_system: FontSystem,
|
||||||
loaded_families: HashMap<String, Arc<str>>,
|
loaded_families: HashMap<String, LoadedFamily>,
|
||||||
swash: SwashCache,
|
swash: SwashCache,
|
||||||
items: Vec<InlineItem>,
|
items: Vec<InlineItem>,
|
||||||
replaced: Vec<ReplacedItem>,
|
replaced: Vec<ReplacedItem>,
|
||||||
@@ -256,11 +334,24 @@ impl TextEngine {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let mut loaded_families = HashMap::new();
|
let mut loaded_families = HashMap::new();
|
||||||
for face in db.faces() {
|
let faces: Vec<_> = db
|
||||||
for (name, _) in &face.families {
|
.faces()
|
||||||
loaded_families
|
.map(|face| (face.id, face.families.clone(), face.weight.0))
|
||||||
|
.collect();
|
||||||
|
for (id, names, weight) in faces {
|
||||||
|
let variable = db
|
||||||
|
.with_face_data(id, face_is_variable)
|
||||||
|
.unwrap_or(false);
|
||||||
|
for (name, _) in names {
|
||||||
|
let family = loaded_families
|
||||||
.entry(name.to_ascii_lowercase())
|
.entry(name.to_ascii_lowercase())
|
||||||
.or_insert_with(|| Arc::from(name.as_str()));
|
.or_insert_with(|| LoadedFamily {
|
||||||
|
name: Arc::from(name.as_str()),
|
||||||
|
weights: Vec::new(),
|
||||||
|
variable: false,
|
||||||
|
});
|
||||||
|
family.weights.push(weight);
|
||||||
|
family.variable |= variable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.set_sans_serif_family(FAMILY);
|
db.set_sans_serif_family(FAMILY);
|
||||||
@@ -293,9 +384,15 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
let base = styles.get(&id)?;
|
let base = styles.get(&id)?;
|
||||||
let mut collector = Collector { last_was_space: true, clip_fills: Vec::new() };
|
let mut collector = Collector { last_was_space: true, clip_fills: Vec::new() };
|
||||||
|
let (family, weight) = resolve_loaded_font(
|
||||||
|
base.font_family.as_deref(),
|
||||||
|
crate::style::used_font_weight(base),
|
||||||
|
&self.loaded_families,
|
||||||
|
);
|
||||||
let ctx = base_span_ctx(
|
let ctx = base_span_ctx(
|
||||||
base,
|
base,
|
||||||
resolve_loaded_font_family(base.font_family.as_deref(), &self.loaded_families),
|
family,
|
||||||
|
weight,
|
||||||
&mut collector,
|
&mut collector,
|
||||||
);
|
);
|
||||||
let mut spans: Vec<(String, SpanAttrs)> = Vec::new();
|
let mut spans: Vec<(String, SpanAttrs)> = Vec::new();
|
||||||
@@ -336,9 +433,15 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
let base = styles.get(&parent)?;
|
let base = styles.get(&parent)?;
|
||||||
let mut collector = Collector { last_was_space: true, clip_fills: Vec::new() };
|
let mut collector = Collector { last_was_space: true, clip_fills: Vec::new() };
|
||||||
|
let (family, weight) = resolve_loaded_font(
|
||||||
|
base.font_family.as_deref(),
|
||||||
|
crate::style::used_font_weight(base),
|
||||||
|
&self.loaded_families,
|
||||||
|
);
|
||||||
let ctx = base_span_ctx(
|
let ctx = base_span_ctx(
|
||||||
base,
|
base,
|
||||||
resolve_loaded_font_family(base.font_family.as_deref(), &self.loaded_families),
|
family,
|
||||||
|
weight,
|
||||||
&mut collector,
|
&mut collector,
|
||||||
);
|
);
|
||||||
let mut spans: Vec<(String, SpanAttrs)> = Vec::new();
|
let mut spans: Vec<(String, SpanAttrs)> = Vec::new();
|
||||||
@@ -509,7 +612,7 @@ impl TextEngine {
|
|||||||
/// A run of same-styled inline text.
|
/// A run of same-styled inline text.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct SpanAttrs {
|
struct SpanAttrs {
|
||||||
bold: bool,
|
weight: u16,
|
||||||
italic: bool,
|
italic: bool,
|
||||||
underline: bool,
|
underline: bool,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
@@ -520,7 +623,7 @@ struct SpanAttrs {
|
|||||||
impl SpanAttrs {
|
impl SpanAttrs {
|
||||||
fn to_attrs(&self) -> Attrs<'_> {
|
fn to_attrs(&self) -> Attrs<'_> {
|
||||||
let mut a = Attrs::new().family(Family::Name(self.family.as_ref()));
|
let mut a = Attrs::new().family(Family::Name(self.family.as_ref()));
|
||||||
a = a.weight(if self.bold { Weight::BOLD } else { Weight::NORMAL });
|
a = a.weight(Weight(self.weight));
|
||||||
a = a.style(if self.italic { Style::Italic } else { Style::Normal });
|
a = a.style(if self.italic { Style::Italic } else { Style::Normal });
|
||||||
// Clip-text glyphs must be shaped with an opaque fill so their coverage
|
// Clip-text glyphs must be shaped with an opaque fill so their coverage
|
||||||
// reaches paint; the real gradient is selected through metadata.
|
// reaches paint; the real gradient is selected through metadata.
|
||||||
@@ -537,7 +640,7 @@ impl SpanAttrs {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct SpanCtx {
|
struct SpanCtx {
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
bold: bool,
|
weight: u16,
|
||||||
italic: bool,
|
italic: bool,
|
||||||
underline: bool,
|
underline: bool,
|
||||||
transform: TextTransform,
|
transform: TextTransform,
|
||||||
@@ -566,6 +669,7 @@ struct Collector {
|
|||||||
fn base_span_ctx(
|
fn base_span_ctx(
|
||||||
base: &LayoutStyle,
|
base: &LayoutStyle,
|
||||||
family: Arc<str>,
|
family: Arc<str>,
|
||||||
|
weight: u16,
|
||||||
collector: &mut Collector,
|
collector: &mut Collector,
|
||||||
) -> SpanCtx {
|
) -> SpanCtx {
|
||||||
let clip_fill = clip_text_fill(base).map(|fill| {
|
let clip_fill = clip_text_fill(base).map(|fill| {
|
||||||
@@ -575,7 +679,7 @@ fn base_span_ctx(
|
|||||||
});
|
});
|
||||||
SpanCtx {
|
SpanCtx {
|
||||||
color: base.color.unwrap_or([0, 0, 0, 255]),
|
color: base.color.unwrap_or([0, 0, 0, 255]),
|
||||||
bold: base.font_weight.as_deref() == Some("bold"),
|
weight,
|
||||||
italic: base.font_style_italic.unwrap_or(false),
|
italic: base.font_style_italic.unwrap_or(false),
|
||||||
underline: base.underline.unwrap_or(false),
|
underline: base.underline.unwrap_or(false),
|
||||||
transform: base.text_transform.unwrap_or(TextTransform::None),
|
transform: base.text_transform.unwrap_or(TextTransform::None),
|
||||||
@@ -591,7 +695,7 @@ fn collect_spans(
|
|||||||
ctx: SpanCtx,
|
ctx: SpanCtx,
|
||||||
out: &mut Vec<(String, SpanAttrs)>,
|
out: &mut Vec<(String, SpanAttrs)>,
|
||||||
c: &mut Collector,
|
c: &mut Collector,
|
||||||
loaded_families: &HashMap<String, Arc<str>>,
|
loaded_families: &HashMap<String, LoadedFamily>,
|
||||||
) {
|
) {
|
||||||
for cid in tree.children(id) {
|
for cid in tree.children(id) {
|
||||||
collect_node_spans(
|
collect_node_spans(
|
||||||
@@ -617,13 +721,13 @@ fn collect_node_spans(
|
|||||||
ctx: SpanCtx,
|
ctx: SpanCtx,
|
||||||
out: &mut Vec<(String, SpanAttrs)>,
|
out: &mut Vec<(String, SpanAttrs)>,
|
||||||
c: &mut Collector,
|
c: &mut Collector,
|
||||||
loaded_families: &HashMap<String, Arc<str>>,
|
loaded_families: &HashMap<String, LoadedFamily>,
|
||||||
) {
|
) {
|
||||||
let Some(node) = tree.get_node(cid) else { return };
|
let Some(node) = tree.get_node(cid) else { return };
|
||||||
match &node.data {
|
match &node.data {
|
||||||
obscura_dom::tree::NodeData::Text { contents } => {
|
obscura_dom::tree::NodeData::Text { contents } => {
|
||||||
let attrs = SpanAttrs {
|
let attrs = SpanAttrs {
|
||||||
bold: ctx.bold,
|
weight: ctx.weight,
|
||||||
italic: ctx.italic,
|
italic: ctx.italic,
|
||||||
underline: ctx.underline,
|
underline: ctx.underline,
|
||||||
color: ctx.color,
|
color: ctx.color,
|
||||||
@@ -640,7 +744,7 @@ fn collect_node_spans(
|
|||||||
}
|
}
|
||||||
if elem.local.as_ref() == "br" {
|
if elem.local.as_ref() == "br" {
|
||||||
out.push(("\n".to_string(), SpanAttrs {
|
out.push(("\n".to_string(), SpanAttrs {
|
||||||
bold: ctx.bold,
|
weight: ctx.weight,
|
||||||
italic: ctx.italic,
|
italic: ctx.italic,
|
||||||
underline: ctx.underline,
|
underline: ctx.underline,
|
||||||
color: ctx.color,
|
color: ctx.color,
|
||||||
@@ -662,20 +766,28 @@ fn collect_node_spans(
|
|||||||
let clip_fill = own_clip_fill.or_else(|| {
|
let clip_fill = own_clip_fill.or_else(|| {
|
||||||
if color[3] == 0 { ctx.clip_fill } else { None }
|
if color[3] == 0 { ctx.clip_fill } else { None }
|
||||||
});
|
});
|
||||||
|
let requested_weight = style
|
||||||
|
.map(crate::style::used_font_weight)
|
||||||
|
.unwrap_or(ctx.weight);
|
||||||
|
let (family, weight) = style
|
||||||
|
.and_then(|style| style.font_family.as_deref())
|
||||||
|
.map(|family| {
|
||||||
|
resolve_loaded_font(
|
||||||
|
Some(family),
|
||||||
|
requested_weight,
|
||||||
|
loaded_families,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| (Arc::clone(&ctx.family), requested_weight));
|
||||||
let child = SpanCtx {
|
let child = SpanCtx {
|
||||||
color,
|
color,
|
||||||
bold: ctx.bold || style.map(|s| s.font_weight.as_deref() == Some("bold")).unwrap_or(false),
|
weight,
|
||||||
italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false),
|
italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false),
|
||||||
// Underline propagates in: an ancestor's underline covers
|
// Underline propagates in: an ancestor's underline covers
|
||||||
// descendant text; an element only sets its own via CSS.
|
// descendant text; an element only sets its own via CSS.
|
||||||
underline: ctx.underline || style.and_then(|s| s.underline).unwrap_or(false),
|
underline: ctx.underline || style.and_then(|s| s.underline).unwrap_or(false),
|
||||||
transform: style.and_then(|s| s.text_transform).unwrap_or(ctx.transform),
|
transform: style.and_then(|s| s.text_transform).unwrap_or(ctx.transform),
|
||||||
family: style
|
family,
|
||||||
.and_then(|s| s.font_family.as_deref())
|
|
||||||
.map(|family| {
|
|
||||||
resolve_loaded_font_family(Some(family), loaded_families)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Arc::clone(&ctx.family)),
|
|
||||||
clip_fill,
|
clip_fill,
|
||||||
};
|
};
|
||||||
collect_spans(
|
collect_spans(
|
||||||
@@ -1103,6 +1215,15 @@ mod tests {
|
|||||||
assert_eq!(normal_line_height(13.0, MONO_FAMILY), 15.0);
|
assert_eq!(normal_line_height(13.0, MONO_FAMILY), 15.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_font_weights_follow_css_search_order() {
|
||||||
|
assert_eq!(match_font_weight(500, &[400, 700]), 400);
|
||||||
|
assert_eq!(match_font_weight(600, &[400, 500, 700]), 700);
|
||||||
|
assert_eq!(match_font_weight(300, &[100, 400, 700]), 100);
|
||||||
|
assert_eq!(match_font_weight(800, &[400, 700]), 700);
|
||||||
|
assert_eq!(match_font_weight(500, &[400, 500, 700]), 500);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clip_fill_only_for_transparent_clip_text() {
|
fn clip_fill_only_for_transparent_clip_text() {
|
||||||
let mut s = LayoutStyle::default();
|
let mut s = LayoutStyle::default();
|
||||||
|
|||||||
@@ -312,6 +312,9 @@ pub struct LayoutStyle {
|
|||||||
/// eagerly treating `9vw` as the number 9 made responsive headings pin to
|
/// eagerly treating `9vw` as the number 9 made responsive headings pin to
|
||||||
/// the minimum arm of their clamp.
|
/// the minimum arm of their clamp.
|
||||||
pub font_size_expression: Option<String>,
|
pub font_size_expression: Option<String>,
|
||||||
|
/// Specified CSS font weight during cascade (`1..1000`, `bolder`, or
|
||||||
|
/// `lighter`), normalized to its numeric computed value by the inheritance
|
||||||
|
/// pass before layout and shaping.
|
||||||
pub font_weight: Option<String>,
|
pub font_weight: Option<String>,
|
||||||
/// The computed `font-family` list, lowercased. Inherited. The text engine
|
/// The computed `font-family` list, lowercased. Inherited. The text engine
|
||||||
/// resolves it to a bundled face (Liberation Sans/Serif/Mono) the way
|
/// resolves it to a bundled face (Liberation Sans/Serif/Mono) the way
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
|||||||
if let Some(runs) = laid.text_runs.get(&nid) {
|
if let Some(runs) = laid.text_runs.get(&nid) {
|
||||||
let color = style.color.unwrap_or([0, 0, 0, 255]);
|
let color = style.color.unwrap_or([0, 0, 0, 255]);
|
||||||
let fsize = style.font_size.unwrap_or(16.0);
|
let fsize = style.font_size.unwrap_or(16.0);
|
||||||
let is_bold = style.font_weight.as_deref() == Some("bold");
|
let is_bold = crate::style::used_font_weight(style) >= 600;
|
||||||
for (word_rect, word) in runs {
|
for (word_rect, word) in runs {
|
||||||
draw_text(
|
draw_text(
|
||||||
&mut pixmap,
|
&mut pixmap,
|
||||||
@@ -721,7 +721,7 @@ fn paint_text_node(
|
|||||||
}
|
}
|
||||||
let color = clip_text_fill_color(style).unwrap_or_else(|| style.color.unwrap_or([0, 0, 0, 255]));
|
let color = clip_text_fill_color(style).unwrap_or_else(|| style.color.unwrap_or([0, 0, 0, 255]));
|
||||||
let fsize = style.font_size.unwrap_or(16.0);
|
let fsize = style.font_size.unwrap_or(16.0);
|
||||||
let is_bold = style.font_weight.as_deref() == Some("bold");
|
let is_bold = crate::style::used_font_weight(style) >= 600;
|
||||||
// A text node has no transform of its own, but any transformed element
|
// A text node has no transform of its own, but any transformed element
|
||||||
// ancestor offsets it (the accumulation covers text nodes too). The clip
|
// ancestor offsets it (the accumulation covers text nodes too). The clip
|
||||||
// is already in screen space and stays put.
|
// is already in screen space and stays put.
|
||||||
|
|||||||
@@ -495,11 +495,9 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
}
|
}
|
||||||
"font" => apply_font_shorthand(style, value),
|
"font" => apply_font_shorthand(style, value),
|
||||||
"font-weight" => {
|
"font-weight" => {
|
||||||
// Normalize to "bold"/"normal" (consumers test for "bold"). Numeric
|
if let Some(weight) = specified_font_weight(value) {
|
||||||
// >= 600 and `bold`/`bolder` are bold; others normal.
|
style.font_weight = Some(weight);
|
||||||
let v = value.trim().to_ascii_lowercase();
|
}
|
||||||
let bold = v == "bold" || v == "bolder" || v.parse::<u32>().map(|n| n >= 600).unwrap_or(false);
|
|
||||||
style.font_weight = Some(if bold { "bold".to_string() } else { "normal".to_string() });
|
|
||||||
}
|
}
|
||||||
"font-family" => {
|
"font-family" => {
|
||||||
let v = value.trim().to_ascii_lowercase();
|
let v = value.trim().to_ascii_lowercase();
|
||||||
@@ -2513,18 +2511,15 @@ fn apply_font_shorthand(style: &mut LayoutStyle, value: &str) {
|
|||||||
|
|
||||||
// The shorthand resets every constituent before applying supplied values.
|
// The shorthand resets every constituent before applying supplied values.
|
||||||
style.font_style_italic = Some(false);
|
style.font_style_italic = Some(false);
|
||||||
style.font_weight = Some("normal".to_string());
|
style.font_weight = Some("400".to_string());
|
||||||
style.line_height = Some(crate::LineHeight::Normal);
|
style.line_height = Some(crate::LineHeight::Normal);
|
||||||
style.line_height_expression = None;
|
style.line_height_expression = None;
|
||||||
for token in &tokens[..size_index] {
|
for token in &tokens[..size_index] {
|
||||||
let lower = token.to_ascii_lowercase();
|
let lower = token.to_ascii_lowercase();
|
||||||
if lower == "italic" || lower.starts_with("oblique") {
|
if lower == "italic" || lower.starts_with("oblique") {
|
||||||
style.font_style_italic = Some(true);
|
style.font_style_italic = Some(true);
|
||||||
} else if lower == "bold"
|
} else if let Some(weight) = specified_font_weight(&lower) {
|
||||||
|| lower == "bolder"
|
style.font_weight = Some(weight);
|
||||||
|| lower.parse::<u32>().map(|weight| weight >= 600).unwrap_or(false)
|
|
||||||
{
|
|
||||||
style.font_weight = Some("bold".to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apply_font_size(style, size);
|
apply_font_size(style, size);
|
||||||
@@ -2534,6 +2529,54 @@ fn apply_font_shorthand(style: &mut LayoutStyle, value: &str) {
|
|||||||
style.font_family = Some(tokens[family_index..].join(" ").to_ascii_lowercase());
|
style.font_family = Some(tokens[family_index..].join(" ").to_ascii_lowercase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize a specified CSS font weight while preserving relative keywords
|
||||||
|
/// until the top-down inheritance pass can see the parent's computed weight.
|
||||||
|
fn specified_font_weight(value: &str) -> Option<String> {
|
||||||
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
|
match lower.as_str() {
|
||||||
|
"normal" => Some("400".to_string()),
|
||||||
|
"bold" => Some("700".to_string()),
|
||||||
|
"bolder" | "lighter" => Some(lower),
|
||||||
|
_ => lower
|
||||||
|
.parse::<f32>()
|
||||||
|
.ok()
|
||||||
|
.filter(|weight| weight.is_finite() && (1.0..=1000.0).contains(weight))
|
||||||
|
.map(|weight| weight.round().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve `font-weight` to the numeric computed value defined by CSS Fonts.
|
||||||
|
/// Relative keywords use the inherited weight table rather than a binary
|
||||||
|
/// normal/bold threshold.
|
||||||
|
pub(crate) fn computed_font_weight(specified: Option<&str>, inherited: u16) -> u16 {
|
||||||
|
match specified {
|
||||||
|
None => inherited,
|
||||||
|
Some("normal") => 400,
|
||||||
|
Some("bold") => 700,
|
||||||
|
Some("bolder") if inherited < 100 => 400,
|
||||||
|
Some("bolder") if inherited < 350 => 400,
|
||||||
|
Some("bolder") if inherited < 550 => 700,
|
||||||
|
Some("bolder") if inherited < 900 => 900,
|
||||||
|
Some("bolder") => inherited,
|
||||||
|
Some("lighter") if inherited < 100 => inherited,
|
||||||
|
Some("lighter") if inherited < 350 => 100,
|
||||||
|
Some("lighter") if inherited < 550 => 100,
|
||||||
|
Some("lighter") if inherited < 750 => 400,
|
||||||
|
Some("lighter") if inherited < 900 => 700,
|
||||||
|
Some("lighter") => 700,
|
||||||
|
Some(weight) => weight
|
||||||
|
.parse::<f32>()
|
||||||
|
.ok()
|
||||||
|
.filter(|weight| weight.is_finite())
|
||||||
|
.map(|weight| weight.round().clamp(1.0, 1000.0) as u16)
|
||||||
|
.unwrap_or(inherited),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn used_font_weight(style: &LayoutStyle) -> u16 {
|
||||||
|
computed_font_weight(style.font_weight.as_deref(), 400)
|
||||||
|
}
|
||||||
|
|
||||||
fn is_font_size_token(value: &str) -> bool {
|
fn is_font_size_token(value: &str) -> bool {
|
||||||
let lower = value.trim().to_ascii_lowercase();
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
if lower == "0" || font_size_keyword(&lower).is_some() {
|
if lower == "0" || font_size_keyword(&lower).is_some() {
|
||||||
@@ -3504,7 +3547,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(s.font_size, Some(64.0));
|
assert_eq!(s.font_size, Some(64.0));
|
||||||
assert_eq!(s.line_height, Some(crate::LineHeight::Px(60.0)));
|
assert_eq!(s.line_height, Some(crate::LineHeight::Px(60.0)));
|
||||||
assert_eq!(s.font_weight.as_deref(), Some("normal"));
|
assert_eq!(s.font_weight.as_deref(), Some("500"));
|
||||||
assert_eq!(s.font_style_italic, Some(false));
|
assert_eq!(s.font_style_italic, Some(false));
|
||||||
assert_eq!(s.font_family.as_deref(), Some("\"google sans\", sans-serif"));
|
assert_eq!(s.font_family.as_deref(), Some("\"google sans\", sans-serif"));
|
||||||
|
|
||||||
@@ -3514,10 +3557,32 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(reset.font_size, Some(20.0));
|
assert_eq!(reset.font_size, Some(20.0));
|
||||||
assert_eq!(reset.line_height, Some(crate::LineHeight::Normal));
|
assert_eq!(reset.line_height, Some(crate::LineHeight::Normal));
|
||||||
assert_eq!(reset.font_weight.as_deref(), Some("normal"));
|
assert_eq!(reset.font_weight.as_deref(), Some("400"));
|
||||||
assert_eq!(reset.font_style_italic, Some(false));
|
assert_eq!(reset.font_style_italic, Some(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn font_weight_preserves_numeric_values_and_resolves_relative_keywords() {
|
||||||
|
let medium = compute_style("div", Some("font-weight:500"));
|
||||||
|
assert_eq!(medium.font_weight.as_deref(), Some("500"));
|
||||||
|
let semibold = compute_style("div", Some("font-weight:600"));
|
||||||
|
assert_eq!(semibold.font_weight.as_deref(), Some("600"));
|
||||||
|
let normal = compute_style("strong", Some("font-weight:normal"));
|
||||||
|
assert_eq!(normal.font_weight.as_deref(), Some("400"));
|
||||||
|
|
||||||
|
assert_eq!(computed_font_weight(Some("bolder"), 99), 400);
|
||||||
|
assert_eq!(computed_font_weight(Some("bolder"), 349), 400);
|
||||||
|
assert_eq!(computed_font_weight(Some("bolder"), 350), 700);
|
||||||
|
assert_eq!(computed_font_weight(Some("bolder"), 550), 900);
|
||||||
|
assert_eq!(computed_font_weight(Some("bolder"), 900), 900);
|
||||||
|
assert_eq!(computed_font_weight(Some("lighter"), 99), 99);
|
||||||
|
assert_eq!(computed_font_weight(Some("lighter"), 100), 100);
|
||||||
|
assert_eq!(computed_font_weight(Some("lighter"), 350), 100);
|
||||||
|
assert_eq!(computed_font_weight(Some("lighter"), 550), 400);
|
||||||
|
assert_eq!(computed_font_weight(Some("lighter"), 750), 700);
|
||||||
|
assert_eq!(computed_font_weight(Some("lighter"), 900), 700);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn containing_block_property_triggers_are_independent() {
|
fn containing_block_property_triggers_are_independent() {
|
||||||
let s = compute_style("div", Some("transform:rotate(0deg);filter:none"));
|
let s = compute_style("div", Some("transform:rotate(0deg);filter:none"));
|
||||||
|
|||||||
@@ -1113,6 +1113,31 @@ fn length_line_height_computes_before_inheritance() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn font_weight_computes_numerically_through_inheritance() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<div id="medium" style="font-weight:500">
|
||||||
|
<span id="inherited">medium</span>
|
||||||
|
<strong id="normal" style="font-weight:normal">normal</strong>
|
||||||
|
<span id="bolder" style="font-weight:bolder">bolder</span>
|
||||||
|
<span id="lighter" style="font-weight:lighter">lighter</span>
|
||||||
|
</div>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (900.0, 1000.0));
|
||||||
|
let weight = |id| {
|
||||||
|
layout.styles[&tree.get_element_by_id(id).unwrap()]
|
||||||
|
.font_weight
|
||||||
|
.as_deref()
|
||||||
|
};
|
||||||
|
assert_eq!(weight("medium"), Some("500"));
|
||||||
|
assert_eq!(weight("inherited"), Some("500"));
|
||||||
|
assert_eq!(weight("normal"), Some("400"));
|
||||||
|
assert_eq!(weight("bolder"), Some("700"));
|
||||||
|
assert_eq!(weight("lighter"), Some("100"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn contextual_right_inset_uses_root_font_tokens() {
|
fn contextual_right_inset_uses_root_font_tokens() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
Reference in new issue
Block a user