Skip to main content

blitz_dom/node/
node.rs

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