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