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::specified::box_::{DisplayInside, DisplayOutside};
33use style_dom::ElementState;
34use style_traits::values::ToCss;
35use taffy::{
36    Cache,
37    prelude::{Layout, Style},
38};
39use thin_vec::ThinVec;
40
41use super::stylo_data::StyloData;
42use super::{Attribute, DocumentData, ElementData};
43
44#[derive(Clone, Copy)]
45enum OutputStyle {
46    Normal,
47    Pretty,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum DisplayOuter {
52    Block,
53    Inline,
54    None,
55}
56
57bitflags! {
58    #[derive(Clone, Copy, PartialEq)]
59    pub struct NodeFlags: u32 {
60        /// Whether the node is the root node of an Inline Formatting Context
61        const IS_INLINE_ROOT = 0b00000001;
62        /// Whether the node is the root node of an Table formatting context
63        const IS_TABLE_ROOT = 0b00000010;
64        /// Whether the node is "in the document" (~= has a parent and isn't a template node)
65        const IS_IN_DOCUMENT = 0b00000100;
66    }
67}
68
69impl NodeFlags {
70    #[inline(always)]
71    pub fn is_inline_root(&self) -> bool {
72        self.contains(Self::IS_INLINE_ROOT)
73    }
74
75    #[inline(always)]
76    pub fn is_table_root(&self) -> bool {
77        self.contains(Self::IS_TABLE_ROOT)
78    }
79
80    #[inline(always)]
81    pub fn is_in_document(&self) -> bool {
82        self.contains(Self::IS_IN_DOCUMENT)
83    }
84
85    #[inline(always)]
86    pub fn reset_construction_flags(&mut self) {
87        self.remove(Self::IS_INLINE_ROOT);
88        self.remove(Self::IS_TABLE_ROOT);
89    }
90}
91
92pub struct Node {
93    // The actual tree we belong to. This is unsafe!!
94    tree: *mut crate::NodeTree,
95
96    /// Our Id
97    pub id: NodeId,
98    /// Our parent's ID
99    pub parent: Option<NodeId>,
100    // What are our children?
101    pub children: ThinVec<NodeId>,
102    /// Our parent in the layout hierachy: a separate list that includes anonymous collections of inline elements
103    pub layout_parent: Cell<Option<NodeId>>,
104    /// A separate child list that includes anonymous collections of inline elements
105    pub layout_children: RefCell<Option<ThinVec<NodeId>>>,
106    /// Anonymous block boxes created for this node during layout construction.
107    ///
108    /// Anonymous blocks live only in the slab (they are not part of the DOM
109    /// `children` list), so we track the ones we own here to be able to
110    /// deallocate them when this node is reconstructed.
111    pub anonymous_blocks: ThinVec<NodeId>,
112    /// The same as layout_children, but sorted by z-index
113    pub paint_children: RefCell<Option<ThinVec<NodeId>>>,
114    pub stacking_context: Option<Box<HoistedPaintChildren>>,
115
116    /// The "flattened tree" children of this node used for layout and painting,
117    /// if it differs from [`children`](Self::children). This is set for shadow
118    /// hosts (where it holds the shadow root's children) and `<slot>` elements
119    /// (where it holds the light-DOM nodes assigned to the slot). When `None`,
120    /// [`children`](Self::children) is used directly.
121    #[cfg(feature = "shadow-dom")]
122    pub flattened_children: Option<Vec<NodeId>>,
123
124    // Flags
125    pub flags: NodeFlags,
126
127    /// Node type (Element, TextNode, etc) specific data.
128    ///
129    /// For element nodes this holds the [`ElementData`], which stores most of
130    /// the per-node style/layout state. For the document node it holds the
131    /// [`DocumentData`]. Access the moved fields through the forwarding methods
132    /// on [`Node`] (e.g. [`Node::style`], [`Node::final_layout`]).
133    pub data: NodeData,
134}
135
136unsafe impl Send for Node {}
137unsafe impl Sync for Node {}
138
139/// Generates forwarding accessors for fields that live on both [`ElementData`]
140/// (element / anonymous block nodes) and [`DocumentData`] (the document node).
141macro_rules! universal_accessors {
142    ($($(#[$meta:meta])* $field:ident / $field_mut:ident : $ty:ty),* $(,)?) => {
143        impl Node {
144            $(
145                $(#[$meta])*
146                #[inline]
147                pub fn $field(&self) -> &$ty {
148                    match &self.data {
149                        NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.$field,
150                        NodeData::Document(data) => &data.$field,
151                        _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
152                    }
153                }
154
155                $(#[$meta])*
156                #[inline]
157                pub fn $field_mut(&mut self) -> &mut $ty {
158                    match &mut self.data {
159                        NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.$field,
160                        NodeData::Document(data) => &mut data.$field,
161                        _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
162                    }
163                }
164            )*
165        }
166    };
167}
168
169universal_accessors! {
170    stylo_element_data / stylo_element_data_mut: StyloData,
171    style / style_mut: Style<Atom>,
172    style_source / style_source_mut: Option<ServoArc<ComputedValues>>,
173    subtree_hoists / subtree_hoists_mut: bool,
174    // `cache` is not here: it is stored as `Option<Box<Cache>>` and reached
175    // through hand-written accessors below, because the read side has to be
176    // able to answer without allocating. See `ElementData::cache`.
177    unrounded_layout / unrounded_layout_mut: Layout,
178    final_layout / final_layout_mut: Layout,
179    scroll_offset / scroll_offset_mut: crate::Point<f64>,
180    scrollable_overflow / scrollable_overflow_mut: KurboRect,
181    transform / transform_mut: Option<Affine>,
182    display_constructed_as / display_constructed_as_mut: StyloDisplay,
183    // The document node is styled/snapshotted like an element, so it also
184    // carries these:
185    element_state / element_state_mut: ElementState,
186    snapshot_handled / snapshot_handled_mut: AtomicBool,
187    // `apply_selector_flags` deposits `for_parent()` flags on the parent node,
188    // and the parent of the root <html> element is the document -- so the
189    // document has to be able to hold selector flags too.
190    selector_flags / selector_flags_mut: Cell<ElementSelectorFlags>,
191}
192
193impl Node {
194    /// This node's taffy layout cache.
195    ///
196    /// Hand-written rather than generated by `universal_accessors!` because
197    /// the cache is `Option<Box<Cache>>`: a node that has never been laid out
198    /// borrows a shared empty one instead of owning 1616 bytes. See the
199    /// [`cache`](super::ElementData::cache) field for the measurement behind
200    /// that. The signature is unchanged, so callers cannot tell.
201    #[inline]
202    pub fn cache(&self) -> &Cache {
203        match &self.data {
204            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache(),
205            NodeData::Document(data) => data.cache(),
206            _ => panic!("`cache` is not available on this node kind"),
207        }
208    }
209
210    /// This node's taffy layout cache, allocating on first use.
211    #[inline]
212    pub fn cache_mut(&mut self) -> &mut Cache {
213        match &mut self.data {
214            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_mut(),
215            NodeData::Document(data) => data.cache_mut(),
216            _ => panic!("`cache_mut` is not available on this node kind"),
217        }
218    }
219
220    /// Release this node's layout cache, returning its memory.
221    #[inline]
222    pub fn cache_release(&mut self) {
223        match &mut self.data {
224            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_release(),
225            NodeData::Document(data) => data.cache_release(),
226            _ => {}
227        }
228    }
229
230    /// Style data from stylo, if this node kind carries it (element or document
231    /// nodes). Returns `None` for text/comment nodes.
232    /// The computed values the cached taffy style was built from, for node
233    /// kinds that carry one. `None` for text and comment nodes, which are never
234    /// styled, so a caller can ask without knowing the kind.
235    #[inline]
236    pub fn style_source_opt(&self) -> Option<&ServoArc<ComputedValues>> {
237        match &self.data {
238            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.style_source.as_ref(),
239            NodeData::Document(data) => data.style_source.as_ref(),
240            _ => None,
241        }
242    }
243
244    #[inline]
245    pub fn stylo_element_data_opt(&self) -> Option<&StyloData> {
246        match &self.data {
247            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
248                Some(&data.stylo_element_data)
249            }
250            NodeData::Document(data) => Some(&data.stylo_element_data),
251            _ => None,
252        }
253    }
254
255    #[inline]
256    pub fn stylo_element_data_opt_mut(&mut self) -> Option<&mut StyloData> {
257        match &mut self.data {
258            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
259                Some(&mut data.stylo_element_data)
260            }
261            NodeData::Document(data) => Some(&mut data.stylo_element_data),
262            _ => None,
263        }
264    }
265
266    /// The `dirty_descendants` flag, if this node kind carries it (element or
267    /// document nodes). Returns `None` for text/comment nodes.
268    #[inline]
269    fn dirty_descendants_flag(&self) -> Option<&AtomicBool> {
270        match &self.data {
271            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
272                Some(&data.dirty_descendants)
273            }
274            NodeData::Document(data) => Some(&data.dirty_descendants),
275            _ => None,
276        }
277    }
278
279    /// The document's shared style lock. Only available on element and
280    /// document nodes.
281    #[inline]
282    pub fn guard(&self) -> &SharedRwLock {
283        let guard = match &self.data {
284            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.guard.as_ref(),
285            NodeData::Document(data) => data.guard.as_ref(),
286            _ => None,
287        };
288        guard.expect("`guard` is not available on this node kind")
289    }
290
291    #[inline]
292    pub fn has_snapshot(&self) -> bool {
293        match &self.data {
294            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot,
295            NodeData::Document(data) => data.has_snapshot,
296            _ => false,
297        }
298    }
299
300    #[inline]
301    pub fn set_has_snapshot(&mut self, value: bool) {
302        match &mut self.data {
303            NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot = value,
304            NodeData::Document(data) => data.has_snapshot = value,
305            _ => {}
306        }
307    }
308
309    #[inline]
310    pub fn before(&self) -> Option<NodeId> {
311        self.element_data().and_then(|data| data.before)
312    }
313
314    #[inline]
315    pub fn after(&self) -> Option<NodeId> {
316        self.element_data().and_then(|data| data.after)
317    }
318}
319
320impl Node {
321    pub(crate) fn new(
322        tree: *mut crate::NodeTree,
323        id: NodeId,
324        guard: SharedRwLock,
325        mut data: NodeData,
326    ) -> Self {
327        // Store a handle to the document's shared style lock on the node data.
328        // Both element and document nodes are styled by stylo and so need it.
329        match &mut data {
330            NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
331                data.guard = Some(guard);
332            }
333            NodeData::Document(data) => data.guard = Some(guard),
334            _ => {}
335        }
336
337        Self {
338            tree,
339
340            id,
341            parent: None,
342            children: ThinVec::new(),
343            layout_parent: Cell::new(None),
344            layout_children: RefCell::new(None),
345            anonymous_blocks: ThinVec::new(),
346            paint_children: RefCell::new(None),
347            stacking_context: None,
348            #[cfg(feature = "shadow-dom")]
349            flattened_children: None,
350
351            flags: NodeFlags::empty(),
352            data,
353        }
354    }
355
356    pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
357        let transform = self.primary_styles().and_then(|s| {
358            let size = self.final_layout().size;
359            let reference_box = Rect::new(
360                Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
361                Size2D::new(
362                    CSSPixelLength::new(size.width),
363                    CSSPixelLength::new(size.height),
364                ),
365            );
366            // Resolve the transform in CSS pixels, then convert it to device-pixel space
367            // (S * T * S^-1): translation components are scaled, linear components are not.
368            crate::resolve_2d_transform(s.get_box(), reference_box).map(|t| {
369                let scale = scale as f64;
370                let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
371                Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
372            })
373        });
374
375        *self.transform_mut() = transform;
376        transform
377    }
378
379    pub fn pe_by_index(&self, index: usize) -> Option<NodeId> {
380        match index {
381            0 => self.after(),
382            1 => self.before(),
383            _ => panic!("Invalid pseudo element index"),
384        }
385    }
386
387    pub fn set_pe_by_index(&mut self, index: usize, value: Option<NodeId>) {
388        let Some(data) = self.element_data_mut() else {
389            return;
390        };
391        match index {
392            0 => data.after = value,
393            1 => data.before = value,
394            _ => panic!("Invalid pseudo element index"),
395        }
396    }
397
398    pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
399        Some(self.primary_styles().as_ref()?.clone_display())
400    }
401
402    /// A compact computed-style view for renderer diagnostics.
403    pub fn diagnostic_computed_style(&self) -> Option<Vec<(&'static str, String)>> {
404        let style = self.primary_styles()?;
405        Some(vec![
406            ("display", style.clone_display().to_css_string()),
407            ("color", style.clone_color().to_css_string()),
408            (
409                "background-color",
410                style.clone_background_color().to_css_string(),
411            ),
412            (
413                "font-size",
414                format!("{}px", style.clone_font_size().computed_size().px()),
415            ),
416            ("width", style.clone_width().to_css_string()),
417        ])
418    }
419
420    /// Whether computed style removes this node from layout.
421    pub fn is_display_none(&self) -> bool {
422        self.display_style()
423            .is_some_and(|display| display.is_none())
424    }
425
426    pub fn is_or_contains_block(&self) -> bool {
427        let style = self.primary_styles();
428        let style = style.as_ref();
429
430        // Ignore out-of-flow items
431        let position = style
432            .map(|s| s.clone_position())
433            .unwrap_or(Position::Relative);
434        let is_in_flow = matches!(
435            position,
436            Position::Static | Position::Relative | Position::Sticky
437        );
438        if !is_in_flow {
439            return false;
440        }
441        // Floated boxes do not break up the inline flow: they participate in the
442        // inline formatting context as out-of-flow inline boxes
443        let is_floating = style
444            .map(|s| s.clone_float().is_floating())
445            .unwrap_or(false);
446        if is_floating {
447            return false;
448        }
449        let display = style
450            .map(|s| s.clone_display())
451            .unwrap_or(StyloDisplay::inline());
452        match display.outside() {
453            DisplayOutside::None => false,
454            DisplayOutside::Block => true,
455            _ => {
456                if display.inside() == DisplayInside::Flow {
457                    self.children
458                        .iter()
459                        .copied()
460                        .any(|child_id| self.tree()[child_id].is_or_contains_block())
461                } else {
462                    false
463                }
464            }
465        }
466    }
467
468    pub fn is_whitespace_node(&self) -> bool {
469        match &self.data {
470            NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
471            _ => false,
472        }
473    }
474
475    pub fn is_focussable(&self) -> bool {
476        self.data
477            .downcast_element()
478            .map(|el| el.is_focussable)
479            .unwrap_or(false)
480    }
481
482    pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
483        if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
484            if let Some(mut element_data) = stylo_element_data.get_mut() {
485                element_data.hint.insert(hint);
486            }
487        }
488        // Mark all ancestors as having dirty descendants so the style traversal
489        // will visit this node's subtree
490        self.mark_ancestors_dirty();
491    }
492
493    /// Returns whether this node has any descendants that need restyling.
494    pub fn has_dirty_descendants(&self) -> bool {
495        self.dirty_descendants_flag()
496            .is_some_and(|flag| flag.load(Ordering::Relaxed))
497    }
498
499    /// Sets the dirty_descendants flag on this node.
500    pub fn set_dirty_descendants(&self) {
501        if let Some(flag) = self.dirty_descendants_flag() {
502            flag.store(true, Ordering::Relaxed);
503        }
504    }
505
506    /// Clears the dirty_descendants flag on this node.
507    pub fn unset_dirty_descendants(&self) {
508        if let Some(flag) = self.dirty_descendants_flag() {
509            flag.store(false, Ordering::Relaxed);
510        }
511    }
512
513    /// Set appropriate damage for Stylo when an element's style attribute is updated
514    pub(crate) fn mark_style_attr_updated(&mut self) {
515        if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
516            if let Some(mut data) = stylo_element_data.get_mut() {
517                data.hint |= RestyleHint::RESTYLE_STYLE_ATTRIBUTE;
518            }
519        }
520        self.set_dirty_descendants();
521        self.mark_ancestors_dirty();
522    }
523
524    /// Marks all ancestors of this node as having dirty descendants.
525    /// This propagates the dirty flag up the tree so that the style traversal
526    /// knows to visit the subtree containing this node.
527    pub fn mark_ancestors_dirty(&self) {
528        let mut current_id = self.parent;
529        while let Some(parent_id) = current_id {
530            let parent = &self.tree()[parent_id];
531            // If this ancestor already has dirty_descendants set, we can stop
532            // because all further ancestors must also have it set
533            if let Some(flag) = parent.dirty_descendants_flag() {
534                if flag.swap(true, Ordering::Relaxed) {
535                    break;
536                }
537            }
538            current_id = parent.parent;
539        }
540    }
541
542    // pub fn damage_mut(&mut self) -> Option<&mut RestyleDamage> {
543    //     self.stylo_element_data
544    //         .get_mut()
545    //         .map(|mut data: ElementDataMut<'a>| &'a mut data.damage)
546    // }
547
548    pub fn damage(&self) -> Option<RestyleDamage> {
549        self.stylo_element_data_opt()
550            .and_then(|stylo| stylo.get().map(|data| data.damage))
551    }
552
553    pub fn set_damage(&mut self, damage: RestyleDamage) {
554        if let Some(stylo) = self.stylo_element_data_opt_mut() {
555            if let Some(mut data) = stylo.get_mut() {
556                data.damage = damage;
557            }
558        }
559    }
560
561    pub fn insert_damage(&mut self, damage: RestyleDamage) {
562        if let Some(stylo) = self.stylo_element_data_opt_mut() {
563            if let Some(mut data) = stylo.get_mut() {
564                data.damage |= damage;
565            }
566        }
567    }
568
569    pub fn remove_damage(&mut self, damage: RestyleDamage) {
570        if let Some(stylo) = self.stylo_element_data_opt_mut() {
571            if let Some(mut data) = stylo.get_mut() {
572                data.damage.remove(damage);
573            }
574        }
575    }
576
577    pub fn clear_damage_mut(&mut self) {
578        if let Some(stylo) = self.stylo_element_data_opt_mut() {
579            if let Some(mut data) = stylo.get_mut() {
580                data.damage = RestyleDamage::empty();
581            }
582        }
583    }
584
585    pub fn hover(&mut self) {
586        if let Some(data) = self.element_data_mut() {
587            data.element_state.insert(ElementState::HOVER);
588        }
589        self.set_restyle_hint(RestyleHint::restyle_subtree());
590    }
591
592    pub fn unhover(&mut self) {
593        if let Some(data) = self.element_data_mut() {
594            data.element_state.remove(ElementState::HOVER);
595        }
596        self.set_restyle_hint(RestyleHint::restyle_subtree());
597    }
598
599    pub fn is_hovered(&self) -> bool {
600        self.element_data()
601            .is_some_and(|data| data.element_state.contains(ElementState::HOVER))
602    }
603
604    pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
605        if let Some(data) = self.element_data_mut() {
606            data.element_state
607                .insert(ElementState::FOCUS | ElementState::FOCUSRING);
608        }
609        self.set_restyle_hint(RestyleHint::restyle_subtree());
610
611        // If focussing a text input, enable IME and set IME area
612        if self
613            .element_data()
614            .and_then(|elem| elem.text_input_data())
615            .is_some()
616        {
617            shell_provider.set_ime_enabled(true);
618            let mut pos = self.absolute_position(0.0, 0.0);
619            pos.x += self.final_layout().content_box_x();
620            pos.y += self.final_layout().content_box_y();
621            let width = self.final_layout().content_box_width();
622            let height = self.final_layout().content_box_height();
623            shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
624        }
625    }
626
627    pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
628        if let Some(data) = self.element_data_mut() {
629            data.element_state
630                .remove(ElementState::FOCUS | ElementState::FOCUSRING);
631        }
632        self.set_restyle_hint(RestyleHint::restyle_subtree());
633
634        // If blurring a text input, disable IME
635        if self
636            .element_data()
637            .and_then(|elem| elem.text_input_data())
638            .is_some()
639        {
640            shell_provider.set_ime_enabled(false);
641        }
642    }
643
644    pub fn is_focussed(&self) -> bool {
645        self.element_data()
646            .is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
647    }
648
649    pub fn active(&mut self) {
650        if let Some(data) = self.element_data_mut() {
651            data.element_state.insert(ElementState::ACTIVE);
652        }
653        self.set_restyle_hint(RestyleHint::restyle_subtree());
654    }
655
656    pub fn unactive(&mut self) {
657        if let Some(data) = self.element_data_mut() {
658            data.element_state.remove(ElementState::ACTIVE);
659        }
660        self.set_restyle_hint(RestyleHint::restyle_subtree());
661    }
662
663    pub fn is_active(&self) -> bool {
664        self.element_data()
665            .is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
666    }
667
668    // Marks the node as disabled if it can be.
669    // It does not disable any children which should be disabled as well (relevant for the `select` element).
670    pub fn disable(&mut self) {
671        if let Some(data) = self.element_data_mut() {
672            if data.can_be_disabled() {
673                data.element_state.insert(ElementState::DISABLED);
674                data.element_state.remove(ElementState::ENABLED);
675            }
676        }
677        self.set_restyle_hint(RestyleHint::restyle_subtree());
678    }
679
680    // Marks the node as enabled if it can be.
681    // It does not enable any children which should be enabled as well (relevant for the `select` element).
682    pub fn enable(&mut self) {
683        if let Some(data) = self.element_data_mut() {
684            if data.can_be_disabled() {
685                data.element_state.insert(ElementState::ENABLED);
686                data.element_state.remove(ElementState::DISABLED);
687            }
688        }
689        self.set_restyle_hint(RestyleHint::restyle_subtree());
690    }
691
692    pub fn subdoc(&self) -> Option<&dyn Document> {
693        self.element_data().and_then(|el| el.sub_doc_data())
694    }
695
696    pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
697        self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
698    }
699
700    pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
701        // For single-line inputs, add an offset to vertically center the text input layout
702        // within the content box of it's node.
703        if let Some(input_data) = self
704            .data
705            .downcast_element()
706            .and_then(|el| el.text_input_data())
707        {
708            if !input_data.is_multiline {
709                let content_box_height = self.final_layout().content_box_height();
710                let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
711                let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
712
713                return y_offset as f64;
714            }
715        }
716
717        0.0
718    }
719}
720
721#[derive(Debug, Clone, Copy, PartialEq)]
722pub enum NodeKind {
723    Document,
724    Element,
725    AnonymousBlock,
726    Text,
727    Comment,
728    ShadowRoot,
729}
730
731/// How much text one click selects.
732///
733/// Click count decides: a second click takes the word, a third takes the line,
734/// matching what a text input does and what every other platform does with the
735/// same gesture.
736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
737pub enum TextGranularity {
738    /// The word under the pointer, by Unicode word segmentation.
739    Word,
740    /// The whole hard line, so a soft-wrapped paragraph selects entire.
741    Line,
742}
743
744impl TextGranularity {
745    /// The granularity a click of this count selects, or `None` for a first
746    /// click, which places a caret rather than selecting anything.
747    pub fn from_click_count(count: u16) -> Option<Self> {
748        match count {
749            0 | 1 => None,
750            2 => Some(Self::Word),
751            _ => Some(Self::Line),
752        }
753    }
754}
755
756/// The encapsulation mode of a shadow root.
757///
758/// Mirrors the `ShadowRootMode` enum from the DOM specification.
759#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760pub enum ShadowRootMode {
761    /// Elements of the shadow root are accessible from JavaScript outside the
762    /// root (e.g. via `Element.shadowRoot`).
763    Open,
764    /// Elements of the shadow root are not accessible from JavaScript outside
765    /// the root.
766    Closed,
767}
768
769/// Data associated with a [`NodeData::ShadowRoot`] node.
770///
771/// A shadow root is a non-element, non-document node that acts as the root of a
772/// shadow tree. Its `children` (on the owning [`Node`]) are the top-level nodes
773/// of the shadow tree. The light-DOM children of the host element are
774/// distributed into any `<slot>` elements within this tree to form the
775/// "flattened tree" that is used for style resolution, layout and painting.
776#[derive(Debug, Clone)]
777pub struct ShadowRootData {
778    /// The node id of the host element that this shadow root is attached to.
779    pub host: NodeId,
780    /// The encapsulation mode of this shadow root.
781    pub mode: ShadowRootMode,
782    /// Node ids of `<style>` elements within this shadow root, in document
783    /// order. Used to build the scoped stylesheet set for this shadow tree.
784    pub stylesheet_nodes: Vec<NodeId>,
785}
786
787impl ShadowRootData {
788    pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
789        Self {
790            host,
791            mode,
792            stylesheet_nodes: Vec::new(),
793        }
794    }
795}
796
797/// The different kinds of nodes in the DOM.
798#[derive(Debug, Clone)]
799pub enum NodeData {
800    /// The `Document` itself - the root node of a HTML document.
801    Document(Box<DocumentData>),
802
803    /// An element with attributes.
804    Element(Box<ElementData>),
805
806    /// An anonymous block box
807    AnonymousBlock(Box<ElementData>),
808
809    /// A text node.
810    Text(TextNodeData),
811
812    /// A comment.
813    Comment {
814        /// The textual content of the comment
815        contents: String,
816    },
817
818    /// The root of a shadow tree attached to a host element.
819    ShadowRoot(ShadowRootData),
820    // /// A `DOCTYPE` with name, public id, and system id. See
821    // /// [document type declaration on wikipedia][https://en.wikipedia.org/wiki/Document_type_declaration]
822    // Doctype { name: String, public_id: String, system_id: String },
823
824    // /// A Processing instruction.
825    // ProcessingInstruction { target: String, contents: String },
826}
827
828impl NodeData {
829    pub fn downcast_element(&self) -> Option<&ElementData> {
830        match self {
831            Self::Element(data) => Some(data),
832            Self::AnonymousBlock(data) => Some(data),
833            _ => None,
834        }
835    }
836
837    pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
838        match self {
839            Self::Element(data) => Some(data),
840            Self::AnonymousBlock(data) => Some(data),
841            _ => None,
842        }
843    }
844
845    pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
846        let Some(elem) = self.downcast_element() else {
847            return false;
848        };
849        *name == elem.name.local
850    }
851
852    pub fn attrs(&self) -> Option<&[Attribute]> {
853        Some(&self.downcast_element()?.attrs)
854    }
855
856    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
857        self.downcast_element()?.attr(name)
858    }
859
860    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
861        self.downcast_element()
862            .is_some_and(|elem| elem.has_attr(name))
863    }
864
865    pub fn kind(&self) -> NodeKind {
866        match self {
867            NodeData::Document(_) => NodeKind::Document,
868            NodeData::Element(_) => NodeKind::Element,
869            NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
870            NodeData::Text(_) => NodeKind::Text,
871            NodeData::Comment { .. } => NodeKind::Comment,
872            NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
873        }
874    }
875
876    pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
877        match self {
878            Self::ShadowRoot(data) => Some(data),
879            _ => None,
880        }
881    }
882
883    pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
884        match self {
885            Self::ShadowRoot(data) => Some(data),
886            _ => None,
887        }
888    }
889}
890
891#[derive(Debug, Clone)]
892pub struct TextNodeData {
893    /// The textual content of the text node
894    pub content: String,
895}
896
897impl TextNodeData {
898    pub fn new(content: String) -> Self {
899        Self { content }
900    }
901}
902
903/*
904-> Computed styles
905-> Layout
906-----> Needs to happen only when styles are computed
907*/
908
909// type DomRefCell<T> = RefCell<T>;
910
911// pub struct DomData {
912//     // ... we can probs just get away with using the html5ever types directly. basically just using the servo dom, but without the bindings
913//     local_name: html5ever::LocalName,
914//     tag_name: html5ever::QualName,
915//     namespace: html5ever::Namespace,
916//     prefix: DomRefCell<Option<html5ever::Prefix>>,
917//     attrs: DomRefCell<Vec<Attr>>,
918//     // attrs: DomRefCell<Vec<Dom<Attr>>>,
919//     id_attribute: DomRefCell<Option<Atom>>,
920//     is: DomRefCell<Option<LocalName>>,
921//     // style_attribute: DomRefCell<Option<Arc<Locked<PropertyDeclarationBlock>>>>,
922//     // attr_list: MutNullableDom<NamedNodeMap>,
923//     // class_list: MutNullableDom<DOMTokenList>,
924//     state: Cell<ElementState>,
925// }
926
927impl Node {
928    pub fn tree(&self) -> &crate::NodeTree {
929        unsafe { &*self.tree }
930    }
931
932    #[track_caller]
933    pub fn with(&self, id: NodeId) -> &Node {
934        self.tree().get(id).unwrap()
935    }
936
937    pub fn print_tree(&self, level: usize) {
938        println!(
939            "{} {} {:?} {} {:?}",
940            "  ".repeat(level),
941            self.id,
942            self.parent,
943            self.node_debug_str().replace('\n', ""),
944            self.children
945        );
946        // println!("{} {:?}", "  ".repeat(level), self.children);
947        for child_id in self.children.iter() {
948            let child = self.with(*child_id);
949            child.print_tree(level + 1)
950        }
951    }
952
953    // Get the index of the current node in the parents child list
954    pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
955        self.children.iter().position(|id| *id == child_id)
956    }
957
958    // Get the index of the current node in the parents child list
959    pub fn child_index(&self) -> Option<usize> {
960        self.tree()[self.parent?]
961            .children
962            .iter()
963            .position(|id| *id == self.id)
964    }
965
966    // Get the nth node in the parents child list
967    pub fn forward(&self, n: usize) -> Option<&Node> {
968        let child_idx = self.child_index().unwrap_or(0);
969        self.tree()[self.parent?]
970            .children
971            .get(child_idx + n)
972            .map(|id| self.with(*id))
973    }
974
975    pub fn backward(&self, n: usize) -> Option<&Node> {
976        let child_idx = self.child_index().unwrap_or(0);
977        if child_idx < n {
978            return None;
979        }
980
981        self.tree()[self.parent?]
982            .children
983            .get(child_idx - n)
984            .map(|id| self.with(*id))
985    }
986
987    pub fn is_element(&self) -> bool {
988        matches!(self.data, NodeData::Element { .. })
989    }
990
991    pub fn is_anonymous(&self) -> bool {
992        matches!(self.data, NodeData::AnonymousBlock { .. })
993    }
994
995    pub fn is_shadow_root(&self) -> bool {
996        matches!(self.data, NodeData::ShadowRoot { .. })
997    }
998
999    pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1000        self.data.shadow_root_data()
1001    }
1002
1003    pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1004        self.data.shadow_root_data_mut()
1005    }
1006
1007    /// If this node is a shadow host (i.e. has an attached shadow root),
1008    /// returns the node id of its shadow root.
1009    pub fn shadow_root_id(&self) -> Option<NodeId> {
1010        self.element_data().and_then(|el| el.shadow_root)
1011    }
1012
1013    /// The children to use for layout and painting. For shadow hosts and
1014    /// `<slot>` elements this is the "flattened tree" children; for all other
1015    /// nodes it is the regular DOM [`children`](Self::children).
1016    #[cfg(feature = "shadow-dom")]
1017    pub fn layout_dom_children(&self) -> &[NodeId] {
1018        match &self.flattened_children {
1019            Some(children) => children,
1020            None => &self.children,
1021        }
1022    }
1023
1024    /// The children to use for layout and painting.
1025    #[cfg(not(feature = "shadow-dom"))]
1026    #[inline(always)]
1027    pub fn layout_dom_children(&self) -> &[NodeId] {
1028        &self.children
1029    }
1030
1031    pub fn is_text_node(&self) -> bool {
1032        matches!(self.data, NodeData::Text { .. })
1033    }
1034
1035    pub fn element_data(&self) -> Option<&ElementData> {
1036        match self.data {
1037            NodeData::Element(ref data) => Some(data),
1038            NodeData::AnonymousBlock(ref data) => Some(data),
1039            _ => None,
1040        }
1041    }
1042
1043    pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1044        match self.data {
1045            NodeData::Element(ref mut data) => Some(data),
1046            NodeData::AnonymousBlock(ref mut data) => Some(data),
1047            _ => None,
1048        }
1049    }
1050
1051    pub fn text_data(&self) -> Option<&TextNodeData> {
1052        match self.data {
1053            NodeData::Text(ref data) => Some(data),
1054            _ => None,
1055        }
1056    }
1057
1058    pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1059        match self.data {
1060            NodeData::Text(ref mut data) => Some(data),
1061            _ => None,
1062        }
1063    }
1064
1065    pub fn node_debug_str(&self) -> String {
1066        let mut s = String::new();
1067
1068        match &self.data {
1069            NodeData::Document(_) => write!(s, "DOCUMENT"),
1070            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1071            NodeData::Text(data) => {
1072                let bytes = data.content.as_bytes();
1073                write!(
1074                    s,
1075                    "TEXT {}",
1076                    std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1077                        .unwrap_or("INVALID UTF8")
1078                )
1079            }
1080            NodeData::Comment { .. } => write!(s, "COMMENT"),
1081            NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1082            NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1083            NodeData::Element(data) => {
1084                let name = &data.name;
1085                let class = self.attr(local_name!("class")).unwrap_or("");
1086                let id = self.attr(local_name!("id")).unwrap_or("");
1087                let display = self.display_constructed_as().to_css_string();
1088                write!(s, "<{}", name.local).unwrap();
1089                if !id.is_empty() {
1090                    write!(s, " #{id}").unwrap();
1091                }
1092                if !class.is_empty() {
1093                    if class.contains(' ') {
1094                        write!(s, " class=\"{class}\"").unwrap()
1095                    } else {
1096                        write!(s, " .{class}").unwrap()
1097                    }
1098                }
1099                write!(s, "> ({display})")
1100            } // NodeData::ProcessingInstruction { .. } => write!(s, "ProcessingInstruction"),
1101        }
1102        .unwrap();
1103        s
1104    }
1105
1106    /// Renders the HTML of this node and all its children as a `String` without extra whitespace.
1107    ///
1108    /// Example output:
1109    ///
1110    /// ```text
1111    /// <html><head /><body><main id="main"><div class="arbitrary-class" /></main></body></html>
1112    /// ```
1113    pub fn outer_html(&self) -> String {
1114        let mut output = String::new();
1115        self.write_outer_html(&mut output);
1116        output
1117    }
1118
1119    /// Renders the HTML of this node and all its children as a `String` with whitespace for human
1120    /// readability.
1121    ///
1122    /// Example output:
1123    ///
1124    /// ```text
1125    /// <html>
1126    ///   <head />
1127    ///   <body>
1128    ///     <main id="main">
1129    ///       <div class="arbitrary-class" />
1130    ///     </main>
1131    ///   </body>
1132    /// </html>
1133    /// ```
1134    pub fn outer_html_pretty(&self) -> String {
1135        let mut output = String::new();
1136        self.write_outer_html_pretty(&mut output);
1137        output
1138    }
1139
1140    pub fn write_outer_html(&self, writer: &mut String) {
1141        self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1142    }
1143
1144    #[cfg(feature = "svg")]
1145    pub(crate) fn write_outer_html_with_current_color(
1146        &self,
1147        writer: &mut String,
1148        current_color: &str,
1149    ) {
1150        self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1151    }
1152
1153    pub fn write_outer_html_pretty(&self, writer: &mut String) {
1154        self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1155    }
1156
1157    fn write_outer_html_in_style(
1158        &self,
1159        writer: &mut String,
1160        style: OutputStyle,
1161        nesting: usize,
1162        current_color_override: Option<&str>,
1163    ) {
1164        const INDENT: &str = "  ";
1165        let has_children = !self.children.is_empty();
1166        let computed_current_color = || {
1167            self.primary_styles()
1168                .map(|style| style.clone_color())
1169                .map(|color| crate::util::absolute_color_to_svg_css(&color))
1170        };
1171        let current_color = current_color_override
1172            .map(ToOwned::to_owned)
1173            .or_else(computed_current_color);
1174
1175        match &self.data {
1176            NodeData::Document(_) => {}
1177            NodeData::Comment { .. } => {}
1178            NodeData::AnonymousBlock(_) => {}
1179            NodeData::ShadowRoot(_) => {}
1180            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
1181            NodeData::Text(data) => {
1182                if matches!(style, OutputStyle::Pretty) {
1183                    for _ in 0..nesting {
1184                        writer.push_str(INDENT);
1185                    }
1186                }
1187                writer.push_str(data.content.as_str());
1188                if matches!(style, OutputStyle::Pretty) {
1189                    writer.push('\n');
1190                }
1191            }
1192            NodeData::Element(data) => {
1193                if matches!(style, OutputStyle::Pretty) {
1194                    for _ in 0..nesting {
1195                        writer.push_str(INDENT);
1196                    }
1197                }
1198                writer.push('<');
1199                writer.push_str(&data.name.local);
1200
1201                for attr in data.attrs() {
1202                    writer.push(' ');
1203                    writer.push_str(&attr.name.local);
1204                    writer.push_str("=\"");
1205                    #[allow(clippy::unnecessary_unwrap)] // Convert to if-let chain once stabilised
1206                    if current_color.is_some() && attr.value.contains("currentColor") {
1207                        let value = attr
1208                            .value
1209                            .replace("currentColor", current_color.as_ref().unwrap());
1210                        encode_quoted_attribute_to_string(&value, writer);
1211                    } else {
1212                        encode_quoted_attribute_to_string(&attr.value, writer);
1213                    }
1214                    writer.push('"');
1215                }
1216                if !has_children {
1217                    writer.push_str(" /");
1218                }
1219                writer.push('>');
1220                if matches!(style, OutputStyle::Pretty) {
1221                    writer.push('\n');
1222                }
1223
1224                if has_children {
1225                    for &child_id in &self.children {
1226                        self.tree()[child_id].write_outer_html_in_style(
1227                            writer,
1228                            style,
1229                            nesting + 1,
1230                            current_color_override,
1231                        );
1232                    }
1233
1234                    if matches!(style, OutputStyle::Pretty) {
1235                        for _ in 0..nesting {
1236                            writer.push_str(INDENT);
1237                        }
1238                    }
1239                    writer.push_str("</");
1240                    writer.push_str(&data.name.local);
1241                    writer.push('>');
1242                    if matches!(style, OutputStyle::Pretty) {
1243                        writer.push('\n');
1244                    }
1245                }
1246            }
1247        }
1248    }
1249
1250    pub fn attrs(&self) -> Option<&[Attribute]> {
1251        Some(&self.element_data()?.attrs)
1252    }
1253
1254    pub fn attr(&self, name: LocalName) -> Option<&str> {
1255        let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
1256        Some(&attr.value)
1257    }
1258
1259    pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1260        self.stylo_element_data_opt()
1261            .and_then(|stylo| stylo.primary_styles())
1262    }
1263
1264    pub fn text_content(&self) -> String {
1265        let mut out = String::new();
1266        self.write_text_content(&mut out);
1267        out
1268    }
1269
1270    /// Write this subtree's text content into any [`std::fmt::Write`] sink.
1271    ///
1272    /// Public and generic so that a caller which does not want a `String` does
1273    /// not have to fork this traversal to avoid one. `blitz-dom-api`'s
1274    /// buffer-writing reader passes a sink that counts, and then one that fills
1275    /// a caller-supplied slice; a private copy of this walk in that crate would
1276    /// silently disagree with this one the first time a `NodeData` variant is
1277    /// added here.
1278    ///
1279    /// The sinks callers pass do not fail, and `String`'s never has, so nothing
1280    /// in this crate inspects the `Result`. It is kept in the signature because
1281    /// it is `fmt::Write`'s, not because there is an error to handle.
1282    pub fn write_text_content<W: Write>(&self, out: &mut W) {
1283        match &self.data {
1284            NodeData::Text(data) => {
1285                let _ = out.write_str(&data.content);
1286            }
1287            NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
1288                for child_id in self.children.iter() {
1289                    self.with(*child_id).write_text_content(out);
1290                }
1291            }
1292            _ => {}
1293        }
1294    }
1295
1296    pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1297        if let NodeData::Element(ref mut elem_data) = self.data {
1298            if let Some(guard) = elem_data.guard.clone() {
1299                elem_data.flush_style_attribute(&guard, url_extra_data);
1300            }
1301        }
1302    }
1303
1304    pub fn order(&self) -> i32 {
1305        self.primary_styles()
1306            .map(|s| match s.pseudo() {
1307                Some(PseudoElement::Before) => i32::MIN,
1308                Some(PseudoElement::After) => i32::MAX,
1309                _ => s.clone_order(),
1310            })
1311            .unwrap_or(0)
1312    }
1313
1314    pub fn z_index(&self) -> i32 {
1315        self.primary_styles()
1316            .map(|s| s.clone_z_index().integer_or(0))
1317            .unwrap_or(0)
1318    }
1319
1320    // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts
1321    pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1322        let Some(style) = self.primary_styles() else {
1323            return false;
1324        };
1325
1326        let position = style.clone_position();
1327        let has_z_index = !style.clone_z_index().is_auto();
1328
1329        if style.clone_opacity() != 1.0 {
1330            return true;
1331        }
1332
1333        let position_based = match position {
1334            Position::Fixed | Position::Sticky => true,
1335            Position::Relative | Position::Absolute => has_z_index,
1336            Position::Static => has_z_index && is_flex_or_grid_item,
1337        };
1338        if position_based {
1339            return true;
1340        }
1341
1342        if self.transform().is_some() {
1343            return true;
1344        }
1345
1346        // `isolation: isolate` exists precisely to create a stacking context
1347        // without any other visual effect. Ignoring it lets a negative z-index
1348        // descendant escape to an ancestor context, where it is painted before
1349        // (and so underneath) the backgrounds of the boxes in between.
1350        if style.get_box().isolation == Isolation::Isolate {
1351            return true;
1352        }
1353
1354        // TODO: mix-blend-mode
1355        // TODO: filter
1356        // TODO: clip-path
1357        // TODO: mask
1358        // TODO: contain
1359
1360        false
1361    }
1362
1363    /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns:
1364    ///    - None if the position is outside of this node's bounds
1365    ///    - Some(HitResult) if the position is within the node but doesn't match any children
1366    ///    - The result of recursively calling child.hit() on the the child element that is
1367    ///      positioned at that position if there is one.
1368    ///
1369    /// TODO: z-index
1370    /// (If multiple children are positioned at the position then a random one will be recursed into)
1371    pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
1372        self.hit_inner(x, y, scale, &mut None)
1373    }
1374
1375    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
1376    /// thumb under the point into `scrollbar` during the same descent (so
1377    /// thumb hit-testing shares the exact coordinate handling — transforms
1378    /// included — of every other hit test).
1379    pub(crate) fn hit_inner(
1380        &self,
1381        x: f32,
1382        y: f32,
1383        scale: f64,
1384        scrollbar: &mut Option<crate::node::ScrollbarRef>,
1385    ) -> Option<HitResult> {
1386        use style::computed_values::pointer_events::T as PointerEvents;
1387        use style::computed_values::visibility::T as Visibility;
1388
1389        // A hidden subtree takes no hits.
1390        //
1391        // This never needed saying while hiding a pane destroyed its boxes: a
1392        // hidden subtree had nothing to test against. It keeps its boxes now,
1393        // and their `final_layout` is whatever it was when the pane was last
1394        // visible — full size, in place, over the tab in front. A retained tab
1395        // measured 331 of its 370 elements still carrying live geometry after
1396        // being hidden, and every one of them was a click target.
1397        if matches!(self.style().display, taffy::Display::None) {
1398            return None;
1399        }
1400
1401        // Don't hit on visbility:hidden elements
1402        if let Some(style) = self.primary_styles() {
1403            if matches!(
1404                style.clone_visibility(),
1405                Visibility::Hidden | Visibility::Collapse
1406            ) {
1407                return None;
1408            }
1409        }
1410
1411        // pointer-events:none makes this element transparent to hits, but its
1412        // descendants are still tested (one may restore pointer-events:auto).
1413        let pointer_events_none = self
1414            .primary_styles()
1415            .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1416
1417        let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
1418        let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
1419
1420        if let Some(t) = *self.transform() {
1421            let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
1422            x = (p.x / scale) as f32;
1423            y = (p.y / scale) as f32;
1424        }
1425
1426        let size = self.final_layout().size;
1427        let matches_self = !(x < 0.0
1428            || x > size.width + self.scroll_offset().x as f32
1429            || y < 0.0
1430            || y > size.height + self.scroll_offset().y as f32);
1431
1432        let content_size = self.final_layout().content_size;
1433        let matches_content = !(x < 0.0
1434            || x > content_size.width + self.scroll_offset().x as f32
1435            || y < 0.0
1436            || y > content_size.height + self.scroll_offset().y as f32);
1437
1438        let matches_hoisted_content = match &self.stacking_context {
1439            Some(sc) => {
1440                let content_area = sc.content_area;
1441                x >= content_area.left + self.scroll_offset().x as f32
1442                    && x <= content_area.right + self.scroll_offset().x as f32
1443                    && y >= content_area.top + self.scroll_offset().y as f32
1444                    && y <= content_area.bottom + self.scroll_offset().y as f32
1445            }
1446            None => false,
1447        };
1448
1449        // `scrollable_overflow` is stored in device (scaled) pixels, whereas the
1450        // coordinates here are in CSS pixels, so unscale it before comparing.
1451        let overflow = *self.scrollable_overflow();
1452
1453        let matches_overflow = x >= (overflow.x0 / scale) as f32
1454            && x <= (overflow.x1 / scale) as f32
1455            && y >= (overflow.y0 / scale) as f32
1456            && y <= (overflow.y1 / scale) as f32;
1457
1458        if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
1459            return None;
1460        }
1461
1462        // Descendants overwrite, so the innermost scroll container's thumb
1463        // wins. Thumb coords are border-box relative (unscrolled).
1464        if matches_self
1465            && let Some(sb) = self.scrollbar_at_local(
1466                (x - self.scroll_offset().x as f32) as f64,
1467                (y - self.scroll_offset().y as f32) as f64,
1468            )
1469        {
1470            *scrollbar = Some(sb);
1471        }
1472
1473        if self.flags.is_inline_root() {
1474            let content_box_offset = taffy::Point {
1475                x: self.final_layout().padding.left + self.final_layout().border.left,
1476                y: self.final_layout().padding.top + self.final_layout().border.top,
1477            };
1478            x -= content_box_offset.x;
1479            y -= content_box_offset.y;
1480        }
1481
1482        // Positive z_index hoisted children
1483        if matches_hoisted_content {
1484            if let Some(hoisted) = &self.stacking_context {
1485                for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
1486                    let x = x - hoisted_child.position.x;
1487                    let y = y - hoisted_child.position.y;
1488                    if let Some(hit) = self
1489                        .with(hoisted_child.node_id)
1490                        .hit_inner(x, y, scale, scrollbar)
1491                    {
1492                        return Some(hit);
1493                    }
1494                }
1495            }
1496        }
1497
1498        // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id).
1499        for child_id in self.paint_children.borrow().iter().flatten().rev() {
1500            if let Some(hit) = self.with(*child_id).hit_inner(x, y, scale, scrollbar) {
1501                return Some(hit);
1502            }
1503        }
1504
1505        // Negative z_index hoisted children
1506        if matches_hoisted_content {
1507            if let Some(hoisted) = &self.stacking_context {
1508                for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
1509                    let x = x - hoisted_child.position.x;
1510                    let y = y - hoisted_child.position.y;
1511                    if let Some(hit) = self
1512                        .with(hoisted_child.node_id)
1513                        .hit_inner(x, y, scale, scrollbar)
1514                    {
1515                        return Some(hit);
1516                    }
1517                }
1518            }
1519        }
1520
1521        // Inline children
1522        if self.flags.is_inline_root() {
1523            let element_data = &self.element_data().unwrap();
1524            if let Some(ild) = element_data.inline_layout_data.as_ref() {
1525                let layout = &ild.layout;
1526                let scale = layout.scale();
1527
1528                if let Some((cluster, _side)) =
1529                    Cluster::from_point_exact(layout, x * scale, y * scale)
1530                {
1531                    let style_index = cluster.glyphs().next()?.style_index();
1532                    let node_id = layout.styles()[style_index].brush.id;
1533                    let text_pointer_events_none = self
1534                        .with(node_id)
1535                        .primary_styles()
1536                        .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1537                    if !text_pointer_events_none {
1538                        return Some(HitResult {
1539                            node_id,
1540                            x,
1541                            y,
1542                            is_text: true,
1543                        });
1544                    }
1545                }
1546            }
1547        }
1548
1549        // Self (this node)
1550        if matches_self && !pointer_events_none {
1551            return Some(HitResult {
1552                node_id: self.id,
1553                x,
1554                y,
1555                is_text: false,
1556            });
1557        }
1558
1559        None
1560    }
1561
1562    /// Find the inline root ancestor of this node (or self if this is an inline root).
1563    /// Returns None if no inline root ancestor exists.
1564    pub fn inline_root_ancestor(&self) -> Option<&Node> {
1565        let mut node = self;
1566        loop {
1567            if node.flags.is_inline_root() {
1568                return Some(node);
1569            }
1570            let id = node.layout_parent.get()?;
1571            node = self.with(id);
1572        }
1573    }
1574
1575    /// Get the text byte offset at a given point, using coordinates already transformed
1576    /// to be relative to this inline root's content box.
1577    /// Returns Some(byte_offset) if the point hits text, None otherwise.
1578    pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
1579        if !self.flags.is_inline_root() {
1580            return None;
1581        }
1582
1583        let element_data = self.element_data()?;
1584        let inline_layout = element_data.inline_layout_data.as_ref()?;
1585        let layout = &inline_layout.layout;
1586        let scale = layout.scale();
1587
1588        // Use Parley's cluster hit testing (from_point is more forgiving than from_point_exact)
1589        let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
1590
1591        // Determine byte offset based on which side of the cluster was clicked
1592        // For LTR text: left side = start of cluster, right side = end of cluster
1593        // For RTL text: left side = end of cluster, right side = start of cluster
1594        // Also, explicit line breaks should always use start to avoid cursor appearing on next line
1595        let is_leading = side == ClusterSide::Left;
1596        let offset = if cluster.is_rtl() {
1597            if is_leading {
1598                cluster.text_range().end
1599            } else {
1600                cluster.text_range().start
1601            }
1602        } else {
1603            // LTR text
1604            if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
1605                cluster.text_range().start
1606            } else {
1607                cluster.text_range().end
1608            }
1609        };
1610
1611        Some(offset)
1612    }
1613
1614    /// The byte range of the word or line at a point, for a multi-click
1615    /// selection.
1616    ///
1617    /// See [`TextGranularity`] for which click count maps to which unit. Coordinates are relative to this inline root's content box,
1618    /// as for [`text_offset_at_point`](Self::text_offset_at_point).
1619    ///
1620    /// Parley owns the boundary rules (it is what an `<input>` already selects
1621    /// with), so this asks it rather than scanning for spaces: word breaks are
1622    /// a Unicode segmentation question, not a whitespace one.
1623    pub fn text_range_at_point(
1624        &self,
1625        x: f32,
1626        y: f32,
1627        granularity: TextGranularity,
1628    ) -> Option<Range<usize>> {
1629        if !self.flags.is_inline_root() {
1630            return None;
1631        }
1632
1633        let element_data = self.element_data()?;
1634        let inline_layout = element_data.inline_layout_data.as_ref()?;
1635        let layout = &inline_layout.layout;
1636        let scale = layout.scale();
1637        let (x, y) = (x * scale, y * scale);
1638
1639        // Bail when the point misses the text entirely: `Selection` answers an
1640        // out-of-range point with a collapsed cursor at the end of the text,
1641        // which would read as "selected nothing at the very end" rather than
1642        // as a miss.
1643        Cluster::from_point(layout, x, y)?;
1644
1645        let selection = match granularity {
1646            TextGranularity::Word => Selection::word_from_point(layout, x, y),
1647            // The hard line, so a soft-wrapped paragraph selects as the whole
1648            // paragraph. That is what a triple click does elsewhere, and it is
1649            // what `select_hard_line_at_point` gives a text input.
1650            TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
1651        };
1652
1653        let range = selection.text_range();
1654        if range.is_empty() { None } else { Some(range) }
1655    }
1656
1657    /// Computes the Document-relative coordinates of the `Node`
1658    pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
1659        // A scroll offset moves this node's descendants, not its own border
1660        // box. Parent recursion applies each ancestor offset to the child.
1661        let x = x + self.final_layout().location.x;
1662        let y = y + self.final_layout().location.y;
1663
1664        // Recurse up the layout hierarchy
1665        self.layout_parent
1666            .get()
1667            .and_then(|id| self.tree().get(id))
1668            .map(|parent| {
1669                parent.absolute_position(
1670                    x - parent.scroll_offset().x as f32,
1671                    y - parent.scroll_offset().y as f32,
1672                )
1673            })
1674            .unwrap_or(crate::util::Point { x, y })
1675    }
1676
1677    /// Whether this node can act as an [`offset_parent`](Self::offset_parent): a positioned
1678    /// element, or one of the elements that always qualify (`body`, `td`, `th`).
1679    fn is_offset_parent(&self) -> bool {
1680        let Some(styles) = self.primary_styles() else {
1681            return false;
1682        };
1683        if styles.get_box().position != Position::Static {
1684            return true;
1685        }
1686        self.data.is_element_with_tag_name(&local_name!("body"))
1687            || self.data.is_element_with_tag_name(&local_name!("td"))
1688            || self.data.is_element_with_tag_name(&local_name!("th"))
1689    }
1690
1691    /// The nearest layout ancestor that [is an offset parent](Self::is_offset_parent), as in
1692    /// CSSOM View's `offsetParent`.
1693    pub fn offset_parent(&self) -> Option<&Node> {
1694        let mut node = self;
1695        loop {
1696            node = self.with(node.layout_parent.get()?);
1697            if node.is_offset_parent() {
1698                return Some(node);
1699            }
1700        }
1701    }
1702
1703    /// CSSOM View's `offsetLeft`/`offsetTop`: the offset of this node's border box from the
1704    /// padding edge of its [`offset_parent`](Self::offset_parent).
1705    pub fn offset_top_left(&self) -> crate::util::Point<f32> {
1706        let mut x = 0.0;
1707        let mut y = 0.0;
1708        let mut current = self;
1709        loop {
1710            let layout = current.final_layout();
1711            x += layout.location.x;
1712            y += layout.location.y;
1713
1714            let Some(parent_id) = current.layout_parent.get() else {
1715                break;
1716            };
1717            let parent = self.with(parent_id);
1718            if parent.is_offset_parent() {
1719                let border = parent.final_layout().border;
1720                x -= border.left;
1721                y -= border.top;
1722                break;
1723            }
1724            current = parent;
1725        }
1726        crate::util::Point { x, y }
1727    }
1728
1729    /// Creates a synthetic click event
1730    pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
1731        DomEventData::Click(self.synthetic_click_event_data(mods))
1732    }
1733
1734    pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
1735        let absolute_position = self.absolute_position(0.0, 0.0);
1736        let x = absolute_position.x + (self.final_layout().size.width / 2.0);
1737        let y = absolute_position.y + (self.final_layout().size.height / 2.0);
1738
1739        BlitzPointerEvent {
1740            id: BlitzPointerId::Mouse,
1741            is_primary: true,
1742            coords: PointerCoords {
1743                page_x: x,
1744                page_y: y,
1745
1746                // TODO: should these be different?
1747                screen_x: x,
1748                screen_y: y,
1749                client_x: x,
1750                client_y: y,
1751            },
1752            mods,
1753            button: Default::default(),
1754            buttons: Default::default(),
1755            details: Default::default(),
1756            element: Default::default(),
1757            active_pointers: Default::default(),
1758        }
1759    }
1760}
1761
1762/// It might be wrong to expose this since what does *equality* mean outside the dom?
1763impl PartialEq for Node {
1764    fn eq(&self, other: &Self) -> bool {
1765        self.id == other.id
1766    }
1767}
1768
1769impl Eq for Node {}
1770
1771impl std::fmt::Debug for Node {
1772    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1773        // FIXME: update to reflect changes to fields
1774        f.debug_struct("NodeData")
1775            .field("parent", &self.parent)
1776            .field("id", &self.id)
1777            .field("is_inline_root", &self.flags.is_inline_root())
1778            .field("children", &self.children)
1779            .field("layout_children", &self.layout_children.borrow())
1780            // .field("style", &self.style)
1781            .field("node", &self.data)
1782            .field("stylo_element_data", &self.stylo_element_data_opt())
1783            // .field("unrounded_layout", &self.unrounded_layout)
1784            // .field("final_layout", &self.final_layout)
1785            .finish()
1786    }
1787}
1788
1789#[cfg(test)]
1790mod test {
1791    use style_dom::ElementState;
1792
1793    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
1794
1795    #[test]
1796    fn absolute_position_tolerates_a_detached_layout_parent() {
1797        let mut document = BaseDocument::new(DocumentConfig::default());
1798        let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
1799            qual_name!("section"),
1800            vec![],
1801        ))));
1802        let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
1803            qual_name!("button"),
1804            vec![],
1805        ))));
1806        document
1807            .get_node(child)
1808            .unwrap()
1809            .layout_parent
1810            .set(Some(parent));
1811
1812        // An event handler may detach its own ancestor before the renderer's
1813        // default-action tail finishes computing event-relative coordinates.
1814        // The child object can still be alive through that dispatch even though
1815        // its recorded layout parent has already left the slot map.
1816        document.remove_node_from_tree(parent);
1817
1818        assert_eq!(
1819            document
1820                .get_node(child)
1821                .unwrap()
1822                .absolute_position(4.0, 7.0),
1823            crate::util::Point { x: 4.0, y: 7.0 },
1824        );
1825    }
1826
1827    #[test]
1828    fn create_node_with_disabled_attr() {
1829        let mut document = BaseDocument::new(DocumentConfig::default());
1830        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1831            qual_name!("button"),
1832            vec![Attribute {
1833                name: qual_name!("disabled"),
1834                value: "".into(),
1835            }],
1836        ))));
1837        let node = document.get_node(node).unwrap();
1838
1839        assert!(
1840            node.element_state().contains(ElementState::DISABLED),
1841            "form node is disabled"
1842        );
1843        assert!(
1844            !node.element_state().contains(ElementState::ENABLED),
1845            "form node is not enabled"
1846        );
1847    }
1848
1849    #[test]
1850    fn ignore_disabled_attr_content() {
1851        let mut document = BaseDocument::new(DocumentConfig::default());
1852        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1853            qual_name!("button"),
1854            vec![Attribute {
1855                name: qual_name!("disabled"),
1856                value: "false".into(),
1857            }],
1858        ))));
1859        let node = document.get_node(node).unwrap();
1860
1861        assert!(
1862            node.element_state().contains(ElementState::DISABLED),
1863            "form node is disabled"
1864        );
1865        assert!(
1866            !node.element_state().contains(ElementState::ENABLED),
1867            "form node is not enabled"
1868        );
1869    }
1870
1871    #[test]
1872    fn create_node_with_ignored_disable() {
1873        let mut document = BaseDocument::new(DocumentConfig::default());
1874        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1875            qual_name!("a"),
1876            vec![Attribute {
1877                name: qual_name!("disabled"),
1878                value: "".into(),
1879            }],
1880        ))));
1881        let node = document.get_node(node).unwrap();
1882
1883        assert!(
1884            !node.element_state().contains(ElementState::DISABLED),
1885            "Non form node cannot be disabled"
1886        );
1887        assert!(
1888            !node.element_state().contains(ElementState::ENABLED),
1889            "Non form node cannot be enabled"
1890        );
1891    }
1892
1893    #[test]
1894    fn create_empty_enabled_node() {
1895        let mut document = BaseDocument::new(DocumentConfig::default());
1896        let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
1897            qual_name!("button"),
1898            vec![],
1899        ))));
1900        let node = document.get_node(node).unwrap();
1901
1902        assert!(
1903            node.element_state().contains(ElementState::ENABLED),
1904            "Button should be enabled by default"
1905        );
1906    }
1907}