Skip to main content

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