mirror of
https://github.com/servo/servo.git
synced 2025-08-05 13:40:08 +01:00
style: Correct style sharing handling for any element that considered :has()
in selector matching
For any element that anchors a `:has()` selector (i.e. Matches a selector that contains a `:has()` on its rightmost side), we prevent style sharing altogether, as evaluation of the `:has()` selector is required in the first place to determine style sharing. On the other hand, any element matching a rule containing `:has()` without anchoring it can do style sharing for siblings, but not cousins. Differential Revision: https://phabricator.services.mozilla.com/D176836
This commit is contained in:
parent
5c0897c8eb
commit
ff8100d396
9 changed files with 91 additions and 6 deletions
|
@ -146,6 +146,7 @@ where
|
||||||
|
|
||||||
/// The current element we're anchoring on for evaluating the relative selector.
|
/// The current element we're anchoring on for evaluating the relative selector.
|
||||||
current_relative_selector_anchor: Option<OpaqueElement>,
|
current_relative_selector_anchor: Option<OpaqueElement>,
|
||||||
|
pub considered_relative_selector: bool,
|
||||||
|
|
||||||
quirks_mode: QuirksMode,
|
quirks_mode: QuirksMode,
|
||||||
needs_selector_flags: NeedsSelectorFlags,
|
needs_selector_flags: NeedsSelectorFlags,
|
||||||
|
@ -199,6 +200,7 @@ where
|
||||||
pseudo_element_matching_fn: None,
|
pseudo_element_matching_fn: None,
|
||||||
extra_data: Default::default(),
|
extra_data: Default::default(),
|
||||||
current_relative_selector_anchor: None,
|
current_relative_selector_anchor: None,
|
||||||
|
considered_relative_selector: false,
|
||||||
_impl: ::std::marker::PhantomData,
|
_impl: ::std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,6 +334,7 @@ where
|
||||||
self.current_relative_selector_anchor = Some(anchor);
|
self.current_relative_selector_anchor = Some(anchor);
|
||||||
let result = self.nest(f);
|
let result = self.nest(f);
|
||||||
self.current_relative_selector_anchor = original_relative_selector_anchor;
|
self.current_relative_selector_anchor = original_relative_selector_anchor;
|
||||||
|
self.considered_relative_selector = true;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,13 +60,18 @@ bitflags! {
|
||||||
/// The element has an empty selector, so when a child is appended we
|
/// The element has an empty selector, so when a child is appended we
|
||||||
/// might need to restyle the parent completely.
|
/// might need to restyle the parent completely.
|
||||||
const HAS_EMPTY_SELECTOR = 1 << 4;
|
const HAS_EMPTY_SELECTOR = 1 << 4;
|
||||||
|
|
||||||
|
/// This element has a relative selector that anchors to it, or may do so
|
||||||
|
/// if its descendants or its later siblings change.
|
||||||
|
const ANCHORS_RELATIVE_SELECTOR = 1 << 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ElementSelectorFlags {
|
impl ElementSelectorFlags {
|
||||||
/// Returns the subset of flags that apply to the element.
|
/// Returns the subset of flags that apply to the element.
|
||||||
pub fn for_self(self) -> ElementSelectorFlags {
|
pub fn for_self(self) -> ElementSelectorFlags {
|
||||||
self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR)
|
self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR |
|
||||||
|
ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the subset of flags that apply to the parent.
|
/// Returns the subset of flags that apply to the parent.
|
||||||
|
@ -365,6 +370,12 @@ fn matches_relative_selectors<E: Element>(
|
||||||
element: &E,
|
element: &E,
|
||||||
context: &mut MatchingContext<E::Impl>,
|
context: &mut MatchingContext<E::Impl>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if context.needs_selector_flags() {
|
||||||
|
// If we've considered anchoring `:has()` selector while trying to match this element,
|
||||||
|
// mark it as such, as it has implications on style sharing (See style sharing
|
||||||
|
// code for further information).
|
||||||
|
element.apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR);
|
||||||
|
}
|
||||||
for RelativeSelector {
|
for RelativeSelector {
|
||||||
match_hint,
|
match_hint,
|
||||||
selector,
|
selector,
|
||||||
|
|
|
@ -894,6 +894,10 @@ pub trait TElement:
|
||||||
/// This will usually be the size of the content area of the primary box,
|
/// This will usually be the size of the content area of the primary box,
|
||||||
/// but can be None if there is no box or if some axis lacks size containment.
|
/// but can be None if there is no box or if some axis lacks size containment.
|
||||||
fn query_container_size(&self, display: &Display) -> euclid::default::Size2D<Option<app_units::Au>>;
|
fn query_container_size(&self, display: &Display) -> euclid::default::Size2D<Option<app_units::Au>>;
|
||||||
|
|
||||||
|
/// Returns true if this element anchors a relative selector, now or after
|
||||||
|
/// a DOM mutation.
|
||||||
|
fn anchors_relative_selector(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TNode and TElement aren't Send because we want to be careful and explicit
|
/// TNode and TElement aren't Send because we want to be careful and explicit
|
||||||
|
|
|
@ -895,6 +895,9 @@ fn selector_flags_to_node_flags(flags: ElementSelectorFlags) -> u32 {
|
||||||
if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) {
|
if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) {
|
||||||
gecko_flags |= NODE_HAS_EMPTY_SELECTOR;
|
gecko_flags |= NODE_HAS_EMPTY_SELECTOR;
|
||||||
}
|
}
|
||||||
|
if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR) {
|
||||||
|
gecko_flags |= NODE_ANCHORS_RELATIVE_SELECTOR;
|
||||||
|
}
|
||||||
|
|
||||||
gecko_flags
|
gecko_flags
|
||||||
}
|
}
|
||||||
|
@ -1729,6 +1732,11 @@ impl<'le> TElement for GeckoElement<'le> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn anchors_relative_selector(&self) -> bool {
|
||||||
|
use crate::gecko_bindings::structs::NODE_ANCHORS_RELATIVE_SELECTOR;
|
||||||
|
self.flags() & NODE_ANCHORS_RELATIVE_SELECTOR != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'le> PartialEq for GeckoElement<'le> {
|
impl<'le> PartialEq for GeckoElement<'le> {
|
||||||
|
|
|
@ -118,6 +118,9 @@ bitflags! {
|
||||||
/// A flag used to mark styles which have `container-type` of `size` or
|
/// A flag used to mark styles which have `container-type` of `size` or
|
||||||
/// `inline-size`, or under one.
|
/// `inline-size`, or under one.
|
||||||
const SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE = 1 << 23;
|
const SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE = 1 << 23;
|
||||||
|
|
||||||
|
/// Whether the style evaluated any relative selector.
|
||||||
|
const CONSIDERED_RELATIVE_SELECTOR = 1 << 24;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +153,7 @@ impl ComputedValueFlags {
|
||||||
/// Flags that are an input to the cascade.
|
/// Flags that are an input to the cascade.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cascade_input_flags() -> Self {
|
fn cascade_input_flags() -> Self {
|
||||||
Self::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES
|
Self::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES | Self::CONSIDERED_RELATIVE_SELECTOR
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the flags that are always propagated to descendants.
|
/// Returns the flags that are always propagated to descendants.
|
||||||
|
|
|
@ -36,6 +36,15 @@ where
|
||||||
|
|
||||||
// Cousins are a bit more complicated.
|
// Cousins are a bit more complicated.
|
||||||
//
|
//
|
||||||
|
// The fact that the candidate is here means that its element does not anchor
|
||||||
|
// the relative selector. However, it may have considered relative selector(s)
|
||||||
|
// to compute its style, i.e. there's a rule `<..> :has(<..>) <..> candidate`.
|
||||||
|
// In this case, evaluating style sharing requires evaluating the relative
|
||||||
|
// selector for the target anyway.
|
||||||
|
if candidate.considered_relative_selector {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If a parent element was already styled and we traversed past it without
|
// If a parent element was already styled and we traversed past it without
|
||||||
// restyling it, that may be because our clever invalidation logic was able
|
// restyling it, that may be because our clever invalidation logic was able
|
||||||
// to prove that the styles of that element would remain unchanged despite
|
// to prove that the styles of that element would remain unchanged despite
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
use crate::applicable_declarations::ApplicableDeclarationBlock;
|
use crate::applicable_declarations::ApplicableDeclarationBlock;
|
||||||
use crate::bloom::StyleBloom;
|
use crate::bloom::StyleBloom;
|
||||||
use crate::context::{SharedStyleContext, StyleContext};
|
use crate::context::{SharedStyleContext, StyleContext};
|
||||||
|
use crate::computed_value_flags::ComputedValueFlags;
|
||||||
use crate::dom::{SendElement, TElement};
|
use crate::dom::{SendElement, TElement};
|
||||||
use crate::properties::ComputedValues;
|
use crate::properties::ComputedValues;
|
||||||
use crate::rule_tree::StrongRuleNode;
|
use crate::rule_tree::StrongRuleNode;
|
||||||
|
@ -275,11 +276,13 @@ pub struct StyleSharingCandidate<E: TElement> {
|
||||||
/// The element.
|
/// The element.
|
||||||
element: E,
|
element: E,
|
||||||
validation_data: ValidationData,
|
validation_data: ValidationData,
|
||||||
|
considered_relative_selector: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FakeCandidate {
|
struct FakeCandidate {
|
||||||
_element: usize,
|
_element: usize,
|
||||||
_validation_data: ValidationData,
|
_validation_data: ValidationData,
|
||||||
|
_considered_relative_selector: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: TElement> Deref for StyleSharingCandidate<E> {
|
impl<E: TElement> Deref for StyleSharingCandidate<E> {
|
||||||
|
@ -466,13 +469,19 @@ impl<Candidate> SharingCacheBase<Candidate> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: TElement> SharingCache<E> {
|
impl<E: TElement> SharingCache<E> {
|
||||||
fn insert(&mut self, element: E, validation_data_holder: Option<&mut StyleSharingTarget<E>>) {
|
fn insert(
|
||||||
|
&mut self,
|
||||||
|
element: E,
|
||||||
|
considered_relative_selector: bool,
|
||||||
|
validation_data_holder: Option<&mut StyleSharingTarget<E>>,
|
||||||
|
) {
|
||||||
let validation_data = match validation_data_holder {
|
let validation_data = match validation_data_holder {
|
||||||
Some(v) => v.take_validation_data(),
|
Some(v) => v.take_validation_data(),
|
||||||
None => ValidationData::default(),
|
None => ValidationData::default(),
|
||||||
};
|
};
|
||||||
self.entries.insert(StyleSharingCandidate {
|
self.entries.insert(StyleSharingCandidate {
|
||||||
element,
|
element,
|
||||||
|
considered_relative_selector,
|
||||||
validation_data,
|
validation_data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -612,6 +621,22 @@ impl<E: TElement> StyleSharingCache<E> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this element was considered for matching a relative selector, we can't
|
||||||
|
// share style, as that requires evaluating the relative selector in the
|
||||||
|
// first place. A couple notes:
|
||||||
|
// - This means that a document that contains a standalone `:has()`
|
||||||
|
// rule would basically turn style sharing off.
|
||||||
|
// - Since the flag is set on the element, we may be overly pessimistic:
|
||||||
|
// For example, given `<div class="foo"><div class="bar"></div></div>`,
|
||||||
|
// if we run into a `.foo:has(.bar) .baz` selector, we refuse any selector
|
||||||
|
// matching `.foo`, even if `:has()` may not even be used. Ideally we'd
|
||||||
|
// have something like `RelativeSelectorConsidered::RightMost`, but the
|
||||||
|
// element flag is required for invalidation, and this reduces more tracking.
|
||||||
|
if element.anchors_relative_selector() {
|
||||||
|
debug!("Failing to insert to the cache: may anchor relative selector");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// In addition to the above running animations check, we also need to
|
// In addition to the above running animations check, we also need to
|
||||||
// check CSS animation and transition styles since it's possible that
|
// check CSS animation and transition styles since it's possible that
|
||||||
// we are about to create CSS animations/transitions.
|
// we are about to create CSS animations/transitions.
|
||||||
|
@ -642,7 +667,15 @@ impl<E: TElement> StyleSharingCache<E> {
|
||||||
self.clear();
|
self.clear();
|
||||||
self.dom_depth = dom_depth;
|
self.dom_depth = dom_depth;
|
||||||
}
|
}
|
||||||
self.cache_mut().insert(*element, validation_data_holder);
|
self.cache_mut().insert(
|
||||||
|
*element,
|
||||||
|
style
|
||||||
|
.style
|
||||||
|
.0
|
||||||
|
.flags
|
||||||
|
.intersects(ComputedValueFlags::CONSIDERED_RELATIVE_SELECTOR),
|
||||||
|
validation_data_holder,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the style sharing candidate cache.
|
/// Clear the style sharing candidate cache.
|
||||||
|
|
|
@ -499,6 +499,16 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matching_context.considered_relative_selector {
|
||||||
|
// This is a bit awkward - ideally, the flag is set directly where `considered_relative_selector`
|
||||||
|
// is; however, in that context, the implementation detail of `extra_data` is not visible, so
|
||||||
|
// it's done here. A trait for manipulating the flags is an option, but not worth it for a single flag.
|
||||||
|
matching_context
|
||||||
|
.extra_data
|
||||||
|
.cascade_input_flags
|
||||||
|
.insert(ComputedValueFlags::CONSIDERED_RELATIVE_SELECTOR);
|
||||||
|
}
|
||||||
|
|
||||||
MatchingResults {
|
MatchingResults {
|
||||||
rule_node,
|
rule_node,
|
||||||
flags: matching_context.extra_data.cascade_input_flags,
|
flags: matching_context.extra_data.cascade_input_flags,
|
||||||
|
|
|
@ -222,7 +222,10 @@ pub trait DomTraversal<E: TElement>: Sync {
|
||||||
if traversal_flags.for_animation_only() {
|
if traversal_flags.for_animation_only() {
|
||||||
return data.map_or(false, |d| d.has_styles()) &&
|
return data.map_or(false, |d| d.has_styles()) &&
|
||||||
(el.has_animation_only_dirty_descendants() ||
|
(el.has_animation_only_dirty_descendants() ||
|
||||||
data.as_ref().unwrap().hint.has_animation_hint_or_recascade());
|
data.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.hint
|
||||||
|
.has_animation_hint_or_recascade());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-incremental layout visits every node.
|
// Non-incremental layout visits every node.
|
||||||
|
@ -420,7 +423,8 @@ pub fn recalc_style_at<E, D, F>(
|
||||||
|
|
||||||
// Compute style for this element if necessary.
|
// Compute style for this element if necessary.
|
||||||
if let Some(restyle_kind) = restyle_kind {
|
if let Some(restyle_kind) = restyle_kind {
|
||||||
child_restyle_requirement = compute_style(traversal_data, context, element, data, restyle_kind);
|
child_restyle_requirement =
|
||||||
|
compute_style(traversal_data, context, element, data, restyle_kind);
|
||||||
|
|
||||||
if !element.matches_user_and_content_rules() {
|
if !element.matches_user_and_content_rules() {
|
||||||
// We must always cascade native anonymous subtrees, since they
|
// We must always cascade native anonymous subtrees, since they
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue