Shape inherited letter spacing
This commit is contained in:
1 parent
0f06ace12b
commit
fd746c13e9
6 files changed
+291
-10
No files matched your search
@@ -21,16 +21,30 @@ use crate::{Rect, to_taffy_style};
|
||||
/// (lightest-weight) feature set, matching RENDER.md's documented "layout
|
||||
/// (default)" mode.
|
||||
#[cfg(feature = "paint")]
|
||||
fn text_width(text: &str, size: f32, is_bold: bool, family: Option<&str>) -> f32 {
|
||||
fn text_width(
|
||||
text: &str,
|
||||
size: f32,
|
||||
is_bold: bool,
|
||||
family: Option<&str>,
|
||||
letter_spacing: f32,
|
||||
) -> f32 {
|
||||
crate::paint::measure_text(text, size, is_bold, family)
|
||||
+ text.chars().filter(|c| !c.is_control()).count() as f32 * letter_spacing
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "paint"))]
|
||||
fn text_width(text: &str, size: f32, is_bold: bool, _family: Option<&str>) -> f32 {
|
||||
fn text_width(
|
||||
text: &str,
|
||||
size: f32,
|
||||
is_bold: bool,
|
||||
_family: Option<&str>,
|
||||
letter_spacing: f32,
|
||||
) -> f32 {
|
||||
const AVG_CHAR_WIDTH_EM: f32 = 0.55;
|
||||
let chars = text.chars().filter(|c| !c.is_control()).count() as f32;
|
||||
let width = chars * size * AVG_CHAR_WIDTH_EM;
|
||||
if is_bold { width * 1.08 } else { width }
|
||||
let glyph_width = chars * size * AVG_CHAR_WIDTH_EM;
|
||||
(if is_bold { glyph_width * 1.08 } else { glyph_width })
|
||||
+ chars * letter_spacing
|
||||
}
|
||||
|
||||
/// Per-element border boxes after layout, in viewport coordinates.
|
||||
@@ -1062,6 +1076,8 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
font_size: Option<f32>,
|
||||
font_weight: u16,
|
||||
font_family: Option<String>,
|
||||
letter_spacing: f32,
|
||||
letter_spacing_non_normal: bool,
|
||||
text_align: Option<taffy::AlignItems>,
|
||||
legacy_center: bool,
|
||||
visibility_hidden: bool,
|
||||
@@ -1092,6 +1108,8 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
font_size: None,
|
||||
font_weight: 400,
|
||||
font_family: None,
|
||||
letter_spacing: 0.0,
|
||||
letter_spacing_non_normal: false,
|
||||
text_align: None,
|
||||
legacy_center: false,
|
||||
visibility_hidden: false,
|
||||
@@ -1183,6 +1201,32 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
// em in non-font-size properties is relative to this element's
|
||||
// OWN computed font-size; resolve every relative length now.
|
||||
let em_px = style.font_size.unwrap_or(parent_fs);
|
||||
if let Some(expression) = style.letter_spacing_expression.as_deref() {
|
||||
style.letter_spacing = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
em_px,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
em_px,
|
||||
);
|
||||
} else if let Some(raw) = style.letter_spacing_raw {
|
||||
style.letter_spacing = match raw.resolve(em_px, root_fs, vw, vh) {
|
||||
crate::Dimension::Px(pixels) if pixels.is_finite() => Some(pixels),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
match style.letter_spacing {
|
||||
Some(spacing) if spacing.is_finite() => inh.letter_spacing = spacing,
|
||||
_ => style.letter_spacing = Some(inh.letter_spacing),
|
||||
}
|
||||
match style.letter_spacing_non_normal {
|
||||
Some(non_normal) => inh.letter_spacing_non_normal = non_normal,
|
||||
None => {
|
||||
style.letter_spacing_non_normal =
|
||||
Some(inh.letter_spacing_non_normal)
|
||||
}
|
||||
}
|
||||
if let Some(expression) = style.row_gap_expression.as_deref() {
|
||||
style.row_gap = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
@@ -1505,6 +1549,9 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
// now-final computed style.
|
||||
let host_color = style.color;
|
||||
let host_font_size = style.font_size.unwrap_or(parent_fs);
|
||||
let host_letter_spacing = style.letter_spacing.unwrap_or(0.0);
|
||||
let host_letter_spacing_non_normal =
|
||||
style.letter_spacing_non_normal.unwrap_or(false);
|
||||
let host_weight = crate::style::used_font_weight(style);
|
||||
let host_family = style.font_family.clone();
|
||||
let host_line_height = style.line_height;
|
||||
@@ -1540,6 +1587,30 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
pseudo.font_size = Some(host_font_size);
|
||||
}
|
||||
let pseudo_em = pseudo.font_size.unwrap_or(host_font_size);
|
||||
if let Some(expression) = pseudo.letter_spacing_expression.as_deref() {
|
||||
pseudo.letter_spacing = crate::style::resolve_contextual_length(
|
||||
expression,
|
||||
pseudo_em,
|
||||
root_fs,
|
||||
vw,
|
||||
vh,
|
||||
pseudo_em,
|
||||
);
|
||||
} else if let Some(raw) = pseudo.letter_spacing_raw {
|
||||
pseudo.letter_spacing =
|
||||
match raw.resolve(pseudo_em, root_fs, vw, vh) {
|
||||
crate::Dimension::Px(pixels) if pixels.is_finite() => {
|
||||
Some(pixels)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
} else if pseudo.letter_spacing.is_none() {
|
||||
pseudo.letter_spacing = Some(host_letter_spacing);
|
||||
}
|
||||
if pseudo.letter_spacing_non_normal.is_none() {
|
||||
pseudo.letter_spacing_non_normal =
|
||||
Some(host_letter_spacing_non_normal);
|
||||
}
|
||||
for index in 0..6 {
|
||||
if let Some(expression) = pseudo.size_expressions[index].as_deref() {
|
||||
let percent_base = if matches!(index, 1 | 3 | 5) {
|
||||
@@ -1749,7 +1820,13 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut content_width =
|
||||
text_width(&label, font_size, bold, style.font_family.as_deref());
|
||||
text_width(
|
||||
&label,
|
||||
font_size,
|
||||
bold,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
);
|
||||
let pseudo_width = |pseudo: Option<&crate::LayoutStyle>| {
|
||||
let Some(pseudo) = pseudo else { return 0.0 };
|
||||
match pseudo.width {
|
||||
@@ -1806,6 +1883,7 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
font_size,
|
||||
bold,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
)
|
||||
})
|
||||
.fold(0.0f32, f32::max);
|
||||
@@ -4241,6 +4319,7 @@ fn build_text_words(
|
||||
let mut family = None;
|
||||
let mut line_height = fsize * 1.2;
|
||||
let mut transform = crate::TextTransform::None;
|
||||
let mut letter_spacing = 0.0;
|
||||
if let Some(parent_id) = node.parent {
|
||||
if let Some(p_style) = styles.get(&parent_id) {
|
||||
fsize = p_style.font_size.unwrap_or(16.0);
|
||||
@@ -4250,6 +4329,7 @@ fn build_text_words(
|
||||
transform = p_style
|
||||
.text_transform
|
||||
.unwrap_or(crate::TextTransform::None);
|
||||
letter_spacing = p_style.letter_spacing.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
display_text = transform_word_leaf_text(&display_text, transform);
|
||||
@@ -4261,6 +4341,7 @@ fn build_text_words(
|
||||
line_height,
|
||||
is_bold,
|
||||
family,
|
||||
letter_spacing,
|
||||
taffy_tree,
|
||||
words,
|
||||
)
|
||||
@@ -4301,13 +4382,14 @@ fn build_word_leaves(
|
||||
line_height: f32,
|
||||
is_bold: bool,
|
||||
family: Option<&str>,
|
||||
letter_spacing: f32,
|
||||
taffy_tree: &mut TaffyTree<usize>,
|
||||
words: &mut HashMap<taffy::NodeId, (NodeId, String)>,
|
||||
) -> Vec<taffy::NodeId> {
|
||||
tokenize_with_spaces(text)
|
||||
.into_iter()
|
||||
.filter_map(|token| {
|
||||
let width = text_width(&token, fsize, is_bold, family);
|
||||
let width = text_width(&token, fsize, is_bold, family, letter_spacing);
|
||||
// A pure-whitespace token is HTML source formatting or a bare
|
||||
// inter-element space; it keeps its (small) width so adjacent
|
||||
// inline content stays visually separated, but contributes no
|
||||
@@ -4351,6 +4433,7 @@ fn build_pseudo_content(
|
||||
crate::inline::used_line_height(style),
|
||||
is_bold,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
taffy_tree,
|
||||
words,
|
||||
)
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use cosmic_text::{
|
||||
Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Color, Family, FontSystem, Metrics, Shaping,
|
||||
Style, SwashCache, SwashImage, Weight, Wrap,
|
||||
Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Color, Family, FeatureTag, FontFeatures,
|
||||
FontSystem, Metrics, Shaping, Style, SwashCache, SwashImage, Weight, Wrap,
|
||||
};
|
||||
use swash::scale::{image::Content as SwashContent, Render, ScaleContext, Source, StrikeWith};
|
||||
use swash::zeno::{Angle, Format, Transform, Vector};
|
||||
@@ -782,6 +782,8 @@ impl TextEngine {
|
||||
let attrs = SpanAttrs {
|
||||
font_size: context.font_size,
|
||||
line_height: context.line_height,
|
||||
letter_spacing: context.letter_spacing,
|
||||
letter_spacing_non_normal: context.letter_spacing_non_normal,
|
||||
weight: context.weight,
|
||||
variation_weight: context.variation_weight,
|
||||
italic: context.italic,
|
||||
@@ -971,6 +973,8 @@ impl TextEngine {
|
||||
struct SpanAttrs {
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
letter_spacing: f32,
|
||||
letter_spacing_non_normal: bool,
|
||||
weight: u16,
|
||||
variation_weight: Option<u16>,
|
||||
italic: bool,
|
||||
@@ -993,6 +997,16 @@ impl SpanAttrs {
|
||||
));
|
||||
a = a.weight(Weight(self.weight));
|
||||
a = a.style(if self.italic { Style::Italic } else { Style::Normal });
|
||||
if self.letter_spacing.is_finite() && self.letter_spacing != 0.0 {
|
||||
a = a.letter_spacing(self.letter_spacing / self.font_size.max(1.0));
|
||||
}
|
||||
if self.letter_spacing_non_normal {
|
||||
let mut features = FontFeatures::new();
|
||||
features
|
||||
.disable(FeatureTag::STANDARD_LIGATURES)
|
||||
.disable(FeatureTag::CONTEXTUAL_LIGATURES);
|
||||
a = a.font_features(features);
|
||||
}
|
||||
// Clip-text glyphs must be shaped with an opaque fill so their coverage
|
||||
// reaches paint; the real gradient is selected through metadata.
|
||||
let color = if self.clip_fill.is_some() { [255, 255, 255, 255] } else { self.color };
|
||||
@@ -1014,6 +1028,8 @@ impl SpanAttrs {
|
||||
struct SpanCtx {
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
letter_spacing: f32,
|
||||
letter_spacing_non_normal: bool,
|
||||
color: [u8; 4],
|
||||
weight: u16,
|
||||
variation_weight: Option<u16>,
|
||||
@@ -1055,6 +1071,8 @@ fn base_span_ctx(
|
||||
SpanCtx {
|
||||
font_size: base.font_size.unwrap_or(16.0),
|
||||
line_height: used_line_height(base),
|
||||
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,
|
||||
variation_weight: font.variation_weight,
|
||||
@@ -1107,6 +1125,8 @@ fn collect_node_spans(
|
||||
let attrs = SpanAttrs {
|
||||
font_size: ctx.font_size,
|
||||
line_height: ctx.line_height,
|
||||
letter_spacing: ctx.letter_spacing,
|
||||
letter_spacing_non_normal: ctx.letter_spacing_non_normal,
|
||||
weight: ctx.weight,
|
||||
variation_weight: ctx.variation_weight,
|
||||
italic: ctx.italic,
|
||||
@@ -1127,6 +1147,8 @@ fn collect_node_spans(
|
||||
out.push(("\n".to_string(), SpanAttrs {
|
||||
font_size: ctx.font_size,
|
||||
line_height: ctx.line_height,
|
||||
letter_spacing: ctx.letter_spacing,
|
||||
letter_spacing_non_normal: ctx.letter_spacing_non_normal,
|
||||
weight: ctx.weight,
|
||||
variation_weight: ctx.variation_weight,
|
||||
italic: ctx.italic,
|
||||
@@ -1183,6 +1205,12 @@ fn collect_node_spans(
|
||||
line_height: style
|
||||
.map(used_line_height)
|
||||
.unwrap_or(ctx.line_height),
|
||||
letter_spacing: style
|
||||
.and_then(|style| style.letter_spacing)
|
||||
.unwrap_or(ctx.letter_spacing),
|
||||
letter_spacing_non_normal: style
|
||||
.and_then(|style| style.letter_spacing_non_normal)
|
||||
.unwrap_or(ctx.letter_spacing_non_normal),
|
||||
color,
|
||||
weight: font.shape_weight,
|
||||
variation_weight: font.variation_weight,
|
||||
@@ -1859,6 +1887,8 @@ mod tests {
|
||||
let attrs = SpanAttrs {
|
||||
font_size: 16.0,
|
||||
line_height: 18.0,
|
||||
letter_spacing: 0.0,
|
||||
letter_spacing_non_normal: false,
|
||||
weight: 400,
|
||||
variation_weight: Some(725),
|
||||
italic: false,
|
||||
@@ -1873,6 +1903,31 @@ mod tests {
|
||||
assert_eq!(metadata_variation_weight(metadata), Some(725));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_normal_letter_spacing_reaches_shaping_features() {
|
||||
let span = SpanAttrs {
|
||||
font_size: 20.0,
|
||||
line_height: 24.0,
|
||||
letter_spacing: 2.0,
|
||||
letter_spacing_non_normal: true,
|
||||
weight: 400,
|
||||
variation_weight: None,
|
||||
italic: false,
|
||||
underline: false,
|
||||
color: [0, 0, 0, 255],
|
||||
family: Arc::from(FAMILY),
|
||||
clip_fill: None,
|
||||
};
|
||||
let attrs = span.to_attrs();
|
||||
assert_eq!(attrs.letter_spacing_opt.unwrap().0, 0.1);
|
||||
assert!(attrs.font_features.features.iter().any(|feature| {
|
||||
feature.tag == FeatureTag::STANDARD_LIGATURES && feature.value == 0
|
||||
}));
|
||||
assert!(attrs.font_features.features.iter().any(|feature| {
|
||||
feature.tag == FeatureTag::CONTEXTUAL_LIGATURES && feature.value == 0
|
||||
}));
|
||||
}
|
||||
|
||||
fn variable_font_fixture() -> Vec<u8> {
|
||||
let encoded: String = include_str!("../tests/fonts/obscura-vf-test.woff2.b64")
|
||||
.chars()
|
||||
|
||||
@@ -334,6 +334,19 @@ pub struct LayoutStyle {
|
||||
/// eagerly treating `9vw` as the number 9 made responsive headings pin to
|
||||
/// the minimum arm of their clamp.
|
||||
pub font_size_expression: Option<String>,
|
||||
/// Computed `letter-spacing` in CSS pixels. This inherited property is
|
||||
/// resolved top-down because `em` is relative to the element's own
|
||||
/// computed font size while `rem` and viewport units need live context.
|
||||
pub letter_spacing: Option<f32>,
|
||||
/// Non-pixel `letter-spacing` retained until the inheritance pass.
|
||||
pub letter_spacing_raw: Option<Dimension>,
|
||||
/// Deferred functional `letter-spacing` (`calc()`, `min()`, `clamp()`).
|
||||
pub letter_spacing_expression: Option<String>,
|
||||
/// Whether the computed value came from a non-`normal` declaration.
|
||||
/// CSS suppresses optional ligatures for every non-normal value, including
|
||||
/// an explicit zero; keeping this provenance avoids conflating it with the
|
||||
/// `normal` initial value.
|
||||
pub letter_spacing_non_normal: Option<bool>,
|
||||
/// 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.
|
||||
|
||||
@@ -819,6 +819,7 @@ fn paint_laid_dom_scrolled(
|
||||
12.0,
|
||||
false,
|
||||
None,
|
||||
0.0,
|
||||
clip,
|
||||
);
|
||||
}
|
||||
@@ -918,6 +919,7 @@ fn paint_laid_dom_scrolled(
|
||||
fsize,
|
||||
false,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
clip,
|
||||
);
|
||||
}
|
||||
@@ -941,6 +943,7 @@ fn paint_laid_dom_scrolled(
|
||||
fsize,
|
||||
is_bold,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
clip,
|
||||
);
|
||||
}
|
||||
@@ -964,6 +967,7 @@ fn paint_laid_dom_scrolled(
|
||||
fsize,
|
||||
crate::style::used_font_weight(style) >= 600,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
Some(visible_rect),
|
||||
);
|
||||
}
|
||||
@@ -1013,6 +1017,7 @@ fn paint_laid_dom_scrolled(
|
||||
fsize,
|
||||
false,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
clip,
|
||||
);
|
||||
}
|
||||
@@ -1602,6 +1607,7 @@ fn paint_text_node(
|
||||
fsize,
|
||||
is_bold,
|
||||
style.font_family.as_deref(),
|
||||
style.letter_spacing.unwrap_or(0.0),
|
||||
clip,
|
||||
);
|
||||
}
|
||||
@@ -1677,6 +1683,7 @@ fn draw_text(
|
||||
size: f32,
|
||||
is_bold: bool,
|
||||
family: Option<&str>,
|
||||
letter_spacing: f32,
|
||||
clip: Option<crate::Rect>,
|
||||
) {
|
||||
// A fully clipped-away run (the common "visually hidden" accessibility
|
||||
@@ -1750,9 +1757,13 @@ fn draw_text(
|
||||
// is wider than what draw_text actually advances through, and
|
||||
// the difference shows up as a visible gap after every word once
|
||||
// each word is its own independently-positioned box.
|
||||
caret.x += scaled_font.h_advance(id) + if is_bold { 1.0 } else { 0.0 };
|
||||
caret.x += scaled_font.h_advance(id)
|
||||
+ if is_bold { 1.0 } else { 0.0 }
|
||||
+ letter_spacing;
|
||||
} else {
|
||||
caret.x += scaled_font.h_advance(id) + if is_bold { 1.0 } else { 0.0 };
|
||||
caret.x += scaled_font.h_advance(id)
|
||||
+ if is_bold { 1.0 } else { 0.0 }
|
||||
+ letter_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +640,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
// to the inheritance pass (they need parent/root font-size).
|
||||
apply_font_size(style, value);
|
||||
}
|
||||
"letter-spacing" => apply_letter_spacing(style, value),
|
||||
"font" => apply_font_shorthand(style, value),
|
||||
"font-weight" => {
|
||||
if let Some(weight) = specified_font_weight(value) {
|
||||
@@ -1151,6 +1152,7 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
||||
| "border-color"
|
||||
| "color-scheme"
|
||||
| "font-size"
|
||||
| "letter-spacing"
|
||||
| "font"
|
||||
| "font-weight"
|
||||
| "font-family"
|
||||
@@ -3159,6 +3161,55 @@ fn apply_font_size(style: &mut LayoutStyle, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_letter_spacing(style: &mut LayoutStyle, value: &str) {
|
||||
let value = value.trim();
|
||||
let lower = value.to_ascii_lowercase();
|
||||
if matches!(
|
||||
lower.as_str(),
|
||||
"normal" | "initial" | "revert" | "revert-layer"
|
||||
) {
|
||||
style.letter_spacing = Some(0.0);
|
||||
style.letter_spacing_raw = None;
|
||||
style.letter_spacing_expression = None;
|
||||
style.letter_spacing_non_normal = Some(false);
|
||||
return;
|
||||
}
|
||||
// `letter-spacing` inherits, so `unset` has the same computed behavior as
|
||||
// `inherit`. Leave this style unspecified for the top-down pass.
|
||||
if matches!(lower.as_str(), "inherit" | "unset") || value.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Percentages are invalid even inside CSS math. Do not clear an earlier
|
||||
// valid cascade winner when a later declaration is invalid.
|
||||
if value.contains('%') {
|
||||
return;
|
||||
}
|
||||
style.letter_spacing_non_normal = Some(true);
|
||||
if value.contains('(') {
|
||||
style.letter_spacing = None;
|
||||
style.letter_spacing_raw = None;
|
||||
style.letter_spacing_expression = Some(value.to_string());
|
||||
return;
|
||||
}
|
||||
style.letter_spacing_expression = None;
|
||||
match dimension_value(value) {
|
||||
crate::Dimension::Px(pixels) if pixels.is_finite() => {
|
||||
style.letter_spacing = Some(pixels);
|
||||
style.letter_spacing_raw = None;
|
||||
}
|
||||
crate::Dimension::Percent(_) | crate::Dimension::Auto => {
|
||||
// Percentages and keywords other than `normal` are invalid.
|
||||
style.letter_spacing = None;
|
||||
style.letter_spacing_raw = None;
|
||||
style.letter_spacing_non_normal = None;
|
||||
}
|
||||
relative => {
|
||||
style.letter_spacing = None;
|
||||
style.letter_spacing_raw = Some(relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_gap_value(style: &mut LayoutStyle, row: bool, value: &str) {
|
||||
let value = value.trim();
|
||||
let lower = value.to_ascii_lowercase();
|
||||
@@ -4369,6 +4420,34 @@ mod tests {
|
||||
assert_eq!(computed_font_weight(Some("lighter"), 900), 700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn letter_spacing_preserves_units_resets_and_invalid_cascade_values() {
|
||||
let relative = compute_style("span", Some("letter-spacing:-.05em"));
|
||||
assert_eq!(
|
||||
relative.letter_spacing_raw,
|
||||
Some(crate::Dimension::Em(-0.05))
|
||||
);
|
||||
assert_eq!(relative.letter_spacing_non_normal, Some(true));
|
||||
|
||||
let reset = compute_style(
|
||||
"span",
|
||||
Some("letter-spacing:4px;letter-spacing:normal"),
|
||||
);
|
||||
assert_eq!(reset.letter_spacing, Some(0.0));
|
||||
assert_eq!(reset.letter_spacing_non_normal, Some(false));
|
||||
|
||||
let invalid = compute_style(
|
||||
"span",
|
||||
Some("letter-spacing:3px;letter-spacing:calc(10% + 1px)"),
|
||||
);
|
||||
assert_eq!(invalid.letter_spacing, Some(3.0));
|
||||
assert_eq!(invalid.letter_spacing_non_normal, Some(true));
|
||||
|
||||
let inherited = compute_style("span", Some("letter-spacing:unset"));
|
||||
assert_eq!(inherited.letter_spacing, None);
|
||||
assert_eq!(inherited.letter_spacing_non_normal, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonzero_unitless_font_size_is_invalid() {
|
||||
let inherited = compute_style("div", Some("font-size:14px;font-size:.813"));
|
||||
|
||||
Reference in new issue
Block a user