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::shell::ShellProvider;
8use euclid::{Point2D, Rect, Size2D};
9use html_escape::encode_quoted_attribute_to_string;
10use keyboard_types::Modifiers;
11use kurbo::{Affine, Rect as KurboRect};
12use markup5ever::{LocalName, local_name};
13use parley::{BreakReason, Cluster, ClusterSide};
14use selectors::matching::ElementSelectorFlags;
15use slab::Slab;
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::{PseudoElement, 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};
38
39use super::stylo_data::StyloData;
40use super::{Attribute, ElementData};
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum DisplayOuter {
44    Block,
45    Inline,
46    None,
47}
48
49bitflags! {
50    #[derive(Clone, Copy, PartialEq)]
51    pub struct NodeFlags: u32 {
52        /// Whether the node is the root node of an Inline Formatting Context
53        const IS_INLINE_ROOT = 0b00000001;
54        /// Whether the node is the root node of an Table formatting context
55        const IS_TABLE_ROOT = 0b00000010;
56        /// Whether the node is "in the document" (~= has a parent and isn't a template node)
57        const IS_IN_DOCUMENT = 0b00000100;
58    }
59}
60
61impl NodeFlags {
62    #[inline(always)]
63    pub fn is_inline_root(&self) -> bool {
64        self.contains(Self::IS_INLINE_ROOT)
65    }
66
67    #[inline(always)]
68    pub fn is_table_root(&self) -> bool {
69        self.contains(Self::IS_TABLE_ROOT)
70    }
71
72    #[inline(always)]
73    pub fn is_in_document(&self) -> bool {
74        self.contains(Self::IS_IN_DOCUMENT)
75    }
76
77    #[inline(always)]
78    pub fn reset_construction_flags(&mut self) {
79        self.remove(Self::IS_INLINE_ROOT);
80        self.remove(Self::IS_TABLE_ROOT);
81    }
82}
83
84pub struct Node {
85    // The actual tree we belong to. This is unsafe!!
86    tree: *mut Slab<Node>,
87
88    /// Our Id
89    pub id: usize,
90    /// Our parent's ID
91    pub parent: Option<usize>,
92    // What are our children?
93    pub children: Vec<usize>,
94    /// Our parent in the layout hierachy: a separate list that includes anonymous collections of inline elements
95    pub layout_parent: Cell<Option<usize>>,
96    /// A separate child list that includes anonymous collections of inline elements
97    pub layout_children: RefCell<Option<Vec<usize>>>,
98    /// The same as layout_children, but sorted by z-index
99    pub paint_children: RefCell<Option<Vec<usize>>>,
100    pub stacking_context: Option<Box<HoistedPaintChildren>>,
101
102    // Flags
103    pub flags: NodeFlags,
104
105    /// Node type (Element, TextNode, etc) specific data
106    pub data: NodeData,
107
108    // This little bundle of joy is our style data from stylo and a lock guard that allows access to it
109    // TODO: See if guard can be hoisted to a higher level
110    pub stylo_element_data: StyloData,
111    pub selector_flags: Cell<ElementSelectorFlags>,
112    pub guard: SharedRwLock,
113    pub element_state: ElementState,
114    pub has_snapshot: bool,
115    pub snapshot_handled: AtomicBool,
116    /// Whether any descendant of this node needs restyling.
117    /// Used by Stylo's incremental style traversal to skip unchanged subtrees.
118    pub dirty_descendants: AtomicBool,
119
120    // Pseudo element nodes
121    pub before: Option<usize>,
122    pub after: Option<usize>,
123
124    // Taffy layout data:
125    pub style: Style<Atom>,
126    pub display_constructed_as: StyloDisplay,
127    pub cache: Cache,
128    pub unrounded_layout: Layout,
129    pub final_layout: Layout,
130    pub scroll_offset: crate::Point<f64>,
131
132    pub scrollable_overflow: KurboRect,
133    pub transform: Option<Affine>,
134}
135
136unsafe impl Send for Node {}
137unsafe impl Sync for Node {}
138
139impl Node {
140    pub(crate) fn new(
141        tree: *mut Slab<Node>,
142        id: usize,
143        guard: SharedRwLock,
144        data: NodeData,
145    ) -> Self {
146        // The element state needs to be modified if the element is disabled
147        let state = match &data {
148            NodeData::Element(data) => {
149                let mut state = ElementState::empty();
150                if data.can_be_disabled() {
151                    state.insert(match data.has_attr(local_name!("disabled")) {
152                        true => ElementState::DISABLED,
153                        false => ElementState::ENABLED,
154                    })
155                }
156
157                state
158            }
159            _ => ElementState::empty(),
160        };
161
162        Self {
163            tree,
164
165            id,
166            parent: None,
167            children: vec![],
168            layout_parent: Cell::new(None),
169            layout_children: RefCell::new(None),
170            paint_children: RefCell::new(None),
171            stacking_context: None,
172
173            flags: NodeFlags::empty(),
174            data,
175
176            stylo_element_data: Default::default(),
177            selector_flags: Cell::new(ElementSelectorFlags::empty()),
178            guard,
179            element_state: state,
180
181            before: None,
182            after: None,
183
184            style: Default::default(),
185            has_snapshot: false,
186            snapshot_handled: AtomicBool::new(false),
187            dirty_descendants: AtomicBool::new(true),
188            display_constructed_as: StyloDisplay::Block,
189            cache: Cache::new(),
190            unrounded_layout: Layout::new(),
191            final_layout: Layout::new(),
192            scroll_offset: crate::Point::ZERO,
193
194            scrollable_overflow: KurboRect::ZERO,
195            transform: None,
196        }
197    }
198
199    pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
200        self.transform = self.primary_styles().and_then(|s| {
201            let w = self.final_layout.size.width * scale;
202            let h = self.final_layout.size.height * scale;
203            let reference_box = Rect::new(
204                Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
205                Size2D::new(CSSPixelLength::new(w), CSSPixelLength::new(h)),
206            );
207            crate::resolve_2d_transform(s.get_box(), reference_box)
208        });
209
210        self.transform
211    }
212
213    pub fn pe_by_index(&self, index: usize) -> Option<usize> {
214        match index {
215            0 => self.after,
216            1 => self.before,
217            _ => panic!("Invalid pseudo element index"),
218        }
219    }
220
221    pub fn set_pe_by_index(&mut self, index: usize, value: Option<usize>) {
222        match index {
223            0 => self.after = value,
224            1 => self.before = value,
225            _ => panic!("Invalid pseudo element index"),
226        }
227    }
228
229    pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
230        Some(self.primary_styles().as_ref()?.clone_display())
231    }
232
233    pub fn is_or_contains_block(&self) -> bool {
234        let style = self.primary_styles();
235        let style = style.as_ref();
236
237        // Ignore out-of-flow items
238        let position = style
239            .map(|s| s.clone_position())
240            .unwrap_or(Position::Relative);
241        let is_in_flow = matches!(
242            position,
243            Position::Static | Position::Relative | Position::Sticky
244        );
245        if !is_in_flow {
246            return false;
247        }
248        let display = style
249            .map(|s| s.clone_display())
250            .unwrap_or(StyloDisplay::inline());
251        match display.outside() {
252            DisplayOutside::None => false,
253            DisplayOutside::Block => true,
254            _ => {
255                if display.inside() == DisplayInside::Flow {
256                    self.children
257                        .iter()
258                        .copied()
259                        .any(|child_id| self.tree()[child_id].is_or_contains_block())
260                } else {
261                    false
262                }
263            }
264        }
265    }
266
267    pub fn is_whitespace_node(&self) -> bool {
268        match &self.data {
269            NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
270            _ => false,
271        }
272    }
273
274    pub fn is_focussable(&self) -> bool {
275        self.data
276            .downcast_element()
277            .map(|el| el.is_focussable)
278            .unwrap_or(false)
279    }
280
281    pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
282        if let Some(mut element_data) = self.stylo_element_data.get_mut() {
283            element_data.hint.insert(hint);
284        }
285        // Mark all ancestors as having dirty descendants so the style traversal
286        // will visit this node's subtree
287        self.mark_ancestors_dirty();
288    }
289
290    /// Returns whether this node has any descendants that need restyling.
291    pub fn has_dirty_descendants(&self) -> bool {
292        self.dirty_descendants.load(Ordering::Relaxed)
293    }
294
295    /// Sets the dirty_descendants flag on this node.
296    pub fn set_dirty_descendants(&self) {
297        self.dirty_descendants.store(true, Ordering::Relaxed);
298    }
299
300    /// Clears the dirty_descendants flag on this node.
301    pub fn unset_dirty_descendants(&self) {
302        self.dirty_descendants.store(false, Ordering::Relaxed);
303    }
304
305    /// Set appropriate damage for Stylo when an element's style attribute is updated
306    pub(crate) fn mark_style_attr_updated(&mut self) {
307        if let Some(mut data) = self.stylo_element_data.get_mut() {
308            data.hint |= RestyleHint::RESTYLE_STYLE_ATTRIBUTE;
309        }
310        self.set_dirty_descendants();
311    }
312
313    /// Marks all ancestors of this node as having dirty descendants.
314    /// This propagates the dirty flag up the tree so that the style traversal
315    /// knows to visit the subtree containing this node.
316    pub fn mark_ancestors_dirty(&self) {
317        let mut current_id = self.parent;
318        while let Some(parent_id) = current_id {
319            let parent = &self.tree()[parent_id];
320            // If this ancestor already has dirty_descendants set, we can stop
321            // because all further ancestors must also have it set
322            if parent.dirty_descendants.swap(true, Ordering::Relaxed) {
323                break;
324            }
325            current_id = parent.parent;
326        }
327    }
328
329    // pub fn damage_mut(&mut self) -> Option<&mut RestyleDamage> {
330    //     self.stylo_element_data
331    //         .get_mut()
332    //         .map(|mut data: ElementDataMut<'a>| &'a mut data.damage)
333    // }
334
335    pub fn damage(&self) -> Option<RestyleDamage> {
336        self.stylo_element_data.get().map(|data| data.damage)
337    }
338
339    pub fn set_damage(&mut self, damage: RestyleDamage) {
340        if let Some(mut data) = self.stylo_element_data.get_mut() {
341            data.damage = damage;
342        }
343    }
344
345    pub fn insert_damage(&mut self, damage: RestyleDamage) {
346        if let Some(mut data) = self.stylo_element_data.get_mut() {
347            data.damage |= damage;
348        }
349    }
350
351    pub fn remove_damage(&mut self, damage: RestyleDamage) {
352        if let Some(mut data) = self.stylo_element_data.get_mut() {
353            data.damage.remove(damage);
354        }
355    }
356
357    pub fn clear_damage_mut(&mut self) {
358        if let Some(mut data) = self.stylo_element_data.get_mut() {
359            data.damage = RestyleDamage::empty();
360        }
361    }
362
363    pub fn hover(&mut self) {
364        self.element_state.insert(ElementState::HOVER);
365        self.set_restyle_hint(RestyleHint::restyle_subtree());
366    }
367
368    pub fn unhover(&mut self) {
369        self.element_state.remove(ElementState::HOVER);
370        self.set_restyle_hint(RestyleHint::restyle_subtree());
371    }
372
373    pub fn is_hovered(&self) -> bool {
374        self.element_state.contains(ElementState::HOVER)
375    }
376
377    pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
378        self.element_state
379            .insert(ElementState::FOCUS | ElementState::FOCUSRING);
380        self.set_restyle_hint(RestyleHint::restyle_subtree());
381
382        // If focussing a text input, enable IME and set IME area
383        if self
384            .element_data()
385            .and_then(|elem| elem.text_input_data())
386            .is_some()
387        {
388            shell_provider.set_ime_enabled(true);
389            let mut pos = self.absolute_position(0.0, 0.0);
390            pos.x += self.final_layout.content_box_x();
391            pos.y += self.final_layout.content_box_y();
392            let width = self.final_layout.content_box_width();
393            let height = self.final_layout.content_box_height();
394            shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
395        }
396    }
397
398    pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
399        self.element_state
400            .remove(ElementState::FOCUS | ElementState::FOCUSRING);
401        self.set_restyle_hint(RestyleHint::restyle_subtree());
402
403        // If blurring a text input, disable IME
404        if self
405            .element_data()
406            .and_then(|elem| elem.text_input_data())
407            .is_some()
408        {
409            shell_provider.set_ime_enabled(false);
410        }
411    }
412
413    pub fn is_focussed(&self) -> bool {
414        self.element_state.contains(ElementState::FOCUS)
415    }
416
417    pub fn active(&mut self) {
418        self.element_state.insert(ElementState::ACTIVE);
419        self.set_restyle_hint(RestyleHint::restyle_subtree());
420    }
421
422    pub fn unactive(&mut self) {
423        self.element_state.remove(ElementState::ACTIVE);
424        self.set_restyle_hint(RestyleHint::restyle_subtree());
425    }
426
427    pub fn is_active(&self) -> bool {
428        self.element_state.contains(ElementState::ACTIVE)
429    }
430
431    // Marks the node as disabled if it can be.
432    // It does not disable any children which should be disabled as well (relevant for the `select` element).
433    pub fn disable(&mut self) {
434        if self
435            .data
436            .downcast_element()
437            .is_some_and(|data| data.can_be_disabled())
438        {
439            self.element_state.insert(ElementState::DISABLED);
440            self.element_state.remove(ElementState::ENABLED);
441        }
442        self.set_restyle_hint(RestyleHint::restyle_subtree());
443    }
444
445    // Marks the node as enabled if it can be.
446    // It does not enable any children which should be enabled as well (relevant for the `select` element).
447    pub fn enable(&mut self) {
448        if self
449            .data
450            .downcast_element()
451            .is_some_and(|data| data.can_be_disabled())
452        {
453            self.element_state.insert(ElementState::ENABLED);
454            self.element_state.remove(ElementState::DISABLED);
455        }
456        self.set_restyle_hint(RestyleHint::restyle_subtree());
457    }
458
459    pub fn subdoc(&self) -> Option<&dyn Document> {
460        self.element_data().and_then(|el| el.sub_doc_data())
461    }
462
463    pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
464        self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
465    }
466
467    pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
468        // For single-line inputs, add an offset to vertically center the text input layout
469        // within the content box of it's node.
470        if let Some(input_data) = self
471            .data
472            .downcast_element()
473            .and_then(|el| el.text_input_data())
474        {
475            if !input_data.is_multiline {
476                let content_box_height = self.final_layout.content_box_height();
477                let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
478                let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
479
480                return y_offset as f64;
481            }
482        }
483
484        0.0
485    }
486}
487
488#[derive(Debug, Clone, Copy, PartialEq)]
489pub enum NodeKind {
490    Document,
491    Element,
492    AnonymousBlock,
493    Text,
494    Comment,
495}
496
497/// The different kinds of nodes in the DOM.
498#[derive(Debug, Clone)]
499pub enum NodeData {
500    /// The `Document` itself - the root node of a HTML document.
501    Document,
502
503    /// An element with attributes.
504    Element(ElementData),
505
506    /// An anonymous block box
507    AnonymousBlock(ElementData),
508
509    /// A text node.
510    Text(TextNodeData),
511
512    /// A comment.
513    Comment,
514    // Comment { contents: String },
515
516    // /// A `DOCTYPE` with name, public id, and system id. See
517    // /// [document type declaration on wikipedia][https://en.wikipedia.org/wiki/Document_type_declaration]
518    // Doctype { name: String, public_id: String, system_id: String },
519
520    // /// A Processing instruction.
521    // ProcessingInstruction { target: String, contents: String },
522}
523
524impl NodeData {
525    pub fn downcast_element(&self) -> Option<&ElementData> {
526        match self {
527            Self::Element(data) => Some(data),
528            Self::AnonymousBlock(data) => Some(data),
529            _ => None,
530        }
531    }
532
533    pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
534        match self {
535            Self::Element(data) => Some(data),
536            Self::AnonymousBlock(data) => Some(data),
537            _ => None,
538        }
539    }
540
541    pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
542        let Some(elem) = self.downcast_element() else {
543            return false;
544        };
545        *name == elem.name.local
546    }
547
548    pub fn attrs(&self) -> Option<&[Attribute]> {
549        Some(&self.downcast_element()?.attrs)
550    }
551
552    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
553        self.downcast_element()?.attr(name)
554    }
555
556    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
557        self.downcast_element()
558            .is_some_and(|elem| elem.has_attr(name))
559    }
560
561    pub fn kind(&self) -> NodeKind {
562        match self {
563            NodeData::Document => NodeKind::Document,
564            NodeData::Element(_) => NodeKind::Element,
565            NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
566            NodeData::Text(_) => NodeKind::Text,
567            NodeData::Comment => NodeKind::Comment,
568        }
569    }
570}
571
572#[derive(Debug, Clone)]
573pub struct TextNodeData {
574    /// The textual content of the text node
575    pub content: String,
576}
577
578impl TextNodeData {
579    pub fn new(content: String) -> Self {
580        Self { content }
581    }
582}
583
584/*
585-> Computed styles
586-> Layout
587-----> Needs to happen only when styles are computed
588*/
589
590// type DomRefCell<T> = RefCell<T>;
591
592// pub struct DomData {
593//     // ... we can probs just get away with using the html5ever types directly. basically just using the servo dom, but without the bindings
594//     local_name: html5ever::LocalName,
595//     tag_name: html5ever::QualName,
596//     namespace: html5ever::Namespace,
597//     prefix: DomRefCell<Option<html5ever::Prefix>>,
598//     attrs: DomRefCell<Vec<Attr>>,
599//     // attrs: DomRefCell<Vec<Dom<Attr>>>,
600//     id_attribute: DomRefCell<Option<Atom>>,
601//     is: DomRefCell<Option<LocalName>>,
602//     // style_attribute: DomRefCell<Option<Arc<Locked<PropertyDeclarationBlock>>>>,
603//     // attr_list: MutNullableDom<NamedNodeMap>,
604//     // class_list: MutNullableDom<DOMTokenList>,
605//     state: Cell<ElementState>,
606// }
607
608impl Node {
609    pub fn tree(&self) -> &Slab<Node> {
610        unsafe { &*self.tree }
611    }
612
613    #[track_caller]
614    pub fn with(&self, id: usize) -> &Node {
615        self.tree().get(id).unwrap()
616    }
617
618    pub fn print_tree(&self, level: usize) {
619        println!(
620            "{} {} {:?} {} {:?}",
621            "  ".repeat(level),
622            self.id,
623            self.parent,
624            self.node_debug_str().replace('\n', ""),
625            self.children
626        );
627        // println!("{} {:?}", "  ".repeat(level), self.children);
628        for child_id in self.children.iter() {
629            let child = self.with(*child_id);
630            child.print_tree(level + 1)
631        }
632    }
633
634    // Get the index of the current node in the parents child list
635    pub fn index_of_child(&self, child_id: usize) -> Option<usize> {
636        self.children.iter().position(|id| *id == child_id)
637    }
638
639    // Get the index of the current node in the parents child list
640    pub fn child_index(&self) -> Option<usize> {
641        self.tree()[self.parent?]
642            .children
643            .iter()
644            .position(|id| *id == self.id)
645    }
646
647    // Get the nth node in the parents child list
648    pub fn forward(&self, n: usize) -> Option<&Node> {
649        let child_idx = self.child_index().unwrap_or(0);
650        self.tree()[self.parent?]
651            .children
652            .get(child_idx + n)
653            .map(|id| self.with(*id))
654    }
655
656    pub fn backward(&self, n: usize) -> Option<&Node> {
657        let child_idx = self.child_index().unwrap_or(0);
658        if child_idx < n {
659            return None;
660        }
661
662        self.tree()[self.parent?]
663            .children
664            .get(child_idx - n)
665            .map(|id| self.with(*id))
666    }
667
668    pub fn is_element(&self) -> bool {
669        matches!(self.data, NodeData::Element { .. })
670    }
671
672    pub fn is_anonymous(&self) -> bool {
673        matches!(self.data, NodeData::AnonymousBlock { .. })
674    }
675
676    pub fn is_text_node(&self) -> bool {
677        matches!(self.data, NodeData::Text { .. })
678    }
679
680    pub fn element_data(&self) -> Option<&ElementData> {
681        match self.data {
682            NodeData::Element(ref data) => Some(data),
683            NodeData::AnonymousBlock(ref data) => Some(data),
684            _ => None,
685        }
686    }
687
688    pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
689        match self.data {
690            NodeData::Element(ref mut data) => Some(data),
691            NodeData::AnonymousBlock(ref mut data) => Some(data),
692            _ => None,
693        }
694    }
695
696    pub fn text_data(&self) -> Option<&TextNodeData> {
697        match self.data {
698            NodeData::Text(ref data) => Some(data),
699            _ => None,
700        }
701    }
702
703    pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
704        match self.data {
705            NodeData::Text(ref mut data) => Some(data),
706            _ => None,
707        }
708    }
709
710    pub fn node_debug_str(&self) -> String {
711        let mut s = String::new();
712
713        match &self.data {
714            NodeData::Document => write!(s, "DOCUMENT"),
715            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
716            NodeData::Text(data) => {
717                let bytes = data.content.as_bytes();
718                write!(
719                    s,
720                    "TEXT {}",
721                    std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
722                        .unwrap_or("INVALID UTF8")
723                )
724            }
725            NodeData::Comment => write!(
726                s,
727                "COMMENT",
728                // &std::str::from_utf8(data.contents.as_bytes().split_at(10).0).unwrap_or("INVALID UTF8")
729            ),
730            NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
731            NodeData::Element(data) => {
732                let name = &data.name;
733                let class = self.attr(local_name!("class")).unwrap_or("");
734                let id = self.attr(local_name!("id")).unwrap_or("");
735                let display = self.display_constructed_as.to_css_string();
736                write!(s, "<{}", name.local).unwrap();
737                if !id.is_empty() {
738                    write!(s, " #{id}").unwrap();
739                }
740                if !class.is_empty() {
741                    if class.contains(' ') {
742                        write!(s, " class=\"{class}\"").unwrap()
743                    } else {
744                        write!(s, " .{class}").unwrap()
745                    }
746                }
747                write!(s, "> ({display})")
748            } // NodeData::ProcessingInstruction { .. } => write!(s, "ProcessingInstruction"),
749        }
750        .unwrap();
751        s
752    }
753
754    pub fn outer_html(&self) -> String {
755        let mut output = String::new();
756        self.write_outer_html(&mut output);
757        output
758    }
759
760    pub fn write_outer_html(&self, writer: &mut String) {
761        let has_children = !self.children.is_empty();
762        let current_color = self
763            .primary_styles()
764            .map(|style| style.clone_color())
765            .map(|color| color.to_css_string());
766
767        match &self.data {
768            NodeData::Document => {}
769            NodeData::Comment => {}
770            NodeData::AnonymousBlock(_) => {}
771            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
772            NodeData::Text(data) => {
773                writer.push_str(data.content.as_str());
774            }
775            NodeData::Element(data) => {
776                writer.push('<');
777                writer.push_str(&data.name.local);
778
779                for attr in data.attrs() {
780                    writer.push(' ');
781                    writer.push_str(&attr.name.local);
782                    writer.push_str("=\"");
783                    #[allow(clippy::unnecessary_unwrap)] // Convert to if-let chain once stabilised
784                    if current_color.is_some() && attr.value.contains("currentColor") {
785                        let value = attr
786                            .value
787                            .replace("currentColor", current_color.as_ref().unwrap());
788                        encode_quoted_attribute_to_string(&value, writer);
789                    } else {
790                        encode_quoted_attribute_to_string(&attr.value, writer);
791                    }
792                    writer.push('"');
793                }
794                if !has_children {
795                    writer.push_str(" /");
796                }
797                writer.push('>');
798
799                if has_children {
800                    for &child_id in &self.children {
801                        self.tree()[child_id].write_outer_html(writer);
802                    }
803
804                    writer.push_str("</");
805                    writer.push_str(&data.name.local);
806                    writer.push('>');
807                }
808            }
809        }
810    }
811
812    pub fn attrs(&self) -> Option<&[Attribute]> {
813        Some(&self.element_data()?.attrs)
814    }
815
816    pub fn attr(&self, name: LocalName) -> Option<&str> {
817        let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
818        Some(&attr.value)
819    }
820
821    pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
822        self.stylo_element_data.primary_styles()
823    }
824
825    pub fn text_content(&self) -> String {
826        let mut out = String::new();
827        self.write_text_content(&mut out);
828        out
829    }
830
831    fn write_text_content(&self, out: &mut String) {
832        match &self.data {
833            NodeData::Text(data) => {
834                out.push_str(&data.content);
835            }
836            NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
837                for child_id in self.children.iter() {
838                    self.with(*child_id).write_text_content(out);
839                }
840            }
841            _ => {}
842        }
843    }
844
845    pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
846        if let NodeData::Element(ref mut elem_data) = self.data {
847            elem_data.flush_style_attribute(&self.guard, url_extra_data);
848        }
849    }
850
851    pub fn order(&self) -> i32 {
852        self.primary_styles()
853            .map(|s| match s.pseudo() {
854                Some(PseudoElement::Before) => i32::MIN,
855                Some(PseudoElement::After) => i32::MAX,
856                _ => s.clone_order(),
857            })
858            .unwrap_or(0)
859    }
860
861    pub fn z_index(&self) -> i32 {
862        self.primary_styles()
863            .map(|s| s.clone_z_index().integer_or(0))
864            .unwrap_or(0)
865    }
866
867    // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts
868    pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
869        let Some(style) = self.primary_styles() else {
870            return false;
871        };
872
873        let position = style.clone_position();
874        let has_z_index = !style.clone_z_index().is_auto();
875
876        if style.clone_opacity() != 1.0 {
877            return true;
878        }
879
880        let position_based = match position {
881            Position::Fixed | Position::Sticky => true,
882            Position::Relative | Position::Absolute => has_z_index,
883            Position::Static => has_z_index && is_flex_or_grid_item,
884        };
885        if position_based {
886            return true;
887        }
888
889        if self.transform.is_some() {
890            return true;
891        }
892
893        // TODO: mix-blend-mode
894        // TODO: filter
895        // TODO: clip-path
896        // TODO: mask
897        // TODO: isolation
898        // TODO: contain
899
900        false
901    }
902
903    /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns:
904    ///    - None if the position is outside of this node's bounds
905    ///    - Some(HitResult) if the position is within the node but doesn't match any children
906    ///    - The result of recursively calling child.hit() on the the child element that is
907    ///      positioned at that position if there is one.
908    ///
909    /// TODO: z-index
910    /// (If multiple children are positioned at the position then a random one will be recursed into)
911    pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
912        self.hit_inner(x, y, scale, &mut None)
913    }
914
915    /// [`hit`](Self::hit), also resolving the innermost overlay scrollbar
916    /// thumb under the point into `scrollbar` during the same descent (so
917    /// thumb hit-testing shares the exact coordinate handling — transforms
918    /// included — of every other hit test).
919    pub(crate) fn hit_inner(
920        &self,
921        x: f32,
922        y: f32,
923        scale: f64,
924        scrollbar: &mut Option<crate::node::ScrollbarRef>,
925    ) -> Option<HitResult> {
926        use style::computed_values::pointer_events::T as PointerEvents;
927        use style::computed_values::visibility::T as Visibility;
928
929        // Don't hit on visbility:hidden elements
930        if let Some(style) = self.primary_styles() {
931            if matches!(
932                style.clone_visibility(),
933                Visibility::Hidden | Visibility::Collapse
934            ) {
935                return None;
936            }
937        }
938
939        // pointer-events:none makes this element transparent to hits, but its
940        // descendants are still tested (one may restore pointer-events:auto).
941        let pointer_events_none = self
942            .primary_styles()
943            .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
944
945        let mut x = x - self.final_layout.location.x + self.scroll_offset.x as f32;
946        let mut y = y - self.final_layout.location.y + self.scroll_offset.y as f32;
947
948        if let Some(t) = self.transform {
949            let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
950            x = (p.x / scale) as f32;
951            y = (p.y / scale) as f32;
952        }
953
954        let size = self.final_layout.size;
955        let matches_self = !(x < 0.0
956            || x > size.width + self.scroll_offset.x as f32
957            || y < 0.0
958            || y > size.height + self.scroll_offset.y as f32);
959
960        let content_size = self.final_layout.content_size;
961        let matches_content = !(x < 0.0
962            || x > content_size.width + self.scroll_offset.x as f32
963            || y < 0.0
964            || y > content_size.height + self.scroll_offset.y as f32);
965
966        let matches_hoisted_content = match &self.stacking_context {
967            Some(sc) => {
968                let content_area = sc.content_area;
969                x >= content_area.left + self.scroll_offset.x as f32
970                    && x <= content_area.right + self.scroll_offset.x as f32
971                    && y >= content_area.top + self.scroll_offset.y as f32
972                    && y <= content_area.bottom + self.scroll_offset.y as f32
973            }
974            None => false,
975        };
976
977        // `scrollable_overflow` is stored in device (scaled) pixels, whereas the
978        // coordinates here are in CSS pixels, so unscale it before comparing.
979        let overflow = self.scrollable_overflow;
980
981        let matches_overflow = x >= (overflow.x0 / scale) as f32
982            && x <= (overflow.x1 / scale) as f32
983            && y >= (overflow.y0 / scale) as f32
984            && y <= (overflow.y1 / scale) as f32;
985
986        if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
987            return None;
988        }
989
990        // Descendants overwrite, so the innermost scroll container's thumb
991        // wins. Thumb coords are border-box relative (unscrolled).
992        if matches_self
993            && let Some(sb) = self.scrollbar_at_local(
994                (x - self.scroll_offset.x as f32) as f64,
995                (y - self.scroll_offset.y as f32) as f64,
996            )
997        {
998            *scrollbar = Some(sb);
999        }
1000
1001        if self.flags.is_inline_root() {
1002            let content_box_offset = taffy::Point {
1003                x: self.final_layout.padding.left + self.final_layout.border.left,
1004                y: self.final_layout.padding.top + self.final_layout.border.top,
1005            };
1006            x -= content_box_offset.x;
1007            y -= content_box_offset.y;
1008        }
1009
1010        // Positive z_index hoisted children
1011        if matches_hoisted_content {
1012            if let Some(hoisted) = &self.stacking_context {
1013                for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
1014                    let x = x - hoisted_child.position.x;
1015                    let y = y - hoisted_child.position.y;
1016                    if let Some(hit) = self
1017                        .with(hoisted_child.node_id)
1018                        .hit_inner(x, y, scale, scrollbar)
1019                    {
1020                        return Some(hit);
1021                    }
1022                }
1023            }
1024        }
1025
1026        // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id).
1027        for child_id in self.paint_children.borrow().iter().flatten().rev() {
1028            if let Some(hit) = self.with(*child_id).hit_inner(x, y, scale, scrollbar) {
1029                return Some(hit);
1030            }
1031        }
1032
1033        // Negative z_index hoisted children
1034        if matches_hoisted_content {
1035            if let Some(hoisted) = &self.stacking_context {
1036                for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
1037                    let x = x - hoisted_child.position.x;
1038                    let y = y - hoisted_child.position.y;
1039                    if let Some(hit) = self
1040                        .with(hoisted_child.node_id)
1041                        .hit_inner(x, y, scale, scrollbar)
1042                    {
1043                        return Some(hit);
1044                    }
1045                }
1046            }
1047        }
1048
1049        // Inline children
1050        if self.flags.is_inline_root() {
1051            let element_data = &self.element_data().unwrap();
1052            if let Some(ild) = element_data.inline_layout_data.as_ref() {
1053                let layout = &ild.layout;
1054                let scale = layout.scale();
1055
1056                if let Some((cluster, _side)) =
1057                    Cluster::from_point_exact(layout, x * scale, y * scale)
1058                {
1059                    let style_index = cluster.glyphs().next()?.style_index();
1060                    let node_id = layout.styles()[style_index].brush.id;
1061                    let text_pointer_events_none = self
1062                        .with(node_id)
1063                        .primary_styles()
1064                        .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
1065                    if !text_pointer_events_none {
1066                        return Some(HitResult {
1067                            node_id,
1068                            x,
1069                            y,
1070                            is_text: true,
1071                        });
1072                    }
1073                }
1074            }
1075        }
1076
1077        // Self (this node)
1078        if matches_self && !pointer_events_none {
1079            return Some(HitResult {
1080                node_id: self.id,
1081                x,
1082                y,
1083                is_text: false,
1084            });
1085        }
1086
1087        None
1088    }
1089
1090    /// Find the inline root ancestor of this node (or self if this is an inline root).
1091    /// Returns None if no inline root ancestor exists.
1092    pub fn inline_root_ancestor(&self) -> Option<&Node> {
1093        let mut node = self;
1094        loop {
1095            if node.flags.is_inline_root() {
1096                return Some(node);
1097            }
1098            let id = node.layout_parent.get()?;
1099            node = self.with(id);
1100        }
1101    }
1102
1103    /// Get the text byte offset at a given point, using coordinates already transformed
1104    /// to be relative to this inline root's content box.
1105    /// Returns Some(byte_offset) if the point hits text, None otherwise.
1106    pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
1107        if !self.flags.is_inline_root() {
1108            return None;
1109        }
1110
1111        let element_data = self.element_data()?;
1112        let inline_layout = element_data.inline_layout_data.as_ref()?;
1113        let layout = &inline_layout.layout;
1114        let scale = layout.scale();
1115
1116        // Use Parley's cluster hit testing (from_point is more forgiving than from_point_exact)
1117        let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
1118
1119        // Determine byte offset based on which side of the cluster was clicked
1120        // For LTR text: left side = start of cluster, right side = end of cluster
1121        // For RTL text: left side = end of cluster, right side = start of cluster
1122        // Also, explicit line breaks should always use start to avoid cursor appearing on next line
1123        let is_leading = side == ClusterSide::Left;
1124        let offset = if cluster.is_rtl() {
1125            if is_leading {
1126                cluster.text_range().end
1127            } else {
1128                cluster.text_range().start
1129            }
1130        } else {
1131            // LTR text
1132            if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
1133                cluster.text_range().start
1134            } else {
1135                cluster.text_range().end
1136            }
1137        };
1138
1139        Some(offset)
1140    }
1141
1142    /// Computes the Document-relative coordinates of the `Node`
1143    pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
1144        let x = x + self.final_layout.location.x - self.scroll_offset.x as f32;
1145        let y = y + self.final_layout.location.y - self.scroll_offset.y as f32;
1146
1147        // Recurse up the layout hierarchy
1148        self.layout_parent
1149            .get()
1150            .map(|i| self.with(i).absolute_position(x, y))
1151            .unwrap_or(crate::util::Point { x, y })
1152    }
1153
1154    /// Creates a synthetic click event
1155    pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
1156        DomEventData::Click(self.synthetic_click_event_data(mods))
1157    }
1158
1159    pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
1160        let absolute_position = self.absolute_position(0.0, 0.0);
1161        let x = absolute_position.x + (self.final_layout.size.width / 2.0);
1162        let y = absolute_position.y + (self.final_layout.size.height / 2.0);
1163
1164        BlitzPointerEvent {
1165            id: BlitzPointerId::Mouse,
1166            is_primary: true,
1167            coords: PointerCoords {
1168                page_x: x,
1169                page_y: y,
1170
1171                // TODO: should these be different?
1172                screen_x: x,
1173                screen_y: y,
1174                client_x: x,
1175                client_y: y,
1176            },
1177            mods,
1178            button: Default::default(),
1179            buttons: Default::default(),
1180            details: Default::default(),
1181            element: Default::default(),
1182            active_pointers: Default::default(),
1183        }
1184    }
1185}
1186
1187/// It might be wrong to expose this since what does *equality* mean outside the dom?
1188impl PartialEq for Node {
1189    fn eq(&self, other: &Self) -> bool {
1190        self.id == other.id
1191    }
1192}
1193
1194impl Eq for Node {}
1195
1196impl std::fmt::Debug for Node {
1197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1198        // FIXME: update to reflect changes to fields
1199        f.debug_struct("NodeData")
1200            .field("parent", &self.parent)
1201            .field("id", &self.id)
1202            .field("is_inline_root", &self.flags.is_inline_root())
1203            .field("children", &self.children)
1204            .field("layout_children", &self.layout_children.borrow())
1205            // .field("style", &self.style)
1206            .field("node", &self.data)
1207            .field("stylo_element_data", &self.stylo_element_data)
1208            // .field("unrounded_layout", &self.unrounded_layout)
1209            // .field("final_layout", &self.final_layout)
1210            .finish()
1211    }
1212}
1213
1214#[cfg(test)]
1215mod test {
1216    use style_dom::ElementState;
1217
1218    use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
1219
1220    #[test]
1221    fn create_node_with_disabled_attr() {
1222        let mut document = BaseDocument::new(DocumentConfig::default());
1223        let node = document.create_node(NodeData::Element(ElementData::new(
1224            qual_name!("button"),
1225            vec![Attribute {
1226                name: qual_name!("disabled"),
1227                value: "".into(),
1228            }],
1229        )));
1230        let node = document.get_node(node).unwrap();
1231
1232        assert!(
1233            node.element_state.contains(ElementState::DISABLED),
1234            "form node is disabled"
1235        );
1236        assert!(
1237            !node.element_state.contains(ElementState::ENABLED),
1238            "form node is not enabled"
1239        );
1240    }
1241
1242    #[test]
1243    fn ignore_disabled_attr_content() {
1244        let mut document = BaseDocument::new(DocumentConfig::default());
1245        let node = document.create_node(NodeData::Element(ElementData::new(
1246            qual_name!("button"),
1247            vec![Attribute {
1248                name: qual_name!("disabled"),
1249                value: "false".into(),
1250            }],
1251        )));
1252        let node = document.get_node(node).unwrap();
1253
1254        assert!(
1255            node.element_state.contains(ElementState::DISABLED),
1256            "form node is disabled"
1257        );
1258        assert!(
1259            !node.element_state.contains(ElementState::ENABLED),
1260            "form node is not enabled"
1261        );
1262    }
1263
1264    #[test]
1265    fn create_node_with_ignored_disable() {
1266        let mut document = BaseDocument::new(DocumentConfig::default());
1267        let node = document.create_node(NodeData::Element(ElementData::new(
1268            qual_name!("a"),
1269            vec![Attribute {
1270                name: qual_name!("disabled"),
1271                value: "".into(),
1272            }],
1273        )));
1274        let node = document.get_node(node).unwrap();
1275
1276        assert!(
1277            !node.element_state.contains(ElementState::DISABLED),
1278            "Non form node cannot be disabled"
1279        );
1280        assert!(
1281            !node.element_state.contains(ElementState::ENABLED),
1282            "Non form node cannot be enabled"
1283        );
1284    }
1285
1286    #[test]
1287    fn create_empty_enabled_node() {
1288        let mut document = BaseDocument::new(DocumentConfig::default());
1289        let node = document.create_node(NodeData::Element(ElementData::new(
1290            qual_name!("button"),
1291            vec![],
1292        )));
1293        let node = document.get_node(node).unwrap();
1294
1295        assert!(
1296            node.element_state.contains(ElementState::ENABLED),
1297            "Button should be enabled by default"
1298        );
1299    }
1300}