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    pub(crate) slottables: Vec<NodeId>,
954    pub(crate) selector_flags: Cell<ElementSelectorFlags>,
955}
956
957impl std::fmt::Debug for ShadowRootData {
958    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
959        formatter
960            .debug_struct("ShadowRootData")
961            .field("host", &self.host)
962            .field("mode", &self.mode)
963            .field("declarative", &self.declarative)
964            .field("clonable", &self.clonable)
965            .field("serializable", &self.serializable)
966            .field("delegates_focus", &self.delegates_focus)
967            .field("stylesheet_nodes", &self.stylesheet_nodes)
968            .field("cascade_dirty", &self.cascade_dirty)
969            .field("slots", &self.slots)
970            .finish()
971    }
972}
973
974impl ShadowRootData {
975    pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
976        Self {
977            host,
978            mode,
979            declarative: false,
980            clonable: false,
981            serializable: false,
982            delegates_focus: false,
983            stylesheet_nodes: Vec::new(),
984            cascade_data: None,
985            cascade_dirty: true,
986            slots: Vec::new(),
987            slottables: Vec::new(),
988            selector_flags: Cell::new(ElementSelectorFlags::empty()),
989        }
990    }
991}
992
993/// Document syntax that generates no layout box.
994#[derive(Debug, PartialEq, Eq)]
995pub enum MarkupNode {
996    ProcessingInstruction {
997        target: String,
998    },
999    Doctype {
1000        name: String,
1001        public_id: String,
1002        system_id: String,
1003    },
1004}
1005
1006/// The different kinds of nodes in the DOM.
1007#[derive(Debug, Clone)]
1008pub enum NodeData {
1009    /// The `Document` itself - the root node of a HTML document.
1010    Document(Box<DocumentData>),
1011
1012    /// An element with attributes.
1013    Element(Box<ElementData>),
1014
1015    /// An anonymous block box
1016    AnonymousBlock(Box<ElementData>),
1017
1018    /// A text node.
1019    Text(TextNodeData),
1020
1021    /// A comment.
1022    Comment {
1023        /// The textual content of the comment
1024        contents: String,
1025    },
1026
1027    /// A `DocumentFragment`: a parentless container whose children move when
1028    /// it is inserted, and which never appears in the box tree itself.
1029    DocumentFragment,
1030
1031    /// The root of a shadow tree attached to a host element.
1032    ShadowRoot(ShadowRootData),
1033    // /// A `DOCTYPE` with name, public id, and system id. See
1034    // /// [document type declaration on wikipedia][https://en.wikipedia.org/wiki/Document_type_declaration]
1035    // Doctype { name: String, public_id: String, system_id: String },
1036
1037    // /// A Processing instruction.
1038    // ProcessingInstruction { target: String, contents: String },
1039}
1040
1041impl NodeData {
1042    pub fn downcast_element(&self) -> Option<&ElementData> {
1043        match self {
1044            Self::Element(data) => Some(data),
1045            Self::AnonymousBlock(data) => Some(data),
1046            _ => None,
1047        }
1048    }
1049
1050    pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
1051        match self {
1052            Self::Element(data) => Some(data),
1053            Self::AnonymousBlock(data) => Some(data),
1054            _ => None,
1055        }
1056    }
1057
1058    pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
1059        let Some(elem) = self.downcast_element() else {
1060            return false;
1061        };
1062        *name == elem.name.local
1063    }
1064
1065    pub fn attrs(&self) -> Option<&[Attribute]> {
1066        Some(&self.downcast_element()?.attrs)
1067    }
1068
1069    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
1070        self.downcast_element()?.attr(name)
1071    }
1072
1073    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
1074        self.downcast_element()
1075            .is_some_and(|elem| elem.has_attr(name))
1076    }
1077
1078    pub fn kind(&self) -> NodeKind {
1079        match self {
1080            NodeData::Document(_) => NodeKind::Document,
1081            NodeData::Element(_) => NodeKind::Element,
1082            NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
1083            NodeData::Text(_) => NodeKind::Text,
1084            NodeData::Comment { .. } => NodeKind::Comment,
1085            NodeData::DocumentFragment => NodeKind::DocumentFragment,
1086            NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
1087        }
1088    }
1089
1090    pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1091        match self {
1092            Self::ShadowRoot(data) => Some(data),
1093            _ => None,
1094        }
1095    }
1096
1097    pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1098        match self {
1099            Self::ShadowRoot(data) => Some(data),
1100            _ => None,
1101        }
1102    }
1103}
1104
1105#[derive(Debug, Clone)]
1106pub struct TextNodeData {
1107    /// The textual content of the text node
1108    pub content: String,
1109}
1110
1111impl TextNodeData {
1112    pub fn new(content: String) -> Self {
1113        Self { content }
1114    }
1115}
1116
1117/*
1118-> Computed styles
1119-> Layout
1120-----> Needs to happen only when styles are computed
1121*/
1122
1123// type DomRefCell<T> = RefCell<T>;
1124
1125// pub struct DomData {
1126//     // ... we can probs just get away with using the html5ever types directly. basically just using the servo dom, but without the bindings
1127//     local_name: html5ever::LocalName,
1128//     tag_name: html5ever::QualName,
1129//     namespace: html5ever::Namespace,
1130//     prefix: DomRefCell<Option<html5ever::Prefix>>,
1131//     attrs: DomRefCell<Vec<Attr>>,
1132//     // attrs: DomRefCell<Vec<Dom<Attr>>>,
1133//     id_attribute: DomRefCell<Option<Atom>>,
1134//     is: DomRefCell<Option<LocalName>>,
1135//     // style_attribute: DomRefCell<Option<Arc<Locked<PropertyDeclarationBlock>>>>,
1136//     // attr_list: MutNullableDom<NamedNodeMap>,
1137//     // class_list: MutNullableDom<DOMTokenList>,
1138//     state: Cell<ElementState>,
1139// }
1140
1141impl Node {
1142    pub fn tree(&self) -> &crate::NodeTree {
1143        unsafe { &*self.tree }
1144    }
1145
1146    pub fn is_html_document(&self) -> bool {
1147        let document = match &self.data {
1148            NodeData::Document(data) => return data.content_type == "text/html",
1149            _ => self.owner_document.and_then(|id| self.tree().get(id)),
1150        };
1151        match document.map(|node| &node.data) {
1152            Some(NodeData::Document(data)) => data.content_type == "text/html",
1153            _ => true,
1154        }
1155    }
1156
1157    pub fn qualified_element_name(&self) -> Option<String> {
1158        let element = self.element_data()?;
1159        let name = match &element.name.prefix {
1160            Some(prefix) => format!("{prefix}:{}", element.name.local),
1161            None => element.name.local.to_string(),
1162        };
1163        Some(
1164            if self.is_html_document() && element.name.ns == markup5ever::ns!(html) {
1165                name.to_ascii_uppercase()
1166            } else {
1167                name
1168            },
1169        )
1170    }
1171
1172    #[track_caller]
1173    pub fn with(&self, id: NodeId) -> &Node {
1174        self.tree().get(id).unwrap()
1175    }
1176
1177    pub fn print_tree(&self, level: usize) {
1178        println!(
1179            "{} {} {:?} {} {:?}",
1180            "  ".repeat(level),
1181            self.id,
1182            self.parent,
1183            self.node_debug_str().replace('\n', ""),
1184            self.children
1185        );
1186        // println!("{} {:?}", "  ".repeat(level), self.children);
1187        for child_id in self.children.iter() {
1188            let child = self.with(*child_id);
1189            child.print_tree(level + 1)
1190        }
1191    }
1192
1193    // Get the index of the current node in the parents child list
1194    pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
1195        self.children.iter().position(|id| *id == child_id)
1196    }
1197
1198    // Get the index of the current node in the parents child list
1199    pub fn child_index(&self) -> Option<usize> {
1200        let children = &self.tree()[self.parent?].children;
1201        let hint = self.child_index_hint.get();
1202        if children.get(hint) == Some(&self.id) {
1203            return Some(hint);
1204        }
1205        let index = children.iter().position(|id| *id == self.id)?;
1206        self.child_index_hint.set(index);
1207        Some(index)
1208    }
1209
1210    /// Read a DOM child and seed its sibling traversal hint.
1211    pub fn dom_child_at(&self, index: usize) -> Option<NodeId> {
1212        let id = *self.children.get(index)?;
1213        self.with(id).child_index_hint.set(index);
1214        Some(id)
1215    }
1216
1217    // Get the nth node in the parents child list
1218    pub fn forward(&self, n: usize) -> Option<&Node> {
1219        let index = self.child_index()?.checked_add(n)?;
1220        let id = *self.tree()[self.parent?].children.get(index)?;
1221        let node = self.with(id);
1222        node.child_index_hint.set(index);
1223        Some(node)
1224    }
1225
1226    pub fn backward(&self, n: usize) -> Option<&Node> {
1227        let index = self.child_index()?.checked_sub(n)?;
1228        let id = *self.tree()[self.parent?].children.get(index)?;
1229        let node = self.with(id);
1230        node.child_index_hint.set(index);
1231        Some(node)
1232    }
1233
1234    pub fn is_element(&self) -> bool {
1235        matches!(self.data, NodeData::Element { .. })
1236    }
1237
1238    pub fn is_anonymous(&self) -> bool {
1239        matches!(self.data, NodeData::AnonymousBlock { .. })
1240    }
1241
1242    pub fn is_shadow_root(&self) -> bool {
1243        matches!(self.data, NodeData::ShadowRoot { .. })
1244    }
1245
1246    pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1247        self.data.shadow_root_data()
1248    }
1249
1250    pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1251        self.data.shadow_root_data_mut()
1252    }
1253
1254    /// If this node is a shadow host (i.e. has an attached shadow root),
1255    /// returns the node id of its shadow root.
1256    pub fn shadow_root_id(&self) -> Option<NodeId> {
1257        self.element_data().and_then(|el| el.shadow_root)
1258    }
1259
1260    /// The parent corresponding to `layout_dom_children`.
1261    ///
1262    /// Shadow roots are transparent, assigned nodes inherit through their
1263    /// slot, and undistributed light children and suppressed fallback children
1264    /// have no edge into the flattened tree.
1265    #[inline]
1266    pub(crate) fn flattened_parent(&self) -> Option<&Node> {
1267        #[cfg(feature = "shadow-dom")]
1268        {
1269            if self.is_shadow_root() {
1270                return None;
1271            }
1272            if let Some(slot_id) = self.assigned_slot {
1273                return Some(self.with(slot_id));
1274            }
1275            let parent = self.with(self.parent?);
1276            if let Some(root) = parent.shadow_root_data() {
1277                return Some(self.with(root.host));
1278            }
1279            if parent.shadow_root_id().is_some()
1280                || parent
1281                    .assigned_nodes
1282                    .as_ref()
1283                    .is_some_and(|nodes| !nodes.is_empty())
1284            {
1285                return None;
1286            }
1287            Some(parent)
1288        }
1289        #[cfg(not(feature = "shadow-dom"))]
1290        {
1291            self.parent.map(|id| self.with(id))
1292        }
1293    }
1294
1295    /// The children to use for layout and painting. For shadow hosts and
1296    /// `<slot>` elements this is the "flattened tree" children; for all other
1297    /// nodes it is the regular DOM [`children`](Self::children).
1298    #[cfg(feature = "shadow-dom")]
1299    pub fn layout_dom_children(&self) -> &[NodeId] {
1300        match &self.flattened_children {
1301            Some(children) => children,
1302            None => &self.children,
1303        }
1304    }
1305
1306    /// The children to use for layout and painting.
1307    #[cfg(not(feature = "shadow-dom"))]
1308    #[inline(always)]
1309    pub fn layout_dom_children(&self) -> &[NodeId] {
1310        &self.children
1311    }
1312
1313    pub fn is_text_node(&self) -> bool {
1314        matches!(self.data, NodeData::Text { .. })
1315    }
1316
1317    pub fn element_data(&self) -> Option<&ElementData> {
1318        match self.data {
1319            NodeData::Element(ref data) => Some(data),
1320            NodeData::AnonymousBlock(ref data) => Some(data),
1321            _ => None,
1322        }
1323    }
1324
1325    pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1326        match self.data {
1327            NodeData::Element(ref mut data) => Some(data),
1328            NodeData::AnonymousBlock(ref mut data) => Some(data),
1329            _ => None,
1330        }
1331    }
1332
1333    pub fn text_data(&self) -> Option<&TextNodeData> {
1334        match self.data {
1335            NodeData::Text(ref data) => Some(data),
1336            _ => None,
1337        }
1338    }
1339
1340    pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1341        match self.data {
1342            NodeData::Text(ref mut data) => Some(data),
1343            _ => None,
1344        }
1345    }
1346
1347    pub fn node_debug_str(&self) -> String {
1348        let mut s = String::new();
1349
1350        match &self.data {
1351            NodeData::Document(_) => write!(s, "DOCUMENT"),
1352            NodeData::DocumentFragment => write!(s, "FRAGMENT"),
1353            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1354            NodeData::Text(data) => {
1355                let bytes = data.content.as_bytes();
1356                write!(
1357                    s,
1358                    "TEXT {}",
1359                    std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1360                        .unwrap_or("INVALID UTF8")
1361                )
1362            }
1363            NodeData::Comment { .. } => write!(s, "COMMENT"),
1364            NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1365            NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1366            NodeData::Element(data) => {
1367                let name = &data.name;
1368                let class = self.attr(local_name!("class")).unwrap_or("");
1369                let id = self.attr(local_name!("id")).unwrap_or("");
1370                let display = self.display_constructed_as().to_css_string();
1371                write!(s, "<{}", name.local).unwrap();
1372                if !id.is_empty() {
1373                    write!(s, " #{id}").unwrap();
1374                }
1375                if !class.is_empty() {
1376                    if class.contains(' ') {
1377                        write!(s, " class=\"{class}\"").unwrap()
1378                    } else {
1379                        write!(s, " .{class}").unwrap()
1380                    }
1381                }
1382                write!(s, "> ({display})")
1383            } // NodeData::ProcessingInstruction { .. } => write!(s, "ProcessingInstruction"),
1384        }
1385        .unwrap();
1386        s
1387    }
1388
1389    /// Renders the HTML of this node and all its children as a `String` without extra whitespace.
1390    ///
1391    /// Example output:
1392    ///
1393    /// ```text
1394    /// <html><head /><body><main id="main"><div class="arbitrary-class" /></main></body></html>
1395    /// ```
1396    pub fn outer_html(&self) -> String {
1397        let mut output = String::new();
1398        self.write_outer_html(&mut output);
1399        output
1400    }
1401
1402    /// Renders the HTML of this node and all its children as a `String` with whitespace for human
1403    /// readability.
1404    ///
1405    /// Example output:
1406    ///
1407    /// ```text
1408    /// <html>
1409    ///   <head />
1410    ///   <body>
1411    ///     <main id="main">
1412    ///       <div class="arbitrary-class" />
1413    ///     </main>
1414    ///   </body>
1415    /// </html>
1416    /// ```
1417    pub fn outer_html_pretty(&self) -> String {
1418        let mut output = String::new();
1419        self.write_outer_html_pretty(&mut output);
1420        output
1421    }
1422
1423    pub fn write_outer_html(&self, writer: &mut String) {
1424        self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1425    }
1426
1427    #[cfg(feature = "svg")]
1428    pub(crate) fn write_outer_html_with_current_color(
1429        &self,
1430        writer: &mut String,
1431        current_color: &str,
1432    ) {
1433        self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1434    }
1435
1436    #[cfg(feature = "svg")]
1437    pub(crate) fn write_outer_html_with_svg_ids(
1438        &self,
1439        writer: &mut String,
1440        ids: &std::collections::HashMap<NodeId, String>,
1441    ) {
1442        self.write_outer_html_in_style_with_svg_ids(
1443            writer,
1444            OutputStyle::Normal,
1445            0,
1446            None,
1447            Some(ids),
1448        );
1449    }
1450
1451    pub fn write_outer_html_pretty(&self, writer: &mut String) {
1452        self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1453    }
1454
1455    fn write_outer_html_in_style(
1456        &self,
1457        writer: &mut String,
1458        style: OutputStyle,
1459        nesting: usize,
1460        current_color_override: Option<&str>,
1461    ) {
1462        self.write_outer_html_in_style_with_svg_ids(
1463            writer,
1464            style,
1465            nesting,
1466            current_color_override,
1467            None,
1468        );
1469    }
1470
1471    fn write_outer_html_in_style_with_svg_ids(
1472        &self,
1473        writer: &mut String,
1474        style: OutputStyle,
1475        nesting: usize,
1476        current_color_override: Option<&str>,
1477        svg_ids: Option<&std::collections::HashMap<NodeId, String>>,
1478    ) {
1479        const INDENT: &str = "  ";
1480        let has_children = !self.children.is_empty();
1481        let computed_current_color = || {
1482            self.primary_styles()
1483                .map(|style| style.clone_color())
1484                .map(|color| crate::util::absolute_color_to_svg_css(&color))
1485        };
1486        let current_color = current_color_override
1487            .map(ToOwned::to_owned)
1488            .or_else(computed_current_color);
1489
1490        match &self.data {
1491            NodeData::Document(_) => {}
1492            NodeData::Comment { .. } => {}
1493            NodeData::AnonymousBlock(_) => {}
1494            NodeData::ShadowRoot(_) => {}
1495            // A fragment serialises as nothing of its own: the spec says its
1496            // children are what gets inserted, and it is never in a document
1497            // to be serialised from in the first place.
1498            NodeData::DocumentFragment => {}
1499            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1500            NodeData::Text(data) => {
1501                if matches!(style, OutputStyle::Pretty) {
1502                    for _ in 0..nesting {
1503                        writer.push_str(INDENT);
1504                    }
1505                }
1506                writer.push_str(data.content.as_str());
1507                if matches!(style, OutputStyle::Pretty) {
1508                    writer.push('\n');
1509                }
1510            }
1511            NodeData::Element(data) => {
1512                if matches!(style, OutputStyle::Pretty) {
1513                    for _ in 0..nesting {
1514                        writer.push_str(INDENT);
1515                    }
1516                }
1517                // A hyperlink is a graphics container in the image source.
1518                // Its live DOM name, attributes and event ancestry stay intact.
1519                let tag_name = if svg_ids.is_some()
1520                    && data.name.ns.as_ref() == "http://www.w3.org/2000/svg"
1521                    && data.name.local == local_name!("a")
1522                {
1523                    "g"
1524                } else {
1525                    data.name.local.as_ref()
1526                };
1527                let svg_id = svg_ids.and_then(|ids| ids.get(&self.id));
1528                writer.push('<');
1529                writer.push_str(tag_name);
1530                if let Some(id) = svg_id {
1531                    writer.push_str(" id=\"");
1532                    encode_quoted_attribute_to_string(id, writer);
1533                    writer.push('"');
1534                }
1535
1536                for attr in data.attrs() {
1537                    if svg_id.is_some()
1538                        && attr.name.ns.as_ref().is_empty()
1539                        && attr.name.local == local_name!("id")
1540                    {
1541                        continue;
1542                    }
1543                    writer.push(' ');
1544                    if let Some(prefix) = attr
1545                        .name
1546                        .prefix
1547                        .as_ref()
1548                        .filter(|prefix| !prefix.is_empty())
1549                    {
1550                        writer.push_str(prefix);
1551                        writer.push(':');
1552                    }
1553                    writer.push_str(&attr.name.local);
1554                    writer.push_str("=\"");
1555                    #[allow(clippy::unnecessary_unwrap)] // Convert to if-let chain once stabilised
1556                    if current_color.is_some() && attr.value.contains("currentColor") {
1557                        let value = attr
1558                            .value
1559                            .replace("currentColor", current_color.as_ref().unwrap());
1560                        encode_quoted_attribute_to_string(&value, writer);
1561                    } else {
1562                        encode_quoted_attribute_to_string(&attr.value, writer);
1563                    }
1564                    writer.push('"');
1565                }
1566                if !has_children {
1567                    writer.push_str(" /");
1568                }
1569                writer.push('>');
1570                if matches!(style, OutputStyle::Pretty) {
1571                    writer.push('\n');
1572                }
1573
1574                if has_children {
1575                    for &child_id in &self.children {
1576                        self.tree()[child_id].write_outer_html_in_style_with_svg_ids(
1577                            writer,
1578                            style,
1579                            nesting + 1,
1580                            current_color_override,
1581                            svg_ids,
1582                        );
1583                    }
1584
1585                    if matches!(style, OutputStyle::Pretty) {
1586                        for _ in 0..nesting {
1587                            writer.push_str(INDENT);
1588                        }
1589                    }
1590                    writer.push_str("</");
1591                    writer.push_str(tag_name);
1592                    writer.push('>');
1593                    if matches!(style, OutputStyle::Pretty) {
1594                        writer.push('\n');
1595                    }
1596                }
1597            }
1598        }
1599    }
1600
1601    /// Serialise this node as the HTML fragment serialisation algorithm does
1602    /// for `outerHTML`: void elements have no end tag, every other element
1603    /// has one, text and attribute values are escaped, comments are kept.
1604    ///
1605    /// Unlike [`Self::outer_html`], which is a debug rendering (`<div />`),
1606    /// this output parses back to the same tree, which `innerHTML +=` and
1607    /// templating code depend on.
1608    pub fn write_html(&self, writer: &mut String) {
1609        match &self.data {
1610            NodeData::Text(data) => {
1611                let raw = self
1612                    .parent
1613                    .and_then(|parent| self.tree()[parent].element_data())
1614                    .is_some_and(|parent| is_raw_text_element(&parent.name.local));
1615                if raw {
1616                    writer.push_str(&data.content);
1617                } else {
1618                    escape_html(&data.content, false, writer);
1619                }
1620            }
1621            NodeData::Comment { contents } => {
1622                writer.push_str("<!--");
1623                writer.push_str(contents);
1624                writer.push_str("-->");
1625            }
1626            NodeData::Element(data) => {
1627                writer.push('<');
1628                writer.push_str(&data.name.local);
1629                for attr in data.attrs() {
1630                    writer.push(' ');
1631                    if let Some(prefix) = attr
1632                        .name
1633                        .prefix
1634                        .as_ref()
1635                        .filter(|prefix| !prefix.is_empty())
1636                    {
1637                        writer.push_str(prefix);
1638                        writer.push(':');
1639                    }
1640                    writer.push_str(&attr.name.local);
1641                    writer.push_str("=\"");
1642                    escape_html(&attr.value, true, writer);
1643                    writer.push('"');
1644                }
1645                writer.push('>');
1646                if is_void_element(&data.name.local) {
1647                    return;
1648                }
1649                self.write_children_html(writer);
1650                writer.push_str("</");
1651                writer.push_str(&data.name.local);
1652                writer.push('>');
1653            }
1654            NodeData::Document(_)
1655            | NodeData::DocumentFragment
1656            | NodeData::ShadowRoot(_)
1657            | NodeData::AnonymousBlock(_) => self.write_children_html(writer),
1658        }
1659    }
1660
1661    /// Serialise this node's children, as `innerHTML` returns them.
1662    pub fn write_children_html(&self, writer: &mut String) {
1663        if let Some(contents) = self
1664            .element_data()
1665            .and_then(|element| element.template_contents)
1666        {
1667            self.tree()[contents].write_children_html(writer);
1668            return;
1669        }
1670        for &child_id in &self.children {
1671            self.tree()[child_id].write_html(writer);
1672        }
1673    }
1674
1675    pub fn attrs(&self) -> Option<&[Attribute]> {
1676        Some(&self.element_data()?.attrs)
1677    }
1678
1679    pub fn attr(&self, name: LocalName) -> Option<&str> {
1680        let attr = self
1681            .attrs()?
1682            .iter()
1683            .find(|attr| attr.name.ns == markup5ever::ns!() && attr.name.local == name)?;
1684        Some(&attr.value)
1685    }
1686
1687    pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1688        self.stylo_element_data_opt()
1689            .and_then(|stylo| stylo.primary_styles())
1690    }
1691
1692    pub fn text_content(&self) -> String {
1693        let mut out = String::new();
1694        self.write_text_content(&mut out);
1695        out
1696    }
1697
1698    /// HTML's rendered-text getter. Text comes from the inline layout's
1699    /// processed buffer, rather than reconstructing whitespace from source.
1700    /// Callers must flush layout before invoking this.
1701    pub fn rendered_inner_text(&self) -> String {
1702        use style::computed_values::visibility::T as Visibility;
1703
1704        enum Piece {
1705            Text(String),
1706            RequiredBreak(usize),
1707        }
1708
1709        fn text(node: &Node) -> String {
1710            let NodeData::Text(data) = &node.data else {
1711                return String::new();
1712            };
1713            // Text that never reached an inline layout is not rendered.
1714            if node.inline_root_ancestor().is_none() {
1715                return String::new();
1716            }
1717            let preserves = node
1718                .parent
1719                .map(|id| node.with(id))
1720                .and_then(Node::primary_styles)
1721                .is_some_and(|style| {
1722                    style.get_inherited_text().white_space_collapse
1723                        != style::computed_values::white_space_collapse::T::Collapse
1724                });
1725            if preserves {
1726                return data.content.to_string();
1727            }
1728            // Collapsing white space: every run of spaces, tabs and newlines
1729            // is one space, as the line layout rendered it.
1730            let mut result = String::with_capacity(data.content.len());
1731            let mut space = false;
1732            for ch in data.content.chars() {
1733                if matches!(ch, ' ' | '\t' | '\n' | '\r' | '\u{c}') {
1734                    space = true;
1735                } else {
1736                    if space {
1737                        result.push(' ');
1738                        space = false;
1739                    }
1740                    result.push(ch);
1741                }
1742            }
1743            if space {
1744                result.push(' ');
1745            }
1746            result
1747        }
1748        fn collect(node: &Node, pieces: &mut Vec<Piece>) {
1749            if node.is_element() && node.is_display_none() {
1750                return;
1751            }
1752            let visible = node
1753                .primary_styles()
1754                .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1755            if node.is_text_node() {
1756                let visible = node
1757                    .parent
1758                    .map(|id| node.with(id))
1759                    .and_then(Node::primary_styles)
1760                    .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1761                if visible {
1762                    let value = text(node);
1763                    if !value.is_empty() {
1764                        pieces.push(Piece::Text(value));
1765                    }
1766                }
1767                return;
1768            }
1769            if !node.is_element() {
1770                return;
1771            }
1772            let tag = node.element_data().expect("element").name.local.as_ref();
1773            if tag == "br" {
1774                if visible {
1775                    pieces.push(Piece::Text("\n".to_owned()));
1776                }
1777                return;
1778            }
1779            let display = node.primary_styles().map(|style| style.clone_display());
1780            let block = display.is_some_and(|display| {
1781                display.outside() == DisplayOutside::Block && !display.is_contents()
1782            });
1783            let breaks = if tag == "p" { 2 } else { usize::from(block) };
1784            if breaks != 0 {
1785                pieces.push(Piece::RequiredBreak(breaks));
1786            }
1787            for child in &node.children {
1788                collect(node.with(*child), pieces);
1789            }
1790            if visible {
1791                if matches!(tag, "td" | "th") {
1792                    let next_cell = node
1793                        .parent
1794                        .map(|parent| node.with(parent))
1795                        .and_then(|parent| {
1796                            parent.index_of_child(node.id).map(|index| (parent, index))
1797                        })
1798                        .is_some_and(|(parent, index)| {
1799                            parent.children[index + 1..].iter().any(|id| {
1800                                let child = parent.with(*id);
1801                                child.data.is_element_with_tag_name(&local_name!("td"))
1802                                    || child.data.is_element_with_tag_name(&local_name!("th"))
1803                            })
1804                        });
1805                    if next_cell {
1806                        pieces.push(Piece::Text("\t".to_owned()));
1807                    }
1808                } else if tag == "tr" {
1809                    pieces.push(Piece::RequiredBreak(1));
1810                }
1811            }
1812            if breaks != 0 {
1813                pieces.push(Piece::RequiredBreak(breaks));
1814            }
1815        }
1816
1817        let mut current = Some(self);
1818        while let Some(node) = current {
1819            if !self.flags.is_in_document() || node.is_display_none() {
1820                return self.text_content();
1821            }
1822            current = node.parent.map(|id| node.with(id));
1823        }
1824        if self.primary_styles().is_none() {
1825            return self.text_content();
1826        }
1827        let mut pieces = Vec::new();
1828        collect(self, &mut pieces);
1829        let mut result = String::new();
1830        let mut pending = 0;
1831        for piece in pieces {
1832            match piece {
1833                Piece::RequiredBreak(count) => pending = pending.max(count),
1834                Piece::Text(value) => {
1835                    if !result.is_empty() && pending != 0 {
1836                        result.truncate(result.trim_end_matches(' ').len());
1837                        for _ in 0..pending {
1838                            result.push('\n');
1839                        }
1840                    }
1841                    pending = 0;
1842                    if result.is_empty() || result.ends_with('\n') || result.ends_with(' ') {
1843                        result.push_str(value.trim_start_matches(' '));
1844                    } else {
1845                        result.push_str(&value);
1846                    }
1847                }
1848            }
1849        }
1850        result.truncate(result.trim_end_matches(' ').len());
1851        result
1852    }
1853
1854    /// CSSOM View's offset parent, excluding nodes without CSS boxes and
1855    /// viewport-fixed elements.
1856    pub fn cssom_offset_parent(&self) -> Option<&Node> {
1857        if !self.flags.is_in_document()
1858            || self.data.is_element_with_tag_name(&local_name!("body"))
1859            || self.data.is_element_with_tag_name(&local_name!("html"))
1860        {
1861            return None;
1862        }
1863        let style = self.primary_styles()?;
1864        if style.clone_position() == Position::Fixed || style.clone_display().is_contents() {
1865            return None;
1866        }
1867        let mut current = Some(self);
1868        while let Some(node) = current {
1869            if node.is_display_none() {
1870                return None;
1871            }
1872            current = node.parent.map(|id| node.with(id));
1873        }
1874        let static_position = style.clone_position() == Position::Static;
1875        let zoom = style.effective_zoom.zoom(1.0);
1876        let mut parent = self.parent.map(|id| self.with(id));
1877        while let Some(node) = parent {
1878            if let Some(style) = node.primary_styles()
1879                && !style.clone_display().is_contents()
1880                && (node.is_offset_parent()
1881                    || style.effective_zoom.zoom(1.0) != zoom
1882                    || (static_position
1883                        && node.data.is_element_with_tag_name(&local_name!("table"))))
1884            {
1885                return Some(node);
1886            }
1887            parent = node.parent.map(|id| node.with(id));
1888        }
1889        None
1890    }
1891
1892    /// Write this subtree's text content into any [`std::fmt::Write`] sink.
1893    ///
1894    /// Public and generic so that a caller which does not want a `String` does
1895    /// not have to fork this traversal to avoid one. `blitz-dom-api`'s
1896    /// buffer-writing reader passes a sink that counts, and then one that fills
1897    /// a caller-supplied slice; a private copy of this walk in that crate would
1898    /// silently disagree with this one the first time a `NodeData` variant is
1899    /// added here.
1900    ///
1901    /// The sinks callers pass do not fail, and `String`'s never has, so nothing
1902    /// in this crate inspects the `Result`. It is kept in the signature because
1903    /// it is `fmt::Write`'s, not because there is an error to handle.
1904    pub fn write_text_content<W: Write>(&self, out: &mut W) {
1905        match &self.data {
1906            NodeData::Text(data) => {
1907                let _ = out.write_str(&data.content);
1908            }
1909            NodeData::Element(..)
1910            | NodeData::AnonymousBlock(..)
1911            | NodeData::Document(..)
1912            | NodeData::DocumentFragment
1913            | NodeData::ShadowRoot(..) => {
1914                for child_id in self.children.iter() {
1915                    self.with(*child_id).write_text_content(out);
1916                }
1917            }
1918            _ => {}
1919        }
1920    }
1921
1922    pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1923        if let NodeData::Element(ref mut elem_data) = self.data {
1924            if let Some(guard) = elem_data.guard.clone() {
1925                elem_data.flush_style_attribute(&guard, url_extra_data);
1926            }
1927        }
1928    }
1929
1930    pub fn order(&self) -> i32 {
1931        self.primary_styles()
1932            .map(|s| match s.pseudo() {
1933                Some(PseudoElement::Before) => i32::MIN,
1934                Some(PseudoElement::After) => i32::MAX,
1935                _ => s.clone_order(),
1936            })
1937            .unwrap_or(0)
1938    }
1939
1940    pub fn z_index(&self) -> i32 {
1941        self.primary_styles()
1942            .map(|s| s.clone_z_index().integer_or(0))
1943            .unwrap_or(0)
1944    }
1945
1946    // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts
1947    pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1948        let Some(style) = self.primary_styles() else {
1949            return false;
1950        };
1951
1952        let position = style.clone_position();
1953        let has_z_index = !style.clone_z_index().is_auto();
1954
1955        if style.clone_opacity() != 1.0 {
1956            return true;
1957        }
1958
1959        let position_based = match position {
1960            Position::Fixed | Position::Sticky => true,
1961            Position::Relative | Position::Absolute => has_z_index,
1962            Position::Static => has_z_index && is_flex_or_grid_item,
1963        };
1964        if position_based {
1965            return true;
1966        }
1967
1968        // This runs while the stacking-context tree is built, before
1969        // `resolve_transforms` populates the cached device-space matrix. Using
1970        // that cache here makes the first frame treat a transformed node as an
1971        // ordinary box, hoist its painted children into the parent, and only
1972        // repair the tree after the first hover/restyle. The computed CSS value
1973        // is already available and is the source of truth for whether the node
1974        // establishes a stacking context.
1975        let box_styles = style.get_box();
1976        if !box_styles.transform.0.is_empty()
1977            || !matches!(box_styles.translate, Translate::None)
1978            || !matches!(box_styles.rotate, Rotate::None)
1979            || !matches!(box_styles.scale, Scale::None)
1980        {
1981            return true;
1982        }
1983
1984        // `isolation: isolate` exists precisely to create a stacking context
1985        // without any other visual effect. Ignoring it lets a negative z-index
1986        // descendant escape to an ancestor context, where it is painted before
1987        // (and so underneath) the backgrounds of the boxes in between.
1988        if box_styles.isolation == Isolation::Isolate {
1989            return true;
1990        }
1991
1992        // TODO: mix-blend-mode
1993        // TODO: filter
1994        // TODO: clip-path
1995        // TODO: mask
1996        // TODO: contain
1997
1998        false
1999    }
2000
2001    /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns:
2002    ///    - None if the position is outside of this node's bounds
2003    ///    - Some(HitResult) if the position is within the node but doesn't match any children
2004    ///    - The result of recursively calling child.hit() on the the child element that is
2005    ///      positioned at that position if there is one.
2006    ///
2007    /// TODO: z-index
2008    /// (If multiple children are positioned at the position then a random one will be recursed into)
2009    pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
2010        self.hit_inner(x, y, scale, &mut None)
2011    }
2012
2013    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
2014    /// thumb under the point into `scrollbar` during the same descent (so
2015    /// thumb hit-testing shares the exact coordinate handling — transforms
2016    /// included — of every other hit test).
2017    pub(crate) fn hit_inner(
2018        &self,
2019        x: f32,
2020        y: f32,
2021        scale: f64,
2022        scrollbar: &mut Option<crate::node::ScrollbarRef>,
2023    ) -> Option<HitResult> {
2024        self.hit_inner_collect(x, y, scale, scrollbar, &mut None)
2025    }
2026
2027    /// Use the input hit traversal either for the foremost hit or for every hit.
2028    pub(crate) fn hit_inner_collect(
2029        &self,
2030        x: f32,
2031        y: f32,
2032        scale: f64,
2033        scrollbar: &mut Option<crate::node::ScrollbarRef>,
2034        hits: &mut Option<Vec<HitResult>>,
2035    ) -> Option<HitResult> {
2036        use style::computed_values::pointer_events::T as PointerEvents;
2037        use style::computed_values::visibility::T as Visibility;
2038
2039        // A hidden subtree takes no hits.
2040        //
2041        // This never needed saying while hiding a pane destroyed its boxes: a
2042        // hidden subtree had nothing to test against. It keeps its boxes now,
2043        // and their `final_layout` is whatever it was when the pane was last
2044        // visible — full size, in place, over the tab in front. A retained tab
2045        // measured 331 of its 370 elements still carrying live geometry after
2046        // being hidden, and every one of them was a click target.
2047        if matches!(self.style().display, taffy::Display::None) {
2048            return None;
2049        }
2050
2051        // Don't hit on visbility:hidden elements
2052        if let Some(style) = self.primary_styles() {
2053            if matches!(
2054                style.clone_visibility(),
2055                Visibility::Hidden | Visibility::Collapse
2056            ) {
2057                return None;
2058            }
2059        }
2060
2061        // pointer-events:none makes this element transparent to hits, but its
2062        // descendants are still tested (one may restore pointer-events:auto).
2063        let pointer_events_none = self
2064            .primary_styles()
2065            .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2066
2067        let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
2068        let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
2069
2070        if let Some(t) = *self.transform() {
2071            let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
2072            x = (p.x / scale) as f32;
2073            y = (p.y / scale) as f32;
2074        }
2075
2076        let size = self.final_layout().size;
2077        let matches_self = !(x < 0.0
2078            || x > size.width + self.scroll_offset().x as f32
2079            || y < 0.0
2080            || y > size.height + self.scroll_offset().y as f32);
2081
2082        let content_size = self.final_layout().content_size;
2083        let matches_content = !(x < 0.0
2084            || x > content_size.width + self.scroll_offset().x as f32
2085            || y < 0.0
2086            || y > content_size.height + self.scroll_offset().y as f32);
2087
2088        let matches_hoisted_content = match &self.stacking_context {
2089            Some(sc) => {
2090                let content_area = sc.content_area;
2091                x >= content_area.left + self.scroll_offset().x as f32
2092                    && x <= content_area.right + self.scroll_offset().x as f32
2093                    && y >= content_area.top + self.scroll_offset().y as f32
2094                    && y <= content_area.bottom + self.scroll_offset().y as f32
2095            }
2096            None => false,
2097        };
2098
2099        // `scrollable_overflow` is stored in device (scaled) pixels, whereas the
2100        // coordinates here are in CSS pixels, so unscale it before comparing.
2101        let overflow = *self.scrollable_overflow();
2102
2103        let matches_overflow = x >= (overflow.x0 / scale) as f32
2104            && x <= (overflow.x1 / scale) as f32
2105            && y >= (overflow.y0 / scale) as f32
2106            && y <= (overflow.y1 / scale) as f32;
2107
2108        if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
2109            return None;
2110        }
2111
2112        // Descendants overwrite, so the innermost scroll container's thumb
2113        // wins. Thumb coords are border-box relative (unscrolled).
2114        if matches_self
2115            && let Some(sb) = self.scrollbar_at_local(
2116                (x - self.scroll_offset().x as f32) as f64,
2117                (y - self.scroll_offset().y as f32) as f64,
2118            )
2119        {
2120            *scrollbar = Some(sb);
2121        }
2122
2123        // Child layouts and hoisted offsets use border-box coordinates.
2124        // Only glyph hit testing below uses the inline content-box origin.
2125
2126        // Positive z_index hoisted children
2127        if matches_hoisted_content {
2128            if let Some(hoisted) = &self.stacking_context {
2129                for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
2130                    if hoisted_child.clips.iter().any(|clip| {
2131                        x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2132                    }) {
2133                        continue;
2134                    }
2135                    let x = x - hoisted_child.position.x;
2136                    let y = y - hoisted_child.position.y;
2137                    if let Some(hit) = self
2138                        .with(hoisted_child.node_id)
2139                        .hit_inner_collect(x, y, scale, scrollbar, hits)
2140                    {
2141                        return Some(hit);
2142                    }
2143                }
2144            }
2145        }
2146
2147        // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id).
2148        for child_id in self.paint_children.borrow().iter().flatten().rev() {
2149            if let Some(hit) = self
2150                .with(*child_id)
2151                .hit_inner_collect(x, y, scale, scrollbar, hits)
2152            {
2153                return Some(hit);
2154            }
2155        }
2156
2157        // Negative z_index hoisted children
2158        if matches_hoisted_content {
2159            if let Some(hoisted) = &self.stacking_context {
2160                for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
2161                    if hoisted_child.clips.iter().any(|clip| {
2162                        x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2163                    }) {
2164                        continue;
2165                    }
2166                    let x = x - hoisted_child.position.x;
2167                    let y = y - hoisted_child.position.y;
2168                    if let Some(hit) = self
2169                        .with(hoisted_child.node_id)
2170                        .hit_inner_collect(x, y, scale, scrollbar, hits)
2171                    {
2172                        return Some(hit);
2173                    }
2174                }
2175            }
2176        }
2177
2178        // Inline SVG descendants have image geometry instead of layout boxes.
2179        #[cfg(feature = "svg")]
2180        if matches_self && let Some(hit) = self.hit_inline_svg(x, y, hits) {
2181            return Some(hit);
2182        }
2183
2184        // Inline children
2185        if self.flags.is_inline_root() {
2186            x -= self.final_layout().padding.left + self.final_layout().border.left;
2187            y -= self.final_layout().padding.top + self.final_layout().border.top;
2188            let element_data = &self.element_data().unwrap();
2189            if let Some(ild) = element_data.inline_layout_data.as_ref() {
2190                let layout = &ild.layout;
2191                let scale = layout.scale();
2192
2193                if let Some((cluster, _side)) =
2194                    Cluster::from_point_exact(layout, x * scale, y * scale)
2195                {
2196                    let style_index = cluster.glyphs().next()?.style_index();
2197                    let node_id = layout.styles()[style_index].brush.id;
2198                    let text_pointer_events_none = self
2199                        .with(node_id)
2200                        .primary_styles()
2201                        .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2202                    if !text_pointer_events_none {
2203                        let hit = HitResult {
2204                            node_id,
2205                            x,
2206                            y,
2207                            is_text: true,
2208                        };
2209                        if let Some(hits) = hits.as_mut() {
2210                            hits.push(hit);
2211                        } else {
2212                            return Some(hit);
2213                        }
2214                    }
2215                }
2216            }
2217        }
2218
2219        // Self (this node)
2220        if matches_self && !pointer_events_none {
2221            let hit = HitResult {
2222                node_id: self.id,
2223                x,
2224                y,
2225                is_text: false,
2226            };
2227            if let Some(hits) = hits.as_mut() {
2228                hits.push(hit);
2229            } else {
2230                return Some(hit);
2231            }
2232        }
2233
2234        None
2235    }
2236
2237    /// Find the inline root ancestor of this node (or self if this is an inline root).
2238    /// Returns None if no inline root ancestor exists.
2239    pub fn inline_root_ancestor(&self) -> Option<&Node> {
2240        let mut node = self;
2241        loop {
2242            if node.flags.is_inline_root() {
2243                return Some(node);
2244            }
2245            let id = node.layout_parent.get()?;
2246            node = self.with(id);
2247        }
2248    }
2249
2250    /// Get the text byte offset at a given point, using coordinates already transformed
2251    /// to be relative to this inline root's content box.
2252    /// Returns Some(byte_offset) if the point hits text, None otherwise.
2253    pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
2254        if !self.flags.is_inline_root() {
2255            return None;
2256        }
2257
2258        let element_data = self.element_data()?;
2259        let inline_layout = element_data.inline_layout_data.as_ref()?;
2260        let layout = &inline_layout.layout;
2261        let scale = layout.scale();
2262
2263        // Use Parley's cluster hit testing (from_point is more forgiving than from_point_exact)
2264        let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
2265
2266        // Determine byte offset based on which side of the cluster was clicked
2267        // For LTR text: left side = start of cluster, right side = end of cluster
2268        // For RTL text: left side = end of cluster, right side = start of cluster
2269        // Also, explicit line breaks should always use start to avoid cursor appearing on next line
2270        let is_leading = side == ClusterSide::Left;
2271        let offset = if cluster.is_rtl() {
2272            if is_leading {
2273                cluster.text_range().end
2274            } else {
2275                cluster.text_range().start
2276            }
2277        } else {
2278            // LTR text
2279            if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
2280                cluster.text_range().start
2281            } else {
2282                cluster.text_range().end
2283            }
2284        };
2285
2286        Some(offset)
2287    }
2288
2289    /// The byte range of the word or line at a point, for a multi-click
2290    /// selection.
2291    ///
2292    /// See [`TextGranularity`] for which click count maps to which unit. Coordinates are relative to this inline root's content box,
2293    /// as for [`text_offset_at_point`](Self::text_offset_at_point).
2294    ///
2295    /// Parley owns the boundary rules (it is what an `<input>` already selects
2296    /// with), so this asks it rather than scanning for spaces: word breaks are
2297    /// a Unicode segmentation question, not a whitespace one.
2298    pub fn text_range_at_point(
2299        &self,
2300        x: f32,
2301        y: f32,
2302        granularity: TextGranularity,
2303    ) -> Option<Range<usize>> {
2304        if !self.flags.is_inline_root() {
2305            return None;
2306        }
2307
2308        let element_data = self.element_data()?;
2309        let inline_layout = element_data.inline_layout_data.as_ref()?;
2310        let layout = &inline_layout.layout;
2311        let scale = layout.scale();
2312        let (x, y) = (x * scale, y * scale);
2313
2314        // Bail when the point misses the text entirely: `Selection` answers an
2315        // out-of-range point with a collapsed cursor at the end of the text,
2316        // which would read as "selected nothing at the very end" rather than
2317        // as a miss.
2318        Cluster::from_point(layout, x, y)?;
2319
2320        let selection = match granularity {
2321            TextGranularity::Word => Selection::word_from_point(layout, x, y),
2322            // The hard line, so a soft-wrapped paragraph selects as the whole
2323            // paragraph. That is what a triple click does elsewhere, and it is
2324            // what `select_hard_line_at_point` gives a text input.
2325            TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
2326        };
2327
2328        let range = selection.text_range();
2329        if range.is_empty() { None } else { Some(range) }
2330    }
2331
2332    /// Computes the Document-relative coordinates of the `Node`
2333    pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
2334        // A scroll offset moves this node's descendants, not its own border
2335        // box. Parent recursion applies each ancestor offset to the child.
2336        let x = x + self.final_layout().location.x;
2337        let y = y + self.final_layout().location.y;
2338
2339        // Recurse up the layout hierarchy
2340        self.layout_parent
2341            .get()
2342            .and_then(|id| self.tree().get(id))
2343            .map(|parent| {
2344                parent.absolute_position(
2345                    x - parent.scroll_offset().x as f32,
2346                    y - parent.scroll_offset().y as f32,
2347                )
2348            })
2349            .unwrap_or(crate::util::Point { x, y })
2350    }
2351
2352    /// Whether this node can act as an [`offset_parent`](Self::offset_parent): a positioned
2353    /// element, or one of the elements that always qualify (`body`, `td`, `th`).
2354    fn is_offset_parent(&self) -> bool {
2355        let Some(styles) = self.primary_styles() else {
2356            return false;
2357        };
2358        if styles.get_box().position != Position::Static {
2359            return true;
2360        }
2361        self.data.is_element_with_tag_name(&local_name!("body"))
2362            || self.data.is_element_with_tag_name(&local_name!("td"))
2363            || self.data.is_element_with_tag_name(&local_name!("th"))
2364    }
2365
2366    /// The nearest layout ancestor that is an offset parent, as in CSSOM View's
2367    /// `offsetParent`.
2368    ///
2369    /// The predicate is `is_offset_parent`, which is private, so this is not a
2370    /// link: rustdoc rejects a public item linking to a private one under
2371    /// `-D warnings`, and the alternative is publishing a helper nothing else
2372    /// needs.
2373    pub fn offset_parent(&self) -> Option<&Node> {
2374        let mut node = self;
2375        loop {
2376            node = self.with(node.layout_parent.get()?);
2377            if node.is_offset_parent() {
2378                return Some(node);
2379            }
2380        }
2381    }
2382
2383    /// CSSOM View's `offsetLeft`/`offsetTop`: the offset of this node's border box from the
2384    /// padding edge of its [`offset_parent`](Self::offset_parent).
2385    pub fn offset_top_left(&self) -> crate::util::Point<f32> {
2386        let mut x = 0.0;
2387        let mut y = 0.0;
2388        let mut current = self;
2389        loop {
2390            let layout = current.final_layout();
2391            x += layout.location.x;
2392            y += layout.location.y;
2393
2394            let Some(parent_id) = current.layout_parent.get() else {
2395                break;
2396            };
2397            let parent = self.with(parent_id);
2398            if parent.is_offset_parent() {
2399                let border = parent.final_layout().border;
2400                x -= border.left;
2401                y -= border.top;
2402                break;
2403            }
2404            current = parent;
2405        }
2406        crate::util::Point { x, y }
2407    }
2408
2409    /// Creates a synthetic click event
2410    pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
2411        DomEventData::Click(self.synthetic_click_event_data(mods))
2412    }
2413
2414    pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
2415        let absolute_position = self.absolute_position(0.0, 0.0);
2416        let x = absolute_position.x + (self.final_layout().size.width / 2.0);
2417        let y = absolute_position.y + (self.final_layout().size.height / 2.0);
2418
2419        BlitzPointerEvent {
2420            id: BlitzPointerId::Mouse,
2421            is_primary: true,
2422            coords: PointerCoords {
2423                page_x: x,
2424                page_y: y,
2425
2426                // TODO: should these be different?
2427                screen_x: x,
2428                screen_y: y,
2429                client_x: x,
2430                client_y: y,
2431            },
2432            mods,
2433            button: Default::default(),
2434            buttons: Default::default(),
2435            details: Default::default(),
2436            element: Default::default(),
2437            active_pointers: Default::default(),
2438        }
2439    }
2440}
2441
2442/// It might be wrong to expose this since what does *equality* mean outside the dom?
2443impl PartialEq for Node {
2444    fn eq(&self, other: &Self) -> bool {
2445        self.id == other.id
2446    }
2447}
2448
2449impl Eq for Node {}
2450
2451impl std::fmt::Debug for Node {
2452    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2453        // FIXME: update to reflect changes to fields
2454        f.debug_struct("NodeData")
2455            .field("parent", &self.parent)
2456            .field("id", &self.id)
2457            .field("is_inline_root", &self.flags.is_inline_root())
2458            .field("children", &self.children)
2459            .field("layout_children", &self.layout_children.borrow())
2460            // .field("style", &self.style)
2461            .field("node", &self.data)
2462            .field("stylo_element_data", &self.stylo_element_data_opt())
2463            // .field("unrounded_layout", &self.unrounded_layout)
2464            // .field("final_layout", &self.final_layout)
2465            .finish()
2466    }
2467}
2468
2469/// Elements whose text children serialise verbatim (HTML fragment
2470/// serialisation, with scripting enabled).
2471fn is_raw_text_element(name: &LocalName) -> bool {
2472    matches!(
2473        name.as_ref(),
2474        "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext" | "noscript"
2475    )
2476}
2477
2478/// Elements that serialise with no end tag.
2479fn is_void_element(name: &LocalName) -> bool {
2480    matches!(
2481        name.as_ref(),
2482        "area"
2483            | "base"
2484            | "basefont"
2485            | "bgsound"
2486            | "br"
2487            | "col"
2488            | "embed"
2489            | "frame"
2490            | "hr"
2491            | "img"
2492            | "input"
2493            | "keygen"
2494            | "link"
2495            | "meta"
2496            | "param"
2497            | "source"
2498            | "track"
2499            | "wbr"
2500    )
2501}
2502
2503/// Escape a string as the HTML serialiser does: `&` and no-break space
2504/// always, `"` in attribute mode, `<` and `>` otherwise.
2505fn escape_html(text: &str, attribute: bool, writer: &mut String) {
2506    for ch in text.chars() {
2507        match ch {
2508            '&' => writer.push_str("&amp;"),
2509            '\u{a0}' => writer.push_str("&nbsp;"),
2510            '"' if attribute => writer.push_str("&quot;"),
2511            '<' if !attribute => writer.push_str("&lt;"),
2512            '>' if !attribute => writer.push_str("&gt;"),
2513            _ => writer.push(ch),
2514        }
2515    }
2516}
2517
2518#[cfg(test)]
2519mod test {
2520    use style_dom::ElementState;
2521
2522    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
2523
2524    #[test]
2525    fn absolute_position_tolerates_a_detached_layout_parent() {
2526        let mut document = BaseDocument::new(DocumentConfig::default());
2527        let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
2528            qual_name!("section"),
2529            vec![],
2530        ))));
2531        let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
2532            qual_name!("button"),
2533            vec![],
2534        ))));
2535        document
2536            .get_node(child)
2537            .unwrap()
2538            .layout_parent
2539            .set(Some(parent));
2540
2541        // An event handler may detach its own ancestor before the renderer's
2542        // default-action tail finishes computing event-relative coordinates.
2543        // The child object can still be alive through that dispatch even though
2544        // its recorded layout parent has already left the slot map.
2545        document.remove_node_from_tree(parent);
2546
2547        assert_eq!(
2548            document
2549                .get_node(child)
2550                .unwrap()
2551                .absolute_position(4.0, 7.0),
2552            crate::util::Point { x: 4.0, y: 7.0 },
2553        );
2554    }
2555
2556    #[test]
2557    fn create_node_with_disabled_attr() {
2558        let mut document = BaseDocument::new(DocumentConfig::default());
2559        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2560            qual_name!("button"),
2561            vec![Attribute {
2562                name: qual_name!("disabled"),
2563                value: "".into(),
2564            }],
2565        ))));
2566        let node = document.get_node(node).unwrap();
2567
2568        assert!(
2569            node.element_state().contains(ElementState::DISABLED),
2570            "form node is disabled"
2571        );
2572        assert!(
2573            !node.element_state().contains(ElementState::ENABLED),
2574            "form node is not enabled"
2575        );
2576    }
2577
2578    #[test]
2579    fn ignore_disabled_attr_content() {
2580        let mut document = BaseDocument::new(DocumentConfig::default());
2581        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2582            qual_name!("button"),
2583            vec![Attribute {
2584                name: qual_name!("disabled"),
2585                value: "false".into(),
2586            }],
2587        ))));
2588        let node = document.get_node(node).unwrap();
2589
2590        assert!(
2591            node.element_state().contains(ElementState::DISABLED),
2592            "form node is disabled"
2593        );
2594        assert!(
2595            !node.element_state().contains(ElementState::ENABLED),
2596            "form node is not enabled"
2597        );
2598    }
2599
2600    #[test]
2601    fn create_node_with_ignored_disable() {
2602        let mut document = BaseDocument::new(DocumentConfig::default());
2603        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2604            qual_name!("a"),
2605            vec![Attribute {
2606                name: qual_name!("disabled"),
2607                value: "".into(),
2608            }],
2609        ))));
2610        let node = document.get_node(node).unwrap();
2611
2612        assert!(
2613            !node.element_state().contains(ElementState::DISABLED),
2614            "Non form node cannot be disabled"
2615        );
2616        assert!(
2617            !node.element_state().contains(ElementState::ENABLED),
2618            "Non form node cannot be enabled"
2619        );
2620    }
2621
2622    #[test]
2623    fn create_empty_enabled_node() {
2624        let mut document = BaseDocument::new(DocumentConfig::default());
2625        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2626            qual_name!("button"),
2627            vec![],
2628        ))));
2629        let node = document.get_node(node).unwrap();
2630
2631        assert!(
2632            node.element_state().contains(ElementState::ENABLED),
2633            "Button should be enabled by default"
2634        );
2635    }
2636}