tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

stylesheets.rs (26038B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! A collection of invalidations due to changes in which stylesheets affect a
      6 //! document.
      7 
      8 #![deny(unsafe_code)]
      9 
     10 use crate::context::QuirksMode;
     11 use crate::data::ElementData;
     12 use crate::derives::*;
     13 use crate::dom::{TDocument, TElement, TNode};
     14 use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
     15 use crate::invalidation::element::restyle_hints::RestyleHint;
     16 use crate::media_queries::Device;
     17 use crate::selector_map::PrecomputedHashSet;
     18 use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap};
     19 use crate::shared_lock::SharedRwLockReadGuard;
     20 use crate::simple_buckets_map::SimpleBucketsMap;
     21 use crate::stylesheets::{
     22    CssRule, CssRuleRef, CustomMediaMap, EffectiveRules, EffectiveRulesIterator,
     23    StylesheetInDocument,
     24 };
     25 use crate::stylist::CascadeDataDifference;
     26 use crate::values::specified::position::PositionTryFallbacksItem;
     27 use crate::values::AtomIdent;
     28 use crate::Atom;
     29 use crate::LocalName as SelectorLocalName;
     30 use selectors::parser::{Component, LocalName, Selector};
     31 
     32 /// The kind of change that happened for a given rule.
     33 #[repr(u32)]
     34 #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
     35 pub enum RuleChangeKind {
     36    /// Some change in the rule which we don't know about, and could have made
     37    /// the rule change in any way.
     38    Generic = 0,
     39    /// The rule was inserted.
     40    Insertion,
     41    /// The rule was removed.
     42    Removal,
     43    /// A change in the declarations of a style rule.
     44    StyleRuleDeclarations,
     45    /// A change in the declarations of an @position-try rule.
     46    PositionTryDeclarations,
     47 }
     48 
     49 /// A style sheet invalidation represents a kind of element or subtree that may
     50 /// need to be restyled. Whether it represents a whole subtree or just a single
     51 /// element is determined by the given InvalidationKind in
     52 /// StylesheetInvalidationSet's maps.
     53 #[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)]
     54 enum Invalidation {
     55    /// An element with a given id.
     56    ID(AtomIdent),
     57    /// An element with a given class name.
     58    Class(AtomIdent),
     59    /// An element with a given local name.
     60    LocalName {
     61        name: SelectorLocalName,
     62        lower_name: SelectorLocalName,
     63    },
     64 }
     65 
     66 impl Invalidation {
     67    fn is_id(&self) -> bool {
     68        matches!(*self, Invalidation::ID(..))
     69    }
     70 
     71    fn is_id_or_class(&self) -> bool {
     72        matches!(*self, Invalidation::ID(..) | Invalidation::Class(..))
     73    }
     74 }
     75 
     76 /// Whether we should invalidate just the element, or the whole subtree within
     77 /// it.
     78 #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)]
     79 enum InvalidationKind {
     80    None = 0,
     81    Element,
     82    Scope,
     83 }
     84 
     85 impl std::ops::BitOrAssign for InvalidationKind {
     86    #[inline]
     87    fn bitor_assign(&mut self, other: Self) {
     88        *self = std::cmp::max(*self, other);
     89    }
     90 }
     91 
     92 impl InvalidationKind {
     93    #[inline]
     94    fn is_scope(self) -> bool {
     95        matches!(self, Self::Scope)
     96    }
     97 
     98    #[inline]
     99    fn add(&mut self, other: Option<&InvalidationKind>) {
    100        if let Some(other) = other {
    101            *self |= *other;
    102        }
    103    }
    104 }
    105 
    106 /// A set of invalidations due to stylesheet changes.
    107 ///
    108 /// TODO(emilio): We might be able to do the same analysis for media query changes too (or even
    109 /// selector changes?) specially now that we take the cascade data difference into account.
    110 #[derive(Debug, Default, MallocSizeOf)]
    111 pub struct StylesheetInvalidationSet {
    112    buckets: SimpleBucketsMap<InvalidationKind>,
    113    style_fully_invalid: bool,
    114    /// The difference between the old and new cascade data, incrementally collected until flush()
    115    /// returns it.
    116    pub cascade_data_difference: CascadeDataDifference,
    117 }
    118 
    119 impl StylesheetInvalidationSet {
    120    /// Create an empty `StylesheetInvalidationSet`.
    121    pub fn new() -> Self {
    122        Default::default()
    123    }
    124 
    125    /// Mark the DOM tree styles' as fully invalid.
    126    pub fn invalidate_fully(&mut self) {
    127        debug!("StylesheetInvalidationSet::invalidate_fully");
    128        self.buckets.clear();
    129        self.style_fully_invalid = true;
    130    }
    131 
    132    /// Analyze the given stylesheet, and collect invalidations from their rules, in order to avoid
    133    /// doing a full restyle when we style the document next time.
    134    pub fn collect_invalidations_for<S>(
    135        &mut self,
    136        device: &Device,
    137        custom_media: &CustomMediaMap,
    138        stylesheet: &S,
    139        guard: &SharedRwLockReadGuard,
    140    ) where
    141        S: StylesheetInDocument,
    142    {
    143        debug!("StylesheetInvalidationSet::collect_invalidations_for");
    144        if self.style_fully_invalid {
    145            debug!(" > Fully invalid already");
    146            return;
    147        }
    148 
    149        if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard)
    150        {
    151            debug!(" > Stylesheet was not effective");
    152            return; // Nothing to do here.
    153        }
    154 
    155        let quirks_mode = device.quirks_mode();
    156        for rule in stylesheet
    157            .contents(guard)
    158            .effective_rules(device, custom_media, guard)
    159        {
    160            self.collect_invalidations_for_rule(
    161                rule,
    162                guard,
    163                device,
    164                quirks_mode,
    165                /* is_generic_change = */ false,
    166                // Note(dshin): Technically, the iterator should provide the ancestor chain as it
    167                // traverses down, but it shouldn't make a difference.
    168                &[],
    169            );
    170            if self.style_fully_invalid {
    171                break;
    172            }
    173        }
    174 
    175        debug!(
    176            " > resulting class invalidations: {:?}",
    177            self.buckets.classes
    178        );
    179        debug!(" > resulting id invalidations: {:?}", self.buckets.ids);
    180        debug!(
    181            " > resulting local name invalidations: {:?}",
    182            self.buckets.local_names
    183        );
    184        debug!(" > style_fully_invalid: {}", self.style_fully_invalid);
    185    }
    186 
    187    /// Returns whether there's no invalidation to process.
    188    pub fn is_empty(&self) -> bool {
    189        !self.style_fully_invalid
    190            && self.buckets.is_empty()
    191            && self.cascade_data_difference.is_empty()
    192    }
    193 
    194    fn invalidation_kind_for<E>(
    195        &self,
    196        element: E,
    197        snapshot: Option<&Snapshot>,
    198        quirks_mode: QuirksMode,
    199    ) -> InvalidationKind
    200    where
    201        E: TElement,
    202    {
    203        debug_assert!(!self.style_fully_invalid);
    204 
    205        let mut kind = InvalidationKind::None;
    206 
    207        if !self.buckets.classes.is_empty() {
    208            element.each_class(|c| {
    209                kind.add(self.buckets.classes.get(c, quirks_mode));
    210            });
    211 
    212            if kind.is_scope() {
    213                return kind;
    214            }
    215 
    216            if let Some(snapshot) = snapshot {
    217                snapshot.each_class(|c| {
    218                    kind.add(self.buckets.classes.get(c, quirks_mode));
    219                });
    220 
    221                if kind.is_scope() {
    222                    return kind;
    223                }
    224            }
    225        }
    226 
    227        if !self.buckets.ids.is_empty() {
    228            if let Some(ref id) = element.id() {
    229                kind.add(self.buckets.ids.get(id, quirks_mode));
    230                if kind.is_scope() {
    231                    return kind;
    232                }
    233            }
    234 
    235            if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) {
    236                kind.add(self.buckets.ids.get(old_id, quirks_mode));
    237                if kind.is_scope() {
    238                    return kind;
    239                }
    240            }
    241        }
    242 
    243        if !self.buckets.local_names.is_empty() {
    244            kind.add(self.buckets.local_names.get(element.local_name()));
    245        }
    246 
    247        kind
    248    }
    249 
    250    /// Processes the style invalidation set, invalidating elements as needed.
    251    /// Returns true if any style invalidations occurred.
    252    pub fn process_style<E>(&self, root: E, snapshots: Option<&SnapshotMap>) -> bool
    253    where
    254        E: TElement,
    255    {
    256        debug!(
    257            "StylesheetInvalidationSet::process_style({root:?}, snapshots: {})",
    258            snapshots.is_some()
    259        );
    260 
    261        {
    262            let mut data = match root.mutate_data() {
    263                Some(data) => data,
    264                None => return false,
    265            };
    266 
    267            if self.style_fully_invalid {
    268                debug!("process_invalidations: fully_invalid({:?})", root);
    269                data.hint.insert(RestyleHint::restyle_subtree());
    270                return true;
    271            }
    272        }
    273 
    274        if self.buckets.is_empty() {
    275            debug!("process_invalidations: empty invalidation set");
    276            return false;
    277        }
    278 
    279        let quirks_mode = root.as_node().owner_doc().quirks_mode();
    280        self.process_invalidations_in_subtree(root, snapshots, quirks_mode)
    281    }
    282 
    283    /// Process style invalidations in a given subtree. This traverses the
    284    /// subtree looking for elements that match the invalidations in our hash
    285    /// map members.
    286    ///
    287    /// Returns whether it invalidated at least one element's style.
    288    #[allow(unsafe_code)]
    289    fn process_invalidations_in_subtree<E>(
    290        &self,
    291        element: E,
    292        snapshots: Option<&SnapshotMap>,
    293        quirks_mode: QuirksMode,
    294    ) -> bool
    295    where
    296        E: TElement,
    297    {
    298        debug!("process_invalidations_in_subtree({:?})", element);
    299        let mut data = match element.mutate_data() {
    300            Some(data) => data,
    301            None => return false,
    302        };
    303 
    304        if !data.has_styles() {
    305            return false;
    306        }
    307 
    308        if data.hint.contains_subtree() {
    309            debug!(
    310                "process_invalidations_in_subtree: {:?} was already invalid",
    311                element
    312            );
    313            return false;
    314        }
    315 
    316        let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s));
    317        let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot());
    318 
    319        match self.invalidation_kind_for(element, snapshot, quirks_mode) {
    320            InvalidationKind::None => {},
    321            InvalidationKind::Element => {
    322                debug!(
    323                    "process_invalidations_in_subtree: {:?} matched self",
    324                    element
    325                );
    326                data.hint.insert(RestyleHint::RESTYLE_SELF);
    327            },
    328            InvalidationKind::Scope => {
    329                debug!(
    330                    "process_invalidations_in_subtree: {:?} matched subtree",
    331                    element
    332                );
    333                data.hint.insert(RestyleHint::restyle_subtree());
    334                return true;
    335            },
    336        }
    337 
    338        let mut any_children_invalid = false;
    339 
    340        for child in element.traversal_children() {
    341            let child = match child.as_element() {
    342                Some(e) => e,
    343                None => continue,
    344            };
    345 
    346            any_children_invalid |=
    347                self.process_invalidations_in_subtree(child, snapshots, quirks_mode);
    348        }
    349 
    350        if any_children_invalid {
    351            debug!(
    352                "Children of {:?} changed, setting dirty descendants",
    353                element
    354            );
    355            unsafe { element.set_dirty_descendants() }
    356        }
    357 
    358        data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid
    359    }
    360 
    361    /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles :is() / :where() etc.
    362    fn scan_component(
    363        component: &Component<SelectorImpl>,
    364        invalidation: &mut Option<Invalidation>,
    365    ) {
    366        match *component {
    367            Component::LocalName(LocalName {
    368                ref name,
    369                ref lower_name,
    370            }) => {
    371                if invalidation.is_none() {
    372                    *invalidation = Some(Invalidation::LocalName {
    373                        name: name.clone(),
    374                        lower_name: lower_name.clone(),
    375                    });
    376                }
    377            },
    378            Component::Class(ref class) => {
    379                if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) {
    380                    *invalidation = Some(Invalidation::Class(class.clone()));
    381                }
    382            },
    383            Component::ID(ref id) => {
    384                if invalidation.as_ref().map_or(true, |s| !s.is_id()) {
    385                    *invalidation = Some(Invalidation::ID(id.clone()));
    386                }
    387            },
    388            _ => {
    389                // Ignore everything else, at least for now.
    390            },
    391        }
    392    }
    393 
    394    /// Collect invalidations for a given selector.
    395    ///
    396    /// We look at the outermost local name, class, or ID selector to the left
    397    /// of an ancestor combinator, in order to restyle only a given subtree.
    398    ///
    399    /// If the selector has no ancestor combinator, then we do the same for
    400    /// the only sequence it has, but record it as an element invalidation
    401    /// instead of a subtree invalidation.
    402    ///
    403    /// We prefer IDs to classs, and classes to local names, on the basis
    404    /// that the former should be more specific than the latter. We also
    405    /// prefer to generate subtree invalidations for the outermost part
    406    /// of the selector, to reduce the amount of traversal we need to do
    407    /// when flushing invalidations.
    408    fn collect_invalidations(
    409        &mut self,
    410        selector: &Selector<SelectorImpl>,
    411        quirks_mode: QuirksMode,
    412    ) {
    413        debug!(
    414            "StylesheetInvalidationSet::collect_invalidations({:?})",
    415            selector
    416        );
    417 
    418        let mut element_invalidation: Option<Invalidation> = None;
    419        let mut subtree_invalidation: Option<Invalidation> = None;
    420 
    421        let mut scan_for_element_invalidation = true;
    422        let mut scan_for_subtree_invalidation = false;
    423 
    424        let mut iter = selector.iter();
    425 
    426        loop {
    427            for component in &mut iter {
    428                if scan_for_element_invalidation {
    429                    Self::scan_component(component, &mut element_invalidation);
    430                } else if scan_for_subtree_invalidation {
    431                    Self::scan_component(component, &mut subtree_invalidation);
    432                }
    433            }
    434            match iter.next_sequence() {
    435                None => break,
    436                Some(combinator) => {
    437                    scan_for_subtree_invalidation = combinator.is_ancestor();
    438                },
    439            }
    440            scan_for_element_invalidation = false;
    441        }
    442 
    443        if let Some(s) = subtree_invalidation {
    444            debug!(" > Found subtree invalidation: {:?}", s);
    445            if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) {
    446                return;
    447            }
    448        }
    449 
    450        if let Some(s) = element_invalidation {
    451            debug!(" > Found element invalidation: {:?}", s);
    452            if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) {
    453                return;
    454            }
    455        }
    456 
    457        // The selector was of a form that we can't handle. Any element could
    458        // match it, so let's just bail out.
    459        debug!(" > Can't handle selector or OOMd, marking fully invalid");
    460        self.invalidate_fully()
    461    }
    462 
    463    fn insert_invalidation(
    464        &mut self,
    465        invalidation: Invalidation,
    466        kind: InvalidationKind,
    467        quirks_mode: QuirksMode,
    468    ) -> bool {
    469        match invalidation {
    470            Invalidation::Class(c) => {
    471                let entry = match self.buckets.classes.try_entry(c.0, quirks_mode) {
    472                    Ok(e) => e,
    473                    Err(..) => return false,
    474                };
    475                *entry.or_insert(InvalidationKind::None) |= kind;
    476            },
    477            Invalidation::ID(i) => {
    478                let entry = match self.buckets.ids.try_entry(i.0, quirks_mode) {
    479                    Ok(e) => e,
    480                    Err(..) => return false,
    481                };
    482                *entry.or_insert(InvalidationKind::None) |= kind;
    483            },
    484            Invalidation::LocalName { name, lower_name } => {
    485                let insert_lower = name != lower_name;
    486                if self.buckets.local_names.try_reserve(1).is_err() {
    487                    return false;
    488                }
    489                let entry = self.buckets.local_names.entry(name);
    490                *entry.or_insert(InvalidationKind::None) |= kind;
    491                if insert_lower {
    492                    if self.buckets.local_names.try_reserve(1).is_err() {
    493                        return false;
    494                    }
    495                    let entry = self.buckets.local_names.entry(lower_name);
    496                    *entry.or_insert(InvalidationKind::None) |= kind;
    497                }
    498            },
    499        }
    500 
    501        true
    502    }
    503 
    504    /// Collects invalidations for a given CSS rule, if not fully invalid already.
    505    pub fn rule_changed<S>(
    506        &mut self,
    507        stylesheet: &S,
    508        rule: &CssRule,
    509        guard: &SharedRwLockReadGuard,
    510        device: &Device,
    511        quirks_mode: QuirksMode,
    512        custom_media: &CustomMediaMap,
    513        change_kind: RuleChangeKind,
    514        ancestors: &[CssRuleRef],
    515    ) where
    516        S: StylesheetInDocument,
    517    {
    518        debug!("StylesheetInvalidationSet::rule_changed");
    519        if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard)
    520        {
    521            debug!(" > Stylesheet was not effective");
    522            return; // Nothing to do here.
    523        }
    524 
    525        if ancestors
    526            .iter()
    527            .any(|r| !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, r))
    528        {
    529            debug!(" > Ancestor rules not effective");
    530            return;
    531        }
    532 
    533        if change_kind == RuleChangeKind::PositionTryDeclarations {
    534            // @position-try declaration changes need to be dealt explicitly, since the
    535            // declarations are mutable and we can't otherwise detect changes to them.
    536            match *rule {
    537                CssRule::PositionTry(ref pt) => {
    538                    self.cascade_data_difference
    539                        .changed_position_try_names
    540                        .insert(pt.read_with(guard).name.0.clone());
    541                },
    542                _ => debug_assert!(false, "how did position-try decls change on anything else?"),
    543            }
    544            return;
    545        }
    546 
    547        if self.style_fully_invalid {
    548            return;
    549        }
    550 
    551        // If the change is generic, we don't have the old rule information to know e.g., the old
    552        // media condition, or the old selector text, so we might need to invalidate more
    553        // aggressively. That only applies to the changed rules, for other rules we can just
    554        // collect invalidations as normal.
    555        let is_generic_change = change_kind == RuleChangeKind::Generic;
    556        self.collect_invalidations_for_rule(
    557            rule,
    558            guard,
    559            device,
    560            quirks_mode,
    561            is_generic_change,
    562            ancestors,
    563        );
    564        if self.style_fully_invalid {
    565            return;
    566        }
    567 
    568        if !is_generic_change
    569            && !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, &rule.into())
    570        {
    571            return;
    572        }
    573 
    574        let rules = EffectiveRulesIterator::effective_children(
    575            device,
    576            quirks_mode,
    577            custom_media,
    578            guard,
    579            rule,
    580        );
    581        for rule in rules {
    582            self.collect_invalidations_for_rule(
    583                rule,
    584                guard,
    585                device,
    586                quirks_mode,
    587                /* is_generic_change = */ false,
    588                // Note(dshin): Technically, the iterator should provide the ancestor chain as it traverses down, which sould be appended to `ancestors`, but it shouldn't matter.
    589                &[],
    590            );
    591            if self.style_fully_invalid {
    592                break;
    593            }
    594        }
    595    }
    596 
    597    /// Collects invalidations for a given CSS rule.
    598    fn collect_invalidations_for_rule(
    599        &mut self,
    600        rule: &CssRule,
    601        guard: &SharedRwLockReadGuard,
    602        device: &Device,
    603        quirks_mode: QuirksMode,
    604        is_generic_change: bool,
    605        ancestors: &[CssRuleRef],
    606    ) {
    607        use crate::stylesheets::CssRule::*;
    608        debug!("StylesheetInvalidationSet::collect_invalidations_for_rule");
    609        debug_assert!(!self.style_fully_invalid, "Not worth being here!");
    610 
    611        match *rule {
    612            Style(ref lock) => {
    613                if is_generic_change {
    614                    // TODO(emilio): We need to do this for selector / keyframe
    615                    // name / font-face changes, because we don't have the old
    616                    // selector / name.  If we distinguish those changes
    617                    // specially, then we can at least use this invalidation for
    618                    // style declaration changes.
    619                    return self.invalidate_fully();
    620                }
    621 
    622                let style_rule = lock.read_with(guard);
    623                for selector in style_rule.selectors.slice() {
    624                    self.collect_invalidations(selector, quirks_mode);
    625                    if self.style_fully_invalid {
    626                        return;
    627                    }
    628                }
    629            },
    630            NestedDeclarations(..) => {
    631                if ancestors.iter().any(|r| matches!(r, CssRuleRef::Scope(_))) {
    632                    self.invalidate_fully();
    633                }
    634            },
    635            Namespace(..) => {
    636                // It's not clear what handling changes for this correctly would
    637                // look like.
    638            },
    639            LayerStatement(..) => {
    640                // Layer statement insertions might alter styling order, so we need to always
    641                // invalidate fully.
    642                return self.invalidate_fully();
    643            },
    644            Document(..) | Import(..) | Media(..) | Supports(..) | Container(..)
    645            | LayerBlock(..) | StartingStyle(..) => {
    646                // Do nothing, relevant nested rules are visited as part of rule iteration.
    647            },
    648            FontFace(..) => {
    649                // Do nothing, @font-face doesn't affect computed style information on it's own.
    650                // We'll restyle when the font face loads, if needed.
    651            },
    652            Page(..) | Margin(..) => {
    653                // Do nothing, we don't support OM mutations on print documents, and page rules
    654                // can't affect anything else.
    655            },
    656            Keyframes(ref lock) => {
    657                if is_generic_change {
    658                    return self.invalidate_fully();
    659                }
    660                let keyframes_rule = lock.read_with(guard);
    661                if device.animation_name_may_be_referenced(&keyframes_rule.name) {
    662                    debug!(
    663                        " > Found @keyframes rule potentially referenced \
    664                         from the page, marking the whole tree invalid."
    665                    );
    666                    self.invalidate_fully();
    667                } else {
    668                    // Do nothing, this animation can't affect the style of existing elements.
    669                }
    670            },
    671            CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => {
    672                debug!(" > Found unsupported rule, marking the whole subtree invalid.");
    673                self.invalidate_fully();
    674            },
    675            Scope(..) => {
    676                // Addition/removal of @scope requires re-evaluation of scope proximity to properly
    677                // figure out the styling order.
    678                self.invalidate_fully();
    679            },
    680            PositionTry(..) => {
    681                // @position-try changes doesn't change style-time information (only layout
    682                // information) and is handled by invalidate_position_try. So do nothing.
    683            },
    684            CustomMedia(..) => {
    685                // @custom-media might be referenced by other rules which we can't get a hand on in
    686                // here, so we don't know which elements are affected.
    687                //
    688                // TODO: Maybe track referenced custom-media rules like we do for @keyframe?
    689                self.invalidate_fully();
    690            },
    691        }
    692    }
    693 }
    694 
    695 /// Invalidates for any absolutely positioned element that references the given @position-try fallback names.
    696 pub fn invalidate_position_try<E>(
    697    element: E,
    698    changed_names: &PrecomputedHashSet<Atom>,
    699    invalidate_self: &mut impl FnMut(E, &mut ElementData),
    700    invalidated_descendants: &mut impl FnMut(E),
    701 ) -> bool
    702 where
    703    E: TElement,
    704 {
    705    debug_assert!(
    706        !changed_names.is_empty(),
    707        "Don't call me if there's nothing to do"
    708    );
    709    let mut data = match element.mutate_data() {
    710        Some(data) => data,
    711        None => return false,
    712    };
    713 
    714    let mut self_invalid = false;
    715    let style = data.styles.primary();
    716    if style.clone_position().is_absolutely_positioned() {
    717        let fallbacks = style.clone_position_try_fallbacks();
    718        let referenced = fallbacks.0.iter().any(|f| match f {
    719            PositionTryFallbacksItem::IdentAndOrTactic(ident_or_tactic) => {
    720                changed_names.contains(&ident_or_tactic.ident.0)
    721            },
    722            PositionTryFallbacksItem::PositionArea(..) => false,
    723        });
    724 
    725        if referenced {
    726            self_invalid = true;
    727            invalidate_self(element, &mut data);
    728        }
    729    }
    730    let mut any_children_invalid = false;
    731    for child in element.traversal_children() {
    732        let Some(e) = child.as_element() else {
    733            continue;
    734        };
    735        any_children_invalid |=
    736            invalidate_position_try(e, changed_names, invalidate_self, invalidated_descendants);
    737    }
    738    if any_children_invalid {
    739        invalidated_descendants(element);
    740    }
    741    self_invalid || any_children_invalid
    742 }