Skip to main content

blitz_dom/node/
node.rs

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