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