diff options
Diffstat (limited to '')
-rw-r--r-- | servo/components/style/properties/cascade.rs | 1213 |
1 files changed, 1213 insertions, 0 deletions
diff --git a/servo/components/style/properties/cascade.rs b/servo/components/style/properties/cascade.rs new file mode 100644 index 0000000000..01b164e017 --- /dev/null +++ b/servo/components/style/properties/cascade.rs @@ -0,0 +1,1213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The main cascading algorithm of the style system. + +use crate::applicable_declarations::CascadePriority; +use crate::context::QuirksMode; +use crate::custom_properties::CustomPropertiesBuilder; +use crate::dom::TElement; +use crate::font_metrics::FontMetricsOrientation; +use crate::values::specified::length::FontBaseSize; +use crate::logical_geometry::WritingMode; +use crate::media_queries::Device; +use crate::properties::{ + CSSWideKeyword, ComputedValueFlags, ComputedValues, DeclarationImportanceIterator, Importance, + LonghandId, LonghandIdSet, PropertyDeclaration, PropertyDeclarationId, PropertyFlags, + ShorthandsWithPropertyReferencesCache, StyleBuilder, CASCADE_PROPERTY, +}; +use crate::rule_cache::{RuleCache, RuleCacheConditions}; +use crate::rule_tree::{StrongRuleNode, CascadeLevel}; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::StylesheetGuards; +use crate::style_adjuster::StyleAdjuster; +use crate::stylesheets::{Origin, layer_rule::LayerOrder}; +use crate::stylesheets::container_rule::ContainerSizeQuery; +use crate::values::{computed, specified}; +use fxhash::FxHashMap; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::mem; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CanHaveLogicalProperties { + No, + Yes, +} + +/// Performs the CSS cascade, computing new styles for an element from its parent style. +/// +/// The arguments are: +/// +/// * `device`: Used to get the initial viewport and other external state. +/// +/// * `rule_node`: The rule node in the tree that represent the CSS rules that +/// matched. +/// +/// * `parent_style`: The parent style, if applicable; if `None`, this is the root node. +/// +/// Returns the computed values. +/// * `flags`: Various flags. +/// +pub fn cascade<E>( + device: &Device, + pseudo: Option<&PseudoElement>, + rule_node: &StrongRuleNode, + guards: &StylesheetGuards, + originating_element_style: Option<&ComputedValues>, + parent_style: Option<&ComputedValues>, + parent_style_ignoring_first_line: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + visited_rules: Option<&StrongRuleNode>, + cascade_input_flags: ComputedValueFlags, + quirks_mode: QuirksMode, + rule_cache: Option<&RuleCache>, + rule_cache_conditions: &mut RuleCacheConditions, + element: Option<E>, +) -> Arc<ComputedValues> +where + E: TElement, +{ + cascade_rules( + device, + pseudo, + rule_node, + guards, + originating_element_style, + parent_style, + parent_style_ignoring_first_line, + layout_parent_style, + CascadeMode::Unvisited { visited_rules }, + cascade_input_flags, + quirks_mode, + rule_cache, + rule_cache_conditions, + element, + ) +} + +struct DeclarationIterator<'a> { + // Global to the iteration. + guards: &'a StylesheetGuards<'a>, + restriction: Option<PropertyFlags>, + // The rule we're iterating over. + current_rule_node: Option<&'a StrongRuleNode>, + // Per rule state. + declarations: DeclarationImportanceIterator<'a>, + origin: Origin, + importance: Importance, + priority: CascadePriority, +} + +impl<'a> DeclarationIterator<'a> { + #[inline] + fn new( + rule_node: &'a StrongRuleNode, + guards: &'a StylesheetGuards, + pseudo: Option<&PseudoElement>, + ) -> Self { + let restriction = pseudo.and_then(|p| p.property_restriction()); + let mut iter = Self { + guards, + current_rule_node: Some(rule_node), + origin: Origin::UserAgent, + importance: Importance::Normal, + priority: CascadePriority::new(CascadeLevel::UANormal, LayerOrder::root()), + declarations: DeclarationImportanceIterator::default(), + restriction, + }; + iter.update_for_node(rule_node); + iter + } + + fn update_for_node(&mut self, node: &'a StrongRuleNode) { + self.priority = node.cascade_priority(); + let level = self.priority.cascade_level(); + self.origin = level.origin(); + self.importance = level.importance(); + let guard = match self.origin { + Origin::Author => self.guards.author, + Origin::User | Origin::UserAgent => self.guards.ua_or_user, + }; + self.declarations = match node.style_source() { + Some(source) => source.read(guard).declaration_importance_iter(), + None => DeclarationImportanceIterator::default(), + }; + } +} + +impl<'a> Iterator for DeclarationIterator<'a> { + type Item = (&'a PropertyDeclaration, CascadePriority); + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + loop { + if let Some((decl, importance)) = self.declarations.next_back() { + if self.importance != importance { + continue; + } + + if let Some(restriction) = self.restriction { + // decl.id() is either a longhand or a custom + // property. Custom properties are always allowed, but + // longhands are only allowed if they have our + // restriction flag set. + if let PropertyDeclarationId::Longhand(id) = decl.id() { + if !id.flags().contains(restriction) && self.origin != Origin::UserAgent { + continue; + } + } + } + + return Some((decl, self.priority)); + } + + let next_node = self.current_rule_node.take()?.parent()?; + self.current_rule_node = Some(next_node); + self.update_for_node(next_node); + } + } +} + +fn cascade_rules<E>( + device: &Device, + pseudo: Option<&PseudoElement>, + rule_node: &StrongRuleNode, + guards: &StylesheetGuards, + originating_element_style: Option<&ComputedValues>, + parent_style: Option<&ComputedValues>, + parent_style_ignoring_first_line: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + cascade_mode: CascadeMode, + cascade_input_flags: ComputedValueFlags, + quirks_mode: QuirksMode, + rule_cache: Option<&RuleCache>, + rule_cache_conditions: &mut RuleCacheConditions, + element: Option<E>, +) -> Arc<ComputedValues> +where + E: TElement, +{ + debug_assert_eq!( + parent_style.is_some(), + parent_style_ignoring_first_line.is_some() + ); + apply_declarations( + device, + pseudo, + rule_node, + guards, + DeclarationIterator::new(rule_node, guards, pseudo), + originating_element_style, + parent_style, + parent_style_ignoring_first_line, + layout_parent_style, + cascade_mode, + cascade_input_flags, + quirks_mode, + rule_cache, + rule_cache_conditions, + element, + ) +} + +/// Whether we're cascading for visited or unvisited styles. +#[derive(Clone, Copy)] +pub enum CascadeMode<'a> { + /// We're cascading for unvisited styles. + Unvisited { + /// The visited rules that should match the visited style. + visited_rules: Option<&'a StrongRuleNode>, + }, + /// We're cascading for visited styles. + Visited { + /// The writing mode of our unvisited style, needed to correctly resolve + /// logical properties.. + writing_mode: WritingMode, + }, +} + +/// NOTE: This function expects the declaration with more priority to appear +/// first. +pub fn apply_declarations<'a, E, I>( + device: &Device, + pseudo: Option<&PseudoElement>, + rules: &StrongRuleNode, + guards: &StylesheetGuards, + iter: I, + originating_element_style: Option<&ComputedValues>, + parent_style: Option<&ComputedValues>, + parent_style_ignoring_first_line: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + cascade_mode: CascadeMode, + cascade_input_flags: ComputedValueFlags, + quirks_mode: QuirksMode, + rule_cache: Option<&RuleCache>, + rule_cache_conditions: &mut RuleCacheConditions, + element: Option<E>, +) -> Arc<ComputedValues> +where + E: TElement, + I: Iterator<Item = (&'a PropertyDeclaration, CascadePriority)>, +{ + debug_assert_eq!( + originating_element_style.is_some(), + element.is_some() && pseudo.is_some() + ); + debug_assert!(layout_parent_style.is_none() || parent_style.is_some()); + debug_assert_eq!( + parent_style.is_some(), + parent_style_ignoring_first_line.is_some() + ); + #[cfg(feature = "gecko")] + debug_assert!( + parent_style.is_none() || + ::std::ptr::eq( + parent_style.unwrap(), + parent_style_ignoring_first_line.unwrap() + ) || + parent_style.unwrap().is_first_line_style() + ); + + let inherited_style = parent_style.unwrap_or(device.default_computed_values()); + + let mut declarations = SmallVec::<[(&_, CascadePriority); 32]>::new(); + let mut referenced_properties = LonghandIdSet::default(); + let custom_properties = { + let mut builder = CustomPropertiesBuilder::new(inherited_style.custom_properties(), device); + + for (declaration, priority) in iter { + declarations.push((declaration, priority)); + if let PropertyDeclaration::Custom(ref declaration) = *declaration { + builder.cascade(declaration, priority); + } else { + referenced_properties.insert(declaration.id().as_longhand().unwrap()); + } + } + + builder.build() + }; + + let is_root_element = pseudo.is_none() && element.map_or(false, |e| e.is_root()); + let container_size_query = + ContainerSizeQuery::for_option_element(element, originating_element_style); + + let mut context = computed::Context::new( + // We'd really like to own the rules here to avoid refcount traffic, but + // animation's usage of `apply_declarations` make this tricky. See bug + // 1375525. + StyleBuilder::new( + device, + parent_style, + parent_style_ignoring_first_line, + pseudo, + Some(rules.clone()), + custom_properties, + is_root_element, + ), + quirks_mode, + rule_cache_conditions, + container_size_query, + ); + + context.style().add_flags(cascade_input_flags); + + let using_cached_reset_properties; + let mut cascade = Cascade::new(&mut context, cascade_mode, &referenced_properties); + let mut shorthand_cache = ShorthandsWithPropertyReferencesCache::default(); + + let properties_to_apply = match cascade.cascade_mode { + CascadeMode::Visited { writing_mode } => { + cascade.context.builder.writing_mode = writing_mode; + // We never insert visited styles into the cache so we don't need to + // try looking it up. It also wouldn't be super-profitable, only a + // handful reset properties are non-inherited. + using_cached_reset_properties = false; + LonghandIdSet::visited_dependent() + }, + CascadeMode::Unvisited { visited_rules } => { + if cascade.apply_properties( + CanHaveLogicalProperties::No, + LonghandIdSet::writing_mode_group(), + declarations.iter().cloned(), + &mut shorthand_cache, + ) { + cascade.compute_writing_mode(); + } + + if cascade.apply_properties( + CanHaveLogicalProperties::No, + LonghandIdSet::fonts_and_color_group(), + declarations.iter().cloned(), + &mut shorthand_cache, + ) { + cascade.fixup_font_stuff(); + } + + if let Some(visited_rules) = visited_rules { + cascade.compute_visited_style_if_needed( + element, + originating_element_style, + parent_style, + parent_style_ignoring_first_line, + layout_parent_style, + visited_rules, + guards, + ); + } + + using_cached_reset_properties = + cascade.try_to_use_cached_reset_properties(rule_cache, guards); + + if using_cached_reset_properties { + LonghandIdSet::late_group_only_inherited() + } else { + LonghandIdSet::late_group() + } + } + }; + + cascade.apply_properties( + CanHaveLogicalProperties::Yes, + properties_to_apply, + declarations.iter().cloned(), + &mut shorthand_cache, + ); + + cascade.finished_applying_properties(); + + context.builder.clear_modified_reset(); + + if matches!(cascade_mode, CascadeMode::Unvisited { .. }) { + StyleAdjuster::new(&mut context.builder) + .adjust(layout_parent_style.unwrap_or(inherited_style), element); + } + + if context.builder.modified_reset() || using_cached_reset_properties { + // If we adjusted any reset structs, we can't cache this ComputedValues. + // + // Also, if we re-used existing reset structs, don't bother caching it + // back again. (Aside from being wasted effort, it will be wrong, since + // context.rule_cache_conditions won't be set appropriately if we didn't + // compute those reset properties.) + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + + context.builder.build() +} + +/// For ignored colors mode, we sometimes want to do something equivalent to +/// "revert-or-initial", where we `revert` for a given origin, but then apply a +/// given initial value if nothing in other origins did override it. +/// +/// This is a bit of a clunky way of achieving this. +type DeclarationsToApplyUnlessOverriden = SmallVec<[PropertyDeclaration; 2]>; + +fn tweak_when_ignoring_colors( + context: &computed::Context, + longhand_id: LonghandId, + origin: Origin, + declaration: &mut Cow<PropertyDeclaration>, + declarations_to_apply_unless_overriden: &mut DeclarationsToApplyUnlessOverriden, +) { + use crate::values::specified::Color; + use crate::values::computed::ToComputedValue; + use cssparser::RGBA; + + if !longhand_id.ignored_when_document_colors_disabled() { + return; + } + + let is_ua_or_user_rule = matches!(origin, Origin::User | Origin::UserAgent); + if is_ua_or_user_rule { + return; + } + + // Don't override background-color on ::-moz-color-swatch. It is set as an + // author style (via the style attribute), but it's pretty important for it + // to show up for obvious reasons :) + if context.builder.pseudo.map_or(false, |p| p.is_color_swatch()) && + longhand_id == LonghandId::BackgroundColor + { + return; + } + + fn alpha_channel(color: &Color, context: &computed::Context) -> u8 { + // We assume here currentColor is opaque. + let color = color.to_computed_value(context).into_rgba(RGBA::new(0, 0, 0, 255)); + color.alpha + } + + // A few special-cases ahead. + match **declaration { + PropertyDeclaration::BackgroundColor(ref color) => { + // We honor system colors and transparent colors unconditionally. + // + // NOTE(emilio): We honor transparent unconditionally, like we do + // for color, even though it causes issues like bug 1625036. The + // reasoning is that the conditions that trigger that (having + // mismatched widget and default backgrounds) are both uncommon, and + // broken in other applications as well, and not honoring + // transparent makes stuff uglier or break unconditionally + // (bug 1666059, bug 1755713). + if color.honored_in_forced_colors_mode(/* allow_transparent = */ true) { + return; + } + // For background-color, we revert or initial-with-preserved-alpha + // otherwise, this is needed to preserve semi-transparent + // backgrounds. + let alpha = alpha_channel(color, context); + if alpha == 0 { + return; + } + let mut color = context.builder.device.default_background_color(); + color.alpha = alpha; + declarations_to_apply_unless_overriden + .push(PropertyDeclaration::BackgroundColor(color.into())) + }, + PropertyDeclaration::Color(ref color) => { + // We honor color: transparent and system colors. + if color.0.honored_in_forced_colors_mode(/* allow_transparent = */ true) { + return; + } + // If the inherited color would be transparent, but we would + // override this with a non-transparent color, then override it with + // the default color. Otherwise just let it inherit through. + if context.builder.get_parent_inherited_text().clone_color().alpha == 0 { + let color = context.builder.device.default_color(); + declarations_to_apply_unless_overriden.push(PropertyDeclaration::Color( + specified::ColorPropertyValue(color.into()), + )) + } + }, + // We honor url background-images if backplating. + #[cfg(feature = "gecko")] + PropertyDeclaration::BackgroundImage(ref bkg) => { + use crate::values::generics::image::Image; + if static_prefs::pref!("browser.display.permit_backplate") { + if bkg.0.iter().all(|image| matches!(*image, Image::Url(..) | Image::None)) { + return; + } + } + }, + _ => { + // We honor system colors more generally for all colors. + // + // We used to honor transparent but that causes accessibility + // regressions like bug 1740924. + // + // NOTE(emilio): This doesn't handle caret-color and accent-color + // because those use a slightly different syntax (<color> | auto for + // example). + // + // That's probably fine though, as using a system color for + // caret-color doesn't make sense (using currentColor is fine), and + // we ignore accent-color in high-contrast-mode anyways. + if let Some(color) = declaration.color_value() { + if color.honored_in_forced_colors_mode(/* allow_transparent = */ false) { + return; + } + } + }, + } + + *declaration.to_mut() = + PropertyDeclaration::css_wide_keyword(longhand_id, CSSWideKeyword::Revert); +} + +struct Cascade<'a, 'b: 'a> { + context: &'a mut computed::Context<'b>, + cascade_mode: CascadeMode<'a>, + /// All the properties that have a declaration in the cascade. + referenced: &'a LonghandIdSet, + seen: LonghandIdSet, + author_specified: LonghandIdSet, + reverted_set: LonghandIdSet, + reverted: FxHashMap<LonghandId, (CascadePriority, bool)>, +} + +impl<'a, 'b: 'a> Cascade<'a, 'b> { + fn new( + context: &'a mut computed::Context<'b>, + cascade_mode: CascadeMode<'a>, + referenced: &'a LonghandIdSet, + ) -> Self { + Self { + context, + cascade_mode, + referenced, + seen: LonghandIdSet::default(), + author_specified: LonghandIdSet::default(), + reverted_set: Default::default(), + reverted: Default::default(), + } + } + + fn substitute_variables_if_needed<'decl, 'cache>( + &mut self, + declaration: &'decl PropertyDeclaration, + cache: &'cache mut ShorthandsWithPropertyReferencesCache, + ) -> Cow<'decl, PropertyDeclaration> + where + 'cache: 'decl, + { + let declaration = match *declaration { + PropertyDeclaration::WithVariables(ref declaration) => declaration, + ref d => return Cow::Borrowed(d), + }; + + if !declaration.id.inherited() { + self.context + .rule_cache_conditions + .borrow_mut() + .set_uncacheable(); + + // NOTE(emilio): We only really need to add the `display` / + // `content` flag if the CSS variable has not been specified on our + // declarations, but we don't have that information at this point, + // and it doesn't seem like an important enough optimization to + // warrant it. + match declaration.id { + LonghandId::Display => { + self.context + .builder + .add_flags(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE); + }, + LonghandId::Content => { + self.context + .builder + .add_flags(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE); + }, + _ => {}, + } + } + + declaration.value.substitute_variables( + declaration.id, + self.context.builder.writing_mode, + self.context.builder.custom_properties.as_ref(), + self.context.quirks_mode, + self.context.device(), + cache, + ) + } + + #[inline(always)] + fn apply_declaration(&mut self, longhand_id: LonghandId, declaration: &PropertyDeclaration) { + // We could (and used to) use a pattern match here, but that bloats this + // function to over 100K of compiled code! + // + // To improve i-cache behavior, we outline the individual functions and + // use virtual dispatch instead. + let discriminant = longhand_id as usize; + (CASCADE_PROPERTY[discriminant])(declaration, &mut self.context); + } + + fn apply_properties<'decls, I>( + &mut self, + can_have_logical_properties: CanHaveLogicalProperties, + properties_to_apply: &'a LonghandIdSet, + declarations: I, + mut shorthand_cache: &mut ShorthandsWithPropertyReferencesCache, + ) -> bool + where + I: Iterator<Item = (&'decls PropertyDeclaration, CascadePriority)>, + { + if !self.referenced.contains_any(properties_to_apply) { + return false; + } + + let can_have_logical_properties = can_have_logical_properties == CanHaveLogicalProperties::Yes; + + let ignore_colors = !self.context.builder.device.use_document_colors(); + let mut declarations_to_apply_unless_overriden = DeclarationsToApplyUnlessOverriden::new(); + + for (declaration, priority) in declarations { + let origin = priority.cascade_level().origin(); + + let declaration_id = declaration.id(); + let longhand_id = match declaration_id { + PropertyDeclarationId::Longhand(id) => id, + PropertyDeclarationId::Custom(..) => continue, + }; + + if !properties_to_apply.contains(longhand_id) { + continue; + } + + debug_assert!(can_have_logical_properties || !longhand_id.is_logical()); + let physical_longhand_id = if can_have_logical_properties { + longhand_id.to_physical(self.context.builder.writing_mode) + } else { + longhand_id + }; + + if self.seen.contains(physical_longhand_id) { + continue; + } + + if self.reverted_set.contains(physical_longhand_id) { + if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&physical_longhand_id) { + if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) { + continue; + } + } + } + + let mut declaration = + self.substitute_variables_if_needed(declaration, &mut shorthand_cache); + + // When document colors are disabled, do special handling of + // properties that are marked as ignored in that mode. + if ignore_colors { + tweak_when_ignoring_colors( + &self.context, + longhand_id, + origin, + &mut declaration, + &mut declarations_to_apply_unless_overriden, + ); + debug_assert_eq!( + declaration.id(), + PropertyDeclarationId::Longhand(longhand_id), + "Shouldn't change the declaration id!", + ); + } + + let is_unset = match declaration.get_css_wide_keyword() { + Some(keyword) => match keyword { + CSSWideKeyword::RevertLayer | + CSSWideKeyword::Revert => { + let origin_revert = keyword == CSSWideKeyword::Revert; + // We intentionally don't want to insert it into + // `self.seen`, `reverted` takes care of rejecting other + // declarations as needed. + self.reverted_set.insert(physical_longhand_id); + self.reverted.insert(physical_longhand_id, (priority, origin_revert)); + continue; + }, + CSSWideKeyword::Unset => true, + CSSWideKeyword::Inherit => longhand_id.inherited(), + CSSWideKeyword::Initial => !longhand_id.inherited(), + }, + None => false, + }; + + self.seen.insert(physical_longhand_id); + if origin == Origin::Author { + self.author_specified.insert(physical_longhand_id); + } + + if is_unset { + continue; + } + + // FIXME(emilio): We should avoid generating code for logical + // longhands and just use the physical ones, then rename + // physical_longhand_id to just longhand_id. + self.apply_declaration(longhand_id, &*declaration); + } + + if ignore_colors { + for declaration in declarations_to_apply_unless_overriden.iter() { + let longhand_id = match declaration.id() { + PropertyDeclarationId::Longhand(id) => id, + PropertyDeclarationId::Custom(..) => unreachable!(), + }; + debug_assert!(!longhand_id.is_logical()); + if self.seen.contains(longhand_id) { + continue; + } + self.apply_declaration(longhand_id, declaration); + } + } + + true + } + + fn compute_writing_mode(&mut self) { + debug_assert!(matches!(self.cascade_mode, CascadeMode::Unvisited { .. })); + self.context.builder.writing_mode = + WritingMode::new(self.context.builder.get_inherited_box()) + } + + fn compute_visited_style_if_needed<E>( + &mut self, + element: Option<E>, + originating_element_style: Option<&ComputedValues>, + parent_style: Option<&ComputedValues>, + parent_style_ignoring_first_line: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + visited_rules: &StrongRuleNode, + guards: &StylesheetGuards, + ) where + E: TElement, + { + debug_assert!(matches!(self.cascade_mode, CascadeMode::Unvisited { .. })); + let is_link = self.context.builder.pseudo.is_none() && element.unwrap().is_link(); + + macro_rules! visited_parent { + ($parent:expr) => { + if is_link { + $parent + } else { + $parent.map(|p| p.visited_style().unwrap_or(p)) + } + }; + } + + let writing_mode = self.context.builder.writing_mode; + + // We could call apply_declarations directly, but that'd cause + // another instantiation of this function which is not great. + let style = cascade_rules( + self.context.builder.device, + self.context.builder.pseudo, + visited_rules, + guards, + visited_parent!(originating_element_style), + visited_parent!(parent_style), + visited_parent!(parent_style_ignoring_first_line), + visited_parent!(layout_parent_style), + CascadeMode::Visited { writing_mode }, + // Cascade input flags don't matter for the visited style, they are + // in the main (unvisited) style. + Default::default(), + self.context.quirks_mode, + // The rule cache doesn't care about caching :visited + // styles, we cache the unvisited style instead. We still do + // need to set the caching dependencies properly if present + // though, so the cache conditions need to match. + None, // rule_cache + &mut *self.context.rule_cache_conditions.borrow_mut(), + element, + ); + self.context.builder.visited_style = Some(style); + } + + fn finished_applying_properties(&mut self) { + let builder = &mut self.context.builder; + + #[cfg(feature = "gecko")] + { + if let Some(bg) = builder.get_background_if_mutated() { + bg.fill_arrays(); + } + + if let Some(svg) = builder.get_svg_if_mutated() { + svg.fill_arrays(); + } + } + + if self + .author_specified + .contains_any(LonghandIdSet::border_background_properties()) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND); + } + + if self + .author_specified + .contains(LonghandId::FontFamily) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY); + } + + if self + .author_specified + .contains(LonghandId::LetterSpacing) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING); + } + + if self + .author_specified + .contains(LonghandId::WordSpacing) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING); + } + + if self + .author_specified + .contains(LonghandId::FontSynthesis) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS); + } + + #[cfg(feature = "servo")] + { + if let Some(font) = builder.get_font_if_mutated() { + font.compute_font_hash(); + } + } + } + + fn try_to_use_cached_reset_properties( + &mut self, + cache: Option<&'b RuleCache>, + guards: &StylesheetGuards, + ) -> bool { + let cache = match cache { + Some(cache) => cache, + None => return false, + }; + + let builder = &mut self.context.builder; + + let cached_style = match cache.find(guards, &builder) { + Some(style) => style, + None => return false, + }; + + builder.copy_reset_from(cached_style); + + // We're using the same reset style as another element, and we'll skip + // applying the relevant properties. So we need to do the relevant + // bookkeeping here to keep these bits correct. + // + // Note that the border/background properties are non-inherited, so we + // don't need to do anything else other than just copying the bits over. + // + // When using this optimization, we also need to copy whether the old + // style specified viewport units / used font-relative lengths, this one + // would as well. It matches the same rules, so it is the right thing + // to do anyways, even if it's only used on inherited properties. + let bits_to_copy = ComputedValueFlags::HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND | + ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS | + ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS | + ComputedValueFlags::USES_VIEWPORT_UNITS; + builder.add_flags(cached_style.flags & bits_to_copy); + + true + } + + /// The initial font depends on the current lang group so we may need to + /// recompute it if the language changed. + #[inline] + #[cfg(feature = "gecko")] + fn recompute_initial_font_family_if_needed(&mut self) { + use crate::gecko_bindings::bindings; + use crate::values::computed::font::FontFamily; + + if !self.seen.contains(LonghandId::XLang) { + return; + } + + let builder = &mut self.context.builder; + let default_font_type = { + let font = builder.get_font().gecko(); + + if !font.mFont.family.is_initial { + return; + } + + let default_font_type = unsafe { + bindings::Gecko_nsStyleFont_ComputeFallbackFontTypeForLanguage( + builder.device.document(), + font.mLanguage.mRawPtr, + ) + }; + + let initial_generic = font.mFont.family.families.single_generic(); + debug_assert!(initial_generic.is_some(), "Initial font should be just one generic font"); + if initial_generic == Some(default_font_type) { + return; + } + + default_font_type + }; + + let font = builder.mutate_font().gecko_mut(); + // NOTE: Leaves is_initial untouched. + font.mFont.family.families = FontFamily::generic(default_font_type).families.clone(); + } + + /// Prioritize user fonts if needed by pref. + #[inline] + #[cfg(feature = "gecko")] + fn prioritize_user_fonts_if_needed(&mut self) { + use crate::gecko_bindings::bindings; + + if !self.seen.contains(LonghandId::FontFamily) { + return; + } + + if static_prefs::pref!("browser.display.use_document_fonts") != 0 { + return; + } + + let builder = &mut self.context.builder; + let default_font_type = { + let font = builder.get_font().gecko(); + + if font.mFont.family.is_system_font { + return; + } + + if !font.mFont.family.families.needs_user_font_prioritization() { + return; + } + + unsafe { + bindings::Gecko_nsStyleFont_ComputeFallbackFontTypeForLanguage( + builder.device.document(), + font.mLanguage.mRawPtr, + ) + } + }; + + let font = builder.mutate_font().gecko_mut(); + font.mFont.family.families.prioritize_first_generic_or_prepend(default_font_type); + } + + /// Some keyword sizes depend on the font family and language. + #[cfg(feature = "gecko")] + fn recompute_keyword_font_size_if_needed(&mut self) { + use crate::values::computed::ToComputedValue; + use crate::values::specified; + + if !self.seen.contains(LonghandId::XLang) && !self.seen.contains(LonghandId::FontFamily) { + return; + } + + let new_size = { + let font = self.context.builder.get_font(); + let info = font.clone_font_size().keyword_info; + let new_size = match info.kw { + specified::FontSizeKeyword::None => return, + _ => { + self.context.for_non_inherited_property = None; + specified::FontSize::Keyword(info).to_computed_value(self.context) + }, + }; + + if font.gecko().mScriptUnconstrainedSize == new_size.computed_size { + return; + } + + new_size + }; + + self.context.builder.mutate_font().set_font_size(new_size); + } + + /// Some properties, plus setting font-size itself, may make us go out of + /// our minimum font-size range. + #[cfg(feature = "gecko")] + fn constrain_font_size_if_needed(&mut self) { + use crate::gecko_bindings::bindings; + use crate::values::generics::NonNegative; + + if !self.seen.contains(LonghandId::XLang) && + !self.seen.contains(LonghandId::FontFamily) && + !self.seen.contains(LonghandId::MozMinFontSizeRatio) && + !self.seen.contains(LonghandId::FontSize) + { + return; + } + + let builder = &mut self.context.builder; + let min_font_size = { + let font = builder.get_font().gecko(); + let min_font_size = unsafe { + bindings::Gecko_nsStyleFont_ComputeMinSize(font, builder.device.document()) + }; + + if font.mFont.size.0 >= min_font_size { + return; + } + + NonNegative(min_font_size) + }; + + builder.mutate_font().gecko_mut().mFont.size = min_font_size; + } + + /// <svg:text> is not affected by text zoom, and it uses a preshint + /// to disable it. We fix up the struct when this happens by + /// unzooming its contained font values, which will have been zoomed + /// in the parent. + /// + /// FIXME(emilio): Also, why doing this _before_ handling font-size? That + /// sounds wrong. + #[cfg(feature = "gecko")] + fn unzoom_fonts_if_needed(&mut self) { + if !self.seen.contains(LonghandId::XTextZoom) { + return; + } + + let builder = &mut self.context.builder; + + let parent_zoom = builder.get_parent_font().gecko().mAllowZoomAndMinSize; + let zoom = builder.get_font().gecko().mAllowZoomAndMinSize; + if zoom == parent_zoom { + return; + } + debug_assert!( + !zoom, + "We only ever disable text zoom (in svg:text), never enable it" + ); + let device = builder.device; + builder.mutate_font().unzoom_fonts(device); + } + + /// MathML script* attributes do some very weird shit with font-size. + /// + /// Handle them specially here, separate from other font-size stuff. + /// + /// How this should interact with lang="" and font-family-dependent sizes is + /// not clear to me. For now just pretend those don't exist here. + #[cfg(feature = "gecko")] + fn handle_mathml_scriptlevel_if_needed(&mut self) { + use crate::values::generics::NonNegative; + + if !self.seen.contains(LonghandId::MathDepth) && + !self.seen.contains(LonghandId::MozScriptMinSize) && + !self.seen.contains(LonghandId::MozScriptSizeMultiplier) + { + return; + } + + // If the user specifies a font-size, just let it be. + if self.seen.contains(LonghandId::FontSize) { + return; + } + + const SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE : f32 = 0.71; + + // Helper function that calculates the scale factor applied to font-size + // when math-depth goes from parent_math_depth to computed_math_depth. + // This function is essentially a modification of the MathML3's formula + // 0.71^(parent_math_depth - computed_math_depth) so that a scale factor + // of parent_script_percent_scale_down is applied when math-depth goes + // from 0 to 1 and parent_script_script_percent_scale_down is applied + // when math-depth goes from 0 to 2. This is also a straightforward + // implementation of the specification's algorithm: + // https://w3c.github.io/mathml-core/#the-math-script-level-property + fn scale_factor_for_math_depth_change( + parent_math_depth: i32, + computed_math_depth: i32, + parent_script_percent_scale_down: Option<f32>, + parent_script_script_percent_scale_down: Option<f32>, + ) -> f32 { + let mut a = parent_math_depth; + let mut b = computed_math_depth; + let c = SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE; + let scale_between_0_and_1 = parent_script_percent_scale_down.unwrap_or_else(|| c); + let scale_between_0_and_2 = parent_script_script_percent_scale_down.unwrap_or_else(|| c * c); + let mut s = 1.0; + let mut invert_scale_factor = false; + if a == b { + return s; + } + if b < a { + mem::swap(&mut a, &mut b); + invert_scale_factor = true; + } + let mut e = b - a; + if a <= 0 && b >= 2 { + s *= scale_between_0_and_2; + e -= 2; + } else if a == 1 { + s *= scale_between_0_and_2 / scale_between_0_and_1; + e -= 1; + } else if b == 1 { + s *= scale_between_0_and_1; + e -= 1; + } + s *= (c as f32).powi(e); + if invert_scale_factor { + 1.0 / s.max(f32::MIN_POSITIVE) + } else { + s + } + } + + let (new_size, new_unconstrained_size) = { + let builder = &self.context.builder; + let font = builder.get_font().gecko(); + let parent_font = builder.get_parent_font().gecko(); + + let delta = font.mMathDepth.saturating_sub(parent_font.mMathDepth); + + if delta == 0 { + return; + } + + let mut min = parent_font.mScriptMinSize; + if font.mAllowZoomAndMinSize { + min = builder.device.zoom_text(min); + } + + // If the scriptsizemultiplier has been set to something other than + // the default scale, use MathML3's implementation for backward + // compatibility. Otherwise, follow MathML Core's algorithm. + let scale = if parent_font.mScriptSizeMultiplier != + SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE { + (parent_font.mScriptSizeMultiplier as f32).powi(delta as i32) + } else { + // Script scale factors are independent of orientation. + let font_metrics = self.context.query_font_metrics( + FontBaseSize::InheritedStyle, + FontMetricsOrientation::Horizontal, + /* retrieve_math_scales = */ true, + ); + scale_factor_for_math_depth_change( + parent_font.mMathDepth as i32, + font.mMathDepth as i32, + font_metrics.script_percent_scale_down, + font_metrics.script_script_percent_scale_down + ) + }; + + let parent_size = parent_font.mSize.0; + let parent_unconstrained_size = parent_font.mScriptUnconstrainedSize.0; + let new_size = parent_size.scale_by(scale); + let new_unconstrained_size = parent_unconstrained_size.scale_by(scale); + + if scale <= 1. { + // The parent size can be smaller than scriptminsize, e.g. if it + // was specified explicitly. Don't scale in this case, but we + // don't want to set it to scriptminsize either since that will + // make it larger. + if parent_size <= min { + (parent_size, new_unconstrained_size) + } else { + (min.max(new_size), new_unconstrained_size) + } + } else { + // If the new unconstrained size is larger than the min size, + // this means we have escaped the grasp of scriptminsize and can + // revert to using the unconstrained size. + // However, if the new size is even larger (perhaps due to usage + // of em units), use that instead. + ( + new_size.min(new_unconstrained_size.max(min)), + new_unconstrained_size, + ) + } + }; + let font = self.context.builder.mutate_font().gecko_mut(); + font.mFont.size = NonNegative(new_size); + font.mSize = NonNegative(new_size); + font.mScriptUnconstrainedSize = NonNegative(new_unconstrained_size); + } + + /// Various properties affect how font-size and font-family are computed. + /// + /// These need to be handled here, since relative lengths and ex / ch units + /// for late properties depend on these. + fn fixup_font_stuff(&mut self) { + #[cfg(feature = "gecko")] + { + self.unzoom_fonts_if_needed(); + self.recompute_initial_font_family_if_needed(); + self.prioritize_user_fonts_if_needed(); + self.recompute_keyword_font_size_if_needed(); + self.handle_mathml_scriptlevel_if_needed(); + self.constrain_font_size_if_needed() + } + } +} |