commit 27635743073f28e912bdb12a61b00d7c0931e5d5 parent 41c8409c535d54126b47dd7ee202e6439e5a009f Author: Sajid Anwar <sajidanwar94@gmail.com> Date: Wed, 3 Dec 2025 17:12:10 +0000 Bug 1740584 - Implement relative root font lengths rcap, rch, rex, ric. r=emilio,firefox-style-system-reviewers,layout-reviewers Differential Revision: https://phabricator.services.mozilla.com/D274527 Diffstat:
25 files changed, 536 insertions(+), 201 deletions(-)
diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp @@ -2149,8 +2149,10 @@ void nsPresContext::UserFontSetUpdated(gfxUserFontEntry* aUpdatedFont) { // TODO(emilio): We could be more granular if we knew which families have // potentially changed. if (!aUpdatedFont) { - auto hint = StyleSet()->UsesFontMetrics() ? RestyleHint::RecascadeSubtree() - : RestyleHint{0}; + auto hint = + (StyleSet()->UsesFontMetrics() || StyleSet()->UsesRootFontMetrics()) + ? RestyleHint::RecascadeSubtree() + : RestyleHint{0}; PostRebuildAllStyleDataEvent(NS_STYLE_HINT_REFLOW, hint); return; } diff --git a/layout/style/ServoStyleSet.cpp b/layout/style/ServoStyleSet.cpp @@ -1186,6 +1186,10 @@ bool ServoStyleSet::UsesFontMetrics() const { return Servo_StyleSet_UsesFontMetrics(mRawData.get()); } +bool ServoStyleSet::UsesRootFontMetrics() const { + return Servo_StyleSet_UsesRootFontMetrics(mRawData.get()); +} + bool ServoStyleSet::EnsureUniqueInnerOnCSSSheets() { using SheetOwner = Variant<ServoStyleSet*, ShadowRoot*>; diff --git a/layout/style/ServoStyleSet.h b/layout/style/ServoStyleSet.h @@ -166,6 +166,8 @@ class ServoStyleSet { bool UsesFontMetrics() const; + bool UsesRootFontMetrics() const; + void SetAuthorStyleDisabled(bool aStyleDisabled); // Get a CopmutedStyle for a text node (which no rules will match). diff --git a/layout/style/nsFontFaceUtils.cpp b/layout/style/nsFontFaceUtils.cpp @@ -172,6 +172,7 @@ void nsFontFaceUtils::MarkDirtyForFontChange(nsIFrame* aSubtreeRoot, PresShell* presShell = pc->PresShell(); const bool usesMetricsFromStyle = pc->StyleSet()->UsesFontMetrics(); + const bool usesRootMetricsFromStyle = pc->StyleSet()->UsesRootFontMetrics(); // StyleSingleFontFamily::IsNamedFamily expects a UTF-16 string. Convert it // once here rather than on each call. @@ -201,7 +202,18 @@ void nsFontFaceUtils::MarkDirtyForFontChange(nsIFrame* aSubtreeRoot, ScheduleReflow(presShell, f); alreadyScheduled = ReflowAlreadyScheduled::Yes; } - if (kind & FontUsageKind::FontMetrics) { + + // If the updated font is used for font metrics, then styles need to be + // recomputed. This can occur if the current frame directly uses the + // font's metrics (ex/ch/...). However, if there are any elements in the + // document using root element relative font metrics (rex/rch/...) and + // the root element itself used the updated font, then the entire + // subtree needs to be restyled. + const bool shouldRestyleForFontMetrics = + (kind & FontUsageKind::FontMetrics) || + (usesRootMetricsFromStyle && f->Style()->IsRootElementStyle()); + + if (shouldRestyleForFontMetrics) { MOZ_ASSERT(f->GetContent() && f->GetContent()->IsElement(), "How could we target a non-element with selectors?"); f->PresContext()->RestyleManager()->PostRestyleEvent( diff --git a/servo/components/style/custom_properties.rs b/servo/components/style/custom_properties.rs @@ -415,7 +415,12 @@ impl NonCustomReferences { if value.eq_ignore_ascii_case(FontRelativeLength::RLH) { return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS; } - if value.eq_ignore_ascii_case(FontRelativeLength::REM) { + if value.eq_ignore_ascii_case(FontRelativeLength::REM) + || value.eq_ignore_ascii_case(FontRelativeLength::REX) + || value.eq_ignore_ascii_case(FontRelativeLength::RCH) + || value.eq_ignore_ascii_case(FontRelativeLength::RCAP) + || value.eq_ignore_ascii_case(FontRelativeLength::RIC) + { return Self::ROOT_FONT_UNITS; } Self::empty() diff --git a/servo/components/style/font_metrics.rs b/servo/components/style/font_metrics.rs @@ -6,7 +6,7 @@ #![deny(missing_docs)] -use crate::values::computed::Length; +use crate::values::computed::{FontSize, Length}; /// Represents the font metrics that style needs from a font to compute the /// value of certain CSS units like `ex`. @@ -44,6 +44,74 @@ impl Default for FontMetrics { } } +impl FontMetrics { + /// Returns the x-height, computing a fallback value if not present + pub fn x_height_or_default(&self, reference_font_size: &FontSize) -> Length { + // https://drafts.csswg.org/css-values/#ex + // + // In the cases where it is impossible or impractical to + // determine the x-height, a value of 0.5em must be + // assumed. + // + // (But note we use 0.5em of the used, not computed + // font-size) + self.x_height + .unwrap_or_else(|| reference_font_size.used_size() * 0.5) + } + + /// Returns the zero advance measure, computing a fallback value if not present + pub fn zero_advance_measure_or_default( + &self, + reference_font_size: &FontSize, + upright: bool, + ) -> Length { + // https://drafts.csswg.org/css-values/#ch + // + // In the cases where it is impossible or impractical to + // determine the measure of the “0” glyph, it must be + // assumed to be 0.5em wide by 1em tall. Thus, the ch + // unit falls back to 0.5em in the general case, and to + // 1em when it would be typeset upright (i.e. + // writing-mode is vertical-rl or vertical-lr and + // text-orientation is upright). + // + // Same caveat about computed vs. used font-size applies + // above. + self.zero_advance_measure.unwrap_or_else(|| { + if upright { + reference_font_size.used_size() + } else { + reference_font_size.used_size() * 0.5 + } + }) + } + + /// Returns the cap-height, computing a fallback value if not present + pub fn cap_height_or_default(&self) -> Length { + // https://drafts.csswg.org/css-values/#cap + // + // In the cases where it is impossible or impractical to + // determine the cap-height, the font’s ascent must be + // used. + // + self.cap_height.unwrap_or_else(|| self.ascent) + } + + /// Returns the ideographic advance measure, computing a fallback value if not present + pub fn ic_width_or_default(&self, reference_font_size: &FontSize) -> Length { + // https://drafts.csswg.org/css-values/#ic + // + // In the cases where it is impossible or impractical to + // determine the ideographic advance measure, it must be + // assumed to be 1em. + // + // Same caveat about computed vs. used as for other + // metric-dependent units. + self.ic_width + .unwrap_or_else(|| reference_font_size.used_size()) + } +} + /// Type of font metrics to retrieve. #[derive(Clone, Debug, PartialEq)] pub enum FontMetricsOrientation { diff --git a/servo/components/style/gecko/media_queries.rs b/servo/components/style/gecko/media_queries.rs @@ -19,7 +19,8 @@ use crate::values::computed::font::GenericFontFamily; use crate::values::computed::{ColorScheme, Length, NonNegativeLength}; use crate::values::specified::color::{ColorSchemeFlags, ForcedColors, SystemColor}; use crate::values::specified::font::{ - QueryFontMetricsFlags, FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX, + QueryFontMetricsFlags, FONT_MEDIUM_CAP_PX, FONT_MEDIUM_CH_PX, FONT_MEDIUM_EX_PX, + FONT_MEDIUM_IC_PX, FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX, }; use crate::values::specified::ViewportVariant; use crate::values::{CustomIdent, KeyframesName}; @@ -50,6 +51,14 @@ pub struct Device { root_font_size: AtomicU32, /// Line height of the root element, used for rlh units in other elements. root_line_height: AtomicU32, + /// X-height of the root element, used for rex units in other elements. + root_font_metrics_ex: AtomicU32, + /// Cap-height of the root element, used for rcap units in other elements. + root_font_metrics_cap: AtomicU32, + /// Advance measure (ch) of the root element, used for rch units in other elements. + root_font_metrics_ch: AtomicU32, + /// Ideographic advance measure of the root element, used for ric units in other elements. + root_font_metrics_ic: AtomicU32, /// The body text color, stored as an `nscolor`, used for the "tables /// inherit from body" quirk. /// @@ -61,6 +70,9 @@ pub struct Device { /// Whether any styles computed in the document relied on the root line-height /// by using rlh units. used_root_line_height: AtomicBool, + /// Whether any styles computed in the document relied on the root font metrics + /// by using rcap, rch, rex, or ric units. + used_root_font_metrics: AtomicBool, /// Whether any styles computed in the document relied on font metrics. used_font_metrics: AtomicBool, /// Whether any styles computed in the document relied on the viewport size @@ -103,11 +115,17 @@ impl Device { default_values: ComputedValues::default_values(doc), root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()), + root_font_metrics_ex: AtomicU32::new(FONT_MEDIUM_EX_PX.to_bits()), + root_font_metrics_cap: AtomicU32::new(FONT_MEDIUM_CAP_PX.to_bits()), + root_font_metrics_ch: AtomicU32::new(FONT_MEDIUM_CH_PX.to_bits()), + root_font_metrics_ic: AtomicU32::new(FONT_MEDIUM_IC_PX.to_bits()), + // This gets updated when we see the <body>, so it doesn't really // matter which color-scheme we look at here. body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), used_root_font_size: AtomicBool::new(false), used_root_line_height: AtomicBool::new(false), + used_root_font_metrics: AtomicBool::new(false), used_font_metrics: AtomicBool::new(false), used_viewport_size: AtomicBool::new(false), used_dynamic_viewport_size: AtomicBool::new(false), @@ -193,6 +211,66 @@ impl Device { .store(size.to_bits(), Ordering::Relaxed); } + /// Get the x-height of the root element (for rex) + pub fn root_font_metrics_ex(&self) -> Length { + self.used_root_font_metrics.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_font_metrics_ex.load(Ordering::Relaxed), + )) + } + + /// Set the x-height of the root element (for rex), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_ex(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_ex.swap(size, Ordering::Relaxed); + previous != size + } + + /// Get the cap-height of the root element (for rcap) + pub fn root_font_metrics_cap(&self) -> Length { + self.used_root_font_metrics.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_font_metrics_cap.load(Ordering::Relaxed), + )) + } + + /// Set the cap-height of the root element (for rcap), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_cap(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_cap.swap(size, Ordering::Relaxed); + previous != size + } + + /// Get the advance measure of the root element (for rch) + pub fn root_font_metrics_ch(&self) -> Length { + self.used_root_font_metrics.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_font_metrics_ch.load(Ordering::Relaxed), + )) + } + + /// Set the advance measure of the root element (for rch), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_ch(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_ch.swap(size, Ordering::Relaxed); + previous != size + } + + /// Get the ideographic advance measure of the root element (for ric) + pub fn root_font_metrics_ic(&self) -> Length { + self.used_root_font_metrics.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_font_metrics_ic.load(Ordering::Relaxed), + )) + } + + /// Set the ideographic advance measure of the root element (for ric), in zoom-independent CSS pixels. + pub fn set_root_font_metrics_ic(&self, size: f32) -> bool { + let size = size.to_bits(); + let previous = self.root_font_metrics_ic.swap(size, Ordering::Relaxed); + previous != size + } + /// The quirks mode of the document. pub fn quirks_mode(&self) -> QuirksMode { self.document().mCompatMode.into() @@ -230,8 +308,11 @@ impl Device { font: &crate::properties::style_structs::Font, base_size: Length, flags: QueryFontMetricsFlags, + track_usage: bool, ) -> FontMetrics { - self.used_font_metrics.store(true, Ordering::Relaxed); + if track_usage { + self.used_font_metrics.store(true, Ordering::Relaxed); + } let pc = match self.pres_context() { Some(pc) => pc, None => return Default::default(), @@ -309,6 +390,7 @@ impl Device { self.reset_computed_values(); self.used_root_font_size.store(false, Ordering::Relaxed); self.used_root_line_height.store(false, Ordering::Relaxed); + self.used_root_font_metrics.store(false, Ordering::Relaxed); self.used_font_metrics.store(false, Ordering::Relaxed); self.used_viewport_size.store(false, Ordering::Relaxed); self.used_dynamic_viewport_size @@ -325,6 +407,11 @@ impl Device { self.used_root_line_height.load(Ordering::Relaxed) } + /// Returns whether we ever looked up the root font metrics of the device. + pub fn used_root_font_metrics(&self) -> bool { + self.used_root_font_metrics.load(Ordering::Relaxed) + } + /// Recreates all the temporary state that the `Device` stores. /// /// This includes the viewport override from `@viewport` rules, and also the diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs @@ -28,6 +28,7 @@ use crate::style_resolver::{PseudoElementResolution, ResolvedElementStyles}; use crate::stylesheets::layer_rule::LayerOrder; use crate::stylist::RuleInclusion; use crate::traversal_flags::TraversalFlags; +use crate::values::computed::font::QueryFontMetricsFlags; use servo_arc::{Arc, ArcBorrow}; /// Represents the result of comparing an element's old and new style. @@ -969,30 +970,6 @@ pub trait MatchMethods: TElement { let new_font_size = new_primary_style.get_font().clone_font_size(); let old_font_size = old_style.map(|s| s.get_font().clone_font_size()); - if old_font_size != Some(new_font_size) { - if is_root { - debug_assert!(self.owner_doc_matches_for_testing(device)); - let size = new_font_size.computed_size(); - device.set_root_font_size(new_primary_style.effective_zoom.unzoom(size.px())); - if device.used_root_font_size() { - // If the root font-size changed since last time, and something - // in the document did use rem units, ensure we recascade the - // entire tree. - restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; - } - } - - if is_container && old_font_size.is_some() { - // TODO(emilio): Maybe only do this if we were matched - // against relative font sizes? - // Also, maybe we should do this as well for font-family / - // etc changes (for ex/ch/ic units to work correctly)? We - // should probably do the optimization mentioned above if - // so. - restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; - } - } - // For line-height, we want the fully resolved value, as `normal` also depends on other // font properties. let new_line_height = device @@ -1008,26 +985,87 @@ pub trait MatchMethods: TElement { .0 }); - if old_line_height != Some(new_line_height) { - if is_root { - debug_assert!(self.owner_doc_matches_for_testing(device)); + if is_root { + debug_assert!(self.owner_doc_matches_for_testing(device)); + + // Update root font size for rem units + if old_font_size != Some(new_font_size) { + let size = new_font_size.computed_size(); + device.set_root_font_size(new_primary_style.effective_zoom.unzoom(size.px())); + if device.used_root_font_size() { + // If the root font-size changed since last time, and something + // in the document did use rem units, ensure we recascade the + // entire tree. + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; + } + } + + // Update root line height for rlh units + if old_line_height != Some(new_line_height) { device.set_root_line_height( new_primary_style .effective_zoom .unzoom(new_line_height.px()), ); if device.used_root_line_height() { - restyle_requirement = std::cmp::max( - restyle_requirement, - ChildRestyleRequirement::MustCascadeDescendants, - ); + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; } } - if is_container && old_line_height.is_some() { - restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + // Update root font metrics for rcap, rch, rex, ric units + let new_font_metrics = device.query_font_metrics( + new_primary_style.writing_mode.is_upright(), + &new_primary_style.get_font(), + new_font_size.computed_size(), + QueryFontMetricsFlags::USE_USER_FONT_SET + | QueryFontMetricsFlags::NEEDS_CH + | QueryFontMetricsFlags::NEEDS_IC, + /* track_usage = */ false, + ); + let mut root_font_metrics_changed = false; + root_font_metrics_changed |= device.set_root_font_metrics_ex( + new_primary_style + .effective_zoom + .unzoom(new_font_metrics.x_height_or_default(&new_font_size).px()), + ); + root_font_metrics_changed |= device.set_root_font_metrics_ch( + new_primary_style.effective_zoom.unzoom( + new_font_metrics + .zero_advance_measure_or_default( + &new_font_size, + new_primary_style.writing_mode.is_upright(), + ) + .px(), + ), + ); + root_font_metrics_changed |= device.set_root_font_metrics_cap( + new_primary_style + .effective_zoom + .unzoom(new_font_metrics.cap_height_or_default().px()), + ); + root_font_metrics_changed |= device.set_root_font_metrics_ic( + new_primary_style + .effective_zoom + .unzoom(new_font_metrics.ic_width_or_default(&new_font_size).px()), + ); + + if device.used_root_font_metrics() && root_font_metrics_changed { + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; } } + + if is_container + && (old_font_size.is_some_and(|old| old != new_font_size) + || old_line_height.is_some_and(|old| old != new_line_height)) + { + // TODO(emilio): Maybe only do this if we were matched + // against relative font sizes? + // Also, maybe we should do this as well for font-family / + // etc changes (for ex/ch/ic units to work correctly)? We + // should probably do the optimization mentioned above if + // so. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } } if context.shared.stylist.quirks_mode() == QuirksMode::Quirks { diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs @@ -399,7 +399,7 @@ impl<'a> Context<'a> { flags |= QueryFontMetricsFlags::USE_USER_FONT_SET } self.device() - .query_font_metrics(vertical, font, size, flags) + .query_font_metrics(vertical, font, size, flags, /* track_changes = */ true) } /// The current viewport size, used to resolve viewport units. diff --git a/servo/components/style/values/generics/calc.rs b/servo/components/style/values/generics/calc.rs @@ -145,7 +145,11 @@ pub enum SortKey { Lvmin, Lvw, Px, + Rcap, + Rch, Rem, + Rex, + Ric, Rlh, Sec, Svb, diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs @@ -297,11 +297,15 @@ impl generic::CalcNodeLeaf for Leaf { Self::Length(ref l) => match *l { NoCalcLength::Absolute(..) => SortKey::Px, NoCalcLength::FontRelative(ref relative) => match *relative { - FontRelativeLength::Ch(..) => SortKey::Ch, FontRelativeLength::Em(..) => SortKey::Em, FontRelativeLength::Ex(..) => SortKey::Ex, + FontRelativeLength::Rex(..) => SortKey::Rex, + FontRelativeLength::Ch(..) => SortKey::Ch, + FontRelativeLength::Rch(..) => SortKey::Rch, FontRelativeLength::Cap(..) => SortKey::Cap, + FontRelativeLength::Rcap(..) => SortKey::Rcap, FontRelativeLength::Ic(..) => SortKey::Ic, + FontRelativeLength::Ric(..) => SortKey::Ric, FontRelativeLength::Rem(..) => SortKey::Rem, FontRelativeLength::Lh(..) => SortKey::Lh, FontRelativeLength::Rlh(..) => SortKey::Rlh, diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs @@ -784,6 +784,18 @@ const LARGER_FONT_SIZE_RATIO: f32 = 1.2; pub const FONT_MEDIUM_PX: f32 = 16.0; /// The default line height. pub const FONT_MEDIUM_LINE_HEIGHT_PX: f32 = FONT_MEDIUM_PX * 1.2; +/// The default ex height -- https://drafts.csswg.org/css-values/#ex +/// > In the cases where it is impossible or impractical to determine the x-height, a value of 0.5em must be assumed +pub const FONT_MEDIUM_EX_PX: f32 = FONT_MEDIUM_PX * 0.5; +/// The default cap height -- https://drafts.csswg.org/css-values/#cap +/// > In the cases where it is impossible or impractical to determine the cap-height, the font’s ascent must be used +pub const FONT_MEDIUM_CAP_PX: f32 = FONT_MEDIUM_PX; +/// The default advance measure -- https://drafts.csswg.org/css-values/#ch +/// > Thus, the ch unit falls back to 0.5em in the general case +pub const FONT_MEDIUM_CH_PX: f32 = FONT_MEDIUM_PX * 0.5; +/// The default idographic advance measure -- https://drafts.csswg.org/css-values/#ic +/// > In the cases where it is impossible or impractical to determine the ideographic advance measure, it must be assumed to be 1em +pub const FONT_MEDIUM_IC_PX: f32 = FONT_MEDIUM_PX; impl FontSizeKeyword { #[inline] diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs @@ -12,7 +12,7 @@ use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; #[cfg(feature = "gecko")] use crate::gecko_bindings::structs::GeckoFontMetrics; use crate::parser::{Parse, ParserContext}; -use crate::values::computed::{self, CSSPixelLength, Context}; +use crate::values::computed::{self, CSSPixelLength, Context, FontSize}; use crate::values::generics::length as generics; use crate::values::generics::length::{ GenericAnchorSizeFunction, GenericLengthOrNumber, GenericLengthPercentageOrNormal, @@ -60,22 +60,34 @@ pub enum FontRelativeLength { /// A "ex" value: https://drafts.csswg.org/css-values/#ex #[css(dimension)] Ex(CSSFloat), + /// A "rex" value: https://drafts.csswg.org/css-values/#rex + #[css(dimension)] + Rex(CSSFloat), /// A "ch" value: https://drafts.csswg.org/css-values/#ch #[css(dimension)] Ch(CSSFloat), + /// A "rch" value: https://drafts.csswg.org/css-values/#rch + #[css(dimension)] + Rch(CSSFloat), /// A "cap" value: https://drafts.csswg.org/css-values/#cap #[css(dimension)] Cap(CSSFloat), + /// A "rcap" value: https://drafts.csswg.org/css-values/#rcap + #[css(dimension)] + Rcap(CSSFloat), /// An "ic" value: https://drafts.csswg.org/css-values/#ic #[css(dimension)] Ic(CSSFloat), + /// A "ric" value: https://drafts.csswg.org/css-values/#ric + #[css(dimension)] + Ric(CSSFloat), /// A "rem" value: https://drafts.csswg.org/css-values/#rem #[css(dimension)] Rem(CSSFloat), /// A "lh" value: https://drafts.csswg.org/css-values/#lh #[css(dimension)] Lh(CSSFloat), - /// A "rlh" value: https://drafts.csswg.org/css-values/#lh + /// A "rlh" value: https://drafts.csswg.org/css-values/#rlh #[css(dimension)] Rlh(CSSFloat), } @@ -119,12 +131,20 @@ impl FontRelativeLength { pub const EM: &'static str = "em"; /// Unit identifier for `ex`. pub const EX: &'static str = "ex"; + /// Unit identifier for `rex`. + pub const REX: &'static str = "rex"; /// Unit identifier for `ch`. pub const CH: &'static str = "ch"; + /// Unit identifier for `rch`. + pub const RCH: &'static str = "rch"; /// Unit identifier for `cap`. pub const CAP: &'static str = "cap"; + /// Unit identifier for `rcap`. + pub const RCAP: &'static str = "rcap"; /// Unit identifier for `ic`. pub const IC: &'static str = "ic"; + /// Unit identifier for `ric`. + pub const RIC: &'static str = "ric"; /// Unit identifier for `rem`. pub const REM: &'static str = "rem"; /// Unit identifier for `lh`. @@ -137,9 +157,13 @@ impl FontRelativeLength { match *self { Self::Em(v) | Self::Ex(v) + | Self::Rex(v) | Self::Ch(v) + | Self::Rch(v) | Self::Cap(v) + | Self::Rcap(v) | Self::Ic(v) + | Self::Ric(v) | Self::Rem(v) | Self::Lh(v) | Self::Rlh(v) => v, @@ -151,9 +175,13 @@ impl FontRelativeLength { match *self { Self::Em(_) => Self::EM, Self::Ex(_) => Self::EX, + Self::Rex(_) => Self::REX, Self::Ch(_) => Self::CH, + Self::Rch(_) => Self::RCH, Self::Cap(_) => Self::CAP, + Self::Rcap(_) => Self::RCAP, Self::Ic(_) => Self::IC, + Self::Ric(_) => Self::RIC, Self::Rem(_) => Self::REM, Self::Lh(_) => Self::LH, Self::Rlh(_) => Self::RLH, @@ -173,9 +201,13 @@ impl FontRelativeLength { Ok(match (self, other) { (&Em(one), &Em(other)) => Em(op(one, other)), (&Ex(one), &Ex(other)) => Ex(op(one, other)), + (&Rex(one), &Rex(other)) => Rex(op(one, other)), (&Ch(one), &Ch(other)) => Ch(op(one, other)), + (&Rch(one), &Rch(other)) => Rch(op(one, other)), (&Cap(one), &Cap(other)) => Cap(op(one, other)), + (&Rcap(one), &Rcap(other)) => Rcap(op(one, other)), (&Ic(one), &Ic(other)) => Ic(op(one, other)), + (&Ric(one), &Ric(other)) => Ric(op(one, other)), (&Rem(one), &Rem(other)) => Rem(op(one, other)), (&Lh(one), &Lh(other)) => Lh(op(one, other)), (&Rlh(one), &Rlh(other)) => Rlh(op(one, other)), @@ -183,7 +215,8 @@ impl FontRelativeLength { // able to figure it own on its own so we help. _ => unsafe { match *self { - Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {}, + Em(..) | Rem(..) | Ex(..) | Rex(..) | Ch(..) | Rch(..) | Cap(..) | Rcap(..) + | Ic(..) | Ric(..) | Lh(..) | Rlh(..) => {}, } debug_unreachable!("Forgot to handle unit in try_op()") }, @@ -194,9 +227,13 @@ impl FontRelativeLength { match self { Self::Em(x) => Self::Em(op(*x)), Self::Ex(x) => Self::Ex(op(*x)), + Self::Rex(x) => Self::Rex(op(*x)), Self::Ch(x) => Self::Ch(op(*x)), + Self::Rch(x) => Self::Rch(op(*x)), Self::Cap(x) => Self::Cap(op(*x)), + Self::Rcap(x) => Self::Rcap(op(*x)), Self::Ic(x) => Self::Ic(op(*x)), + Self::Ric(x) => Self::Ric(op(*x)), Self::Rem(x) => Self::Rem(op(*x)), Self::Lh(x) => Self::Lh(op(*x)), Self::Rlh(x) => Self::Rlh(op(*x)), @@ -228,8 +265,15 @@ impl FontRelativeLength { Self::Ch(v) => v * metrics.mChSize.px(), Self::Cap(v) => v * metrics.mCapHeight.px(), Self::Ic(v) => v * metrics.mIcWidth.px(), - // `lh`, `rlh` & `rem` are unsupported as we have no context for it. - Self::Rem(_) | Self::Lh(_) | Self::Rlh(_) => return Err(()), + // `lh`, `rlh` are unsupported as we have no line-height context + // `rem`, `rex`, `rch`, `rcap`, and `ric` are unsupported as we have no root font context. + Self::Lh(_) + | Self::Rlh(_) + | Self::Rem(_) + | Self::Rex(_) + | Self::Rch(_) + | Self::Rcap(_) + | Self::Ric(_) => return Err(()), }) } @@ -255,8 +299,72 @@ impl FontRelativeLength { context.query_font_metrics(base_size, orientation, flags) } + fn ex_size( + context: &Context, + base_size: FontBaseSize, + reference_font_size: &FontSize, + ) -> computed::Length { + // The x-height is an intrinsically horizontal metric. + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::Horizontal, + QueryFontMetricsFlags::empty(), + ); + metrics.x_height_or_default(&reference_font_size) + } + + fn ch_size( + context: &Context, + base_size: FontBaseSize, + reference_font_size: &FontSize, + ) -> computed::Length { + // https://drafts.csswg.org/css-values/#ch: + // + // Equal to the used advance measure of the “0” (ZERO, + // U+0030) glyph in the font used to render it. (The advance + // measure of a glyph is its advance width or height, + // whichever is in the inline axis of the element.) + // + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::MatchContextPreferHorizontal, + QueryFontMetricsFlags::NEEDS_CH, + ); + metrics.zero_advance_measure_or_default( + &reference_font_size, + context.style().writing_mode.is_upright(), + ) + } + + fn cap_size(context: &Context, base_size: FontBaseSize) -> computed::Length { + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::Horizontal, + QueryFontMetricsFlags::empty(), + ); + metrics.cap_height_or_default() + } + + fn ic_size( + context: &Context, + base_size: FontBaseSize, + reference_font_size: &FontSize, + ) -> computed::Length { + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::MatchContextPreferVertical, + QueryFontMetricsFlags::NEEDS_IC, + ); + metrics.ic_width_or_default(&reference_font_size) + } + let reference_font_size = base_size.resolve(context); match *self { + // Local font-relative units Self::Em(length) => { if context.for_non_inherited_property && base_size == FontBaseSize::CurrentStyle { context @@ -267,118 +375,6 @@ impl FontRelativeLength { (reference_font_size.computed_size(), length) }, - Self::Ex(length) => { - // The x-height is an intrinsically horizontal metric. - let metrics = query_font_metrics( - context, - base_size, - FontMetricsOrientation::Horizontal, - QueryFontMetricsFlags::empty(), - ); - let reference_size = metrics.x_height.unwrap_or_else(|| { - // https://drafts.csswg.org/css-values/#ex - // - // In the cases where it is impossible or impractical to - // determine the x-height, a value of 0.5em must be - // assumed. - // - // (But note we use 0.5em of the used, not computed - // font-size) - reference_font_size.used_size() * 0.5 - }); - (reference_size, length) - }, - Self::Ch(length) => { - // https://drafts.csswg.org/css-values/#ch: - // - // Equal to the used advance measure of the “0” (ZERO, - // U+0030) glyph in the font used to render it. (The advance - // measure of a glyph is its advance width or height, - // whichever is in the inline axis of the element.) - // - let metrics = query_font_metrics( - context, - base_size, - FontMetricsOrientation::MatchContextPreferHorizontal, - QueryFontMetricsFlags::NEEDS_CH, - ); - let reference_size = metrics.zero_advance_measure.unwrap_or_else(|| { - // https://drafts.csswg.org/css-values/#ch - // - // In the cases where it is impossible or impractical to - // determine the measure of the “0” glyph, it must be - // assumed to be 0.5em wide by 1em tall. Thus, the ch - // unit falls back to 0.5em in the general case, and to - // 1em when it would be typeset upright (i.e. - // writing-mode is vertical-rl or vertical-lr and - // text-orientation is upright). - // - // Same caveat about computed vs. used font-size applies - // above. - let wm = context.style().writing_mode; - if wm.is_vertical() && wm.is_upright() { - reference_font_size.used_size() - } else { - reference_font_size.used_size() * 0.5 - } - }); - (reference_size, length) - }, - Self::Cap(length) => { - let metrics = query_font_metrics( - context, - base_size, - FontMetricsOrientation::Horizontal, - QueryFontMetricsFlags::empty(), - ); - let reference_size = metrics.cap_height.unwrap_or_else(|| { - // https://drafts.csswg.org/css-values/#cap - // - // In the cases where it is impossible or impractical to - // determine the cap-height, the font’s ascent must be - // used. - // - metrics.ascent - }); - (reference_size, length) - }, - Self::Ic(length) => { - let metrics = query_font_metrics( - context, - base_size, - FontMetricsOrientation::MatchContextPreferVertical, - QueryFontMetricsFlags::NEEDS_IC, - ); - let reference_size = metrics.ic_width.unwrap_or_else(|| { - // https://drafts.csswg.org/css-values/#ic - // - // In the cases where it is impossible or impractical to - // determine the ideographic advance measure, it must be - // assumed to be 1em. - // - // Same caveat about computed vs. used as for other - // metric-dependent units. - reference_font_size.used_size() - }); - (reference_size, length) - }, - Self::Rem(length) => { - // https://drafts.csswg.org/css-values/#rem: - // - // When specified on the font-size property of the root - // element, the rem units refer to the property's initial - // value. - // - let reference_size = if context.builder.is_root_element || context.in_media_query { - reference_font_size.computed_size() - } else { - context - .device() - .root_font_size() - .zoom(context.builder.effective_zoom) - }; - (reference_size, length) - }, Self::Lh(length) => { // https://drafts.csswg.org/css-values-4/#lh // @@ -412,6 +408,73 @@ impl FontRelativeLength { }; (reference_size, length) }, + Self::Ex(length) => (ex_size(context, base_size, &reference_font_size), length), + Self::Ch(length) => (ch_size(context, base_size, &reference_font_size), length), + Self::Cap(length) => (cap_size(context, base_size), length), + Self::Ic(length) => (ic_size(context, base_size, &reference_font_size), length), + + // Root font relative units + Self::Rex(length) => { + let reference_size = if context.builder.is_root_element || context.in_media_query { + ex_size(context, base_size, &reference_font_size) + } else { + context + .device() + .root_font_metrics_ex() + .zoom(context.builder.effective_zoom) + }; + (reference_size, length) + }, + Self::Rch(length) => { + let reference_size = if context.builder.is_root_element || context.in_media_query { + ch_size(context, base_size, &reference_font_size) + } else { + context + .device() + .root_font_metrics_ch() + .zoom(context.builder.effective_zoom) + }; + (reference_size, length) + }, + Self::Rcap(length) => { + let reference_size = if context.builder.is_root_element || context.in_media_query { + cap_size(context, base_size) + } else { + context + .device() + .root_font_metrics_cap() + .zoom(context.builder.effective_zoom) + }; + (reference_size, length) + }, + Self::Ric(length) => { + let reference_size = if context.builder.is_root_element || context.in_media_query { + ic_size(context, base_size, &reference_font_size) + } else { + context + .device() + .root_font_metrics_ic() + .zoom(context.builder.effective_zoom) + }; + (reference_size, length) + }, + Self::Rem(length) => { + // https://drafts.csswg.org/css-values/#rem: + // + // When specified on the font-size property of the root + // element, the rem units refer to the property's initial + // value. + // + let reference_size = if context.builder.is_root_element || context.in_media_query { + reference_font_size.computed_size() + } else { + context + .device() + .root_font_size() + .zoom(context.builder.effective_zoom) + }; + (reference_size, length) + }, Self::Rlh(length) => { // https://drafts.csswg.org/css-values-4/#rlh // @@ -1082,9 +1145,13 @@ impl NoCalcLength { // font-relative "em" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Em(value)), "ex" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Ex(value)), + "rex" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Rex(value)), "ch" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Ch(value)), + "rch" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Rch(value)), "cap" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Cap(value)), + "rcap" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Rcap(value)), "ic" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Ic(value)), + "ric" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Ric(value)), "rem" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Rem(value)), "lh" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Lh(value)), "rlh" if context.allows_computational_dependence() => Self::FontRelative(FontRelativeLength::Rlh(value)), @@ -1367,9 +1434,13 @@ impl PartialOrd for FontRelativeLength { match (self, other) { (&Em(ref one), &Em(ref other)) => one.partial_cmp(other), (&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other), + (&Rex(ref one), &Rex(ref other)) => one.partial_cmp(other), (&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other), + (&Rch(ref one), &Rch(ref other)) => one.partial_cmp(other), (&Cap(ref one), &Cap(ref other)) => one.partial_cmp(other), + (&Rcap(ref one), &Rcap(ref other)) => one.partial_cmp(other), (&Ic(ref one), &Ic(ref other)) => one.partial_cmp(other), + (&Ric(ref one), &Ric(ref other)) => one.partial_cmp(other), (&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other), (&Lh(ref one), &Lh(ref other)) => one.partial_cmp(other), (&Rlh(ref one), &Rlh(ref other)) => one.partial_cmp(other), @@ -1377,7 +1448,8 @@ impl PartialOrd for FontRelativeLength { // able to figure it own on its own so we help. _ => unsafe { match *self { - Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {}, + Em(..) | Ex(..) | Rex(..) | Ch(..) | Rch(..) | Cap(..) | Rcap(..) | Ic(..) + | Ric(..) | Rem(..) | Lh(..) | Rlh(..) => {}, } debug_unreachable!("Forgot an arm in partial_cmp?") }, diff --git a/servo/ports/geckolib/glue.rs b/servo/ports/geckolib/glue.rs @@ -2055,6 +2055,12 @@ pub extern "C" fn Servo_StyleSet_UsesFontMetrics(raw_data: &PerDocumentStyleData } #[no_mangle] +pub extern "C" fn Servo_StyleSet_UsesRootFontMetrics(raw_data: &PerDocumentStyleData) -> bool { + let doc_data = raw_data; + doc_data.borrow().stylist.device().used_root_font_metrics() +} + +#[no_mangle] pub extern "C" fn Servo_StyleSheet_HasRules(raw_contents: &StylesheetContents) -> bool { let global_style_data = &*GLOBAL_STYLE_DATA; let guard = global_style_data.shared_lock.read(); diff --git a/testing/web-platform/meta/css/css-conditional/container-queries/font-relative-units-dynamic.html.ini b/testing/web-platform/meta/css/css-conditional/container-queries/font-relative-units-dynamic.html.ini @@ -1,17 +1,8 @@ [font-relative-units-dynamic.html] - [rex units respond to changes] - expected: FAIL - [cap units respond to changes] expected: if (os == "android") and fission: PASS FAIL - [rch units respond to changes] - expected: FAIL - - [ric units respond to changes] - expected: FAIL - [rcap units respond to changes] expected: FAIL diff --git a/testing/web-platform/meta/css/css-conditional/container-queries/font-relative-units.html.ini b/testing/web-platform/meta/css/css-conditional/container-queries/font-relative-units.html.ini @@ -1,12 +0,0 @@ -[font-relative-units.html] - [rex relative inline-size] - expected: FAIL - - [rch relative inline-size] - expected: FAIL - - [ric relative inline-size] - expected: FAIL - - [rcap relative inline-size] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-fonts/rcap-in-monospace.html.ini b/testing/web-platform/meta/css/css-fonts/rcap-in-monospace.html.ini @@ -1,2 +0,0 @@ -[rcap-in-monospace.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-fonts/rch-in-monospace.html.ini b/testing/web-platform/meta/css/css-fonts/rch-in-monospace.html.ini @@ -1,2 +0,0 @@ -[rch-in-monospace.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-fonts/rex-in-monospace.html.ini b/testing/web-platform/meta/css/css-fonts/rex-in-monospace.html.ini @@ -1,2 +0,0 @@ -[rex-in-monospace.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-fonts/ric-in-monospace.html.ini b/testing/web-platform/meta/css/css-fonts/ric-in-monospace.html.ini @@ -1,2 +0,0 @@ -[ric-in-monospace.html] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/rcap-invalidation.html.ini b/testing/web-platform/meta/css/css-values/rcap-invalidation.html.ini @@ -1,3 +0,0 @@ -[rcap-invalidation.html] - [CSS Values and Units Test: rcap invalidation] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/rch-invalidation.html.ini b/testing/web-platform/meta/css/css-values/rch-invalidation.html.ini @@ -1,3 +0,0 @@ -[rch-invalidation.html] - [CSS Values and Units Test: rch invalidation] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/rex-invalidation.html.ini b/testing/web-platform/meta/css/css-values/rex-invalidation.html.ini @@ -1,3 +0,0 @@ -[rex-invalidation.html] - [CSS Values and Units Test: rex invalidation] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/ric-invalidation.html.ini b/testing/web-platform/meta/css/css-values/ric-invalidation.html.ini @@ -1,3 +0,0 @@ -[ric-invalidation.html] - [CSS Values and Units Test: ric invalidation] - expected: FAIL diff --git a/testing/web-platform/tests/css/css-viewport/zoom/font-relative-units.html b/testing/web-platform/tests/css/css-viewport/zoom/font-relative-units.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <link rel="help" href="https://drafts.csswg.org/css-viewport/#zoom-property"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <meta name="assert" content="test font-relative units and zoom"> + <style> + html { + font-size: 20px; + line-height: 1; + zoom: 2; + } + + .unit { + height: 20px; + outline: 1px solid black; + outline-offset: -1px; + } + + .unit::after { + content: attr(id); + font-size: 10px; + } + </style> +</head> +<body> + <div id="div"></div> + <script> + const units = [ + ['em', 'rem'], + ['lh', 'rlh'], + ['ex', 'rex'], + ['cap', 'rcap'], + ['ch', 'rch'], + ['ic', 'ric'], + ]; + + for (const pair of units) { + for (const unit of pair) { + div.insertAdjacentHTML("beforeend", `<div class="unit" id="${unit}" style="width: 5${unit}"></div>`); + } + } + + setup({ explicit_done: true }); + document.fonts.ready.then(() => { + for (const [localUnit, rootUnit] of units) { + test(() => { + let localUnitWidth = document.getElementById(localUnit).getBoundingClientRect().width; + let rootUnitWidth = document.getElementById(rootUnit).getBoundingClientRect().width; + assert_equals(localUnitWidth, rootUnitWidth, `expect 1${localUnit} = 1${rootUnit} at the same effective zoom`); + }, `${localUnit} = ${rootUnit}`); + } + done(); + }); + </script> +</body> +</html> +\ No newline at end of file