This makes the style system pass flags down to GetMetricsForCSSUnits to specify whether it needs the 'ch' or 'ic' units, which may trigger downloading additional font resources. There should be no change in user-visible behavior, except by observing (e.g. in the devtools network panel) what resources end up being fetched in an example like the reporter's. Differential Revision: https://phabricator.services.mozilla.com/D246780
1424 lines
52 KiB
Rust
1424 lines
52 KiB
Rust
/* 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::color::AbsoluteColor;
|
|
use crate::computed_value_flags::ComputedValueFlags;
|
|
use crate::custom_properties::{
|
|
CustomPropertiesBuilder, DeferFontRelativeCustomPropertyResolution,
|
|
};
|
|
use crate::dom::TElement;
|
|
#[cfg(feature = "gecko")]
|
|
use crate::font_metrics::FontMetricsOrientation;
|
|
use crate::logical_geometry::WritingMode;
|
|
use crate::properties::{
|
|
property_counts, CSSWideKeyword, ComputedValues, DeclarationImportanceIterator, Importance,
|
|
LonghandId, LonghandIdSet, PrioritaryPropertyId, PropertyDeclaration, PropertyDeclarationId,
|
|
PropertyFlags, ShorthandsWithPropertyReferencesCache, StyleBuilder, CASCADE_PROPERTY,
|
|
};
|
|
use crate::rule_cache::{RuleCache, RuleCacheConditions};
|
|
use crate::rule_tree::{CascadeLevel, StrongRuleNode};
|
|
use crate::selector_parser::PseudoElement;
|
|
use crate::shared_lock::StylesheetGuards;
|
|
use crate::style_adjuster::StyleAdjuster;
|
|
use crate::stylesheets::container_rule::ContainerSizeQuery;
|
|
use crate::stylesheets::{layer_rule::LayerOrder, Origin};
|
|
use crate::stylist::Stylist;
|
|
#[cfg(feature = "gecko")]
|
|
use crate::values::specified::length::FontBaseSize;
|
|
use crate::values::{computed, specified};
|
|
use fxhash::FxHashMap;
|
|
use servo_arc::Arc;
|
|
use smallvec::SmallVec;
|
|
use std::borrow::Cow;
|
|
|
|
/// Whether we're resolving a style with the purposes of reparenting for ::first-line.
|
|
#[derive(Copy, Clone)]
|
|
#[allow(missing_docs)]
|
|
pub enum FirstLineReparenting<'a> {
|
|
No,
|
|
Yes {
|
|
/// The style we're re-parenting for ::first-line. ::first-line only affects inherited
|
|
/// properties so we use this to avoid some work and also ensure correctness by copying the
|
|
/// reset structs from this style.
|
|
style_to_reparent: &'a ComputedValues,
|
|
},
|
|
}
|
|
|
|
/// 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>(
|
|
stylist: &Stylist,
|
|
pseudo: Option<&PseudoElement>,
|
|
rule_node: &StrongRuleNode,
|
|
guards: &StylesheetGuards,
|
|
parent_style: Option<&ComputedValues>,
|
|
layout_parent_style: Option<&ComputedValues>,
|
|
first_line_reparenting: FirstLineReparenting,
|
|
visited_rules: Option<&StrongRuleNode>,
|
|
cascade_input_flags: ComputedValueFlags,
|
|
rule_cache: Option<&RuleCache>,
|
|
rule_cache_conditions: &mut RuleCacheConditions,
|
|
element: Option<E>,
|
|
) -> Arc<ComputedValues>
|
|
where
|
|
E: TElement,
|
|
{
|
|
cascade_rules(
|
|
stylist,
|
|
pseudo,
|
|
rule_node,
|
|
guards,
|
|
parent_style,
|
|
layout_parent_style,
|
|
first_line_reparenting,
|
|
CascadeMode::Unvisited { visited_rules },
|
|
cascade_input_flags,
|
|
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>(
|
|
stylist: &Stylist,
|
|
pseudo: Option<&PseudoElement>,
|
|
rule_node: &StrongRuleNode,
|
|
guards: &StylesheetGuards,
|
|
parent_style: Option<&ComputedValues>,
|
|
layout_parent_style: Option<&ComputedValues>,
|
|
first_line_reparenting: FirstLineReparenting,
|
|
cascade_mode: CascadeMode,
|
|
cascade_input_flags: ComputedValueFlags,
|
|
rule_cache: Option<&RuleCache>,
|
|
rule_cache_conditions: &mut RuleCacheConditions,
|
|
element: Option<E>,
|
|
) -> Arc<ComputedValues>
|
|
where
|
|
E: TElement,
|
|
{
|
|
apply_declarations(
|
|
stylist,
|
|
pseudo,
|
|
rule_node,
|
|
guards,
|
|
DeclarationIterator::new(rule_node, guards, pseudo),
|
|
parent_style,
|
|
layout_parent_style,
|
|
first_line_reparenting,
|
|
cascade_mode,
|
|
cascade_input_flags,
|
|
rule_cache,
|
|
rule_cache_conditions,
|
|
element,
|
|
)
|
|
}
|
|
|
|
/// Whether we're cascading for visited or unvisited styles.
|
|
#[derive(Clone, Copy)]
|
|
pub enum CascadeMode<'a, 'b> {
|
|
/// 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 cascade for our unvisited style.
|
|
unvisited_context: &'a computed::Context<'b>,
|
|
},
|
|
}
|
|
|
|
fn iter_declarations<'builder, 'decls: 'builder>(
|
|
iter: impl Iterator<Item = (&'decls PropertyDeclaration, CascadePriority)>,
|
|
declarations: &mut Declarations<'decls>,
|
|
mut custom_builder: Option<&mut CustomPropertiesBuilder<'builder, 'decls>>,
|
|
) {
|
|
for (declaration, priority) in iter {
|
|
if let PropertyDeclaration::Custom(ref declaration) = *declaration {
|
|
if let Some(ref mut builder) = custom_builder {
|
|
builder.cascade(declaration, priority);
|
|
}
|
|
} else {
|
|
let id = declaration.id().as_longhand().unwrap();
|
|
declarations.note_declaration(declaration, priority, id);
|
|
if CustomPropertiesBuilder::might_have_non_custom_dependency(id, declaration) {
|
|
if let Some(ref mut builder) = custom_builder {
|
|
builder.maybe_note_non_custom_dependency(id, declaration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// NOTE: This function expects the declaration with more priority to appear
|
|
/// first.
|
|
pub fn apply_declarations<'a, E, I>(
|
|
stylist: &'a Stylist,
|
|
pseudo: Option<&'a PseudoElement>,
|
|
rules: &StrongRuleNode,
|
|
guards: &StylesheetGuards,
|
|
iter: I,
|
|
parent_style: Option<&'a ComputedValues>,
|
|
layout_parent_style: Option<&ComputedValues>,
|
|
first_line_reparenting: FirstLineReparenting<'a>,
|
|
cascade_mode: CascadeMode,
|
|
cascade_input_flags: ComputedValueFlags,
|
|
rule_cache: Option<&'a RuleCache>,
|
|
rule_cache_conditions: &'a mut RuleCacheConditions,
|
|
element: Option<E>,
|
|
) -> Arc<ComputedValues>
|
|
where
|
|
E: TElement + 'a,
|
|
I: Iterator<Item = (&'a PropertyDeclaration, CascadePriority)>,
|
|
{
|
|
debug_assert!(layout_parent_style.is_none() || parent_style.is_some());
|
|
let device = stylist.device();
|
|
let inherited_style = parent_style.unwrap_or(device.default_computed_values());
|
|
let is_root_element = pseudo.is_none() && element.map_or(false, |e| e.is_root());
|
|
|
|
let container_size_query =
|
|
ContainerSizeQuery::for_option_element(element, Some(inherited_style), pseudo.is_some());
|
|
|
|
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,
|
|
Some(stylist),
|
|
parent_style,
|
|
pseudo,
|
|
Some(rules.clone()),
|
|
is_root_element,
|
|
),
|
|
stylist.quirks_mode(),
|
|
rule_cache_conditions,
|
|
container_size_query,
|
|
);
|
|
|
|
context.style().add_flags(cascade_input_flags);
|
|
|
|
let using_cached_reset_properties;
|
|
let ignore_colors = context.builder.device.forced_colors().is_active();
|
|
let mut cascade = Cascade::new(first_line_reparenting, ignore_colors);
|
|
let mut declarations = Default::default();
|
|
let mut shorthand_cache = ShorthandsWithPropertyReferencesCache::default();
|
|
let properties_to_apply = match cascade_mode {
|
|
CascadeMode::Visited { unvisited_context } => {
|
|
context.builder.custom_properties = unvisited_context.builder.custom_properties.clone();
|
|
context.builder.writing_mode = unvisited_context.builder.writing_mode;
|
|
context.builder.color_scheme = unvisited_context.builder.color_scheme;
|
|
// 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 :visited properties are
|
|
// non-inherited.
|
|
using_cached_reset_properties = false;
|
|
// TODO(bug 1859385): If we match the same rules when visited and unvisited, we could
|
|
// try to avoid gathering the declarations. That'd be:
|
|
// unvisited_context.builder.rules.as_ref() == Some(rules)
|
|
iter_declarations(iter, &mut declarations, None);
|
|
|
|
LonghandIdSet::visited_dependent()
|
|
},
|
|
CascadeMode::Unvisited { visited_rules } => {
|
|
let deferred_custom_properties = {
|
|
let mut builder = CustomPropertiesBuilder::new(stylist, &mut context);
|
|
iter_declarations(iter, &mut declarations, Some(&mut builder));
|
|
// Detect cycles, remove properties participating in them, and resolve properties, except:
|
|
// * Registered custom properties that depend on font-relative properties (Resolved)
|
|
// when prioritary properties are resolved), and
|
|
// * Any property that, in turn, depend on properties like above.
|
|
builder.build(DeferFontRelativeCustomPropertyResolution::Yes)
|
|
};
|
|
|
|
// Resolve prioritary properties - Guaranteed to not fall into a cycle with existing custom
|
|
// properties.
|
|
cascade.apply_prioritary_properties(&mut context, &declarations, &mut shorthand_cache);
|
|
|
|
// Resolve the deferred custom properties.
|
|
if let Some(deferred) = deferred_custom_properties {
|
|
CustomPropertiesBuilder::build_deferred(deferred, stylist, &mut context);
|
|
}
|
|
|
|
if let Some(visited_rules) = visited_rules {
|
|
cascade.compute_visited_style_if_needed(
|
|
&mut context,
|
|
element,
|
|
parent_style,
|
|
layout_parent_style,
|
|
visited_rules,
|
|
guards,
|
|
);
|
|
}
|
|
|
|
using_cached_reset_properties = cascade.try_to_use_cached_reset_properties(
|
|
&mut context.builder,
|
|
rule_cache,
|
|
guards,
|
|
);
|
|
|
|
if using_cached_reset_properties {
|
|
LonghandIdSet::late_group_only_inherited()
|
|
} else {
|
|
LonghandIdSet::late_group()
|
|
}
|
|
},
|
|
};
|
|
|
|
cascade.apply_non_prioritary_properties(
|
|
&mut context,
|
|
&declarations.longhand_declarations,
|
|
&mut shorthand_cache,
|
|
&properties_to_apply,
|
|
);
|
|
|
|
cascade.finished_applying_properties(&mut context.builder);
|
|
|
|
std::mem::drop(cascade);
|
|
|
|
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_overridden: &mut DeclarationsToApplyUnlessOverriden,
|
|
) {
|
|
use crate::values::computed::ToComputedValue;
|
|
use crate::values::specified::Color;
|
|
|
|
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;
|
|
}
|
|
|
|
// Always honor colors if forced-color-adjust is set to none.
|
|
#[cfg(feature = "gecko")]
|
|
{
|
|
let forced = context
|
|
.builder
|
|
.get_inherited_text()
|
|
.clone_forced_color_adjust();
|
|
if forced == computed::ForcedColorAdjust::None {
|
|
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) -> f32 {
|
|
// We assume here currentColor is opaque.
|
|
color
|
|
.to_computed_value(context)
|
|
.resolve_to_absolute(&AbsoluteColor::BLACK)
|
|
.alpha
|
|
}
|
|
|
|
// A few special-cases ahead.
|
|
match **declaration {
|
|
// Honor CSS-wide keywords like unset / revert / initial...
|
|
PropertyDeclaration::CSSWideKeyword(..) => return,
|
|
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.0 {
|
|
return;
|
|
}
|
|
let mut color = context.builder.device.default_background_color();
|
|
color.alpha = alpha;
|
|
declarations_to_apply_unless_overridden
|
|
.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.0
|
|
{
|
|
let color = context.builder.device.default_color();
|
|
declarations_to_apply_unless_overridden.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);
|
|
}
|
|
|
|
/// We track the index only for prioritary properties. For other properties we can just iterate.
|
|
type DeclarationIndex = u16;
|
|
|
|
/// "Prioritary" properties are properties that other properties depend on in one way or another.
|
|
///
|
|
/// We keep track of their position in the declaration vector, in order to be able to cascade them
|
|
/// separately in precise order.
|
|
#[derive(Copy, Clone)]
|
|
struct PrioritaryDeclarationPosition {
|
|
// DeclarationIndex::MAX signals no index.
|
|
most_important: DeclarationIndex,
|
|
least_important: DeclarationIndex,
|
|
}
|
|
|
|
impl Default for PrioritaryDeclarationPosition {
|
|
fn default() -> Self {
|
|
Self {
|
|
most_important: DeclarationIndex::MAX,
|
|
least_important: DeclarationIndex::MAX,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct Declaration<'a> {
|
|
decl: &'a PropertyDeclaration,
|
|
priority: CascadePriority,
|
|
next_index: DeclarationIndex,
|
|
}
|
|
|
|
/// The set of property declarations from our rules.
|
|
#[derive(Default)]
|
|
struct Declarations<'a> {
|
|
/// Whether we have any prioritary property. This is just a minor optimization.
|
|
has_prioritary_properties: bool,
|
|
/// A list of all the applicable longhand declarations.
|
|
longhand_declarations: SmallVec<[Declaration<'a>; 64]>,
|
|
/// The prioritary property position data.
|
|
prioritary_positions: [PrioritaryDeclarationPosition; property_counts::PRIORITARY],
|
|
}
|
|
|
|
impl<'a> Declarations<'a> {
|
|
fn note_prioritary_property(&mut self, id: PrioritaryPropertyId) {
|
|
let new_index = self.longhand_declarations.len();
|
|
if new_index >= DeclarationIndex::MAX as usize {
|
|
// This prioritary property is past the amount of declarations we can track. Let's give
|
|
// up applying it to prevent getting confused.
|
|
return;
|
|
}
|
|
|
|
self.has_prioritary_properties = true;
|
|
let new_index = new_index as DeclarationIndex;
|
|
let position = &mut self.prioritary_positions[id as usize];
|
|
if position.most_important == DeclarationIndex::MAX {
|
|
// We still haven't seen this property, record the current position as the most
|
|
// prioritary index.
|
|
position.most_important = new_index;
|
|
} else {
|
|
// Let the previous item in the list know about us.
|
|
self.longhand_declarations[position.least_important as usize].next_index = new_index;
|
|
}
|
|
position.least_important = new_index;
|
|
}
|
|
|
|
fn note_declaration(
|
|
&mut self,
|
|
decl: &'a PropertyDeclaration,
|
|
priority: CascadePriority,
|
|
id: LonghandId,
|
|
) {
|
|
if let Some(id) = PrioritaryPropertyId::from_longhand(id) {
|
|
self.note_prioritary_property(id);
|
|
}
|
|
self.longhand_declarations.push(Declaration {
|
|
decl,
|
|
priority,
|
|
next_index: 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
struct Cascade<'b> {
|
|
first_line_reparenting: FirstLineReparenting<'b>,
|
|
ignore_colors: bool,
|
|
seen: LonghandIdSet,
|
|
author_specified: LonghandIdSet,
|
|
reverted_set: LonghandIdSet,
|
|
reverted: FxHashMap<LonghandId, (CascadePriority, bool)>,
|
|
declarations_to_apply_unless_overridden: DeclarationsToApplyUnlessOverriden,
|
|
}
|
|
|
|
impl<'b> Cascade<'b> {
|
|
fn new(first_line_reparenting: FirstLineReparenting<'b>, ignore_colors: bool) -> Self {
|
|
Self {
|
|
first_line_reparenting,
|
|
ignore_colors,
|
|
seen: LonghandIdSet::default(),
|
|
author_specified: LonghandIdSet::default(),
|
|
reverted_set: Default::default(),
|
|
reverted: Default::default(),
|
|
declarations_to_apply_unless_overridden: Default::default(),
|
|
}
|
|
}
|
|
|
|
fn substitute_variables_if_needed<'cache, 'decl>(
|
|
&self,
|
|
context: &mut computed::Context,
|
|
shorthand_cache: &'cache mut ShorthandsWithPropertyReferencesCache,
|
|
declaration: &'decl PropertyDeclaration,
|
|
) -> 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() {
|
|
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 => {
|
|
context
|
|
.builder
|
|
.add_flags(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE);
|
|
},
|
|
LonghandId::Content => {
|
|
context
|
|
.builder
|
|
.add_flags(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE);
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
debug_assert!(
|
|
context.builder.stylist.is_some(),
|
|
"Need a Stylist to substitute variables!"
|
|
);
|
|
declaration.value.substitute_variables(
|
|
declaration.id,
|
|
context.builder.custom_properties(),
|
|
context.builder.stylist.unwrap(),
|
|
context,
|
|
shorthand_cache,
|
|
)
|
|
}
|
|
|
|
fn apply_one_prioritary_property(
|
|
&mut self,
|
|
context: &mut computed::Context,
|
|
decls: &Declarations,
|
|
cache: &mut ShorthandsWithPropertyReferencesCache,
|
|
id: PrioritaryPropertyId,
|
|
) -> bool {
|
|
let mut index = decls.prioritary_positions[id as usize].most_important;
|
|
if index == DeclarationIndex::MAX {
|
|
return false;
|
|
}
|
|
|
|
let longhand_id = id.to_longhand();
|
|
debug_assert!(
|
|
!longhand_id.is_logical(),
|
|
"That could require more book-keeping"
|
|
);
|
|
loop {
|
|
let decl = decls.longhand_declarations[index as usize];
|
|
self.apply_one_longhand(context, longhand_id, decl.decl, decl.priority, cache);
|
|
if self.seen.contains(longhand_id) {
|
|
return true; // Common case, we're done.
|
|
}
|
|
debug_assert!(
|
|
self.reverted_set.contains(longhand_id),
|
|
"How else can we fail to apply a prioritary property?"
|
|
);
|
|
debug_assert!(
|
|
decl.next_index == 0 || decl.next_index > index,
|
|
"should make progress! {} -> {}",
|
|
index,
|
|
decl.next_index,
|
|
);
|
|
index = decl.next_index;
|
|
if index == 0 {
|
|
break;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn apply_prioritary_properties(
|
|
&mut self,
|
|
context: &mut computed::Context,
|
|
decls: &Declarations,
|
|
cache: &mut ShorthandsWithPropertyReferencesCache,
|
|
) {
|
|
// Keeps apply_one_prioritary_property calls readable, considering the repititious
|
|
// arguments.
|
|
macro_rules! apply {
|
|
($prop:ident) => {
|
|
self.apply_one_prioritary_property(
|
|
context,
|
|
decls,
|
|
cache,
|
|
PrioritaryPropertyId::$prop,
|
|
)
|
|
};
|
|
}
|
|
|
|
if !decls.has_prioritary_properties {
|
|
return;
|
|
}
|
|
|
|
let has_writing_mode = apply!(WritingMode) | apply!(Direction);
|
|
#[cfg(feature = "gecko")]
|
|
let has_writing_mode = has_writing_mode | apply!(TextOrientation);
|
|
|
|
if has_writing_mode {
|
|
context.builder.writing_mode = WritingMode::new(context.builder.get_inherited_box())
|
|
}
|
|
|
|
if apply!(Zoom) {
|
|
context.builder.effective_zoom = context
|
|
.builder
|
|
.inherited_effective_zoom()
|
|
.compute_effective(context.builder.specified_zoom());
|
|
// NOTE(emilio): This is a bit of a hack, but matches the shipped WebKit and Blink
|
|
// behavior for now. Ideally, in the future, we have a pass over all
|
|
// implicitly-or-explicitly-inherited properties that can contain lengths and
|
|
// re-compute them properly, see https://github.com/w3c/csswg-drafts/issues/9397.
|
|
self.recompute_font_size_for_zoom_change(&mut context.builder);
|
|
}
|
|
|
|
// Compute font-family.
|
|
let has_font_family = apply!(FontFamily);
|
|
let has_lang = apply!(XLang);
|
|
#[cfg(feature = "gecko")]
|
|
{
|
|
if has_lang {
|
|
self.recompute_initial_font_family_if_needed(&mut context.builder);
|
|
}
|
|
if has_font_family {
|
|
self.prioritize_user_fonts_if_needed(&mut context.builder);
|
|
}
|
|
|
|
// Compute font-size.
|
|
if apply!(XTextScale) {
|
|
self.unzoom_fonts_if_needed(&mut context.builder);
|
|
}
|
|
let has_font_size = apply!(FontSize);
|
|
let has_math_depth = apply!(MathDepth);
|
|
let has_min_font_size_ratio = apply!(MozMinFontSizeRatio);
|
|
|
|
if has_math_depth && has_font_size {
|
|
self.recompute_math_font_size_if_needed(context);
|
|
}
|
|
if has_lang || has_font_family {
|
|
self.recompute_keyword_font_size_if_needed(context);
|
|
}
|
|
if has_font_size || has_min_font_size_ratio || has_lang || has_font_family {
|
|
self.constrain_font_size_if_needed(&mut context.builder);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "servo")]
|
|
{
|
|
apply!(FontSize);
|
|
if has_lang || has_font_family {
|
|
self.recompute_keyword_font_size_if_needed(context);
|
|
}
|
|
}
|
|
|
|
// Compute the rest of the first-available-font-affecting properties.
|
|
apply!(FontWeight);
|
|
apply!(FontStretch);
|
|
apply!(FontStyle);
|
|
#[cfg(feature = "gecko")]
|
|
apply!(FontSizeAdjust);
|
|
|
|
#[cfg(feature = "gecko")]
|
|
apply!(ForcedColorAdjust);
|
|
// color-scheme needs to be after forced-color-adjust, since it's one of the "skipped in
|
|
// forced-colors-mode" properties.
|
|
if apply!(ColorScheme) {
|
|
context.builder.color_scheme = context.builder.get_inherited_ui().color_scheme_bits();
|
|
}
|
|
apply!(LineHeight);
|
|
}
|
|
|
|
fn apply_non_prioritary_properties(
|
|
&mut self,
|
|
context: &mut computed::Context,
|
|
longhand_declarations: &[Declaration],
|
|
shorthand_cache: &mut ShorthandsWithPropertyReferencesCache,
|
|
properties_to_apply: &LonghandIdSet,
|
|
) {
|
|
debug_assert!(!properties_to_apply.contains_any(LonghandIdSet::prioritary_properties()));
|
|
debug_assert!(self.declarations_to_apply_unless_overridden.is_empty());
|
|
for declaration in &*longhand_declarations {
|
|
let mut longhand_id = declaration.decl.id().as_longhand().unwrap();
|
|
if !properties_to_apply.contains(longhand_id) {
|
|
continue;
|
|
}
|
|
debug_assert!(PrioritaryPropertyId::from_longhand(longhand_id).is_none());
|
|
let is_logical = longhand_id.is_logical();
|
|
if is_logical {
|
|
let wm = context.builder.writing_mode;
|
|
context
|
|
.rule_cache_conditions
|
|
.borrow_mut()
|
|
.set_writing_mode_dependency(wm);
|
|
longhand_id = longhand_id.to_physical(wm);
|
|
}
|
|
self.apply_one_longhand(
|
|
context,
|
|
longhand_id,
|
|
declaration.decl,
|
|
declaration.priority,
|
|
shorthand_cache,
|
|
);
|
|
}
|
|
if !self.declarations_to_apply_unless_overridden.is_empty() {
|
|
debug_assert!(self.ignore_colors);
|
|
for declaration in std::mem::take(&mut self.declarations_to_apply_unless_overridden) {
|
|
let longhand_id = declaration.id().as_longhand().unwrap();
|
|
debug_assert!(!longhand_id.is_logical());
|
|
if !self.seen.contains(longhand_id) {
|
|
unsafe {
|
|
self.do_apply_declaration(context, longhand_id, &declaration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_one_longhand(
|
|
&mut self,
|
|
context: &mut computed::Context,
|
|
longhand_id: LonghandId,
|
|
declaration: &PropertyDeclaration,
|
|
priority: CascadePriority,
|
|
cache: &mut ShorthandsWithPropertyReferencesCache,
|
|
) {
|
|
debug_assert!(!longhand_id.is_logical());
|
|
let origin = priority.cascade_level().origin();
|
|
if self.seen.contains(longhand_id) {
|
|
return;
|
|
}
|
|
|
|
if self.reverted_set.contains(longhand_id) {
|
|
if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&longhand_id) {
|
|
if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut declaration = self.substitute_variables_if_needed(context, cache, declaration);
|
|
|
|
// When document colors are disabled, do special handling of
|
|
// properties that are marked as ignored in that mode.
|
|
if self.ignore_colors {
|
|
tweak_when_ignoring_colors(
|
|
context,
|
|
longhand_id,
|
|
origin,
|
|
&mut declaration,
|
|
&mut self.declarations_to_apply_unless_overridden,
|
|
);
|
|
}
|
|
|
|
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(longhand_id);
|
|
self.reverted.insert(longhand_id, (priority, origin_revert));
|
|
return;
|
|
},
|
|
CSSWideKeyword::Unset => true,
|
|
CSSWideKeyword::Inherit => longhand_id.inherited(),
|
|
CSSWideKeyword::Initial => !longhand_id.inherited(),
|
|
},
|
|
None => false,
|
|
};
|
|
|
|
self.seen.insert(longhand_id);
|
|
if origin == Origin::Author {
|
|
self.author_specified.insert(longhand_id);
|
|
}
|
|
|
|
if is_unset {
|
|
return;
|
|
}
|
|
|
|
unsafe { self.do_apply_declaration(context, longhand_id, &declaration) }
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn do_apply_declaration(
|
|
&self,
|
|
context: &mut computed::Context,
|
|
longhand_id: LonghandId,
|
|
declaration: &PropertyDeclaration,
|
|
) {
|
|
debug_assert!(!longhand_id.is_logical());
|
|
// 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.
|
|
(CASCADE_PROPERTY[longhand_id as usize])(&declaration, context);
|
|
}
|
|
|
|
fn compute_visited_style_if_needed<E>(
|
|
&self,
|
|
context: &mut computed::Context,
|
|
element: Option<E>,
|
|
parent_style: Option<&ComputedValues>,
|
|
layout_parent_style: Option<&ComputedValues>,
|
|
visited_rules: &StrongRuleNode,
|
|
guards: &StylesheetGuards,
|
|
) where
|
|
E: TElement,
|
|
{
|
|
let is_link = 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))
|
|
}
|
|
};
|
|
}
|
|
|
|
// We could call apply_declarations directly, but that'd cause
|
|
// another instantiation of this function which is not great.
|
|
let style = cascade_rules(
|
|
context.builder.stylist.unwrap(),
|
|
context.builder.pseudo,
|
|
visited_rules,
|
|
guards,
|
|
visited_parent!(parent_style),
|
|
visited_parent!(layout_parent_style),
|
|
self.first_line_reparenting,
|
|
CascadeMode::Visited {
|
|
unvisited_context: &*context,
|
|
},
|
|
// Cascade input flags don't matter for the visited style, they are
|
|
// in the main (unvisited) style.
|
|
Default::default(),
|
|
// 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 *context.rule_cache_conditions.borrow_mut(),
|
|
element,
|
|
);
|
|
context.builder.visited_style = Some(style);
|
|
}
|
|
|
|
fn finished_applying_properties(&self, builder: &mut StyleBuilder) {
|
|
#[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_any(LonghandIdSet::margin_properties()) &&
|
|
self.author_specified.contains(LonghandId::FontSize)
|
|
{
|
|
builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_MARGIN_AND_FONT_SIZE);
|
|
}
|
|
|
|
if self.author_specified.contains(LonghandId::Color) {
|
|
builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_TEXT_COLOR);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
if self
|
|
.author_specified
|
|
.contains(LonghandId::FontSynthesisWeight)
|
|
{
|
|
builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT);
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
if self
|
|
.author_specified
|
|
.contains(LonghandId::FontSynthesisStyle)
|
|
{
|
|
builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE);
|
|
}
|
|
|
|
#[cfg(feature = "servo")]
|
|
{
|
|
if let Some(font) = builder.get_font_if_mutated() {
|
|
font.compute_font_hash();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_to_use_cached_reset_properties(
|
|
&self,
|
|
builder: &mut StyleBuilder<'b>,
|
|
cache: Option<&'b RuleCache>,
|
|
guards: &StylesheetGuards,
|
|
) -> bool {
|
|
let style = match self.first_line_reparenting {
|
|
FirstLineReparenting::Yes { style_to_reparent } => style_to_reparent,
|
|
FirstLineReparenting::No => {
|
|
let Some(cache) = cache else { return false };
|
|
let Some(style) = cache.find(guards, builder) else {
|
|
return false;
|
|
};
|
|
style
|
|
},
|
|
};
|
|
|
|
builder.copy_reset_from(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_CONTAINER_UNITS |
|
|
ComputedValueFlags::USES_VIEWPORT_UNITS;
|
|
builder.add_flags(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(&self, builder: &mut StyleBuilder) {
|
|
use crate::gecko_bindings::bindings;
|
|
use crate::values::computed::font::FontFamily;
|
|
|
|
let default_font_type = {
|
|
let font = builder.get_font();
|
|
|
|
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
|
|
};
|
|
|
|
// NOTE: Leaves is_initial untouched.
|
|
builder.mutate_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(&self, builder: &mut StyleBuilder) {
|
|
use crate::gecko_bindings::bindings;
|
|
|
|
// Check the use_document_fonts setting for content, but for chrome
|
|
// documents they're treated as always enabled.
|
|
if static_prefs::pref!("browser.display.use_document_fonts") != 0 ||
|
|
builder.device.chrome_rules_enabled_for_document()
|
|
{
|
|
return;
|
|
}
|
|
|
|
let default_font_type = {
|
|
let font = builder.get_font();
|
|
|
|
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();
|
|
font.mFont
|
|
.family
|
|
.families
|
|
.prioritize_first_generic_or_prepend(default_font_type);
|
|
}
|
|
|
|
/// Some keyword sizes depend on the font family and language.
|
|
fn recompute_keyword_font_size_if_needed(&self, context: &mut computed::Context) {
|
|
use crate::values::computed::ToComputedValue;
|
|
|
|
if !self.seen.contains(LonghandId::XLang) && !self.seen.contains(LonghandId::FontFamily) {
|
|
return;
|
|
}
|
|
|
|
let new_size = {
|
|
let font = context.builder.get_font();
|
|
let info = font.clone_font_size().keyword_info;
|
|
let new_size = match info.kw {
|
|
specified::FontSizeKeyword::None => return,
|
|
_ => {
|
|
context.for_non_inherited_property = false;
|
|
specified::FontSize::Keyword(info).to_computed_value(context)
|
|
},
|
|
};
|
|
|
|
#[cfg(feature = "gecko")]
|
|
if font.mScriptUnconstrainedSize == new_size.computed_size {
|
|
return;
|
|
}
|
|
|
|
new_size
|
|
};
|
|
|
|
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(&self, builder: &mut StyleBuilder) {
|
|
use crate::gecko_bindings::bindings;
|
|
use crate::values::generics::NonNegative;
|
|
|
|
let min_font_size = {
|
|
let font = builder.get_font();
|
|
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().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.
|
|
#[cfg(feature = "gecko")]
|
|
fn unzoom_fonts_if_needed(&self, builder: &mut StyleBuilder) {
|
|
debug_assert!(self.seen.contains(LonghandId::XTextScale));
|
|
|
|
let parent_text_scale = builder.get_parent_font().clone__x_text_scale();
|
|
let text_scale = builder.get_font().clone__x_text_scale();
|
|
if parent_text_scale == text_scale {
|
|
return;
|
|
}
|
|
debug_assert_ne!(
|
|
parent_text_scale.text_zoom_enabled(),
|
|
text_scale.text_zoom_enabled(),
|
|
"There's only one value that disables it"
|
|
);
|
|
debug_assert!(
|
|
!text_scale.text_zoom_enabled(),
|
|
"We only ever disable text zoom never enable it"
|
|
);
|
|
let device = builder.device;
|
|
builder.mutate_font().unzoom_fonts(device);
|
|
}
|
|
|
|
fn recompute_font_size_for_zoom_change(&self, builder: &mut StyleBuilder) {
|
|
debug_assert!(self.seen.contains(LonghandId::Zoom));
|
|
// NOTE(emilio): Intentionally not using the effective zoom here, since all the inherited
|
|
// zooms are already applied.
|
|
let old_size = builder.get_font().clone_font_size();
|
|
let new_size = old_size.zoom(builder.resolved_specified_zoom());
|
|
if old_size == new_size {
|
|
return;
|
|
}
|
|
builder.mutate_font().set_font_size(new_size);
|
|
}
|
|
|
|
/// Special handling of font-size: math (used for MathML).
|
|
/// https://w3c.github.io/mathml-core/#the-math-script-level-property
|
|
/// TODO: Bug: 1548471: MathML Core also does not specify a script min size
|
|
/// should we unship that feature or standardize it?
|
|
#[cfg(feature = "gecko")]
|
|
fn recompute_math_font_size_if_needed(&self, context: &mut computed::Context) {
|
|
use crate::values::generics::NonNegative;
|
|
|
|
// Do not do anything if font-size: math or math-depth is not set.
|
|
if context.builder.get_font().clone_font_size().keyword_info.kw !=
|
|
specified::FontSizeKeyword::Math
|
|
{
|
|
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 {
|
|
std::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) = {
|
|
use crate::gecko::media_queries::QueryFontMetricsFlags;
|
|
|
|
let builder = &context.builder;
|
|
let font = builder.get_font();
|
|
let parent_font = builder.get_parent_font();
|
|
|
|
let delta = font.mMathDepth.saturating_sub(parent_font.mMathDepth);
|
|
|
|
if delta == 0 {
|
|
return;
|
|
}
|
|
|
|
let mut min = parent_font.mScriptMinSize;
|
|
if font.mXTextScale.text_zoom_enabled() {
|
|
min = builder.device.zoom_text(min);
|
|
}
|
|
|
|
// Calculate scale factor following MathML Core's algorithm.
|
|
let scale = {
|
|
// Script scale factors are independent of orientation.
|
|
let font_metrics = context.query_font_metrics(
|
|
FontBaseSize::InheritedStyle,
|
|
FontMetricsOrientation::Horizontal,
|
|
QueryFontMetricsFlags::NEEDS_MATH_SCALES,
|
|
);
|
|
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 = context.builder.mutate_font();
|
|
font.mFont.size = NonNegative(new_size);
|
|
font.mSize = NonNegative(new_size);
|
|
font.mScriptUnconstrainedSize = NonNegative(new_unconstrained_size);
|
|
}
|
|
}
|