Skip to main content

blitz_dom/node/
node.rs

1use crate::Document;
2use crate::layout::cache::Cache;
3use crate::layout::damage::HoistedPaintChildren;
4use bitflags::bitflags;
5use blitz_traits::events::{
6    BlitzPointerEvent, BlitzPointerId, DomEventData, HitResult, PointerCoords,
7};
8use blitz_traits::node_id::NodeId;
9use blitz_traits::shell::ShellProvider;
10use euclid::{Point2D, Rect, Size2D};
11use html_escape::encode_quoted_attribute_to_string;
12use keyboard_types::Modifiers;
13use kurbo::{Affine, Rect as KurboRect};
14use markup5ever::{LocalName, local_name};
15use parley::{BreakReason, Cluster, ClusterSide, Selection};
16use selectors::matching::ElementSelectorFlags;
17use std::cell::{Cell, RefCell};
18use std::fmt::Write;
19use std::ops::{Deref, Range};
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22use style::Atom;
23use style::computed_values::isolation::T as Isolation;
24use style::invalidation::element::restyle_hints::RestyleHint;
25use style::properties::ComputedValues;
26use style::properties::generated::longhands::position::computed_value::T as Position;
27use style::selector_parser::{PseudoElement, RestyleDamage};
28use style::servo_arc::Arc as ServoArc;
29use style::shared_lock::SharedRwLock;
30use style::stylesheets::UrlExtraData;
31use style::values::computed::CSSPixelLength;
32use style::values::computed::Display as StyloDisplay;
33use style::values::computed::Rotate;
34use style::values::generics::transform::{Scale, Translate};
35use style::values::specified::box_::{DisplayInside, DisplayOutside};
36use style_dom::ElementState;
37use style_traits::values::ToCss;
38use taffy::prelude::{Layout, Style};
39use thin_vec::ThinVec;
40
41use super::stylo_data::StyloData;
42use super::{Attribute, DocumentData, ElementData};
43
44#[derive(Clone, Copy)]
45enum OutputStyle {
46    Normal,
47    Pretty,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum DisplayOuter {
52    Block,
53    Inline,
54    None,
55}
56
57bitflags! {
58    #[derive(Clone, Copy, PartialEq)]
59    pub struct NodeFlags: u32 {
60        /// Whether the node is the root node of an Inline Formatting Context
61        const IS_INLINE_ROOT = 0b00000001;
62        /// Whether the node is the root node of an Table formatting context
63        const IS_TABLE_ROOT = 0b00000010;
64        /// Whether the node is "in the document" (~= has a parent and isn't a template node)
65        const IS_IN_DOCUMENT = 0b00000100;
66        /// Parser-created scripts remain inert after cloning or adoption.
67        const IS_PARSER_INERT_SCRIPT = 0b00001000;
68    }
69}
70
71impl NodeFlags {
72    #[inline(always)]
73    pub fn is_inline_root(&self) -> bool {
74        self.contains(Self::IS_INLINE_ROOT)
75    }
76
77    #[inline(always)]
78    pub fn is_table_root(&self) -> bool {
79        self.contains(Self::IS_TABLE_ROOT)
80    }
81
82    #[inline(always)]
83    pub fn is_in_document(&self) -> bool {
84        self.contains(Self::IS_IN_DOCUMENT)
85    }
86
87    #[inline(always)]
88    pub fn reset_construction_flags(&mut self) {
89        self.remove(Self::IS_INLINE_ROOT);
90        self.remove(Self::IS_TABLE_ROOT);
91    }
92}
93
94pub struct Node {
95    // The actual tree we belong to. This is unsafe!!
96    tree: *mut crate::NodeTree,
97
98    /// Our Id
99    pub id: NodeId,
100    /// Our parent's ID
101    pub parent: Option<NodeId>,
102    /// A validated hint into the parent's DOM child list.
103    ///
104    /// Adjacent traversal seeds the next node's hint, avoiding repeated linear
105    /// searches while walking an unchanged sibling list. Mutations need not
106    /// maintain this field: readers always validate it against the child list.
107    child_index_hint: Cell<usize>,
108    /// The owning document, including when this node is detached.
109    /// Document nodes themselves have no owner.
110    pub owner_document: Option<NodeId>,
111    /// Non-rendering document syntax, stored independently of layout data.
112    pub markup: Option<Arc<MarkupNode>>,
113    /// Script custom element candidates in this shadow-including subtree.
114    pub custom_element_subtree_count: usize,
115    // What are our children?
116    pub children: ThinVec<NodeId>,
117    /// Our parent in the layout hierachy: a separate list that includes anonymous collections of inline elements
118    pub layout_parent: Cell<Option<NodeId>>,
119    /// A separate child list that includes anonymous collections of inline elements
120    pub layout_children: RefCell<Option<ThinVec<NodeId>>>,
121    /// Anonymous block boxes created for this node during layout construction.
122    ///
123    /// Anonymous blocks live only in the slab (they are not part of the DOM
124    /// `children` list), so we track the ones we own here to be able to
125    /// deallocate them when this node is reconstructed.
126    pub anonymous_blocks: ThinVec<NodeId>,
127    /// The same as layout_children, but sorted by z-index
128    pub paint_children: RefCell<Option<ThinVec<NodeId>>>,
129    pub stacking_context: Option<Box<HoistedPaintChildren>>,
130
131    /// The "flattened tree" children of this node used for layout and painting,
132    /// if it differs from [`children`](Self::children). This is set for shadow
133    /// hosts (where it holds the shadow root's children) and `<slot>` elements
134    /// (where it holds the light-DOM nodes assigned to the slot). When `None`,
135    /// [`children`](Self::children) is used directly.
136    #[cfg(feature = "shadow-dom")]
137    pub flattened_children: Option<Vec<NodeId>>,
138    /// Slot assignment belongs to slottable nodes, including text nodes.
139    #[cfg(feature = "shadow-dom")]
140    pub assigned_slot: Option<NodeId>,
141    /// Present only on slots. An empty list is distinct from fallback children.
142    #[cfg(feature = "shadow-dom")]
143    pub assigned_nodes: Option<Vec<NodeId>>,
144    pub(crate) children_to_process: std::sync::atomic::AtomicIsize,
145
146    // Flags
147    pub flags: NodeFlags,
148
149    /// Node type (Element, TextNode, etc) specific data.
150    ///
151    /// For element nodes this holds the [`ElementData`], which stores most of
152    /// the per-node style/layout state. For the document node it holds the
153    /// [`DocumentData`]. Access the moved fields through the forwarding methods
154    /// on [`Node`] (e.g. [`Node::style`], [`Node::final_layout`]).
155    pub data: NodeData,
156}
157
158unsafe impl Send for Node {}
159unsafe impl Sync for Node {}
160
161/// Generates forwarding accessors for fields that live on both [`ElementData`]
162/// (element / anonymous block nodes) and [`DocumentData`] (the document node).
163macro_rules! universal_accessors {
164    ($($(#[$meta:meta])* $field:ident / $field_mut:ident : $ty:ty),* $(,)?) => {
165        impl Node {
166            $(
167                $(#[$meta])*
168                #[inline]
169                pub fn $field(&self) -> &$ty {
170                    match &self.data {
171                        NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.$field,
172                        NodeData::Document(data) => &data.$field,
173                        _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
174                    }
175                }
176
177                $(#[$meta])*
178                #[inline]
179                pub fn $field_mut(&mut self) -> &mut $ty {
180                    match &mut self.data {
181                        NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.$field,
182                        NodeData::Document(data) => &mut data.$field,
183                        _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
184                    }
185                }
186            )*
187        }
188    };
189}
190
191universal_accessors! {
192    stylo_element_data / stylo_element_data_mut: StyloData,
193    style / style_mut: Style<Atom>,
194    style_source / style_source_mut: Option<ServoArc<ComputedValues>>,
195    subtree_hoists / subtree_hoists_mut: bool,
196    // `cache` is not here: it is stored as `Option<Box<Cache>>` and reached
197    // through hand-written accessors below, because the read side has to be
198    // able to answer without allocating. See `ElementData::cache`.
199    unrounded_layout / unrounded_layout_mut: Layout,
200    final_layout / final_layout_mut: Layout,
201    scroll_offset / scroll_offset_mut: crate::Point<f64>,
202    scrollable_overflow / scrollable_overflow_mut: KurboRect,
203    transform / transform_mut: Option<Affine>,
204    display_constructed_as / display_constructed_as_mut: StyloDisplay,
205    // The document node is styled/snapshotted like an element, so it also
206    // carries these:
207    element_state / element_state_mut: ElementState,
208    snapshot_handled / snapshot_handled_mut: AtomicBool,
209    // `apply_selector_flags` deposits `for_parent()` flags on the parent node,
210    // and the parent of the root <html> element is the document -- so the
211    // document has to be able to hold selector flags too.
212
213}
214
215impl Node {
216    pub fn selector_flags(&self) -> &Cell<ElementSelectorFlags> {
217        match &self.data {
218            NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.selector_flags,
219            NodeData::Document(data) => &data.selector_flags,
220            NodeData::ShadowRoot(data) => &data.selector_flags,
221            _ => panic!("selector flags require a parent node"),
222        }
223    }
224
225    pub fn selector_flags_mut(&mut self) -> &mut Cell<ElementSelectorFlags> {
226        match &mut self.data {
227            NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.selector_flags,
228            NodeData::Document(data) => &mut data.selector_flags,
229            NodeData::ShadowRoot(data) => &mut data.selector_flags,
230            _ => panic!("selector flags require a parent node"),
231        }
232    }
233
234    /// This node's taffy layout cache.
235    ///
236    /// Hand-written rather than generated by `universal_accessors!` because
237    /// the cache is `Option<Box<Cache>>`: a node that has never been laid out
238    /// borrows a shared empty one instead of owning 1616 bytes. See the
239    /// [`cache`](super::ElementData::cache) field for the measurement behind
240    /// that. The signature is unchanged, so callers cannot tell.
241    #[inline]
242    pub fn cache(&self) -> &Cache {
243        match &self.data {
244            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache(),
245            NodeData::Document(data) => data.cache(),
246            _ => panic!("`cache` is not available on this node kind"),
247        }
248    }
249
250    /// This node's taffy layout cache, allocating on first use.
251    #[inline]
252    pub fn cache_mut(&mut self) -> &mut Cache {
253        match &mut self.data {
254            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_mut(),
255            NodeData::Document(data) => data.cache_mut(),
256            _ => panic!("`cache_mut` is not available on this node kind"),
257        }
258    }
259
260    /// Release this node's layout cache, returning its memory.
261    #[inline]
262    pub fn cache_release(&mut self) {
263        match &mut self.data {
264            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_release(),
265            NodeData::Document(data) => data.cache_release(),
266            _ => {}
267        }
268    }
269
270    /// Style data from stylo, if this node kind carries it (element or document
271    /// nodes). Returns `None` for text/comment nodes.
272    /// The computed values the cached taffy style was built from, for node
273    /// kinds that carry one. `None` for text and comment nodes, which are never
274    /// styled, so a caller can ask without knowing the kind.
275    #[inline]
276    pub fn style_source_opt(&self) -> Option<&ServoArc<ComputedValues>> {
277        match &self.data {
278            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.style_source.as_ref(),
279            NodeData::Document(data) => data.style_source.as_ref(),
280            _ => None,
281        }
282    }
283
284    #[inline]
285    pub fn stylo_element_data_opt(&self) -> Option<&StyloData> {
286        match &self.data {
287            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
288                Some(&data.stylo_element_data)
289            }
290            NodeData::Document(data) => Some(&data.stylo_element_data),
291            _ => None,
292        }
293    }
294
295    #[inline]
296    pub fn stylo_element_data_opt_mut(&mut self) -> Option<&mut StyloData> {
297        match &mut self.data {
298            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
299                Some(&mut data.stylo_element_data)
300            }
301            NodeData::Document(data) => Some(&mut data.stylo_element_data),
302            _ => None,
303        }
304    }
305
306    /// The `dirty_descendants` flag, if this node kind carries it (element or
307    /// document nodes). Returns `None` for text/comment nodes.
308    #[inline]
309    fn dirty_descendants_flag(&self) -> Option<&AtomicBool> {
310        match &self.data {
311            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
312                Some(&data.dirty_descendants)
313            }
314            NodeData::Document(data) => Some(&data.dirty_descendants),
315            _ => None,
316        }
317    }
318
319    /// The document's shared style lock. Only available on element and
320    /// document nodes.
321    #[inline]
322    pub fn guard(&self) -> &SharedRwLock {
323        let guard = match &self.data {
324            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.guard.as_ref(),
325            NodeData::Document(data) => data.guard.as_ref(),
326            _ => None,
327        };
328        guard.expect("`guard` is not available on this node kind")
329    }
330
331    #[inline]
332    pub fn has_snapshot(&self) -> bool {
333        match &self.data {
334            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot,
335            NodeData::Document(data) => data.has_snapshot,
336            _ => false,
337        }
338    }
339
340    #[inline]
341    pub fn set_has_snapshot(&mut self, value: bool) {
342        match &mut self.data {
343            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot = value,
344            NodeData::Document(data) => data.has_snapshot = value,
345            _ => {}
346        }
347    }
348
349    #[inline]
350    pub fn before(&self) -> Option<NodeId> {
351        self.element_data().and_then(|data| data.before)
352    }
353
354    #[inline]
355    pub fn after(&self) -> Option<NodeId> {
356        self.element_data().and_then(|data| data.after)
357    }
358}
359
360impl Node {
361    pub(crate) fn new(
362        tree: *mut crate::NodeTree,
363        id: NodeId,
364        guard: SharedRwLock,
365        mut data: NodeData,
366    ) -> Self {
367        // Store a handle to the document's shared style lock on the node data.
368        // Both element and document nodes are styled by stylo and so need it.
369        match &mut data {
370            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
371                data.guard = Some(guard);
372            }
373            NodeData::Document(data) => data.guard = Some(guard),
374            _ => {}
375        }
376
377        Self {
378            tree,
379
380            id,
381            parent: None,
382            child_index_hint: Cell::new(0),
383            owner_document: None,
384            markup: None,
385            custom_element_subtree_count: 0,
386            children: ThinVec::new(),
387            layout_parent: Cell::new(None),
388            layout_children: RefCell::new(None),
389            anonymous_blocks: ThinVec::new(),
390            paint_children: RefCell::new(None),
391            stacking_context: None,
392            #[cfg(feature = "shadow-dom")]
393            flattened_children: None,
394            #[cfg(feature = "shadow-dom")]
395            assigned_slot: None,
396            #[cfg(feature = "shadow-dom")]
397            assigned_nodes: None,
398            children_to_process: std::sync::atomic::AtomicIsize::new(0),
399
400            flags: NodeFlags::empty(),
401            data,
402        }
403    }
404
405    pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
406        let transform = self.primary_styles().and_then(|s| {
407            let size = self.final_layout().size;
408            let reference_box = Rect::new(
409                Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
410                Size2D::new(
411                    CSSPixelLength::new(size.width),
412                    CSSPixelLength::new(size.height),
413                ),
414            );
415            // Resolve the transform in CSS pixels, then convert it to device-pixel space
416            // (S * T * S^-1): translation components are scaled, linear components are not.
417            crate::resolve_2d_transform(s.get_box(), reference_box).map(|t| {
418                let scale = scale as f64;
419                let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
420                Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
421            })
422        });
423
424        *self.transform_mut() = transform;
425        transform
426    }
427
428    pub fn pe_by_index(&self, index: usize) -> Option<NodeId> {
429        match index {
430            0 => self.after(),
431            1 => self.before(),
432            _ => panic!("Invalid pseudo element index"),
433        }
434    }
435
436    pub fn set_pe_by_index(&mut self, index: usize, value: Option<NodeId>) {
437        let Some(data) = self.element_data_mut() else {
438            return;
439        };
440        match index {
441            0 => data.after = value,
442            1 => data.before = value,
443            _ => panic!("Invalid pseudo element index"),
444        }
445    }
446
447    pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
448        Some(self.primary_styles().as_ref()?.clone_display())
449    }
450
451    /// A compact computed-style view for renderer diagnostics.
452    /// Computed values for the properties a page is most likely to read.
453    ///
454    /// Deliberately a fixed set rather than every property stylo computes.
455    /// `getComputedStyle` in a real browser answers for all of them, and this
456    /// does not; a caller asking for something outside this list gets an empty
457    /// string, which is what an unsupported property returns anyway.
458    ///
459    /// The alternative was to keep throwing `getComputedStyle is not defined`,
460    /// which stops the whole script. Measured over a hundred-site corpus, three
461    /// sites died on exactly that. A page that reads `display` and gets the
462    /// right answer is strictly better off, and one that reads something absent
463    /// is no worse off than before.
464    ///
465    /// Grow the list when a page is found needing more. It is cheap: every
466    /// entry is one `clone_*` call stylo already provides.
467    pub fn computed_style_properties(&self) -> Option<Vec<(&'static str, String)>> {
468        let style = self.primary_styles()?;
469        let layout = *self.final_layout();
470        let (used_width, used_height) = if matches!(
471            style.clone_box_sizing(),
472            style::computed_values::box_sizing::T::BorderBox
473        ) {
474            (layout.size.width, layout.size.height)
475        } else {
476            (
477                (layout.size.width
478                    - layout.padding.left
479                    - layout.padding.right
480                    - layout.border.left
481                    - layout.border.right)
482                    .max(0.0),
483                (layout.size.height
484                    - layout.padding.top
485                    - layout.padding.bottom
486                    - layout.border.top
487                    - layout.border.bottom)
488                    .max(0.0),
489            )
490        };
491        Some(vec![
492            ("display", style.clone_display().to_css_string()),
493            ("position", style.clone_position().to_css_string()),
494            ("visibility", style.clone_visibility().to_css_string()),
495            ("opacity", style.clone_opacity().to_css_string()),
496            ("color", style.clone_color().to_css_string()),
497            (
498                "background-color",
499                style.clone_background_color().to_css_string(),
500            ),
501            (
502                "font-size",
503                format!("{}px", style.clone_font_size().computed_size().px()),
504            ),
505            ("font-weight", style.clone_font_weight().to_css_string()),
506            ("font-style", style.clone_font_style().to_css_string()),
507            ("font-family", style.clone_font_family().to_css_string()),
508            ("text-align", style.clone_text_align().to_css_string()),
509            ("overflow-x", style.clone_overflow_x().to_css_string()),
510            ("overflow-y", style.clone_overflow_y().to_css_string()),
511            ("z-index", style.clone_z_index().to_css_string()),
512            ("box-sizing", style.clone_box_sizing().to_css_string()),
513            (
514                "flex-direction",
515                style.clone_flex_direction().to_css_string(),
516            ),
517            (
518                "justify-content",
519                style.clone_justify_content().to_css_string(),
520            ),
521            ("align-items", style.clone_align_items().to_css_string()),
522            // The used values, not the specified ones. A page reading `width`
523            // off `getComputedStyle` is nearly always asking how wide the box
524            // ended up, and `auto` is not an answer it can use.
525            //
526            // Which box follows `box-sizing`, as in a browser: the content box
527            // unless the element is `border-box`. Reporting the border box for
528            // every element made `width - paddingLeft - paddingRight` (the
529            // usual content-width arithmetic) subtract the padding twice.
530            ("width", format!("{}px", used_width)),
531            ("height", format!("{}px", used_height)),
532            // The box model's edges, used values in px. Absent, a read of
533            // `paddingLeft` came back `undefined`, and `parseFloat(undefined)`
534            // is `NaN`, which then travelled silently into whatever position or
535            // size the page was computing: measuring code that subtracts
536            // padding or margins from a box placed things at NaN.
537            ("padding-top", format!("{}px", layout.padding.top)),
538            ("padding-right", format!("{}px", layout.padding.right)),
539            ("padding-bottom", format!("{}px", layout.padding.bottom)),
540            ("padding-left", format!("{}px", layout.padding.left)),
541            ("border-top-width", format!("{}px", layout.border.top)),
542            ("border-right-width", format!("{}px", layout.border.right)),
543            ("border-bottom-width", format!("{}px", layout.border.bottom)),
544            ("border-left-width", format!("{}px", layout.border.left)),
545            ("margin-top", format!("{}px", layout.margin.top)),
546            ("margin-right", format!("{}px", layout.margin.right)),
547            ("margin-bottom", format!("{}px", layout.margin.bottom)),
548            ("margin-left", format!("{}px", layout.margin.left)),
549        ])
550    }
551
552    pub fn diagnostic_computed_style(&self) -> Option<Vec<(&'static str, String)>> {
553        let style = self.primary_styles()?;
554        Some(vec![
555            ("display", style.clone_display().to_css_string()),
556            ("color", style.clone_color().to_css_string()),
557            (
558                "background-color",
559                style.clone_background_color().to_css_string(),
560            ),
561            (
562                "font-size",
563                format!("{}px", style.clone_font_size().computed_size().px()),
564            ),
565            ("width", style.clone_width().to_css_string()),
566        ])
567    }
568
569    /// Whether computed style removes this node from layout.
570    pub fn is_display_none(&self) -> bool {
571        self.display_style()
572            .is_some_and(|display| display.is_none())
573    }
574
575    pub fn is_or_contains_block(&self) -> bool {
576        let style = self.primary_styles();
577        let style = style.as_ref();
578
579        // Ignore out-of-flow items
580        let position = style
581            .map(|s| s.clone_position())
582            .unwrap_or(Position::Relative);
583        let is_in_flow = matches!(
584            position,
585            Position::Static | Position::Relative | Position::Sticky
586        );
587        if !is_in_flow {
588            return false;
589        }
590        // Floated boxes do not break up the inline flow: they participate in the
591        // inline formatting context as out-of-flow inline boxes
592        let is_floating = style
593            .map(|s| s.clone_float().is_floating())
594            .unwrap_or(false);
595        if is_floating {
596            return false;
597        }
598        let display = style
599            .map(|s| s.clone_display())
600            .unwrap_or(StyloDisplay::inline());
601        match display.outside() {
602            DisplayOutside::None => false,
603            DisplayOutside::Block => true,
604            _ => {
605                if display.inside() == DisplayInside::Flow {
606                    self.children
607                        .iter()
608                        .copied()
609                        .any(|child_id| self.tree()[child_id].is_or_contains_block())
610                } else {
611                    false
612                }
613            }
614        }
615    }
616
617    pub fn is_whitespace_node(&self) -> bool {
618        match &self.data {
619            NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
620            _ => false,
621        }
622    }
623
624    pub fn is_focussable(&self) -> bool {
625        self.data
626            .downcast_element()
627            .map(|el| el.is_focussable)
628            .unwrap_or(false)
629    }
630
631    pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
632        if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
633            // A new shadow host or slot can acquire descendants that already
634            // have styles. Preserve the subtree hint before its first style
635            // traversal so those descendants also rematch their rules.
636            let mut element_data = stylo_element_data.ensure_init_mut();
637            element_data.hint.insert(hint);
638        }
639        // Mark all ancestors as having dirty descendants so the style traversal
640        // will visit this node's subtree
641        self.mark_ancestors_dirty();
642    }
643
644    /// Returns whether this node has any descendants that need restyling.
645    pub fn has_dirty_descendants(&self) -> bool {
646        self.dirty_descendants_flag()
647            .is_some_and(|flag| flag.load(Ordering::Relaxed))
648    }
649
650    /// Sets the dirty_descendants flag on this node.
651    pub fn set_dirty_descendants(&self) {
652        if let Some(flag) = self.dirty_descendants_flag() {
653            flag.store(true, Ordering::Relaxed);
654        }
655    }
656
657    /// Clears the dirty_descendants flag on this node.
658    pub fn unset_dirty_descendants(&self) {
659        if let Some(flag) = self.dirty_descendants_flag() {
660            flag.store(false, Ordering::Relaxed);
661        }
662    }
663
664    /// Set appropriate damage for Stylo when an element's style attribute is updated
665    pub(crate) fn mark_style_attr_updated(&mut self) {
666        if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
667            if let Some(mut data) = stylo_element_data.get_mut() {
668                data.hint |= RestyleHint::RESTYLE_STYLE_ATTRIBUTE;
669            }
670        }
671        self.set_dirty_descendants();
672        self.mark_ancestors_dirty();
673    }
674
675    /// Marks all ancestors of this node as having dirty descendants.
676    /// This propagates the dirty flag up the tree so that the style traversal
677    /// knows to visit the subtree containing this node.
678    pub fn mark_ancestors_dirty(&self) {
679        // Mutating a shadow root must dirty its host even though the root
680        // itself is omitted from the flattened tree.
681        let mut current = self
682            .shadow_root_data()
683            .map(|root| self.with(root.host))
684            .or_else(|| self.flattened_parent());
685
686        while let Some(parent) = current {
687            if let Some(flag) = parent.dirty_descendants_flag() {
688                // New elements start dirty, and insertion or slot assignment
689                // can change the ancestry of an already-dirty element. Its
690                // flag does not prove that this ancestor chain is dirty.
691                flag.store(true, Ordering::Relaxed);
692            }
693            current = parent.flattened_parent();
694        }
695    }
696
697    // pub fn damage_mut(&mut self) -> Option<&mut RestyleDamage> {
698    //     self.stylo_element_data
699    //         .get_mut()
700    //         .map(|mut data: ElementDataMut<'a>| &'a mut data.damage)
701    // }
702
703    pub fn damage(&self) -> Option<RestyleDamage> {
704        self.stylo_element_data_opt()
705            .and_then(|stylo| stylo.get().map(|data| data.damage))
706    }
707
708    pub fn set_damage(&mut self, damage: RestyleDamage) {
709        if let Some(stylo) = self.stylo_element_data_opt_mut() {
710            if let Some(mut data) = stylo.get_mut() {
711                data.damage = damage;
712            }
713        }
714    }
715
716    pub fn insert_damage(&mut self, damage: RestyleDamage) {
717        if let Some(stylo) = self.stylo_element_data_opt_mut() {
718            if let Some(mut data) = stylo.get_mut() {
719                data.damage |= damage;
720            }
721        }
722    }
723
724    pub fn remove_damage(&mut self, damage: RestyleDamage) {
725        if let Some(stylo) = self.stylo_element_data_opt_mut() {
726            if let Some(mut data) = stylo.get_mut() {
727                data.damage.remove(damage);
728            }
729        }
730    }
731
732    pub fn clear_damage_mut(&mut self) {
733        if let Some(stylo) = self.stylo_element_data_opt_mut() {
734            if let Some(mut data) = stylo.get_mut() {
735                data.damage = RestyleDamage::empty();
736            }
737        }
738    }
739
740    pub fn hover(&mut self) {
741        if let Some(data) = self.element_data_mut() {
742            data.element_state.insert(ElementState::HOVER);
743        }
744        self.set_restyle_hint(RestyleHint::restyle_subtree());
745    }
746
747    pub fn unhover(&mut self) {
748        if let Some(data) = self.element_data_mut() {
749            data.element_state.remove(ElementState::HOVER);
750        }
751        self.set_restyle_hint(RestyleHint::restyle_subtree());
752    }
753
754    pub fn is_hovered(&self) -> bool {
755        self.element_data()
756            .is_some_and(|data| data.element_state.contains(ElementState::HOVER))
757    }
758
759    pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
760        if let Some(data) = self.element_data_mut() {
761            data.element_state
762                .insert(ElementState::FOCUS | ElementState::FOCUSRING);
763        }
764        self.set_restyle_hint(RestyleHint::restyle_subtree());
765
766        // If focussing a text input, enable IME and set IME area
767        if self
768            .element_data()
769            .and_then(|elem| elem.text_input_data())
770            .is_some()
771        {
772            shell_provider.set_ime_enabled(true);
773            let mut pos = self.absolute_position(0.0, 0.0);
774            pos.x += self.final_layout().content_box_x();
775            pos.y += self.final_layout().content_box_y();
776            let width = self.final_layout().content_box_width();
777            let height = self.final_layout().content_box_height();
778            shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
779        }
780    }
781
782    pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
783        if let Some(data) = self.element_data_mut() {
784            data.element_state
785                .remove(ElementState::FOCUS | ElementState::FOCUSRING);
786        }
787        self.set_restyle_hint(RestyleHint::restyle_subtree());
788
789        // If blurring a text input, disable IME
790        if self
791            .element_data()
792            .and_then(|elem| elem.text_input_data())
793            .is_some()
794        {
795            shell_provider.set_ime_enabled(false);
796        }
797    }
798
799    pub fn is_focussed(&self) -> bool {
800        self.element_data()
801            .is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
802    }
803
804    pub fn active(&mut self) {
805        if let Some(data) = self.element_data_mut() {
806            data.element_state.insert(ElementState::ACTIVE);
807        }
808        self.set_restyle_hint(RestyleHint::restyle_subtree());
809    }
810
811    pub fn unactive(&mut self) {
812        if let Some(data) = self.element_data_mut() {
813            data.element_state.remove(ElementState::ACTIVE);
814        }
815        self.set_restyle_hint(RestyleHint::restyle_subtree());
816    }
817
818    pub fn is_active(&self) -> bool {
819        self.element_data()
820            .is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
821    }
822
823    // Marks the node as disabled if it can be.
824    // It does not disable any children which should be disabled as well (relevant for the `select` element).
825    pub fn disable(&mut self) {
826        if let Some(data) = self.element_data_mut() {
827            if data.can_be_disabled() {
828                data.element_state.insert(ElementState::DISABLED);
829                data.element_state.remove(ElementState::ENABLED);
830            }
831        }
832        self.set_restyle_hint(RestyleHint::restyle_subtree());
833    }
834
835    // Marks the node as enabled if it can be.
836    // It does not enable any children which should be enabled as well (relevant for the `select` element).
837    pub fn enable(&mut self) {
838        if let Some(data) = self.element_data_mut() {
839            if data.can_be_disabled() {
840                data.element_state.insert(ElementState::ENABLED);
841                data.element_state.remove(ElementState::DISABLED);
842            }
843        }
844        self.set_restyle_hint(RestyleHint::restyle_subtree());
845    }
846
847    pub fn subdoc(&self) -> Option<&dyn Document> {
848        self.element_data().and_then(|el| el.sub_doc_data())
849    }
850
851    pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
852        self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
853    }
854
855    pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
856        // For single-line inputs, add an offset to vertically center the text input layout
857        // within the content box of it's node.
858        if let Some(input_data) = self
859            .data
860            .downcast_element()
861            .and_then(|el| el.text_input_data())
862        {
863            if !input_data.is_multiline {
864                let content_box_height = self.final_layout().content_box_height();
865                let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
866                let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
867
868                return y_offset as f64;
869            }
870        }
871
872        0.0
873    }
874}
875
876#[derive(Debug, Clone, Copy, PartialEq)]
877pub enum NodeKind {
878    Document,
879    Element,
880    AnonymousBlock,
881    Text,
882    Comment,
883    DocumentFragment,
884    ShadowRoot,
885}
886
887/// How much text one click selects.
888///
889/// Click count decides: a second click takes the word, a third takes the line,
890/// matching what a text input does and what every other platform does with the
891/// same gesture.
892#[derive(Debug, Clone, Copy, PartialEq, Eq)]
893pub enum TextGranularity {
894    /// The word under the pointer, by Unicode word segmentation.
895    Word,
896    /// The whole hard line, so a soft-wrapped paragraph selects entire.
897    Line,
898}
899
900impl TextGranularity {
901    /// The granularity a click of this count selects, or `None` for a first
902    /// click, which places a caret rather than selecting anything.
903    pub fn from_click_count(count: u16) -> Option<Self> {
904        match count {
905            0 | 1 => None,
906            2 => Some(Self::Word),
907            _ => Some(Self::Line),
908        }
909    }
910}
911
912/// The encapsulation mode of a shadow root.
913///
914/// Mirrors the `ShadowRootMode` enum from the DOM specification.
915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916pub enum ShadowRootMode {
917    /// Elements of the shadow root are accessible from JavaScript outside the
918    /// root (e.g. via `Element.shadowRoot`).
919    Open,
920    /// Elements of the shadow root are not accessible from JavaScript outside
921    /// the root.
922    Closed,
923}
924
925/// Data associated with a [`NodeData::ShadowRoot`] node.
926///
927/// A shadow root is a non-element, non-document node that acts as the root of a
928/// shadow tree. Its `children` (on the owning [`Node`]) are the top-level nodes
929/// of the shadow tree. The light-DOM children of the host element are
930/// distributed into any `<slot>` elements within this tree to form the
931/// "flattened tree" that is used for style resolution, layout and painting.
932#[derive(Clone)]
933pub struct ShadowRootData {
934    /// The node id of the host element that this shadow root is attached to.
935    pub host: NodeId,
936    /// The encapsulation mode of this shadow root.
937    pub mode: ShadowRootMode,
938    /// Set when the root was created from a declarative `<template>`.
939    pub declarative: bool,
940    /// When set, `cloneNode` copies this shadow root.
941    pub clonable: bool,
942    /// Stored for `ShadowRoot.serializable`. Serialization is separate.
943    pub serializable: bool,
944    /// Stored for `ShadowRoot.delegatesFocus`.
945    pub delegates_focus: bool,
946    /// Stylesheet owners in shadow-tree order.
947    pub stylesheet_nodes: Vec<NodeId>,
948    pub(crate) cascade_data: Option<ServoArc<style::stylist::CascadeData>>,
949    pub(crate) cascade_dirty: bool,
950    pub(crate) slots: Vec<NodeId>,
951    #[cfg(feature = "shadow-dom")]
952    pub(crate) slottables: Vec<NodeId>,
953    pub(crate) selector_flags: Cell<ElementSelectorFlags>,
954}
955
956impl std::fmt::Debug for ShadowRootData {
957    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
958        formatter
959            .debug_struct("ShadowRootData")
960            .field("host", &self.host)
961            .field("mode", &self.mode)
962            .field("declarative", &self.declarative)
963            .field("clonable", &self.clonable)
964            .field("serializable", &self.serializable)
965            .field("delegates_focus", &self.delegates_focus)
966            .field("stylesheet_nodes", &self.stylesheet_nodes)
967            .field("cascade_dirty", &self.cascade_dirty)
968            .field("slots", &self.slots)
969            .finish()
970    }
971}
972
973impl ShadowRootData {
974    pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
975        Self {
976            host,
977            mode,
978            declarative: false,
979            clonable: false,
980            serializable: false,
981            delegates_focus: false,
982            stylesheet_nodes: Vec::new(),
983            cascade_data: None,
984            cascade_dirty: true,
985            slots: Vec::new(),
986            #[cfg(feature = "shadow-dom")]
987            slottables: Vec::new(),
988            selector_flags: Cell::new(ElementSelectorFlags::empty()),
989        }
990    }
991}
992
993/// Document syntax that generates no layout box.
994#[derive(Debug, PartialEq, Eq)]
995pub enum MarkupNode {
996    ProcessingInstruction {
997        target: String,
998    },
999    Doctype {
1000        name: String,
1001        public_id: String,
1002        system_id: String,
1003    },
1004}
1005
1006/// The different kinds of nodes in the DOM.
1007#[derive(Debug, Clone)]
1008pub enum NodeData {
1009    /// The `Document` itself - the root node of a HTML document.
1010    Document(Box<DocumentData>),
1011
1012    /// An element with attributes.
1013    Element(Box<ElementData>),
1014
1015    /// An anonymous block box
1016    AnonymousBlock(Box<ElementData>),
1017
1018    /// A text node.
1019    Text(TextNodeData),
1020
1021    /// A comment.
1022    Comment {
1023        /// The textual content of the comment
1024        contents: String,
1025    },
1026
1027    /// A `DocumentFragment`: a parentless container whose children move when
1028    /// it is inserted, and which never appears in the box tree itself.
1029    DocumentFragment,
1030
1031    /// The root of a shadow tree attached to a host element.
1032    ShadowRoot(ShadowRootData),
1033    // /// A `DOCTYPE` with name, public id, and system id. See
1034    // /// [document type declaration on wikipedia][https://en.wikipedia.org/wiki/Document_type_declaration]
1035    // Doctype { name: String, public_id: String, system_id: String },
1036
1037    // /// A Processing instruction.
1038    // ProcessingInstruction { target: String, contents: String },
1039}
1040
1041impl NodeData {
1042    pub fn downcast_element(&self) -> Option<&ElementData> {
1043        match self {
1044            Self::Element(data) => Some(data),
1045            Self::AnonymousBlock(data) => Some(data),
1046            _ => None,
1047        }
1048    }
1049
1050    pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
1051        match self {
1052            Self::Element(data) => Some(data),
1053            Self::AnonymousBlock(data) => Some(data),
1054            _ => None,
1055        }
1056    }
1057
1058    pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
1059        let Some(elem) = self.downcast_element() else {
1060            return false;
1061        };
1062        *name == elem.name.local
1063    }
1064
1065    pub fn attrs(&self) -> Option<&[Attribute]> {
1066        Some(&self.downcast_element()?.attrs)
1067    }
1068
1069    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
1070        self.downcast_element()?.attr(name)
1071    }
1072
1073    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
1074        self.downcast_element()
1075            .is_some_and(|elem| elem.has_attr(name))
1076    }
1077
1078    pub fn kind(&self) -> NodeKind {
1079        match self {
1080            NodeData::Document(_) => NodeKind::Document,
1081            NodeData::Element(_) => NodeKind::Element,
1082            NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
1083            NodeData::Text(_) => NodeKind::Text,
1084            NodeData::Comment { .. } => NodeKind::Comment,
1085            NodeData::DocumentFragment => NodeKind::DocumentFragment,
1086            NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
1087        }
1088    }
1089
1090    pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1091        match self {
1092            Self::ShadowRoot(data) => Some(data),
1093            _ => None,
1094        }
1095    }
1096
1097    pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1098        match self {
1099            Self::ShadowRoot(data) => Some(data),
1100            _ => None,
1101        }
1102    }
1103}
1104
1105#[derive(Debug, Clone)]
1106pub struct TextNodeData {
1107    /// The textual content of the text node
1108    pub content: String,
1109}
1110
1111impl TextNodeData {
1112    pub fn new(content: String) -> Self {
1113        Self { content }
1114    }
1115}
1116
1117/*
1118-> Computed styles
1119-> Layout
1120-----> Needs to happen only when styles are computed
1121*/
1122
1123// type DomRefCell<T> = RefCell<T>;
1124
1125// pub struct DomData {
1126//     // ... we can probs just get away with using the html5ever types directly. basically just using the servo dom, but without the bindings
1127//     local_name: html5ever::LocalName,
1128//     tag_name: html5ever::QualName,
1129//     namespace: html5ever::Namespace,
1130//     prefix: DomRefCell<Option<html5ever::Prefix>>,
1131//     attrs: DomRefCell<Vec<Attr>>,
1132//     // attrs: DomRefCell<Vec<Dom<Attr>>>,
1133//     id_attribute: DomRefCell<Option<Atom>>,
1134//     is: DomRefCell<Option<LocalName>>,
1135//     // style_attribute: DomRefCell<Option<Arc<Locked<PropertyDeclarationBlock>>>>,
1136//     // attr_list: MutNullableDom<NamedNodeMap>,
1137//     // class_list: MutNullableDom<DOMTokenList>,
1138//     state: Cell<ElementState>,
1139// }
1140
1141impl Node {
1142    pub fn tree(&self) -> &crate::NodeTree {
1143        unsafe { &*self.tree }
1144    }
1145
1146    pub fn is_html_document(&self) -> bool {
1147        let document = match &self.data {
1148            NodeData::Document(data) => return data.content_type == "text/html",
1149            _ => self.owner_document.and_then(|id| self.tree().get(id)),
1150        };
1151        match document.map(|node| &node.data) {
1152            Some(NodeData::Document(data)) => data.content_type == "text/html",
1153            _ => true,
1154        }
1155    }
1156
1157    pub fn qualified_element_name(&self) -> Option<String> {
1158        let element = self.element_data()?;
1159        let name = match &element.name.prefix {
1160            Some(prefix) => format!("{prefix}:{}", element.name.local),
1161            None => element.name.local.to_string(),
1162        };
1163        Some(
1164            if self.is_html_document() && element.name.ns == markup5ever::ns!(html) {
1165                name.to_ascii_uppercase()
1166            } else {
1167                name
1168            },
1169        )
1170    }
1171
1172    #[track_caller]
1173    pub fn with(&self, id: NodeId) -> &Node {
1174        self.tree().get(id).unwrap()
1175    }
1176
1177    pub fn print_tree(&self, level: usize) {
1178        println!(
1179            "{} {} {:?} {} {:?}",
1180            "  ".repeat(level),
1181            self.id,
1182            self.parent,
1183            self.node_debug_str().replace('\n', ""),
1184            self.children
1185        );
1186        // println!("{} {:?}", "  ".repeat(level), self.children);
1187        for child_id in self.children.iter() {
1188            let child = self.with(*child_id);
1189            child.print_tree(level + 1)
1190        }
1191    }
1192
1193    // Get the index of the current node in the parents child list
1194    pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
1195        self.children.iter().position(|id| *id == child_id)
1196    }
1197
1198    // Get the index of the current node in the parents child list
1199    pub fn child_index(&self) -> Option<usize> {
1200        let children = &self.tree()[self.parent?].children;
1201        let hint = self.child_index_hint.get();
1202        if children.get(hint) == Some(&self.id) {
1203            return Some(hint);
1204        }
1205        let index = children.iter().position(|id| *id == self.id)?;
1206        self.child_index_hint.set(index);
1207        Some(index)
1208    }
1209
1210    /// Read a DOM child and seed its sibling traversal hint.
1211    pub fn dom_child_at(&self, index: usize) -> Option<NodeId> {
1212        let id = *self.children.get(index)?;
1213        self.with(id).child_index_hint.set(index);
1214        Some(id)
1215    }
1216
1217    // Get the nth node in the parents child list
1218    pub fn forward(&self, n: usize) -> Option<&Node> {
1219        let index = self.child_index()?.checked_add(n)?;
1220        let id = *self.tree()[self.parent?].children.get(index)?;
1221        let node = self.with(id);
1222        node.child_index_hint.set(index);
1223        Some(node)
1224    }
1225
1226    pub fn backward(&self, n: usize) -> Option<&Node> {
1227        let index = self.child_index()?.checked_sub(n)?;
1228        let id = *self.tree()[self.parent?].children.get(index)?;
1229        let node = self.with(id);
1230        node.child_index_hint.set(index);
1231        Some(node)
1232    }
1233
1234    pub fn is_element(&self) -> bool {
1235        matches!(self.data, NodeData::Element { .. })
1236    }
1237
1238    pub fn is_anonymous(&self) -> bool {
1239        matches!(self.data, NodeData::AnonymousBlock { .. })
1240    }
1241
1242    pub fn is_shadow_root(&self) -> bool {
1243        matches!(self.data, NodeData::ShadowRoot { .. })
1244    }
1245
1246    pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1247        self.data.shadow_root_data()
1248    }
1249
1250    pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1251        self.data.shadow_root_data_mut()
1252    }
1253
1254    /// If this node is a shadow host (i.e. has an attached shadow root),
1255    /// returns the node id of its shadow root.
1256    pub fn shadow_root_id(&self) -> Option<NodeId> {
1257        self.element_data().and_then(|el| el.shadow_root)
1258    }
1259
1260    /// The parent corresponding to `layout_dom_children`.
1261    ///
1262    /// Shadow roots are transparent, assigned nodes inherit through their
1263    /// slot, and undistributed light children and suppressed fallback children
1264    /// have no edge into the flattened tree.
1265    #[inline]
1266    pub(crate) fn flattened_parent(&self) -> Option<&Node> {
1267        #[cfg(feature = "shadow-dom")]
1268        {
1269            if self.is_shadow_root() {
1270                return None;
1271            }
1272            if let Some(slot_id) = self.assigned_slot {
1273                return Some(self.with(slot_id));
1274            }
1275            let parent = self.with(self.parent?);
1276            if let Some(root) = parent.shadow_root_data() {
1277                return Some(self.with(root.host));
1278            }
1279            if parent.shadow_root_id().is_some()
1280                || parent
1281                    .assigned_nodes
1282                    .as_ref()
1283                    .is_some_and(|nodes| !nodes.is_empty())
1284            {
1285                return None;
1286            }
1287            Some(parent)
1288        }
1289        #[cfg(not(feature = "shadow-dom"))]
1290        {
1291            self.parent.map(|id| self.with(id))
1292        }
1293    }
1294
1295    /// The children to use for layout and painting. For shadow hosts and
1296    /// `<slot>` elements this is the "flattened tree" children; for all other
1297    /// nodes it is the regular DOM [`children`](Self::children).
1298    #[cfg(feature = "shadow-dom")]
1299    pub fn layout_dom_children(&self) -> &[NodeId] {
1300        match &self.flattened_children {
1301            Some(children) => children,
1302            None => &self.children,
1303        }
1304    }
1305
1306    /// The children to use for layout and painting.
1307    #[cfg(not(feature = "shadow-dom"))]
1308    #[inline(always)]
1309    pub fn layout_dom_children(&self) -> &[NodeId] {
1310        &self.children
1311    }
1312
1313    pub fn is_text_node(&self) -> bool {
1314        matches!(self.data, NodeData::Text { .. })
1315    }
1316
1317    pub fn element_data(&self) -> Option<&ElementData> {
1318        match self.data {
1319            NodeData::Element(ref data) => Some(data),
1320            NodeData::AnonymousBlock(ref data) => Some(data),
1321            _ => None,
1322        }
1323    }
1324
1325    pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1326        match self.data {
1327            NodeData::Element(ref mut data) => Some(data),
1328            NodeData::AnonymousBlock(ref mut data) => Some(data),
1329            _ => None,
1330        }
1331    }
1332
1333    pub fn text_data(&self) -> Option<&TextNodeData> {
1334        match self.data {
1335            NodeData::Text(ref data) => Some(data),
1336            _ => None,
1337        }
1338    }
1339
1340    pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1341        match self.data {
1342            NodeData::Text(ref mut data) => Some(data),
1343            _ => None,
1344        }
1345    }
1346
1347    pub fn node_debug_str(&self) -> String {
1348        let mut s = String::new();
1349
1350        match &self.data {
1351            NodeData::Document(_) => write!(s, "DOCUMENT"),
1352            NodeData::DocumentFragment => write!(s, "FRAGMENT"),
1353            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1354            NodeData::Text(data) => {
1355                let bytes = data.content.as_bytes();
1356                write!(
1357                    s,
1358                    "TEXT {}",
1359                    std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1360                        .unwrap_or("INVALID UTF8")
1361                )
1362            }
1363            NodeData::Comment { .. } => write!(s, "COMMENT"),
1364            NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1365            NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1366            NodeData::Element(data) => {
1367                let name = &data.name;
1368                let class = self.attr(local_name!("class")).unwrap_or("");
1369                let id = self.attr(local_name!("id")).unwrap_or("");
1370                let display = self.display_constructed_as().to_css_string();
1371                write!(s, "<{}", name.local).unwrap();
1372                if !id.is_empty() {
1373                    write!(s, " #{id}").unwrap();
1374                }
1375                if !class.is_empty() {
1376                    if class.contains(' ') {
1377                        write!(s, " class=\"{class}\"").unwrap()
1378                    } else {
1379                        write!(s, " .{class}").unwrap()
1380                    }
1381                }
1382                write!(s, "> ({display})")
1383            } // NodeData::ProcessingInstruction { .. } => write!(s, "ProcessingInstruction"),
1384        }
1385        .unwrap();
1386        s
1387    }
1388
1389    /// Renders the HTML of this node and all its children as a `String` without extra whitespace.
1390    ///
1391    /// Example output:
1392    ///
1393    /// ```text
1394    /// <html><head /><body><main id="main"><div class="arbitrary-class" /></main></body></html>
1395    /// ```
1396    pub fn outer_html(&self) -> String {
1397        let mut output = String::new();
1398        self.write_outer_html(&mut output);
1399        output
1400    }
1401
1402    /// Renders the HTML of this node and all its children as a `String` with whitespace for human
1403    /// readability.
1404    ///
1405    /// Example output:
1406    ///
1407    /// ```text
1408    /// <html>
1409    ///   <head />
1410    ///   <body>
1411    ///     <main id="main">
1412    ///       <div class="arbitrary-class" />
1413    ///     </main>
1414    ///   </body>
1415    /// </html>
1416    /// ```
1417    pub fn outer_html_pretty(&self) -> String {
1418        let mut output = String::new();
1419        self.write_outer_html_pretty(&mut output);
1420        output
1421    }
1422
1423    pub fn write_outer_html(&self, writer: &mut String) {
1424        self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1425    }
1426
1427    #[cfg(feature = "svg")]
1428    pub(crate) fn write_outer_html_with_current_color(
1429        &self,
1430        writer: &mut String,
1431        current_color: &str,
1432    ) {
1433        self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1434    }
1435
1436    #[cfg(feature = "svg")]
1437    pub(crate) fn write_outer_html_with_svg_ids(
1438        &self,
1439        writer: &mut String,
1440        ids: &std::collections::HashMap<NodeId, String>,
1441    ) {
1442        self.write_outer_html_in_style_with_svg_ids(
1443            writer,
1444            OutputStyle::Normal,
1445            0,
1446            None,
1447            Some(ids),
1448        );
1449    }
1450
1451    pub fn write_outer_html_pretty(&self, writer: &mut String) {
1452        self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1453    }
1454
1455    fn write_outer_html_in_style(
1456        &self,
1457        writer: &mut String,
1458        style: OutputStyle,
1459        nesting: usize,
1460        current_color_override: Option<&str>,
1461    ) {
1462        self.write_outer_html_in_style_with_svg_ids(
1463            writer,
1464            style,
1465            nesting,
1466            current_color_override,
1467            None,
1468        );
1469    }
1470
1471    fn write_outer_html_in_style_with_svg_ids(
1472        &self,
1473        writer: &mut String,
1474        style: OutputStyle,
1475        nesting: usize,
1476        current_color_override: Option<&str>,
1477        svg_ids: Option<&std::collections::HashMap<NodeId, String>>,
1478    ) {
1479        const INDENT: &str = "  ";
1480        let has_children = !self.children.is_empty();
1481        let computed_current_color = || {
1482            self.primary_styles()
1483                .map(|style| style.clone_color())
1484                .map(|color| crate::util::absolute_color_to_svg_css(&color))
1485        };
1486        let current_color = current_color_override
1487            .map(ToOwned::to_owned)
1488            .or_else(computed_current_color);
1489
1490        match &self.data {
1491            NodeData::Document(_) => {}
1492            NodeData::Comment { .. } => {}
1493            NodeData::AnonymousBlock(_) => {}
1494            NodeData::ShadowRoot(_) => {}
1495            // A fragment serialises as nothing of its own: the spec says its
1496            // children are what gets inserted, and it is never in a document
1497            // to be serialised from in the first place.
1498            NodeData::DocumentFragment => {}
1499            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1500            NodeData::Text(data) => {
1501                if matches!(style, OutputStyle::Pretty) {
1502                    for _ in 0..nesting {
1503                        writer.push_str(INDENT);
1504                    }
1505                }
1506                writer.push_str(data.content.as_str());
1507                if matches!(style, OutputStyle::Pretty) {
1508                    writer.push('\n');
1509                }
1510            }
1511            NodeData::Element(data) => {
1512                if matches!(style, OutputStyle::Pretty) {
1513                    for _ in 0..nesting {
1514                        writer.push_str(INDENT);
1515                    }
1516                }
1517                // A hyperlink is a graphics container in the image source.
1518                // Its live DOM name, attributes and event ancestry stay intact.
1519                let tag_name = if svg_ids.is_some()
1520                    && data.name.ns.as_ref() == "http://www.w3.org/2000/svg"
1521                    && data.name.local == local_name!("a")
1522                {
1523                    "g"
1524                } else {
1525                    data.name.local.as_ref()
1526                };
1527                let svg_id = svg_ids.and_then(|ids| ids.get(&self.id));
1528                writer.push('<');
1529                writer.push_str(tag_name);
1530                if let Some(id) = svg_id {
1531                    writer.push_str(" id=\"");
1532                    encode_quoted_attribute_to_string(id, writer);
1533                    writer.push('"');
1534                }
1535
1536                for attr in data.attrs() {
1537                    if svg_id.is_some()
1538                        && attr.name.ns.as_ref().is_empty()
1539                        && attr.name.local == local_name!("id")
1540                    {
1541                        continue;
1542                    }
1543                    writer.push(' ');
1544                    if let Some(prefix) = attr
1545                        .name
1546                        .prefix
1547                        .as_ref()
1548                        .filter(|prefix| !prefix.is_empty())
1549                    {
1550                        writer.push_str(prefix);
1551                        writer.push(':');
1552                    }
1553                    writer.push_str(&attr.name.local);
1554                    writer.push_str("=\"");
1555                    #[allow(clippy::unnecessary_unwrap)] // Convert to if-let chain once stabilised
1556                    if current_color.is_some() && attr.value.contains("currentColor") {
1557                        let value = attr
1558                            .value
1559                            .replace("currentColor", current_color.as_ref().unwrap());
1560                        encode_quoted_attribute_to_string(&value, writer);
1561                    } else {
1562                        encode_quoted_attribute_to_string(&attr.value, writer);
1563                    }
1564                    writer.push('"');
1565                }
1566                if !has_children {
1567                    writer.push_str(" /");
1568                }
1569                writer.push('>');
1570                if matches!(style, OutputStyle::Pretty) {
1571                    writer.push('\n');
1572                }
1573
1574                if has_children {
1575                    for &child_id in &self.children {
1576                        self.tree()[child_id].write_outer_html_in_style_with_svg_ids(
1577                            writer,
1578                            style,
1579                            nesting + 1,
1580                            current_color_override,
1581                            svg_ids,
1582                        );
1583                    }
1584
1585                    if matches!(style, OutputStyle::Pretty) {
1586                        for _ in 0..nesting {
1587                            writer.push_str(INDENT);
1588                        }
1589                    }
1590                    writer.push_str("</");
1591                    writer.push_str(tag_name);
1592                    writer.push('>');
1593                    if matches!(style, OutputStyle::Pretty) {
1594                        writer.push('\n');
1595                    }
1596                }
1597            }
1598        }
1599    }
1600
1601    /// Serialise this node as the HTML fragment serialisation algorithm does
1602    /// for `outerHTML`: void elements have no end tag, every other element
1603    /// has one, text and attribute values are escaped, comments are kept.
1604    ///
1605    /// Unlike [`Self::outer_html`], which is a debug rendering (`<div />`),
1606    /// this output parses back to the same tree, which `innerHTML +=` and
1607    /// templating code depend on.
1608    pub fn write_html(&self, writer: &mut String) {
1609        match &self.data {
1610            NodeData::Text(data) => {
1611                let raw = self
1612                    .parent
1613                    .and_then(|parent| self.tree()[parent].element_data())
1614                    .is_some_and(|parent| is_raw_text_element(&parent.name.local));
1615                if raw {
1616                    writer.push_str(&data.content);
1617                } else {
1618                    escape_html(&data.content, false, writer);
1619                }
1620            }
1621            NodeData::Comment { contents } => {
1622                writer.push_str("<!--");
1623                writer.push_str(contents);
1624                writer.push_str("-->");
1625            }
1626            NodeData::Element(data) => {
1627                writer.push('<');
1628                writer.push_str(&data.name.local);
1629                for attr in data.attrs() {
1630                    writer.push(' ');
1631                    if let Some(prefix) = attr
1632                        .name
1633                        .prefix
1634                        .as_ref()
1635                        .filter(|prefix| !prefix.is_empty())
1636                    {
1637                        writer.push_str(prefix);
1638                        writer.push(':');
1639                    }
1640                    writer.push_str(&attr.name.local);
1641                    writer.push_str("=\"");
1642                    escape_html(&attr.value, true, writer);
1643                    writer.push('"');
1644                }
1645                writer.push('>');
1646                if is_void_element(&data.name.local) {
1647                    return;
1648                }
1649                self.write_children_html(writer);
1650                writer.push_str("</");
1651                writer.push_str(&data.name.local);
1652                writer.push('>');
1653            }
1654            NodeData::Document(_)
1655            | NodeData::DocumentFragment
1656            | NodeData::ShadowRoot(_)
1657            | NodeData::AnonymousBlock(_) => self.write_children_html(writer),
1658        }
1659    }
1660
1661    /// Serialise this node's children, as `innerHTML` returns them.
1662    pub fn write_children_html(&self, writer: &mut String) {
1663        if let Some(contents) = self
1664            .element_data()
1665            .and_then(|element| element.template_contents)
1666        {
1667            self.tree()[contents].write_children_html(writer);
1668            return;
1669        }
1670        for &child_id in &self.children {
1671            self.tree()[child_id].write_html(writer);
1672        }
1673    }
1674
1675    pub fn attrs(&self) -> Option<&[Attribute]> {
1676        Some(&self.element_data()?.attrs)
1677    }
1678
1679    pub fn attr(&self, name: LocalName) -> Option<&str> {
1680        let attr = self
1681            .attrs()?
1682            .iter()
1683            .find(|attr| attr.name.ns == markup5ever::ns!() && attr.name.local == name)?;
1684        Some(&attr.value)
1685    }
1686
1687    pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1688        self.stylo_element_data_opt()
1689            .and_then(|stylo| stylo.primary_styles())
1690    }
1691
1692    pub fn text_content(&self) -> String {
1693        let mut out = String::new();
1694        self.write_text_content(&mut out);
1695        out
1696    }
1697
1698    /// HTML's rendered-text getter. Text comes from the inline layout's
1699    /// processed buffer, rather than reconstructing whitespace from source.
1700    /// Callers must flush layout before invoking this.
1701    pub fn rendered_inner_text(&self) -> String {
1702        use style::computed_values::visibility::T as Visibility;
1703
1704        enum Piece {
1705            Text(String),
1706            RequiredBreak(usize),
1707        }
1708
1709        fn text(node: &Node) -> String {
1710            let NodeData::Text(data) = &node.data else {
1711                return String::new();
1712            };
1713            // Text that never reached an inline layout is not rendered.
1714            if node.inline_root_ancestor().is_none() {
1715                return String::new();
1716            }
1717            let preserves = node
1718                .parent
1719                .map(|id| node.with(id))
1720                .and_then(Node::primary_styles)
1721                .is_some_and(|style| {
1722                    style.get_inherited_text().white_space_collapse
1723                        != style::computed_values::white_space_collapse::T::Collapse
1724                });
1725            if preserves {
1726                return data.content.to_string();
1727            }
1728            // Collapsing white space: every run of spaces, tabs and newlines
1729            // is one space, as the line layout rendered it.
1730            let mut result = String::with_capacity(data.content.len());
1731            let mut space = false;
1732            for ch in data.content.chars() {
1733                if matches!(ch, ' ' | '\t' | '\n' | '\r' | '\u{c}') {
1734                    space = true;
1735                } else {
1736                    if space {
1737                        result.push(' ');
1738                        space = false;
1739                    }
1740                    result.push(ch);
1741                }
1742            }
1743            if space {
1744                result.push(' ');
1745            }
1746            result
1747        }
1748        fn collect(node: &Node, pieces: &mut Vec<Piece>) {
1749            if node.is_element() && node.is_display_none() {
1750                return;
1751            }
1752            let visible = node
1753                .primary_styles()
1754                .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1755            if node.is_text_node() {
1756                let visible = node
1757                    .parent
1758                    .map(|id| node.with(id))
1759                    .and_then(Node::primary_styles)
1760                    .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1761                if visible {
1762                    let value = text(node);
1763                    if !value.is_empty() {
1764                        pieces.push(Piece::Text(value));
1765                    }
1766                }
1767                return;
1768            }
1769            if !node.is_element() {
1770                return;
1771            }
1772            let tag = node.element_data().expect("element").name.local.as_ref();
1773            if tag == "br" {
1774                if visible {
1775                    pieces.push(Piece::Text("\n".to_owned()));
1776                }
1777                return;
1778            }
1779            let display = node.primary_styles().map(|style| style.clone_display());
1780            let block = display.is_some_and(|display| {
1781                display.outside() == DisplayOutside::Block && !display.is_contents()
1782            });
1783            let breaks = if tag == "p" { 2 } else { usize::from(block) };
1784            if breaks != 0 {
1785                pieces.push(Piece::RequiredBreak(breaks));
1786            }
1787            for child in &node.children {
1788                collect(node.with(*child), pieces);
1789            }
1790            if visible {
1791                if matches!(tag, "td" | "th") {
1792                    let next_cell = node
1793                        .parent
1794                        .map(|parent| node.with(parent))
1795                        .and_then(|parent| {
1796                            parent.index_of_child(node.id).map(|index| (parent, index))
1797                        })
1798                        .is_some_and(|(parent, index)| {
1799                            parent.children[index + 1..].iter().any(|id| {
1800                                let child = parent.with(*id);
1801                                child.data.is_element_with_tag_name(&local_name!("td"))
1802                                    || child.data.is_element_with_tag_name(&local_name!("th"))
1803                            })
1804                        });
1805                    if next_cell {
1806                        pieces.push(Piece::Text("\t".to_owned()));
1807                    }
1808                } else if tag == "tr" {
1809                    pieces.push(Piece::RequiredBreak(1));
1810                }
1811            }
1812            if breaks != 0 {
1813                pieces.push(Piece::RequiredBreak(breaks));
1814            }
1815        }
1816
1817        let mut current = Some(self);
1818        while let Some(node) = current {
1819            if !self.flags.is_in_document() || node.is_display_none() {
1820                return self.text_content();
1821            }
1822            current = node.parent.map(|id| node.with(id));
1823        }
1824        if self.primary_styles().is_none() {
1825            return self.text_content();
1826        }
1827        let mut pieces = Vec::new();
1828        collect(self, &mut pieces);
1829        let mut result = String::new();
1830        let mut pending = 0;
1831        for piece in pieces {
1832            match piece {
1833                Piece::RequiredBreak(count) => pending = pending.max(count),
1834                Piece::Text(value) => {
1835                    if !result.is_empty() && pending != 0 {
1836                        result.truncate(result.trim_end_matches(' ').len());
1837                        for _ in 0..pending {
1838                            result.push('\n');
1839                        }
1840                    }
1841                    pending = 0;
1842                    if result.is_empty() || result.ends_with('\n') || result.ends_with(' ') {
1843                        result.push_str(value.trim_start_matches(' '));
1844                    } else {
1845                        result.push_str(&value);
1846                    }
1847                }
1848            }
1849        }
1850        result.truncate(result.trim_end_matches(' ').len());
1851        result
1852    }
1853
1854    /// CSSOM View's offset parent, excluding nodes without CSS boxes and
1855    /// viewport-fixed elements.
1856    pub fn cssom_offset_parent(&self) -> Option<&Node> {
1857        if !self.flags.is_in_document()
1858            || self.data.is_element_with_tag_name(&local_name!("body"))
1859            || self.data.is_element_with_tag_name(&local_name!("html"))
1860        {
1861            return None;
1862        }
1863        let style = self.primary_styles()?;
1864        if style.clone_position() == Position::Fixed || style.clone_display().is_contents() {
1865            return None;
1866        }
1867        let mut current = Some(self);
1868        while let Some(node) = current {
1869            if node.is_display_none() {
1870                return None;
1871            }
1872            current = node.parent.map(|id| node.with(id));
1873        }
1874        let static_position = style.clone_position() == Position::Static;
1875        let zoom = style.effective_zoom.zoom(1.0);
1876        let mut parent = self.parent.map(|id| self.with(id));
1877        while let Some(node) = parent {
1878            if let Some(style) = node.primary_styles()
1879                && !style.clone_display().is_contents()
1880                && (node.is_offset_parent()
1881                    || style.effective_zoom.zoom(1.0) != zoom
1882                    || (static_position
1883                        && node.data.is_element_with_tag_name(&local_name!("table"))))
1884            {
1885                return Some(node);
1886            }
1887            parent = node.parent.map(|id| node.with(id));
1888        }
1889        None
1890    }
1891
1892    /// Write this subtree's text content into any [`std::fmt::Write`] sink.
1893    ///
1894    /// Public and generic so that a caller which does not want a `String` does
1895    /// not have to fork this traversal to avoid one. `blitz-dom-api`'s
1896    /// buffer-writing reader passes a sink that counts, and then one that fills
1897    /// a caller-supplied slice; a private copy of this walk in that crate would
1898    /// silently disagree with this one the first time a `NodeData` variant is
1899    /// added here.
1900    ///
1901    /// The sinks callers pass do not fail, and `String`'s never has, so nothing
1902    /// in this crate inspects the `Result`. It is kept in the signature because
1903    /// it is `fmt::Write`'s, not because there is an error to handle.
1904    pub fn write_text_content<W: Write>(&self, out: &mut W) {
1905        match &self.data {
1906            NodeData::Text(data) => {
1907                let _ = out.write_str(&data.content);
1908            }
1909            NodeData::Element(..)
1910            | NodeData::AnonymousBlock(..)
1911            | NodeData::Document(..)
1912            | NodeData::DocumentFragment
1913            | NodeData::ShadowRoot(..) => {
1914                for child_id in self.children.iter() {
1915                    self.with(*child_id).write_text_content(out);
1916                }
1917            }
1918            _ => {}
1919        }
1920    }
1921
1922    pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1923        if let NodeData::Element(ref mut elem_data) = self.data {
1924            if let Some(guard) = elem_data.guard.clone() {
1925                elem_data.flush_style_attribute(&guard, url_extra_data);
1926            }
1927        }
1928    }
1929
1930    pub fn order(&self) -> i32 {
1931        self.primary_styles()
1932            .map(|s| match s.pseudo() {
1933                Some(PseudoElement::Before) => i32::MIN,
1934                Some(PseudoElement::After) => i32::MAX,
1935                _ => s.clone_order(),
1936            })
1937            .unwrap_or(0)
1938    }
1939
1940    pub fn z_index(&self) -> i32 {
1941        self.primary_styles()
1942            .map(|s| s.clone_z_index().integer_or(0))
1943            .unwrap_or(0)
1944    }
1945
1946    // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts
1947    pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1948        let Some(style) = self.primary_styles() else {
1949            return false;
1950        };
1951
1952        let position = style.clone_position();
1953        let has_z_index = !style.clone_z_index().is_auto();
1954
1955        if style.clone_opacity() != 1.0 {
1956            return true;
1957        }
1958
1959        let position_based = match position {
1960            Position::Fixed | Position::Sticky => true,
1961            Position::Relative | Position::Absolute => has_z_index,
1962            Position::Static => has_z_index && is_flex_or_grid_item,
1963        };
1964        if position_based {
1965            return true;
1966        }
1967
1968        // This runs while the stacking-context tree is built, before
1969        // `resolve_transforms` populates the cached device-space matrix. Using
1970        // that cache here makes the first frame treat a transformed node as an
1971        // ordinary box, hoist its painted children into the parent, and only
1972        // repair the tree after the first hover/restyle. The computed CSS value
1973        // is already available and is the source of truth for whether the node
1974        // establishes a stacking context.
1975        let box_styles = style.get_box();
1976        if !box_styles.transform.0.is_empty()
1977            || !matches!(box_styles.translate, Translate::None)
1978            || !matches!(box_styles.rotate, Rotate::None)
1979            || !matches!(box_styles.scale, Scale::None)
1980        {
1981            return true;
1982        }
1983
1984        // `isolation: isolate` exists precisely to create a stacking context
1985        // without any other visual effect. Ignoring it lets a negative z-index
1986        // descendant escape to an ancestor context, where it is painted before
1987        // (and so underneath) the backgrounds of the boxes in between.
1988        if box_styles.isolation == Isolation::Isolate {
1989            return true;
1990        }
1991
1992        // TODO: mix-blend-mode
1993        // TODO: filter
1994        // TODO: clip-path
1995        // TODO: mask
1996        // TODO: contain
1997
1998        false
1999    }
2000
2001    /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns:
2002    ///    - None if the position is outside of this node's bounds
2003    ///    - Some(HitResult) if the position is within the node but doesn't match any children
2004    ///    - The result of recursively calling child.hit() on the the child element that is
2005    ///      positioned at that position if there is one.
2006    ///
2007    /// TODO: z-index
2008    /// (If multiple children are positioned at the position then a random one will be recursed into)
2009    pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
2010        self.hit_inner(x, y, scale, &mut None)
2011    }
2012
2013    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
2014    /// thumb under the point into `scrollbar` during the same descent (so
2015    /// thumb hit-testing shares the exact coordinate handling — transforms
2016    /// included — of every other hit test).
2017    pub(crate) fn hit_inner(
2018        &self,
2019        x: f32,
2020        y: f32,
2021        scale: f64,
2022        scrollbar: &mut Option<crate::node::ScrollbarRef>,
2023    ) -> Option<HitResult> {
2024        self.hit_inner_collect(x, y, scale, scrollbar, &mut None)
2025    }
2026
2027    /// Use the input hit traversal either for the foremost hit or for every hit.
2028    pub(crate) fn hit_inner_collect(
2029        &self,
2030        x: f32,
2031        y: f32,
2032        scale: f64,
2033        scrollbar: &mut Option<crate::node::ScrollbarRef>,
2034        hits: &mut Option<Vec<HitResult>>,
2035    ) -> Option<HitResult> {
2036        use style::computed_values::pointer_events::T as PointerEvents;
2037        use style::computed_values::visibility::T as Visibility;
2038
2039        // A hidden subtree takes no hits.
2040        //
2041        // This never needed saying while hiding a pane destroyed its boxes: a
2042        // hidden subtree had nothing to test against. It keeps its boxes now,
2043        // and their `final_layout` is whatever it was when the pane was last
2044        // visible — full size, in place, over the tab in front. A retained tab
2045        // measured 331 of its 370 elements still carrying live geometry after
2046        // being hidden, and every one of them was a click target.
2047        if matches!(self.style().display, taffy::Display::None) {
2048            return None;
2049        }
2050
2051        // Don't hit on visbility:hidden elements
2052        if let Some(style) = self.primary_styles() {
2053            if matches!(
2054                style.clone_visibility(),
2055                Visibility::Hidden | Visibility::Collapse
2056            ) {
2057                return None;
2058            }
2059        }
2060
2061        // pointer-events:none makes this element transparent to hits, but its
2062        // descendants are still tested (one may restore pointer-events:auto).
2063        let pointer_events_none = self
2064            .primary_styles()
2065            .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2066
2067        let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
2068        let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
2069
2070        if let Some(t) = *self.transform() {
2071            let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
2072            x = (p.x / scale) as f32;
2073            y = (p.y / scale) as f32;
2074        }
2075
2076        let size = self.final_layout().size;
2077        let matches_self = !(x < 0.0
2078            || x > size.width + self.scroll_offset().x as f32
2079            || y < 0.0
2080            || y > size.height + self.scroll_offset().y as f32);
2081
2082        let content_size = self.final_layout().content_size;
2083        let matches_content = !(x < 0.0
2084            || x > content_size.width + self.scroll_offset().x as f32
2085            || y < 0.0
2086            || y > content_size.height + self.scroll_offset().y as f32);
2087
2088        let matches_hoisted_content = match &self.stacking_context {
2089            Some(sc) => {
2090                let content_area = sc.content_area;
2091                x >= content_area.left + self.scroll_offset().x as f32
2092                    && x <= content_area.right + self.scroll_offset().x as f32
2093                    && y >= content_area.top + self.scroll_offset().y as f32
2094                    && y <= content_area.bottom + self.scroll_offset().y as f32
2095            }
2096            None => false,
2097        };
2098
2099        // `scrollable_overflow` is stored in device (scaled) pixels, whereas the
2100        // coordinates here are in CSS pixels, so unscale it before comparing.
2101        let overflow = *self.scrollable_overflow();
2102
2103        let matches_overflow = x >= (overflow.x0 / scale) as f32
2104            && x <= (overflow.x1 / scale) as f32
2105            && y >= (overflow.y0 / scale) as f32
2106            && y <= (overflow.y1 / scale) as f32;
2107
2108        if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
2109            return None;
2110        }
2111
2112        // Descendants overwrite, so the innermost scroll container's thumb
2113        // wins. Thumb coords are border-box relative (unscrolled).
2114        if matches_self
2115            && let Some(sb) = self.scrollbar_at_local(
2116                (x - self.scroll_offset().x as f32) as f64,
2117                (y - self.scroll_offset().y as f32) as f64,
2118            )
2119        {
2120            *scrollbar = Some(sb);
2121        }
2122
2123        // Child layouts and hoisted offsets use border-box coordinates.
2124        // Only glyph hit testing below uses the inline content-box origin.
2125
2126        // Positive z_index hoisted children
2127        if matches_hoisted_content {
2128            if let Some(hoisted) = &self.stacking_context {
2129                for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
2130                    if hoisted_child.clips.iter().any(|clip| {
2131                        x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2132                    }) {
2133                        continue;
2134                    }
2135                    let x = x - hoisted_child.position.x;
2136                    let y = y - hoisted_child.position.y;
2137                    if let Some(hit) = self
2138                        .with(hoisted_child.node_id)
2139                        .hit_inner_collect(x, y, scale, scrollbar, hits)
2140                    {
2141                        return Some(hit);
2142                    }
2143                }
2144            }
2145        }
2146
2147        // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id).
2148        for child_id in self.paint_children.borrow().iter().flatten().rev() {
2149            if let Some(hit) = self
2150                .with(*child_id)
2151                .hit_inner_collect(x, y, scale, scrollbar, hits)
2152            {
2153                return Some(hit);
2154            }
2155        }
2156
2157        // Negative z_index hoisted children
2158        if matches_hoisted_content {
2159            if let Some(hoisted) = &self.stacking_context {
2160                for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
2161                    if hoisted_child.clips.iter().any(|clip| {
2162                        x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2163                    }) {
2164                        continue;
2165                    }
2166                    let x = x - hoisted_child.position.x;
2167                    let y = y - hoisted_child.position.y;
2168                    if let Some(hit) = self
2169                        .with(hoisted_child.node_id)
2170                        .hit_inner_collect(x, y, scale, scrollbar, hits)
2171                    {
2172                        return Some(hit);
2173                    }
2174                }
2175            }
2176        }
2177
2178        // Inline SVG descendants have image geometry instead of layout boxes.
2179        #[cfg(feature = "svg")]
2180        if matches_self && let Some(hit) = self.hit_inline_svg(x, y, hits) {
2181            return Some(hit);
2182        }
2183
2184        // Inline children
2185        if self.flags.is_inline_root() {
2186            x -= self.final_layout().padding.left + self.final_layout().border.left;
2187            y -= self.final_layout().padding.top + self.final_layout().border.top;
2188            let element_data = &self.element_data().unwrap();
2189            if let Some(ild) = element_data.inline_layout_data.as_ref() {
2190                let layout = &ild.layout;
2191                let scale = layout.scale();
2192
2193                if let Some((cluster, _side)) =
2194                    Cluster::from_point_exact(layout, x * scale, y * scale)
2195                {
2196                    let style_index = cluster.glyphs().next()?.style_index();
2197                    let node_id = layout.styles()[style_index].brush.id;
2198                    let text_pointer_events_none = self
2199                        .with(node_id)
2200                        .primary_styles()
2201                        .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2202                    if !text_pointer_events_none {
2203                        let hit = HitResult {
2204                            node_id,
2205                            x,
2206                            y,
2207                            is_text: true,
2208                        };
2209                        if let Some(hits) = hits.as_mut() {
2210                            hits.push(hit);
2211                        } else {
2212                            return Some(hit);
2213                        }
2214                    }
2215                }
2216            }
2217        }
2218
2219        // Self (this node)
2220        if matches_self && !pointer_events_none {
2221            let hit = HitResult {
2222                node_id: self.id,
2223                x,
2224                y,
2225                is_text: false,
2226            };
2227            if let Some(hits) = hits.as_mut() {
2228                hits.push(hit);
2229            } else {
2230                return Some(hit);
2231            }
2232        }
2233
2234        None
2235    }
2236
2237    /// Find the inline root ancestor of this node (or self if this is an inline root).
2238    /// Returns None if no inline root ancestor exists.
2239    pub fn inline_root_ancestor(&self) -> Option<&Node> {
2240        let mut node = self;
2241        loop {
2242            if node.flags.is_inline_root() {
2243                return Some(node);
2244            }
2245            let id = node.layout_parent.get()?;
2246            node = self.with(id);
2247        }
2248    }
2249
2250    /// Get the text byte offset at a given point, using coordinates already transformed
2251    /// to be relative to this inline root's content box.
2252    /// Returns Some(byte_offset) if the point hits text, None otherwise.
2253    pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
2254        if !self.flags.is_inline_root() {
2255            return None;
2256        }
2257
2258        let element_data = self.element_data()?;
2259        let inline_layout = element_data.inline_layout_data.as_ref()?;
2260        let layout = &inline_layout.layout;
2261        let scale = layout.scale();
2262
2263        // Use Parley's cluster hit testing (from_point is more forgiving than from_point_exact)
2264        let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
2265
2266        // Determine byte offset based on which side of the cluster was clicked
2267        // For LTR text: left side = start of cluster, right side = end of cluster
2268        // For RTL text: left side = end of cluster, right side = start of cluster
2269        // Also, explicit line breaks should always use start to avoid cursor appearing on next line
2270        let is_leading = side == ClusterSide::Left;
2271        let offset = if cluster.is_rtl() {
2272            if is_leading {
2273                cluster.text_range().end
2274            } else {
2275                cluster.text_range().start
2276            }
2277        } else {
2278            // LTR text
2279            if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
2280                cluster.text_range().start
2281            } else {
2282                cluster.text_range().end
2283            }
2284        };
2285
2286        Some(offset)
2287    }
2288
2289    /// The byte range of the word or line at a point, for a multi-click
2290    /// selection.
2291    ///
2292    /// See [`TextGranularity`] for which click count maps to which unit. Coordinates are relative to this inline root's content box,
2293    /// as for [`text_offset_at_point`](Self::text_offset_at_point).
2294    ///
2295    /// Parley owns the boundary rules (it is what an `<input>` already selects
2296    /// with), so this asks it rather than scanning for spaces: word breaks are
2297    /// a Unicode segmentation question, not a whitespace one.
2298    pub fn text_range_at_point(
2299        &self,
2300        x: f32,
2301        y: f32,
2302        granularity: TextGranularity,
2303    ) -> Option<Range<usize>> {
2304        if !self.flags.is_inline_root() {
2305            return None;
2306        }
2307
2308        let element_data = self.element_data()?;
2309        let inline_layout = element_data.inline_layout_data.as_ref()?;
2310        let layout = &inline_layout.layout;
2311        let scale = layout.scale();
2312        let (x, y) = (x * scale, y * scale);
2313
2314        // Bail when the point misses the text entirely: `Selection` answers an
2315        // out-of-range point with a collapsed cursor at the end of the text,
2316        // which would read as "selected nothing at the very end" rather than
2317        // as a miss.
2318        Cluster::from_point(layout, x, y)?;
2319
2320        let selection = match granularity {
2321            TextGranularity::Word => Selection::word_from_point(layout, x, y),
2322            // The hard line, so a soft-wrapped paragraph selects as the whole
2323            // paragraph. That is what a triple click does elsewhere, and it is
2324            // what `select_hard_line_at_point` gives a text input.
2325            TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
2326        };
2327
2328        let range = selection.text_range();
2329        if range.is_empty() { None } else { Some(range) }
2330    }
2331
2332    /// Computes the Document-relative coordinates of the `Node`
2333    pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
2334        // A scroll offset moves this node's descendants, not its own border
2335        // box. Parent recursion applies each ancestor offset to the child.
2336        let x = x + self.final_layout().location.x;
2337        let y = y + self.final_layout().location.y;
2338
2339        // Recurse up the layout hierarchy
2340        self.layout_parent
2341            .get()
2342            .and_then(|id| self.tree().get(id))
2343            .map(|parent| {
2344                parent.absolute_position(
2345                    x - parent.scroll_offset().x as f32,
2346                    y - parent.scroll_offset().y as f32,
2347                )
2348            })
2349            .unwrap_or(crate::util::Point { x, y })
2350    }
2351
2352    /// Whether this node can act as an [`offset_parent`](Self::offset_parent): a positioned
2353    /// element, or one of the elements that always qualify (`body`, `td`, `th`).
2354    fn is_offset_parent(&self) -> bool {
2355        let Some(styles) = self.primary_styles() else {
2356            return false;
2357        };
2358        if styles.get_box().position != Position::Static {
2359            return true;
2360        }
2361        self.data.is_element_with_tag_name(&local_name!("body"))
2362            || self.data.is_element_with_tag_name(&local_name!("td"))
2363            || self.data.is_element_with_tag_name(&local_name!("th"))
2364    }
2365
2366    /// The nearest layout ancestor that is an offset parent, as in CSSOM View's
2367    /// `offsetParent`.
2368    ///
2369    /// The predicate is `is_offset_parent`, which is private, so this is not a
2370    /// link: rustdoc rejects a public item linking to a private one under
2371    /// `-D warnings`, and the alternative is publishing a helper nothing else
2372    /// needs.
2373    pub fn offset_parent(&self) -> Option<&Node> {
2374        let mut node = self;
2375        loop {
2376            node = self.with(node.layout_parent.get()?);
2377            if node.is_offset_parent() {
2378                return Some(node);
2379            }
2380        }
2381    }
2382
2383    /// CSSOM View's `offsetLeft`/`offsetTop`: the offset of this node's border box from the
2384    /// padding edge of its [`offset_parent`](Self::offset_parent).
2385    pub fn offset_top_left(&self) -> crate::util::Point<f32> {
2386        let mut x = 0.0;
2387        let mut y = 0.0;
2388        let mut current = self;
2389        loop {
2390            let layout = current.final_layout();
2391            x += layout.location.x;
2392            y += layout.location.y;
2393
2394            let Some(parent_id) = current.layout_parent.get() else {
2395                break;
2396            };
2397            let parent = self.with(parent_id);
2398            if parent.is_offset_parent() {
2399                let border = parent.final_layout().border;
2400                x -= border.left;
2401                y -= border.top;
2402                break;
2403            }
2404            current = parent;
2405        }
2406        crate::util::Point { x, y }
2407    }
2408
2409    /// Creates a synthetic click event
2410    pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
2411        DomEventData::Click(self.synthetic_click_event_data(mods))
2412    }
2413
2414    pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
2415        let absolute_position = self.absolute_position(0.0, 0.0);
2416        let x = absolute_position.x + (self.final_layout().size.width / 2.0);
2417        let y = absolute_position.y + (self.final_layout().size.height / 2.0);
2418
2419        BlitzPointerEvent {
2420            id: BlitzPointerId::Mouse,
2421            is_primary: true,
2422            coords: PointerCoords {
2423                page_x: x,
2424                page_y: y,
2425
2426                // TODO: should these be different?
2427                screen_x: x,
2428                screen_y: y,
2429                client_x: x,
2430                client_y: y,
2431            },
2432            mods,
2433            button: Default::default(),
2434            buttons: Default::default(),
2435            details: Default::default(),
2436            element: Default::default(),
2437            active_pointers: Default::default(),
2438        }
2439    }
2440}
2441
2442/// It might be wrong to expose this since what does *equality* mean outside the dom?
2443impl PartialEq for Node {
2444    fn eq(&self, other: &Self) -> bool {
2445        self.id == other.id
2446    }
2447}
2448
2449impl Eq for Node {}
2450
2451impl std::fmt::Debug for Node {
2452    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2453        // FIXME: update to reflect changes to fields
2454        f.debug_struct("NodeData")
2455            .field("parent", &self.parent)
2456            .field("id", &self.id)
2457            .field("is_inline_root", &self.flags.is_inline_root())
2458            .field("children", &self.children)
2459            .field("layout_children", &self.layout_children.borrow())
2460            // .field("style", &self.style)
2461            .field("node", &self.data)
2462            .field("stylo_element_data", &self.stylo_element_data_opt())
2463            // .field("unrounded_layout", &self.unrounded_layout)
2464            // .field("final_layout", &self.final_layout)
2465            .finish()
2466    }
2467}
2468
2469/// Elements whose text children serialise verbatim (HTML fragment
2470/// serialisation, with scripting enabled).
2471fn is_raw_text_element(name: &LocalName) -> bool {
2472    matches!(
2473        name.as_ref(),
2474        "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext" | "noscript"
2475    )
2476}
2477
2478/// Elements that serialise with no end tag.
2479fn is_void_element(name: &LocalName) -> bool {
2480    matches!(
2481        name.as_ref(),
2482        "area"
2483            | "base"
2484            | "basefont"
2485            | "bgsound"
2486            | "br"
2487            | "col"
2488            | "embed"
2489            | "frame"
2490            | "hr"
2491            | "img"
2492            | "input"
2493            | "keygen"
2494            | "link"
2495            | "meta"
2496            | "param"
2497            | "source"
2498            | "track"
2499            | "wbr"
2500    )
2501}
2502
2503/// Escape a string as the HTML serialiser does: `&` and no-break space
2504/// always, `"` in attribute mode, `<` and `>` otherwise.
2505fn escape_html(text: &str, attribute: bool, writer: &mut String) {
2506    for ch in text.chars() {
2507        match ch {
2508            '&' => writer.push_str("&amp;"),
2509            '\u{a0}' => writer.push_str("&nbsp;"),
2510            '"' if attribute => writer.push_str("&quot;"),
2511            '<' if !attribute => writer.push_str("&lt;"),
2512            '>' if !attribute => writer.push_str("&gt;"),
2513            _ => writer.push(ch),
2514        }
2515    }
2516}
2517
2518#[cfg(test)]
2519mod test {
2520    use style_dom::ElementState;
2521
2522    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
2523
2524    #[test]
2525    fn absolute_position_tolerates_a_detached_layout_parent() {
2526        let mut document = BaseDocument::new(DocumentConfig::default());
2527        let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
2528            qual_name!("section"),
2529            vec![],
2530        ))));
2531        let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
2532            qual_name!("button"),
2533            vec![],
2534        ))));
2535        document
2536            .get_node(child)
2537            .unwrap()
2538            .layout_parent
2539            .set(Some(parent));
2540
2541        // An event handler may detach its own ancestor before the renderer's
2542        // default-action tail finishes computing event-relative coordinates.
2543        // The child object can still be alive through that dispatch even though
2544        // its recorded layout parent has already left the slot map.
2545        document.remove_node_from_tree(parent);
2546
2547        assert_eq!(
2548            document
2549                .get_node(child)
2550                .unwrap()
2551                .absolute_position(4.0, 7.0),
2552            crate::util::Point { x: 4.0, y: 7.0 },
2553        );
2554    }
2555
2556    #[test]
2557    fn create_node_with_disabled_attr() {
2558        let mut document = BaseDocument::new(DocumentConfig::default());
2559        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2560            qual_name!("button"),
2561            vec![Attribute {
2562                name: qual_name!("disabled"),
2563                value: "".into(),
2564            }],
2565        ))));
2566        let node = document.get_node(node).unwrap();
2567
2568        assert!(
2569            node.element_state().contains(ElementState::DISABLED),
2570            "form node is disabled"
2571        );
2572        assert!(
2573            !node.element_state().contains(ElementState::ENABLED),
2574            "form node is not enabled"
2575        );
2576    }
2577
2578    #[test]
2579    fn ignore_disabled_attr_content() {
2580        let mut document = BaseDocument::new(DocumentConfig::default());
2581        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2582            qual_name!("button"),
2583            vec![Attribute {
2584                name: qual_name!("disabled"),
2585                value: "false".into(),
2586            }],
2587        ))));
2588        let node = document.get_node(node).unwrap();
2589
2590        assert!(
2591            node.element_state().contains(ElementState::DISABLED),
2592            "form node is disabled"
2593        );
2594        assert!(
2595            !node.element_state().contains(ElementState::ENABLED),
2596            "form node is not enabled"
2597        );
2598    }
2599
2600    #[test]
2601    fn create_node_with_ignored_disable() {
2602        let mut document = BaseDocument::new(DocumentConfig::default());
2603        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2604            qual_name!("a"),
2605            vec![Attribute {
2606                name: qual_name!("disabled"),
2607                value: "".into(),
2608            }],
2609        ))));
2610        let node = document.get_node(node).unwrap();
2611
2612        assert!(
2613            !node.element_state().contains(ElementState::DISABLED),
2614            "Non form node cannot be disabled"
2615        );
2616        assert!(
2617            !node.element_state().contains(ElementState::ENABLED),
2618            "Non form node cannot be enabled"
2619        );
2620    }
2621
2622    #[test]
2623    fn create_empty_enabled_node() {
2624        let mut document = BaseDocument::new(DocumentConfig::default());
2625        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2626            qual_name!("button"),
2627            vec![],
2628        ))));
2629        let node = document.get_node(node).unwrap();
2630
2631        assert!(
2632            node.element_state().contains(ElementState::ENABLED),
2633            "Button should be enabled by default"
2634        );
2635    }
2636}