Skip to main content

bliss_dom/node/
node.rs

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