Skip to main content

blitz_dom/
stylo.rs

1//! Enable the dom to participate in styling by servo
2//!
3
4use blitz_traits::node_id::NodeId;
5use std::ptr::NonNull;
6use std::sync::Mutex;
7use std::sync::atomic::Ordering;
8
9use crate::StyleThreading;
10use crate::layout::damage::compute_layout_damage;
11use crate::node::Node;
12use crate::node::NodeData;
13use markup5ever::{LocalName, LocalNameStaticSet, Namespace, NamespaceStaticSet, local_name, ns};
14use selectors::bloom::BLOOM_HASH_MASK;
15use selectors::{
16    Element, OpaqueElement,
17    attr::{AttrSelectorOperation, NamespaceConstraint},
18    matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode},
19    sink::Push,
20};
21use style::CaseSensitivityExt;
22use style::animation::AnimationSetKey;
23use style::animation::AnimationState;
24use style::applicable_declarations::ApplicableDeclarationBlock;
25use style::bloom::each_relevant_element_hash;
26use style::color::AbsoluteColor;
27use style::data::{ElementDataMut, ElementDataRef};
28use style::global_style_data::STYLE_THREAD_POOL;
29use style::invalidation::element::restyle_hints::RestyleHint;
30use style::properties::ComputedValues;
31use style::properties::{Importance, PropertyDeclaration};
32use style::rule_tree::CascadeLevel;
33use style::rule_tree::CascadeOrigin;
34use style::selector_parser::PseudoElement;
35use style::selector_parser::RestyleDamage;
36use style::stylesheets::layer_rule::LayerOrder;
37use style::stylesheets::scope_rule::ImplicitScopeRoot;
38use style::values::AtomString;
39use style::values::specified::{NoCalcPercentage, XLang};
40use style::{
41    Atom,
42    context::{
43        QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters,
44        SharedStyleContext, StyleContext, ThreadLocalStyleContext,
45    },
46    dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot},
47    global_style_data::GLOBAL_STYLE_DATA,
48    properties::PropertyDeclarationBlock,
49    selector_parser::{NonTSPseudoClass, SelectorImpl},
50    servo_arc::{Arc, ArcBorrow},
51    shared_lock::{Locked, SharedRwLock, StylesheetGuards},
52    stylist::RuleInclusion,
53    thread_state::ThreadState,
54    traversal::{DomTraversal, resolve_style},
55    traversal_flags::TraversalFlags,
56    values::{AtomIdent, GenericAtomIdent},
57};
58use style_dom::ElementState;
59
60use style::values::computed::text::TextAlign as StyloTextAlign;
61
62impl crate::document::BaseDocument {
63    pub fn resolve_stylist(&mut self, now: f64) {
64        style::thread_state::enter(ThreadState::LAYOUT);
65
66        let guard = &self.guard;
67        let guards = StylesheetGuards {
68            author: &guard.read(),
69            ua_or_user: &guard.read(),
70        };
71
72        let root = TDocument::as_node(&&self.nodes[self.root_node_id])
73            .first_element_child()
74            .unwrap()
75            .as_element()
76            .unwrap();
77
78        self.stylist
79            .flush(&guards)
80            .process_style(root, Some(&self.snapshots));
81
82        // Mark actively animating nodes as dirty
83        let mut sets = self.animations.sets.write();
84        for (key, set) in sets.iter_mut() {
85            let node_id = NodeId::from_u64(key.node.id() as u64);
86
87            // Drop animations belonging to nodes that are no longer in the
88            // document. A removed element is never restyled, so it would never
89            // get a chance to cancel its own animations; an infinite animation
90            // would then keep `has_active_animations` set forever and force a
91            // redraw every frame. Emptying the set here lets the `retain` below
92            // discard it so the flag can clear on this same pass.
93            let in_document = self
94                .nodes
95                .get(node_id)
96                .is_some_and(|node| node.flags.is_in_document());
97            if !in_document {
98                set.animations.clear();
99                set.transitions.clear();
100                continue;
101            }
102
103            self.nodes[node_id].set_restyle_hint(RestyleHint::RESTYLE_SELF);
104
105            for animation in set.animations.iter_mut() {
106                if animation.state == AnimationState::Pending && animation.started_at <= now {
107                    animation.state = AnimationState::Running;
108                }
109                animation.iterate_if_necessary(now);
110
111                if animation.state == AnimationState::Running && animation.has_ended(now) {
112                    animation.state = AnimationState::Finished;
113                }
114            }
115
116            for transition in set.transitions.iter_mut() {
117                if transition.state == AnimationState::Pending && transition.start_time <= now {
118                    transition.state = AnimationState::Running;
119                }
120                if transition.state == AnimationState::Running && transition.has_ended(now) {
121                    transition.state = AnimationState::Finished;
122                }
123            }
124        }
125        drop(sets);
126
127        // Build the style context used by the style traversal
128        let context = SharedStyleContext {
129            traversal_flags: TraversalFlags::empty(),
130            stylist: &self.stylist,
131            options: GLOBAL_STYLE_DATA.options.clone(),
132            guards,
133            visited_styles_enabled: false,
134            animations: self.animations.clone(),
135            current_time_for_animations: now,
136            snapshot_map: &self.snapshots,
137            registered_speculative_painters: &RegisteredPaintersImpl,
138        };
139
140        // components/layout_2020/lib.rs:983
141        let root = self.root_element();
142        // dbg!(root);
143        let token = RecalcStyle::pre_traverse(root, &context);
144
145        let mut nodes_needing_style_image_flush = Vec::new();
146        if token.should_traverse() {
147            // Style the elements, resolving their data
148            let mut traverser = RecalcStyle::new(context);
149            // `Sequential` bypasses Stylo's global pool. See `StyleThreading`.
150            let pool_guard = matches!(self.style_threading, StyleThreading::Parallel)
151                .then(|| STYLE_THREAD_POOL.pool());
152            let rayon_pool = pool_guard.as_ref().and_then(|g| g.as_ref());
153            style::driver::traverse_dom(&traverser, token, rayon_pool);
154            nodes_needing_style_image_flush =
155                std::mem::take(traverser.nodes_needing_style_image_flush.get_mut().unwrap());
156        }
157        self.pending_style_image_nodes
158            .extend(nodes_needing_style_image_flush);
159
160        for opaque in self.snapshots.keys() {
161            let id = NodeId::from_u64(opaque.id() as u64);
162            if let Some(node) = self.nodes.get_mut(id) {
163                node.set_has_snapshot(false);
164            }
165        }
166        self.snapshots.clear();
167
168        let mut sets = self.animations.sets.write();
169        for set in sets.values_mut() {
170            set.clear_canceled_animations();
171            for animation in set.animations.iter_mut() {
172                animation.is_new = false;
173            }
174            for transition in set.transitions.iter_mut() {
175                transition.is_new = false;
176            }
177        }
178        sets.retain(|_, state| !state.is_empty());
179        self.has_active_animations = sets.values().any(|state| state.needs_animation_ticks());
180
181        // Maybe run garbage collection. Stylo has internal to determine whether to run or not.
182        self.stylist.rule_tree().maybe_gc();
183
184        style::thread_state::exit(ThreadState::LAYOUT);
185    }
186
187    /// Compute the style of an element which the regular style traversal
188    /// skipped because it lives inside a `display: none` subtree.
189    ///
190    /// The style traversal culls `display: none` subtrees, so such elements
191    /// never have styles stored on them, but `getComputedStyle()` must still
192    /// return their computed values. This resolves the style on demand
193    /// (styling any unstyled ancestors along the way) without storing it on the
194    /// node. Returns `None` for non-elements and elements outside the document.
195    pub fn resolve_undisplayed_style(&self, node_id: NodeId) -> Option<Arc<ComputedValues>> {
196        let node = self.nodes.get(node_id)?;
197        if !node.is_element() || !node.flags.is_in_document() {
198            return None;
199        }
200
201        style::thread_state::enter(ThreadState::LAYOUT);
202
203        let guard = &self.guard;
204        let guards = StylesheetGuards {
205            author: &guard.read(),
206            ua_or_user: &guard.read(),
207        };
208        let shared = SharedStyleContext {
209            traversal_flags: TraversalFlags::empty(),
210            stylist: &self.stylist,
211            options: GLOBAL_STYLE_DATA.options.clone(),
212            guards,
213            visited_styles_enabled: false,
214            animations: self.animations.clone(),
215            current_time_for_animations: 0.0,
216            snapshot_map: &self.snapshots,
217            registered_speculative_painters: &RegisteredPaintersImpl,
218        };
219        let mut thread_local = ThreadLocalStyleContext::new();
220        let mut context = StyleContext {
221            shared: &shared,
222            thread_local: &mut thread_local,
223        };
224        let styles = resolve_style(&mut context, node, RuleInclusion::All, None, None);
225
226        style::thread_state::exit(ThreadState::LAYOUT);
227
228        Some(styles.primary().clone())
229    }
230}
231
232/// A handle to a node that Servo's style traits are implemented against
233///
234/// Since BlitzNodes are not persistent (IE we don't keep the pointers around between frames), we choose to just implement
235/// the tree structure in the nodes themselves, and temporarily give out pointers during the layout phase.
236type BlitzNode<'a> = &'a Node;
237
238impl<'a> TDocument for BlitzNode<'a> {
239    type ConcreteNode = BlitzNode<'a>;
240
241    fn as_node(&self) -> Self::ConcreteNode {
242        self
243    }
244
245    fn is_html_document(&self) -> bool {
246        true
247    }
248
249    fn quirks_mode(&self) -> QuirksMode {
250        QuirksMode::NoQuirks
251    }
252
253    fn shared_lock(&self) -> &SharedRwLock {
254        self.guard()
255    }
256}
257
258impl NodeInfo for BlitzNode<'_> {
259    fn is_element(&self) -> bool {
260        Node::is_element(self)
261    }
262
263    fn is_text_node(&self) -> bool {
264        Node::is_text_node(self)
265    }
266}
267
268impl<'a> TShadowRoot for BlitzNode<'a> {
269    type ConcreteNode = BlitzNode<'a>;
270
271    fn as_node(&self) -> Self::ConcreteNode {
272        self
273    }
274
275    fn host(&self) -> <Self::ConcreteNode as TNode>::ConcreteElement {
276        todo!("Shadow roots not implemented")
277    }
278
279    fn style_data<'b>(&self) -> Option<&'b style::stylist::CascadeData>
280    where
281        Self: 'b,
282    {
283        todo!("Shadow roots not implemented")
284    }
285}
286
287// components/styleaapper.rs:
288impl<'a> TNode for BlitzNode<'a> {
289    type ConcreteElement = BlitzNode<'a>;
290    type ConcreteDocument = BlitzNode<'a>;
291    type ConcreteShadowRoot = BlitzNode<'a>;
292
293    fn parent_node(&self) -> Option<Self> {
294        self.parent.map(|id| self.with(id))
295    }
296
297    fn first_child(&self) -> Option<Self> {
298        self.children.first().map(|id| self.with(*id))
299    }
300
301    fn last_child(&self) -> Option<Self> {
302        self.children.last().map(|id| self.with(*id))
303    }
304
305    fn prev_sibling(&self) -> Option<Self> {
306        self.backward(1)
307    }
308
309    fn next_sibling(&self) -> Option<Self> {
310        self.forward(1)
311    }
312
313    fn owner_doc(&self) -> Self::ConcreteDocument {
314        // Walk up the (layout-)parent chain to the root Document node.
315        let mut node = *self;
316        while let Some(parent_id) = node.parent {
317            node = node.with(parent_id);
318        }
319        node
320    }
321
322    fn is_in_document(&self) -> bool {
323        true
324    }
325
326    // I think this is the same as parent_node only in the cases when the direct parent is not a real element, forcing us
327    // to travel upwards
328    //
329    // For the sake of this demo, we're just going to return the parent node ann
330    fn traversal_parent(&self) -> Option<Self::ConcreteElement> {
331        self.parent_node().and_then(|node| node.as_element())
332    }
333
334    fn opaque(&self) -> OpaqueNode {
335        OpaqueNode(self.id.as_u64() as usize)
336    }
337
338    fn debug_id(self) -> usize {
339        self.id.as_u64() as usize
340    }
341
342    fn as_element(&self) -> Option<Self::ConcreteElement> {
343        match self.data {
344            NodeData::Element { .. } => Some(self),
345            _ => None,
346        }
347    }
348
349    fn as_document(&self) -> Option<Self::ConcreteDocument> {
350        match self.data {
351            NodeData::Document(_) => Some(self),
352            _ => None,
353        }
354    }
355
356    fn as_shadow_root(&self) -> Option<Self::ConcreteShadowRoot> {
357        // TODO: implement shadow DOM
358        None
359    }
360}
361
362impl selectors::Element for BlitzNode<'_> {
363    type Impl = SelectorImpl;
364
365    fn opaque(&self) -> selectors::OpaqueElement {
366        // This correctly uses a unique id for the OpaqueElement (unlike using a pointer to the "slot")
367        // However, it makes it impossible for us to "rehydrate" the OpaqueElement back into an actual Element
368        // which is required to implement the `implicit_scope_for_sheet_in_shadow_root` method below
369        //
370        // We should see if selectors will accept a PR that allows us to use 128bits for the OpaqueElement. Or
371        // find some other solution that will enable "rehydration". This is required to enable and use the
372        // Shadow DOM functionality in Stylo.
373        let non_null =
374            NonNull::new((self.id.as_u64() as usize).wrapping_add(1) as *mut ()).unwrap();
375        OpaqueElement::from_non_null_ptr(non_null)
376    }
377
378    fn parent_element(&self) -> Option<Self> {
379        TElement::traversal_parent(self)
380    }
381
382    fn parent_node_is_shadow_root(&self) -> bool {
383        false
384    }
385
386    fn containing_shadow_host(&self) -> Option<Self> {
387        None
388    }
389
390    fn is_pseudo_element(&self) -> bool {
391        matches!(self.data, NodeData::AnonymousBlock(_))
392    }
393
394    // These methods are implemented naively since we only threaded real nodes and not fake nodes
395    // we should try and use `find` instead of this foward/backward stuff since its ugly and slow
396    fn prev_sibling_element(&self) -> Option<Self> {
397        let mut n = 1;
398        while let Some(node) = self.backward(n) {
399            if node.is_element() {
400                return Some(node);
401            }
402            n += 1;
403        }
404
405        None
406    }
407
408    fn next_sibling_element(&self) -> Option<Self> {
409        let mut n = 1;
410        while let Some(node) = self.forward(n) {
411            if node.is_element() {
412                return Some(node);
413            }
414            n += 1;
415        }
416
417        None
418    }
419
420    fn first_element_child(&self) -> Option<Self> {
421        let mut children = self.dom_children();
422        children.find(|child| child.is_element())
423    }
424
425    fn is_html_element_in_html_document(&self) -> bool {
426        true // self.has_namespace(ns!(html))
427    }
428
429    fn has_local_name(&self, local_name: &LocalName) -> bool {
430        self.data.is_element_with_tag_name(local_name)
431    }
432
433    fn has_namespace(&self, ns: &Namespace) -> bool {
434        self.element_data().expect("Not an element").name.ns == *ns
435    }
436
437    fn is_same_type(&self, other: &Self) -> bool {
438        self.local_name() == other.local_name() && self.namespace() == other.namespace()
439    }
440
441    fn attr_matches(
442        &self,
443        ns: &NamespaceConstraint<&GenericAtomIdent<NamespaceStaticSet>>,
444        local_name: &GenericAtomIdent<LocalNameStaticSet>,
445        operation: &AttrSelectorOperation<&AtomString>,
446    ) -> bool {
447        let Some(attrs) = self.data.attrs() else {
448            return false;
449        };
450        attrs.iter().any(|attr| {
451            attr.name.local == local_name.0
452                && match ns {
453                    NamespaceConstraint::Any => true,
454                    NamespaceConstraint::Specific(ns) => attr.name.ns == ns.0,
455                }
456                && operation.eval_str(&attr.value)
457        })
458    }
459
460    fn match_non_ts_pseudo_class(
461        &self,
462        pseudo_class: &<Self::Impl as selectors::SelectorImpl>::NonTSPseudoClass,
463        _context: &mut MatchingContext<Self::Impl>,
464    ) -> bool {
465        match *pseudo_class {
466            NonTSPseudoClass::Active => self.element_state().contains(ElementState::ACTIVE),
467            NonTSPseudoClass::AnyLink => self
468                .element_state()
469                .intersects(ElementState::VISITED_OR_UNVISITED),
470            NonTSPseudoClass::Checked => self.element_state().contains(ElementState::CHECKED),
471            NonTSPseudoClass::Valid => false,
472            NonTSPseudoClass::Invalid => false,
473            NonTSPseudoClass::Defined => false,
474            NonTSPseudoClass::Disabled => self.element_state().contains(ElementState::DISABLED),
475            NonTSPseudoClass::Enabled => self.element_state().contains(ElementState::ENABLED),
476            NonTSPseudoClass::Focus => self.element_state().contains(ElementState::FOCUS),
477            NonTSPseudoClass::FocusWithin => false,
478            NonTSPseudoClass::FocusVisible => false,
479            NonTSPseudoClass::Fullscreen => false,
480            NonTSPseudoClass::Hover => self.element_state().contains(ElementState::HOVER),
481            NonTSPseudoClass::Indeterminate => false,
482            NonTSPseudoClass::Lang(_) => false,
483            NonTSPseudoClass::CustomState(_) => false,
484            NonTSPseudoClass::Link => self.element_state().contains(ElementState::UNVISITED),
485            NonTSPseudoClass::PlaceholderShown => false,
486            NonTSPseudoClass::ReadWrite => false,
487            NonTSPseudoClass::ReadOnly => false,
488            NonTSPseudoClass::ServoNonZeroBorder => false,
489            NonTSPseudoClass::Target => false,
490            NonTSPseudoClass::Visited => false,
491            NonTSPseudoClass::Autofill => false,
492            NonTSPseudoClass::Default => false,
493
494            NonTSPseudoClass::InRange => false,
495            NonTSPseudoClass::Modal => false,
496            NonTSPseudoClass::Open => false,
497            NonTSPseudoClass::Optional => false,
498            NonTSPseudoClass::OutOfRange => false,
499            NonTSPseudoClass::PopoverOpen => false,
500            NonTSPseudoClass::Required => false,
501            NonTSPseudoClass::UserInvalid => false,
502            NonTSPseudoClass::UserValid => false,
503            NonTSPseudoClass::MozMeterOptimum => false,
504            NonTSPseudoClass::MozMeterSubOptimum => false,
505            NonTSPseudoClass::MozMeterSubSubOptimum => false,
506        }
507    }
508
509    fn match_pseudo_element(
510        &self,
511        pe: &PseudoElement,
512        _context: &mut MatchingContext<Self::Impl>,
513    ) -> bool {
514        let pseudo = match self.try_stylo_element_data().and_then(|s| s.get()) {
515            Some(el) => el
516                .styles
517                .get_primary()
518                .and_then(|s| s.pseudo())
519                .or(match &self.data {
520                    NodeData::AnonymousBlock(_) => Some(PseudoElement::ServoAnonymousBox),
521                    _ => None,
522                }),
523            None => None,
524        };
525
526        pseudo.is_some_and(|psuedo| psuedo == *pe)
527    }
528
529    fn apply_selector_flags(&self, flags: ElementSelectorFlags) {
530        // Handle flags that apply to the element.
531        let self_flags = flags.for_self();
532        if !self_flags.is_empty() {
533            self.selector_flags()
534                .set(self.selector_flags().get() | self_flags);
535        }
536
537        // Handle flags that apply to the parent.
538        let parent_flags = flags.for_parent();
539        if !parent_flags.is_empty() {
540            if let Some(parent) = self.parent_node() {
541                parent
542                    .selector_flags()
543                    .set(parent.selector_flags().get() | parent_flags);
544            }
545        }
546    }
547
548    fn is_link(&self) -> bool {
549        self.data.is_element_with_tag_name(&local_name!("a"))
550    }
551
552    fn is_html_slot_element(&self) -> bool {
553        false
554    }
555
556    fn has_id(
557        &self,
558        id: &<Self::Impl as selectors::SelectorImpl>::Identifier,
559        case_sensitivity: selectors::attr::CaseSensitivity,
560    ) -> bool {
561        self.element_data()
562            .and_then(|data| data.id.as_ref())
563            .map(|id_attr| case_sensitivity.eq_atom(id_attr, id))
564            .unwrap_or(false)
565    }
566
567    fn has_class(
568        &self,
569        search_name: &<Self::Impl as selectors::SelectorImpl>::Identifier,
570        case_sensitivity: selectors::attr::CaseSensitivity,
571    ) -> bool {
572        let class_attr = self.data.attr(local_name!("class"));
573        if let Some(class_attr) = class_attr {
574            // split the class attribute
575            for pheme in class_attr.split_ascii_whitespace() {
576                let atom = Atom::from(pheme);
577                if case_sensitivity.eq_atom(&atom, search_name) {
578                    return true;
579                }
580            }
581        }
582
583        false
584    }
585
586    fn imported_part(
587        &self,
588        _name: &<Self::Impl as selectors::SelectorImpl>::Identifier,
589    ) -> Option<<Self::Impl as selectors::SelectorImpl>::Identifier> {
590        None
591    }
592
593    fn is_part(&self, _name: &<Self::Impl as selectors::SelectorImpl>::Identifier) -> bool {
594        false
595    }
596
597    fn is_empty(&self) -> bool {
598        self.dom_children().next().is_none()
599    }
600
601    fn is_root(&self) -> bool {
602        self.parent_node()
603            .and_then(|parent| parent.parent_node())
604            .is_none()
605    }
606
607    fn has_custom_state(
608        &self,
609        _name: &<Self::Impl as selectors::SelectorImpl>::Identifier,
610    ) -> bool {
611        false
612    }
613
614    fn add_element_unique_hashes(&self, filter: &mut selectors::bloom::BloomFilter) -> bool {
615        each_relevant_element_hash(*self, |hash| filter.insert_hash(hash & BLOOM_HASH_MASK));
616        true
617    }
618}
619
620impl<'a> TElement for BlitzNode<'a> {
621    type ConcreteNode = BlitzNode<'a>;
622
623    type TraversalChildrenIterator = Traverser<'a>;
624
625    fn as_node(&self) -> Self::ConcreteNode {
626        self
627    }
628
629    fn implicit_scope_for_sheet_in_shadow_root(
630        _opaque_host: OpaqueElement,
631        _sheet_index: usize,
632    ) -> Option<ImplicitScopeRoot> {
633        // We cannot currently implement this as we are using the NodeId as the OpaqueElement,
634        // and need a reference to the Slab to convert it back into an Element
635        //
636        // Luckily it is only needed for shadow dom.
637        todo!();
638    }
639
640    fn traversal_children(&self) -> style::dom::LayoutIterator<Self::TraversalChildrenIterator> {
641        LayoutIterator(Traverser {
642            // dom: self.tree(),
643            parent: self,
644            child_index: 0,
645        })
646    }
647
648    fn is_html_element(&self) -> bool {
649        self.is_element()
650    }
651
652    // not implemented.....
653    fn is_mathml_element(&self) -> bool {
654        false
655    }
656
657    // need to check the namespace
658    fn is_svg_element(&self) -> bool {
659        false
660    }
661
662    fn style_attribute(&self) -> Option<ArcBorrow<'_, Locked<PropertyDeclarationBlock>>> {
663        self.element_data()
664            .expect("Not an element")
665            .style_attribute
666            .as_ref()
667            .map(|f| f.borrow_arc())
668    }
669
670    fn state(&self) -> ElementState {
671        *self.element_state()
672    }
673
674    fn has_part_attr(&self) -> bool {
675        false
676    }
677
678    fn exports_any_part(&self) -> bool {
679        false
680    }
681
682    fn id(&self) -> Option<&style::Atom> {
683        self.element_data().and_then(|data| data.id.as_ref())
684    }
685
686    fn each_class<F>(&self, mut callback: F)
687    where
688        F: FnMut(&style::values::AtomIdent),
689    {
690        let class_attr = self.data.attr(local_name!("class"));
691        if let Some(class_attr) = class_attr {
692            // split the class attribute
693            for pheme in class_attr.split_ascii_whitespace() {
694                let atom = Atom::from(pheme); // interns the string
695                callback(AtomIdent::cast(&atom));
696            }
697        }
698    }
699
700    fn each_attr_name<F>(&self, mut callback: F)
701    where
702        F: FnMut(&style::LocalName),
703    {
704        if let Some(attrs) = self.data.attrs() {
705            for attr in attrs.iter() {
706                callback(&GenericAtomIdent(attr.name.local.clone()));
707            }
708        }
709    }
710
711    fn has_dirty_descendants(&self) -> bool {
712        Node::has_dirty_descendants(self)
713    }
714
715    fn has_snapshot(&self) -> bool {
716        Node::has_snapshot(self)
717    }
718
719    fn handled_snapshot(&self) -> bool {
720        self.snapshot_handled().load(Ordering::SeqCst)
721    }
722
723    unsafe fn set_handled_snapshot(&self) {
724        self.snapshot_handled().store(true, Ordering::SeqCst);
725    }
726
727    // Stylo calls this on elements it is already traversing (or has already
728    // reached via invalidation), so the ancestor chain must not be re-marked:
729    // ancestors visited earlier in the preorder traversal have had their bits
730    // cleared, and re-flagging them leaves stale `dirty_descendants` bits that
731    // break the early-out invariant of `Node::mark_ancestors_dirty` (a set bit
732    // implies all ancestors are set).
733    unsafe fn set_dirty_descendants(&self) {
734        Node::set_dirty_descendants(self);
735    }
736
737    unsafe fn unset_dirty_descendants(&self) {
738        Node::unset_dirty_descendants(self);
739    }
740
741    fn store_children_to_process(&self, _n: isize) {
742        unimplemented!()
743    }
744
745    fn did_process_child(&self) -> isize {
746        unimplemented!()
747    }
748
749    unsafe fn ensure_data(&self) -> ElementDataMut<'_> {
750        // SAFETY: stylo traversal has exclusive access to nodes
751        unsafe { self.stylo_element_data().ensure_init() }
752    }
753
754    unsafe fn clear_data(&self) {
755        // SAFETY: stylo traversal has exclusive access to nodes
756        unsafe { self.stylo_element_data().clear() }
757    }
758
759    fn has_data(&self) -> bool {
760        self.try_stylo_element_data().is_some_and(|s| s.has_data())
761    }
762
763    fn borrow_data(&self) -> Option<ElementDataRef<'_>> {
764        self.try_stylo_element_data().and_then(|s| s.get())
765    }
766
767    fn mutate_data(&self) -> Option<ElementDataMut<'_>> {
768        unsafe { self.stylo_element_data().unsafe_stylo_only_mut() }
769    }
770
771    fn skip_item_display_fixup(&self) -> bool {
772        false
773    }
774
775    fn may_have_animations(&self) -> bool {
776        true
777    }
778
779    fn has_animations(&self, context: &SharedStyleContext) -> bool {
780        self.has_css_animations(context, None) || self.has_css_transitions(context, None)
781    }
782
783    fn has_css_animations(
784        &self,
785        context: &SharedStyleContext,
786        pseudo_element: Option<PseudoElement>,
787    ) -> bool {
788        let key = AnimationSetKey::new(TNode::opaque(&TElement::as_node(self)), pseudo_element);
789        context.animations.has_active_animations(&key)
790    }
791
792    fn has_css_transitions(
793        &self,
794        context: &SharedStyleContext,
795        pseudo_element: Option<PseudoElement>,
796    ) -> bool {
797        let key = AnimationSetKey::new(TNode::opaque(&TElement::as_node(self)), pseudo_element);
798        context.animations.has_active_transitions(&key)
799    }
800
801    fn animation_rule(
802        &self,
803        context: &SharedStyleContext,
804    ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
805        let opaque = TNode::opaque(&TElement::as_node(self));
806        context.animations.get_animation_declarations(
807            &AnimationSetKey::new_for_non_pseudo(opaque),
808            context.current_time_for_animations,
809            self.guard(),
810        )
811    }
812
813    fn transition_rule(
814        &self,
815        context: &SharedStyleContext,
816    ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
817        let opaque = TNode::opaque(&TElement::as_node(self));
818        context.animations.get_transition_declarations(
819            &AnimationSetKey::new_for_non_pseudo(opaque),
820            context.current_time_for_animations,
821            self.guard(),
822        )
823    }
824
825    fn shadow_root(&self) -> Option<<Self::ConcreteNode as TNode>::ConcreteShadowRoot> {
826        None
827    }
828
829    fn containing_shadow(&self) -> Option<<Self::ConcreteNode as TNode>::ConcreteShadowRoot> {
830        None
831    }
832
833    fn get_attr(&self, attr: &style::LocalName, ns: &style::Namespace) -> Option<String> {
834        // TODO: case-insensitive matching for HTML-ns attrs
835        self.attrs()?
836            .iter()
837            .find(|a| a.name.local == attr.0 && a.name.ns == ns.0)
838            .map(|a| a.value.clone())
839    }
840
841    fn lang_attr(&self) -> Option<style::selector_parser::AttrValue> {
842        None
843    }
844
845    fn match_element_lang(
846        &self,
847        _override_lang: Option<Option<style::selector_parser::AttrValue>>,
848        _value: &style::selector_parser::Lang,
849    ) -> bool {
850        false
851    }
852
853    fn is_html_document_body_element(&self) -> bool {
854        // Check node is a <body> element
855        let is_body_element = self.data.is_element_with_tag_name(&local_name!("body"));
856
857        // If it isn't then return early
858        if !is_body_element {
859            return false;
860        }
861
862        // If it is then check if it is a child of the root (<html>) element
863        let root_node = TNode::owner_doc(self);
864        let root_element = TDocument::as_node(&root_node)
865            .first_element_child()
866            .unwrap();
867        root_element.children.contains(&self.id)
868    }
869
870    fn synthesize_presentational_hints_for_legacy_attributes<V>(
871        &self,
872        _visited_handling: VisitedHandlingMode,
873        hints: &mut V,
874    ) where
875        V: Push<style::applicable_declarations::ApplicableDeclarationBlock>,
876    {
877        let Some(elem) = self.data.downcast_element() else {
878            return;
879        };
880
881        let tag = &elem.name.local;
882
883        let mut push_style = |decl: PropertyDeclaration| {
884            hints.push(ApplicableDeclarationBlock::from_declarations(
885                Arc::new(
886                    self.guard()
887                        .wrap(PropertyDeclarationBlock::with_one(decl, Importance::Normal)),
888                ),
889                CascadeLevel::new(CascadeOrigin::PresHints),
890                LayerOrder::root(),
891            ));
892        };
893
894        fn parse_color_attr(value: &str) -> Option<(u8, u8, u8, f32)> {
895            if !value.starts_with('#') {
896                return None;
897            }
898
899            let value = &value[1..];
900            if value.len() == 3 {
901                let r = u8::from_str_radix(&value[0..1], 16).ok()?;
902                let g = u8::from_str_radix(&value[1..2], 16).ok()?;
903                let b = u8::from_str_radix(&value[2..3], 16).ok()?;
904                return Some((r, g, b, 1.0));
905            }
906
907            if value.len() == 6 {
908                let r = u8::from_str_radix(&value[0..2], 16).ok()?;
909                let g = u8::from_str_radix(&value[2..4], 16).ok()?;
910                let b = u8::from_str_radix(&value[4..6], 16).ok()?;
911                return Some((r, g, b, 1.0));
912            }
913
914            None
915        }
916
917        /// The HTML "rules for parsing dimension values" -- Stylo's
918        /// implementation of them -- packaged as a specified
919        /// `<length-percentage>`. `ignoring_zero` selects the spec's separate
920        /// "maps to the dimension property (ignoring zero)" mapping, where a
921        /// zero value is dropped rather than honoured.
922        ///
923        /// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-dimension-values
924        fn parse_size_attr(
925            value: &str,
926            ignoring_zero: bool,
927        ) -> Option<style::values::specified::LengthPercentage> {
928            use style::servo::attr::{
929                LengthOrPercentageOrAuto, parse_length, parse_nonzero_length,
930            };
931            use style::values::specified::{LengthPercentage, NoCalcLength};
932            let parsed = if ignoring_zero {
933                parse_nonzero_length(value)
934            } else {
935                parse_length(value)
936            };
937            match parsed {
938                LengthOrPercentageOrAuto::Length(length) => Some(LengthPercentage::Length(
939                    NoCalcLength::from_px(length.to_f32_px()),
940                )),
941                LengthOrPercentageOrAuto::Percentage(fraction) => Some(
942                    LengthPercentage::Percentage(NoCalcPercentage::new(fraction)),
943                ),
944                LengthOrPercentageOrAuto::Auto => None,
945            }
946        }
947
948        /// Parse the value of an SVG `width`/`height` presentation attribute.
949        /// Unlike the legacy HTML dimension attributes, these accept any CSS
950        /// <length-percentage> (e.g. `1em`), and a unitless number means user
951        /// units, which map to CSS px.
952        fn parse_svg_size_attr(value: &str) -> Option<style::values::specified::LengthPercentage> {
953            use style::values::specified::{LengthPercentage, NoCalcLength};
954            use style_traits::ParsingMode;
955
956            let value = value.trim();
957            if let Some(number) = value.strip_suffix('%') {
958                let val: f32 = number.trim().parse().ok()?;
959                return (val >= 0.0)
960                    .then(|| LengthPercentage::Percentage(NoCalcPercentage::new(val / 100.0)));
961            }
962
963            // Split into number and unit: the unit is the trailing run of
964            // ASCII alphabetic characters (this never eats into a scientific
965            // exponent such as `1e3`, which ends in a digit).
966            let number_len = value
967                .trim_end_matches(|c: char| c.is_ascii_alphabetic())
968                .len();
969            let (number, unit) = value.split_at(number_len);
970            let val: f32 = number.trim().parse().ok().filter(|v| *v >= 0.0)?;
971            let length = if unit.is_empty() {
972                NoCalcLength::from_px(val)
973            } else {
974                NoCalcLength::parse_dimension_with_flags(ParsingMode::DEFAULT, false, val, unit)
975                    .ok()?
976            };
977            Some(LengthPercentage::Length(length))
978        }
979
980        // `<input type=image>` is the only input type that is replaced
981        // content, and it is the only one that takes the embedded-content
982        // presentational attributes. The type attribute is matched ASCII
983        // case-insensitively, as attribute keywords always are.
984        let is_image_input = *tag == local_name!("input")
985            && elem.attrs().iter().any(|attr| {
986                attr.name.local == local_name!("type") && attr.value.eq_ignore_ascii_case("image")
987            });
988
989        // https://html.spec.whatwg.org/multipage/#the-lang-and-xml:lang-attributes
990        let accepts_lang = elem.name.ns == ns!(html) || elem.name.ns == ns!(svg);
991        let mut lang = None;
992
993        for attr in elem.attrs() {
994            let name = &attr.name.local;
995            let value = attr.value.as_str();
996
997            if *name == local_name!("lang") {
998                if attr.name.ns == ns!(xml) {
999                    lang = Some(value);
1000                } else if attr.name.ns == ns!() && accepts_lang {
1001                    lang = lang.or(Some(value));
1002                }
1003            }
1004
1005            if *name == local_name!("align") {
1006                use style::values::specified::TextAlign;
1007                let keyword = match value {
1008                    "left" => Some(StyloTextAlign::MozLeft),
1009                    "right" => Some(StyloTextAlign::MozRight),
1010                    "center" => Some(StyloTextAlign::MozCenter),
1011                    _ => None,
1012                };
1013
1014                if let Some(keyword) = keyword {
1015                    push_style(PropertyDeclaration::TextAlign(TextAlign::Keyword(keyword)));
1016                }
1017            }
1018
1019            // The width/height attributes on these elements map to the
1020            // corresponding dimension properties (percentages allowed):
1021            // https://html.spec.whatwg.org/multipage/rendering.html#dimRendering
1022            // https://html.spec.whatwg.org/multipage/rendering.html#attributes-for-embedded-content-and-images
1023            let is_width = *name == local_name!("width");
1024            let is_height = *name == local_name!("height");
1025            // The elements whose width/height attributes map to the dimension
1026            // properties. `input` only joins them as type=image, which is the
1027            // one replaced input type.
1028            let is_embedded = *tag == local_name!("iframe")
1029                || *tag == local_name!("embed")
1030                || *tag == local_name!("video")
1031                || *tag == local_name!("object")
1032                || *tag == local_name!("img")
1033                || *tag == local_name!("marquee")
1034                || is_image_input;
1035            let maps_to_dimension = if is_width {
1036                is_embedded
1037                    || *tag == local_name!("table")
1038                    || *tag == local_name!("col")
1039                    || *tag == local_name!("colgroup")
1040                    || *tag == local_name!("tr")
1041                    || *tag == local_name!("td")
1042                    || *tag == local_name!("th")
1043                    || *tag == local_name!("hr")
1044            } else if is_height {
1045                is_embedded
1046                    || *tag == local_name!("table")
1047                    || *tag == local_name!("thead")
1048                    || *tag == local_name!("tbody")
1049                    || *tag == local_name!("tfoot")
1050                    || *tag == local_name!("tr")
1051                    || *tag == local_name!("td")
1052                    || *tag == local_name!("th")
1053            } else {
1054                false
1055            };
1056            if maps_to_dimension {
1057                // Three of these use the "(ignoring zero)" variant of the
1058                // mapping, where a zero is dropped instead of honoured:
1059                // `table width`, and `td`/`th` in both axes.
1060                let is_cell = *tag == local_name!("td") || *tag == local_name!("th");
1061                let ignoring_zero = is_cell || (is_width && *tag == local_name!("table"));
1062                if let Some(size) = parse_size_attr(value, ignoring_zero) {
1063                    use style::values::generics::{NonNegative, length::Size};
1064                    let size = Size::LengthPercentage(NonNegative(size));
1065                    push_style(if is_width {
1066                        PropertyDeclaration::Width(size)
1067                    } else {
1068                        PropertyDeclaration::Height(size)
1069                    });
1070                }
1071            }
1072
1073            // hspace/vspace map to the horizontal and vertical margins as
1074            // dimension properties. This is a *smaller* set than the one that
1075            // takes width/height: `iframe` and `video` take width and height
1076            // but not hspace/vspace, and html/rendering/unmapped-attributes
1077            // checks exactly that -- browsers have got it wrong before.
1078            let takes_spacing = *tag == local_name!("embed")
1079                || *tag == local_name!("img")
1080                || *tag == local_name!("object")
1081                || *tag == local_name!("marquee")
1082                || is_image_input;
1083            if takes_spacing {
1084                let is_hspace = *name == local_name!("hspace");
1085                let is_vspace = *name == local_name!("vspace");
1086                if is_hspace || is_vspace {
1087                    if let Some(size) = parse_size_attr(value, false) {
1088                        use style::values::generics::length::GenericMargin;
1089                        let margin = GenericMargin::LengthPercentage(size);
1090                        if is_hspace {
1091                            push_style(PropertyDeclaration::MarginLeft(margin.clone()));
1092                            push_style(PropertyDeclaration::MarginRight(margin));
1093                        } else {
1094                            push_style(PropertyDeclaration::MarginTop(margin.clone()));
1095                            push_style(PropertyDeclaration::MarginBottom(margin));
1096                        }
1097                    }
1098                }
1099            }
1100
1101            // https://svgwg.org/svg2-draft/geometry.html#Sizing
1102            // The `width` and `height` attributes on an `<svg>` element are
1103            // presentation attributes that map to the CSS `width`/`height`
1104            // properties, so e.g. `width="1em"` must resolve against the
1105            // element's font-size like any other CSS length.
1106            if *tag == local_name!("svg")
1107                && (*name == local_name!("width") || *name == local_name!("height"))
1108            {
1109                if let Some(size) = parse_svg_size_attr(value) {
1110                    use style::values::generics::{NonNegative, length::Size};
1111                    let size = Size::LengthPercentage(NonNegative(size));
1112                    push_style(if *name == local_name!("width") {
1113                        PropertyDeclaration::Width(size)
1114                    } else {
1115                        PropertyDeclaration::Height(size)
1116                    });
1117                }
1118            }
1119
1120            // The `border` attribute maps to the four border widths as a
1121            // pixel length, plus the four border styles as `solid` -- width
1122            // alone would compute back to zero against the default
1123            // border-style of `none`. It is only these three elements:
1124            // `embed`, `iframe`, `marquee` and non-image `input` all have a
1125            // `border` attribute that must stay unmapped.
1126            if *name == local_name!("border")
1127                && (*tag == local_name!("img") || *tag == local_name!("object") || is_image_input)
1128            {
1129                if let Ok(px) = style::servo::attr::parse_unsigned_integer(value.chars()) {
1130                    use style::values::specified::{BorderSideWidth, BorderStyle};
1131                    let width = BorderSideWidth::from_px(px as f32);
1132                    push_style(PropertyDeclaration::BorderTopWidth(width.clone()));
1133                    push_style(PropertyDeclaration::BorderRightWidth(width.clone()));
1134                    push_style(PropertyDeclaration::BorderBottomWidth(width.clone()));
1135                    push_style(PropertyDeclaration::BorderLeftWidth(width));
1136                    push_style(PropertyDeclaration::BorderTopStyle(BorderStyle::Solid));
1137                    push_style(PropertyDeclaration::BorderRightStyle(BorderStyle::Solid));
1138                    push_style(PropertyDeclaration::BorderBottomStyle(BorderStyle::Solid));
1139                    push_style(PropertyDeclaration::BorderLeftStyle(BorderStyle::Solid));
1140                }
1141            }
1142
1143            // `body` carries four legacy margin attributes, as pixel lengths:
1144            // marginwidth and marginheight set both sides of an axis, and
1145            // leftmargin and topmargin set one side each.
1146            //
1147            // There is deliberately no `rightmargin` or `bottommargin`. They
1148            // look like the obvious counterparts to the two that exist, but
1149            // the spec does not define them and browsers ignore them in both
1150            // standards and quirks mode -- body-margin-3a/3b assert exactly
1151            // that.
1152            if *tag == local_name!("body") {
1153                // Matched as strings: these are not in the static atom set,
1154                // so `local_name!` will not compile for them.
1155                let sides: &[u8] = match &**name {
1156                    "marginwidth" => b"lr",
1157                    "marginheight" => b"tb",
1158                    "leftmargin" => b"l",
1159                    "topmargin" => b"t",
1160                    _ => b"",
1161                };
1162                if !sides.is_empty() {
1163                    if let Ok(px) = style::servo::attr::parse_unsigned_integer(value.chars()) {
1164                        use style::values::generics::length::GenericMargin;
1165                        use style::values::specified::{LengthPercentage, NoCalcLength};
1166                        let margin = GenericMargin::LengthPercentage(LengthPercentage::Length(
1167                            NoCalcLength::from_px(px as f32),
1168                        ));
1169                        for side in sides {
1170                            push_style(match side {
1171                                b'l' => PropertyDeclaration::MarginLeft(margin.clone()),
1172                                b'r' => PropertyDeclaration::MarginRight(margin.clone()),
1173                                b't' => PropertyDeclaration::MarginTop(margin.clone()),
1174                                b'b' => PropertyDeclaration::MarginBottom(margin.clone()),
1175                                _ => unreachable!("side table above only yields lrtb"),
1176                            });
1177                        }
1178                    }
1179                }
1180            }
1181
1182            if *name == local_name!("bgcolor") {
1183                use style::values::specified::Color;
1184                if let Some((r, g, b, a)) = parse_color_attr(value) {
1185                    push_style(PropertyDeclaration::BackgroundColor(
1186                        Color::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a)),
1187                    ));
1188                }
1189            }
1190
1191            if *name == local_name!("hidden") {
1192                use style::values::specified::Display;
1193                push_style(PropertyDeclaration::Display(Display::None));
1194            }
1195        }
1196
1197        if let Some(lang) = lang {
1198            push_style(PropertyDeclaration::XLang(XLang(Atom::from(lang))));
1199        }
1200    }
1201
1202    fn local_name(&self) -> &LocalName {
1203        &self.element_data().expect("Not an element").name.local
1204    }
1205
1206    fn namespace(&self) -> &Namespace {
1207        &self.element_data().expect("Not an element").name.ns
1208    }
1209
1210    fn query_container_size(
1211        &self,
1212        _display: &style::values::specified::Display,
1213    ) -> euclid::default::Size2D<Option<app_units::Au>> {
1214        // FIXME: Implement container queries. For now this effectively disables them without panicking.
1215        Default::default()
1216    }
1217
1218    fn each_custom_state<F>(&self, _callback: F)
1219    where
1220        F: FnMut(&AtomIdent),
1221    {
1222        todo!()
1223    }
1224
1225    fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool {
1226        self.selector_flags().get().contains(flags)
1227    }
1228
1229    fn relative_selector_search_direction(&self) -> ElementSelectorFlags {
1230        let flags = self.selector_flags().get();
1231        if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING)
1232        {
1233            ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING
1234        } else if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR)
1235        {
1236            ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
1237        } else if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING) {
1238            ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
1239        } else {
1240            ElementSelectorFlags::empty()
1241        }
1242    }
1243
1244    fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) -> RestyleDamage {
1245        compute_layout_damage(old, new)
1246        // ALL_DAMAGE
1247    }
1248
1249    // fn update_animations(
1250    //     &self,
1251    //     before_change_style: Option<Arc<ComputedValues>>,
1252    //     tasks: style::context::UpdateAnimationsTasks,
1253    // ) {
1254    //     todo!()
1255    // }
1256
1257    // fn process_post_animation(&self, tasks: style::context::PostAnimationTasks) {
1258    //     todo!()
1259    // }
1260
1261    // fn needs_transitions_update(
1262    //     &self,
1263    //     before_change_style: &ComputedValues,
1264    //     after_change_style: &ComputedValues,
1265    // ) -> bool {
1266    //     todo!()
1267    // }
1268}
1269
1270pub struct Traverser<'a> {
1271    // dom: &'a Slab<Node>,
1272    parent: BlitzNode<'a>,
1273    child_index: usize,
1274}
1275
1276impl<'a> Iterator for Traverser<'a> {
1277    type Item = BlitzNode<'a>;
1278
1279    fn next(&mut self) -> Option<Self::Item> {
1280        let node_id = self.parent.children.get(self.child_index)?;
1281        let node = self.parent.with(*node_id);
1282
1283        self.child_index += 1;
1284
1285        Some(node)
1286    }
1287}
1288
1289impl std::hash::Hash for BlitzNode<'_> {
1290    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1291        state.write_u64(self.id.as_u64())
1292    }
1293}
1294
1295/// Handle custom painters like images for layouting
1296///
1297/// todo: actually implement this
1298pub struct RegisteredPaintersImpl;
1299impl RegisteredSpeculativePainters for RegisteredPaintersImpl {
1300    fn get(&self, _name: &Atom) -> Option<&dyn RegisteredSpeculativePainter> {
1301        None
1302    }
1303}
1304
1305use style::traversal::recalc_style_at;
1306
1307pub struct RecalcStyle<'a> {
1308    context: SharedStyleContext<'a>,
1309    /// Nodes whose `background-image`/`mask-image` layers need flushing to
1310    /// dedicated storage on the node (see `flush_image_layers_from_style`)
1311    /// because their style changed during this traversal.
1312    nodes_needing_style_image_flush: Mutex<Vec<NodeId>>,
1313}
1314
1315impl<'a> RecalcStyle<'a> {
1316    pub fn new(context: SharedStyleContext<'a>) -> Self {
1317        RecalcStyle {
1318            context,
1319            nodes_needing_style_image_flush: Mutex::new(Vec::new()),
1320        }
1321    }
1322}
1323
1324#[allow(unsafe_code)]
1325impl<'dom> DomTraversal<BlitzNode<'dom>> for RecalcStyle<'_> {
1326    fn process_preorder<F: FnMut(BlitzNode<'dom>)>(
1327        &self,
1328        context: &mut StyleContext<BlitzNode<'dom>>,
1329        node: BlitzNode<'dom>,
1330        note_child: F,
1331    ) {
1332        if let Some(el) = node.as_element() {
1333            // let mut data = el.mutate_data().unwrap();
1334            let mut data = unsafe { el.ensure_data() };
1335            recalc_style_at(self, context, el, &mut data, note_child);
1336
1337            sync_pseudo_element_styles(el, &data, &self.nodes_needing_style_image_flush);
1338
1339            if !data.damage.is_empty() {
1340                // Mark the ancestor chain so that the damage propagation pass
1341                // visits this element.
1342                el.mark_damaged();
1343
1344                if data
1345                    .styles
1346                    .get_primary()
1347                    .is_some_and(|style| needs_style_image_flush(el, style))
1348                {
1349                    self.nodes_needing_style_image_flush
1350                        .lock()
1351                        .unwrap()
1352                        .push(el.id);
1353                }
1354            }
1355
1356            // Gets set later on
1357            el.unset_dirty_descendants();
1358        }
1359    }
1360
1361    #[inline]
1362    fn needs_postorder_traversal() -> bool {
1363        false
1364    }
1365
1366    fn process_postorder(
1367        &self,
1368        _style_context: &mut StyleContext<BlitzNode<'dom>>,
1369        _node: BlitzNode<'dom>,
1370    ) {
1371        panic!("this should never be called")
1372    }
1373
1374    #[inline]
1375    fn shared_context(&self) -> &SharedStyleContext<'_> {
1376        &self.context
1377    }
1378}
1379
1380/// Flush updated pseudo-element (`::before`/`::after`) styles from the owning
1381/// element's freshly-computed stylo data to the pseudo-element's anonymous node.
1382///
1383/// Pseudo-element styles are normally flushed to the pseudo-element's node
1384/// during box construction (see `flush_pseudo_elements`), but in incremental
1385/// mode box construction only runs for nodes with construction damage.
1386/// Pseudo-element style changes which don't require reconstruction (e.g.
1387/// animations/transitions of repaint- or relayout-only properties) must still
1388/// be flushed to the pseudo-element's node - along with the damage they imply -
1389/// so that layout and paint see the new style.
1390///
1391/// This runs during the style traversal, immediately after the owning element
1392/// has been restyled, because that is where the old and new pseudo styles can
1393/// be diffed precisely (the pseudo node's stored primary style is the "old"
1394/// style; the owner's just-computed pseudo styles are the "new" ones).
1395///
1396/// SAFETY: the style traversal has exclusive access to the tree, and each
1397/// pseudo-element node is only ever accessed from its owning element's
1398/// `process_preorder` (pseudo nodes are not part of the DOM child list and are
1399/// not themselves visited by the traversal), so mutating the pseudo node's
1400/// stylo data here cannot race with any other traversal thread.
1401#[allow(unsafe_code)]
1402fn sync_pseudo_element_styles(
1403    el: &Node,
1404    data: &style::data::ElementData,
1405    nodes_needing_style_image_flush: &Mutex<Vec<NodeId>>,
1406) {
1407    let before_node_id = el.before();
1408    let after_node_id = el.after();
1409    if before_node_id.is_none() && after_node_id.is_none() {
1410        return;
1411    }
1412
1413    // Note: yes these are kinda backwards (see `flush_pseudo_elements`)
1414    let pseudos = data.styles.pseudos.as_array();
1415    let before_style = pseudos[1].clone();
1416    let after_style = pseudos[0].clone();
1417
1418    // Creation and removal of pseudo-elements is handled during box construction
1419    // (Stylo generates construction damage for those cases), so only the case
1420    // where the pseudo-element both was and remains present is handled here.
1421    for (pe_node_id, pe_style) in [(before_node_id, before_style), (after_node_id, after_style)] {
1422        let (Some(pe_node_id), Some(pe_style)) = (pe_node_id, pe_style) else {
1423            continue;
1424        };
1425        let pe_node = el.with(pe_node_id);
1426        let Some(stylo_data) = pe_node.try_stylo_element_data() else {
1427            continue;
1428        };
1429        let mut pe_data = match unsafe { stylo_data.unsafe_stylo_only_mut() } {
1430            Some(data) => data,
1431            None => continue,
1432        };
1433        let Some(old_style) = pe_data.styles.primary.clone() else {
1434            continue;
1435        };
1436        if std::ptr::eq(&*old_style, &*pe_style) {
1437            continue;
1438        }
1439
1440        let diff = RestyleDamage::compute_style_difference::<&Node>(&old_style, &pe_style);
1441        if !diff.damage.is_empty() {
1442            pe_data.damage.insert(diff.damage);
1443            pe_node.mark_damaged();
1444
1445            if needs_style_image_flush(pe_node, &pe_style) {
1446                nodes_needing_style_image_flush
1447                    .lock()
1448                    .unwrap()
1449                    .push(pe_node_id);
1450            }
1451        }
1452        pe_data.styles.primary = Some(pe_style);
1453        pe_data.set_restyled();
1454    }
1455}
1456
1457/// Whether a node's `background-image`/`mask-image` layers need flushing to
1458/// dedicated storage on the node: the URLs referenced by the new style differ
1459/// from the image data currently stored on the node.
1460fn needs_style_image_flush(node: &Node, style: &ComputedValues) -> bool {
1461    use crate::node::ImageResourceData;
1462    use style::url::ComputedUrl;
1463    use style::values::computed::image::Image;
1464
1465    fn out_of_sync(style_images: &[Image], stored: &[Option<ImageResourceData>]) -> bool {
1466        if style_images.len() != stored.len() {
1467            // Differing lengths only matter if either side references an image
1468            // (a style list of `none` layers and empty storage are in sync).
1469            return style_images
1470                .iter()
1471                .any(|image| matches!(image, Image::Url(_)))
1472                || stored.iter().any(Option::is_some);
1473        }
1474        std::iter::zip(style_images, stored).any(|(style_image, stored)| {
1475            match (style_image, stored) {
1476                (Image::Url(ComputedUrl::Valid(url)), Some(data)) => **url != *data.url,
1477                (Image::Url(ComputedUrl::Valid(_)), None) => true,
1478                (_, Some(_)) => true,
1479                (_, None) => false,
1480            }
1481        })
1482    }
1483
1484    node.data.downcast_element().is_some_and(|el| {
1485        out_of_sync(
1486            &style.get_background().background_image.0,
1487            &el.background_images,
1488        ) || out_of_sync(&style.get_svg().mask_image.0, &el.mask_images)
1489    })
1490}
1491
1492#[test]
1493fn assert_size_of_equals() {
1494    // use std::mem;
1495
1496    // fn assert_layout<E>() {
1497    //     assert_eq!(
1498    //         mem::size_of::<SharingCache<E>>(),
1499    //         mem::size_of::<TypelessSharingCache>()
1500    //     );
1501    //     assert_eq!(
1502    //         mem::align_of::<SharingCache<E>>(),
1503    //         mem::align_of::<TypelessSharingCache>()
1504    //     );
1505    // }
1506
1507    // let size = mem::size_of::<StyleSharingCandidate<BlitzNode>>();
1508    // dbg!(size);
1509}
1510
1511#[test]
1512fn parse_inline() {
1513    // let attrs = style::attr::AttrValue::from_serialized_tokenlist(
1514    //     r#"visibility: hidden; left: 1306.5px; top: 50px; display: none;"#.to_string(),
1515    // );
1516
1517    // let val = CSSInlineStyleDeclaration();
1518}