blitz_dom/node/
node.rs

1use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
2use bitflags::bitflags;
3use blitz_traits::events::{BlitzMouseButtonEvent, DomEventData, HitResult};
4use keyboard_types::Modifiers;
5use markup5ever::{LocalName, local_name};
6use parley::Cluster;
7use selectors::matching::ElementSelectorFlags;
8use slab::Slab;
9use std::cell::{Cell, RefCell};
10use std::fmt::Write;
11use std::sync::atomic::AtomicBool;
12use style::Atom;
13use style::invalidation::element::restyle_hints::RestyleHint;
14use style::properties::ComputedValues;
15use style::properties::generated::longhands::position::computed_value::T as Position;
16use style::selector_parser::{PseudoElement, RestyleDamage};
17use style::stylesheets::UrlExtraData;
18use style::values::computed::Display as StyloDisplay;
19use style::values::specified::box_::{DisplayInside, DisplayOutside};
20use style::{data::ElementData as StyloElementData, shared_lock::SharedRwLock};
21use style_dom::ElementState;
22use style_traits::values::ToCss;
23use taffy::{
24    Cache,
25    prelude::{Layout, Style},
26};
27
28use super::{Attribute, ElementData};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum DisplayOuter {
32    Block,
33    Inline,
34    None,
35}
36
37bitflags! {
38    #[derive(Clone, Copy, PartialEq)]
39    pub struct NodeFlags: u32 {
40        /// Whether the node is the root node of an Inline Formatting Context
41        const IS_INLINE_ROOT = 0b00000001;
42        /// Whether the node is the root node of an Table formatting context
43        const IS_TABLE_ROOT = 0b00000010;
44        /// Whether the node is "in the document" (~= has a parent and isn't a template node)
45        const IS_IN_DOCUMENT = 0b00000100;
46    }
47}
48
49impl NodeFlags {
50    #[inline(always)]
51    pub fn is_inline_root(&self) -> bool {
52        self.contains(Self::IS_INLINE_ROOT)
53    }
54
55    #[inline(always)]
56    pub fn is_table_root(&self) -> bool {
57        self.contains(Self::IS_TABLE_ROOT)
58    }
59
60    #[inline(always)]
61    pub fn is_in_document(&self) -> bool {
62        self.contains(Self::IS_IN_DOCUMENT)
63    }
64
65    #[inline(always)]
66    pub fn reset_construction_flags(&mut self) {
67        self.remove(Self::IS_INLINE_ROOT);
68        self.remove(Self::IS_TABLE_ROOT);
69    }
70}
71
72pub struct Node {
73    // The actual tree we belong to. This is unsafe!!
74    tree: *mut Slab<Node>,
75
76    /// Our Id
77    pub id: usize,
78    /// Our parent's ID
79    pub parent: Option<usize>,
80    // What are our children?
81    pub children: Vec<usize>,
82    /// Our parent in the layout hierachy: a separate list that includes anonymous collections of inline elements
83    pub layout_parent: Cell<Option<usize>>,
84    /// A separate child list that includes anonymous collections of inline elements
85    pub layout_children: RefCell<Option<Vec<usize>>>,
86    /// The same as layout_children, but sorted by z-index
87    pub paint_children: RefCell<Option<Vec<usize>>>,
88
89    // Flags
90    pub flags: NodeFlags,
91
92    /// Node type (Element, TextNode, etc) specific data
93    pub data: NodeData,
94
95    // This little bundle of joy is our style data from stylo and a lock guard that allows access to it
96    // TODO: See if guard can be hoisted to a higher level
97    pub stylo_element_data: AtomicRefCell<Option<StyloElementData>>,
98    pub selector_flags: AtomicRefCell<ElementSelectorFlags>,
99    pub guard: SharedRwLock,
100    pub element_state: ElementState,
101
102    // Pseudo element nodes
103    pub before: Option<usize>,
104    pub after: Option<usize>,
105
106    // Taffy layout data:
107    pub style: Style<Atom>,
108    pub has_snapshot: bool,
109    pub snapshot_handled: AtomicBool,
110    pub display_constructed_as: StyloDisplay,
111    pub cache: Cache,
112    pub unrounded_layout: Layout,
113    pub final_layout: Layout,
114    pub scroll_offset: crate::Point<f64>,
115}
116
117unsafe impl Send for Node {}
118unsafe impl Sync for Node {}
119
120impl Node {
121    pub(crate) fn new(
122        tree: *mut Slab<Node>,
123        id: usize,
124        guard: SharedRwLock,
125        data: NodeData,
126    ) -> Self {
127        Self {
128            tree,
129
130            id,
131            parent: None,
132            children: vec![],
133            layout_parent: Cell::new(None),
134            layout_children: RefCell::new(None),
135            paint_children: RefCell::new(None),
136
137            flags: NodeFlags::empty(),
138            data,
139
140            stylo_element_data: Default::default(),
141            selector_flags: AtomicRefCell::new(ElementSelectorFlags::empty()),
142            guard,
143            element_state: ElementState::empty(),
144
145            before: None,
146            after: None,
147
148            style: Default::default(),
149            has_snapshot: false,
150            snapshot_handled: AtomicBool::new(false),
151            display_constructed_as: StyloDisplay::Block,
152            cache: Cache::new(),
153            unrounded_layout: Layout::new(),
154            final_layout: Layout::new(),
155            scroll_offset: crate::Point::ZERO,
156        }
157    }
158
159    pub fn pe_by_index(&self, index: usize) -> Option<usize> {
160        match index {
161            0 => self.after,
162            1 => self.before,
163            _ => panic!("Invalid pseudo element index"),
164        }
165    }
166
167    pub fn set_pe_by_index(&mut self, index: usize, value: Option<usize>) {
168        match index {
169            0 => self.after = value,
170            1 => self.before = value,
171            _ => panic!("Invalid pseudo element index"),
172        }
173    }
174
175    pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
176        Some(self.primary_styles().as_ref()?.clone_display())
177    }
178
179    pub fn is_or_contains_block(&self) -> bool {
180        let style = self.primary_styles();
181        let style = style.as_ref();
182
183        // Ignore out-of-flow items
184        let position = style
185            .map(|s| s.clone_position())
186            .unwrap_or(Position::Relative);
187        let is_in_flow = matches!(
188            position,
189            Position::Static | Position::Relative | Position::Sticky
190        );
191        if !is_in_flow {
192            return false;
193        }
194        let display = style
195            .map(|s| s.clone_display())
196            .unwrap_or(StyloDisplay::inline());
197        match display.outside() {
198            DisplayOutside::None => false,
199            DisplayOutside::Block => true,
200            _ => {
201                if display.inside() == DisplayInside::Flow {
202                    self.children
203                        .iter()
204                        .copied()
205                        .any(|child_id| self.tree()[child_id].is_or_contains_block())
206                } else {
207                    false
208                }
209            }
210        }
211    }
212
213    pub fn is_focussable(&self) -> bool {
214        self.data
215            .downcast_element()
216            .map(|el| el.is_focussable)
217            .unwrap_or(false)
218    }
219
220    pub fn set_restyle_hint(&self, hint: RestyleHint) {
221        if let Some(element_data) = self.stylo_element_data.borrow_mut().as_mut() {
222            element_data.hint.insert(hint);
223        }
224    }
225
226    pub fn damage_mut(&self) -> Option<AtomicRefMut<'_, RestyleDamage>> {
227        let element_data = self.stylo_element_data.borrow_mut();
228        #[allow(clippy::manual_map, reason = "false positive")]
229        match *element_data {
230            Some(_) => Some(AtomicRefMut::map(
231                element_data,
232                |data: &mut Option<StyloElementData>| &mut data.as_mut().unwrap().damage,
233            )),
234            None => None,
235        }
236    }
237
238    pub fn damage(&mut self) -> Option<RestyleDamage> {
239        self.stylo_element_data
240            .get_mut()
241            .as_ref()
242            .map(|data| data.damage)
243    }
244
245    pub fn set_damage(&self, damage: RestyleDamage) {
246        if let Some(data) = self.stylo_element_data.borrow_mut().as_mut() {
247            data.damage = damage;
248        }
249    }
250
251    pub fn insert_damage(&mut self, damage: RestyleDamage) {
252        if let Some(data) = self.stylo_element_data.get_mut().as_mut() {
253            data.damage |= damage;
254        }
255    }
256
257    pub fn remove_damage(&self, damage: RestyleDamage) {
258        if let Some(data) = self.stylo_element_data.borrow_mut().as_mut() {
259            data.damage.remove(damage);
260        }
261    }
262
263    pub fn clear_damage_mut(&mut self) {
264        if let Some(data) = self.stylo_element_data.get_mut() {
265            data.damage = RestyleDamage::empty();
266        }
267    }
268
269    pub fn hover(&mut self) {
270        self.element_state.insert(ElementState::HOVER);
271        self.set_restyle_hint(RestyleHint::restyle_subtree());
272    }
273
274    pub fn unhover(&mut self) {
275        self.element_state.remove(ElementState::HOVER);
276        self.set_restyle_hint(RestyleHint::restyle_subtree());
277    }
278
279    pub fn is_hovered(&self) -> bool {
280        self.element_state.contains(ElementState::HOVER)
281    }
282
283    pub fn focus(&mut self) {
284        self.element_state
285            .insert(ElementState::FOCUS | ElementState::FOCUSRING);
286        self.set_restyle_hint(RestyleHint::restyle_subtree());
287    }
288
289    pub fn blur(&mut self) {
290        self.element_state
291            .remove(ElementState::FOCUS | ElementState::FOCUSRING);
292        self.set_restyle_hint(RestyleHint::restyle_subtree());
293    }
294
295    pub fn is_focussed(&self) -> bool {
296        self.element_state.contains(ElementState::FOCUS)
297    }
298
299    pub fn active(&mut self) {
300        self.element_state.insert(ElementState::ACTIVE);
301        self.set_restyle_hint(RestyleHint::restyle_subtree());
302    }
303
304    pub fn unactive(&mut self) {
305        self.element_state.remove(ElementState::ACTIVE);
306        self.set_restyle_hint(RestyleHint::restyle_subtree());
307    }
308
309    pub fn is_active(&self) -> bool {
310        self.element_state.contains(ElementState::ACTIVE)
311    }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq)]
315pub enum NodeKind {
316    Document,
317    Element,
318    AnonymousBlock,
319    Text,
320    Comment,
321}
322
323/// The different kinds of nodes in the DOM.
324#[derive(Debug, Clone)]
325pub enum NodeData {
326    /// The `Document` itself - the root node of a HTML document.
327    Document,
328
329    /// An element with attributes.
330    Element(ElementData),
331
332    /// An anonymous block box
333    AnonymousBlock(ElementData),
334
335    /// A text node.
336    Text(TextNodeData),
337
338    /// A comment.
339    Comment,
340    // Comment { contents: String },
341
342    // /// A `DOCTYPE` with name, public id, and system id. See
343    // /// [document type declaration on wikipedia][https://en.wikipedia.org/wiki/Document_type_declaration]
344    // Doctype { name: String, public_id: String, system_id: String },
345
346    // /// A Processing instruction.
347    // ProcessingInstruction { target: String, contents: String },
348}
349
350impl NodeData {
351    pub fn downcast_element(&self) -> Option<&ElementData> {
352        match self {
353            Self::Element(data) => Some(data),
354            Self::AnonymousBlock(data) => Some(data),
355            _ => None,
356        }
357    }
358
359    pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
360        match self {
361            Self::Element(data) => Some(data),
362            Self::AnonymousBlock(data) => Some(data),
363            _ => None,
364        }
365    }
366
367    pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
368        let Some(elem) = self.downcast_element() else {
369            return false;
370        };
371        *name == elem.name.local
372    }
373
374    pub fn attrs(&self) -> Option<&[Attribute]> {
375        Some(&self.downcast_element()?.attrs)
376    }
377
378    pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
379        self.downcast_element()?.attr(name)
380    }
381
382    pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
383        self.downcast_element()
384            .is_some_and(|elem| elem.has_attr(name))
385    }
386
387    pub fn kind(&self) -> NodeKind {
388        match self {
389            NodeData::Document => NodeKind::Document,
390            NodeData::Element(_) => NodeKind::Element,
391            NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
392            NodeData::Text(_) => NodeKind::Text,
393            NodeData::Comment => NodeKind::Comment,
394        }
395    }
396}
397
398#[derive(Debug, Clone)]
399pub struct TextNodeData {
400    /// The textual content of the text node
401    pub content: String,
402}
403
404impl TextNodeData {
405    pub fn new(content: String) -> Self {
406        Self { content }
407    }
408}
409
410/*
411-> Computed styles
412-> Layout
413-----> Needs to happen only when styles are computed
414*/
415
416// type DomRefCell<T> = RefCell<T>;
417
418// pub struct DomData {
419//     // ... we can probs just get away with using the html5ever types directly. basically just using the servo dom, but without the bindings
420//     local_name: html5ever::LocalName,
421//     tag_name: html5ever::QualName,
422//     namespace: html5ever::Namespace,
423//     prefix: DomRefCell<Option<html5ever::Prefix>>,
424//     attrs: DomRefCell<Vec<Attr>>,
425//     // attrs: DomRefCell<Vec<Dom<Attr>>>,
426//     id_attribute: DomRefCell<Option<Atom>>,
427//     is: DomRefCell<Option<LocalName>>,
428//     // style_attribute: DomRefCell<Option<Arc<Locked<PropertyDeclarationBlock>>>>,
429//     // attr_list: MutNullableDom<NamedNodeMap>,
430//     // class_list: MutNullableDom<DOMTokenList>,
431//     state: Cell<ElementState>,
432// }
433
434impl Node {
435    pub fn tree(&self) -> &Slab<Node> {
436        unsafe { &*self.tree }
437    }
438
439    #[track_caller]
440    pub fn with(&self, id: usize) -> &Node {
441        self.tree().get(id).unwrap()
442    }
443
444    pub fn print_tree(&self, level: usize) {
445        println!(
446            "{} {} {:?} {} {:?}",
447            "  ".repeat(level),
448            self.id,
449            self.parent,
450            self.node_debug_str().replace('\n', ""),
451            self.children
452        );
453        // println!("{} {:?}", "  ".repeat(level), self.children);
454        for child_id in self.children.iter() {
455            let child = self.with(*child_id);
456            child.print_tree(level + 1)
457        }
458    }
459
460    // Get the index of the current node in the parents child list
461    pub fn index_of_child(&self, child_id: usize) -> Option<usize> {
462        self.children.iter().position(|id| *id == child_id)
463    }
464
465    // Get the index of the current node in the parents child list
466    pub fn child_index(&self) -> Option<usize> {
467        self.tree()[self.parent?]
468            .children
469            .iter()
470            .position(|id| *id == self.id)
471    }
472
473    // Get the nth node in the parents child list
474    pub fn forward(&self, n: usize) -> Option<&Node> {
475        let child_idx = self.child_index().unwrap_or(0);
476        self.tree()[self.parent?]
477            .children
478            .get(child_idx + n)
479            .map(|id| self.with(*id))
480    }
481
482    pub fn backward(&self, n: usize) -> Option<&Node> {
483        let child_idx = self.child_index().unwrap_or(0);
484        if child_idx < n {
485            return None;
486        }
487
488        self.tree()[self.parent?]
489            .children
490            .get(child_idx - n)
491            .map(|id| self.with(*id))
492    }
493
494    pub fn is_element(&self) -> bool {
495        matches!(self.data, NodeData::Element { .. })
496    }
497
498    pub fn is_anonymous(&self) -> bool {
499        matches!(self.data, NodeData::AnonymousBlock { .. })
500    }
501
502    pub fn is_text_node(&self) -> bool {
503        matches!(self.data, NodeData::Text { .. })
504    }
505
506    pub fn element_data(&self) -> Option<&ElementData> {
507        match self.data {
508            NodeData::Element(ref data) => Some(data),
509            NodeData::AnonymousBlock(ref data) => Some(data),
510            _ => None,
511        }
512    }
513
514    pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
515        match self.data {
516            NodeData::Element(ref mut data) => Some(data),
517            NodeData::AnonymousBlock(ref mut data) => Some(data),
518            _ => None,
519        }
520    }
521
522    pub fn text_data(&self) -> Option<&TextNodeData> {
523        match self.data {
524            NodeData::Text(ref data) => Some(data),
525            _ => None,
526        }
527    }
528
529    pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
530        match self.data {
531            NodeData::Text(ref mut data) => Some(data),
532            _ => None,
533        }
534    }
535
536    pub fn node_debug_str(&self) -> String {
537        let mut s = String::new();
538
539        match &self.data {
540            NodeData::Document => write!(s, "DOCUMENT"),
541            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
542            NodeData::Text(data) => {
543                let bytes = data.content.as_bytes();
544                write!(
545                    s,
546                    "TEXT {}",
547                    &std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
548                        .unwrap_or("INVALID UTF8")
549                )
550            }
551            NodeData::Comment => write!(
552                s,
553                "COMMENT",
554                // &std::str::from_utf8(data.contents.as_bytes().split_at(10).0).unwrap_or("INVALID UTF8")
555            ),
556            NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
557            NodeData::Element(data) => {
558                let name = &data.name;
559                let class = self.attr(local_name!("class")).unwrap_or("");
560                let id = self.attr(local_name!("id")).unwrap_or("");
561                let display = self.display_constructed_as.to_css_string();
562                write!(s, "<{}", name.local).unwrap();
563                if !id.is_empty() {
564                    write!(s, " #{id}").unwrap();
565                }
566                if !class.is_empty() {
567                    if class.contains(' ') {
568                        write!(s, " class=\"{class}\"").unwrap()
569                    } else {
570                        write!(s, " .{class}").unwrap()
571                    }
572                }
573                write!(s, "> ({display})")
574            } // NodeData::ProcessingInstruction { .. } => write!(s, "ProcessingInstruction"),
575        }
576        .unwrap();
577        s
578    }
579
580    pub fn outer_html(&self) -> String {
581        let mut output = String::new();
582        self.write_outer_html(&mut output);
583        output
584    }
585
586    pub fn write_outer_html(&self, writer: &mut String) {
587        let has_children = !self.children.is_empty();
588        let current_color = self
589            .primary_styles()
590            .map(|style| style.clone_color())
591            .map(|color| color.to_css_string());
592
593        match &self.data {
594            NodeData::Document => {}
595            NodeData::Comment => {}
596            NodeData::AnonymousBlock(_) => {}
597            // NodeData::Doctype { name, .. } => write!(s, "DOCTYPE {name}"),
598            NodeData::Text(data) => {
599                writer.push_str(data.content.as_str());
600            }
601            NodeData::Element(data) => {
602                writer.push('<');
603                writer.push_str(&data.name.local);
604
605                for attr in data.attrs() {
606                    writer.push(' ');
607                    writer.push_str(&attr.name.local);
608                    writer.push_str("=\"");
609                    #[allow(clippy::unnecessary_unwrap)] // Convert to if-let chain once stabilised
610                    if current_color.is_some() && attr.value.contains("currentColor") {
611                        writer.push_str(
612                            &attr
613                                .value
614                                .replace("currentColor", current_color.as_ref().unwrap()),
615                        );
616                    } else {
617                        writer.push_str(&attr.value);
618                    }
619                    writer.push('"');
620                }
621                if !has_children {
622                    writer.push_str(" /");
623                }
624                writer.push('>');
625
626                if has_children {
627                    for &child_id in &self.children {
628                        self.tree()[child_id].write_outer_html(writer);
629                    }
630
631                    writer.push_str("</");
632                    writer.push_str(&data.name.local);
633                    writer.push('>');
634                }
635            }
636        }
637    }
638
639    pub fn attrs(&self) -> Option<&[Attribute]> {
640        Some(&self.element_data()?.attrs)
641    }
642
643    pub fn attr(&self, name: LocalName) -> Option<&str> {
644        let attr = self.attrs()?.iter().find(|id| id.name.local == name)?;
645        Some(&attr.value)
646    }
647
648    pub fn primary_styles(&self) -> Option<AtomicRef<'_, ComputedValues>> {
649        let stylo_element_data = self.stylo_element_data.borrow();
650        if stylo_element_data
651            .as_ref()
652            .and_then(|d| d.styles.get_primary())
653            .is_some()
654        {
655            Some(AtomicRef::map(
656                stylo_element_data,
657                |data: &Option<StyloElementData>| -> &ComputedValues {
658                    data.as_ref().unwrap().styles.get_primary().unwrap()
659                },
660            ))
661        } else {
662            None
663        }
664    }
665
666    pub fn text_content(&self) -> String {
667        let mut out = String::new();
668        self.write_text_content(&mut out);
669        out
670    }
671
672    fn write_text_content(&self, out: &mut String) {
673        match &self.data {
674            NodeData::Text(data) => {
675                out.push_str(&data.content);
676            }
677            NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
678                for child_id in self.children.iter() {
679                    self.with(*child_id).write_text_content(out);
680                }
681            }
682            _ => {}
683        }
684    }
685
686    pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
687        if let NodeData::Element(ref mut elem_data) = self.data {
688            elem_data.flush_style_attribute(&self.guard, url_extra_data);
689        }
690    }
691
692    pub fn order(&self) -> i32 {
693        self.primary_styles()
694            .map(|s| match s.pseudo() {
695                Some(PseudoElement::Before) => i32::MIN,
696                Some(PseudoElement::After) => i32::MAX,
697                _ => s.clone_order(),
698            })
699            .unwrap_or(0)
700    }
701
702    pub fn z_index(&self) -> i32 {
703        self.primary_styles()
704            .map(|s| s.clone_z_index().integer_or(0))
705            .unwrap_or(0)
706    }
707
708    /// Takes an (x, y) position (relative to the *parent's* top-left corner) and returns:
709    ///    - None if the position is outside of this node's bounds
710    ///    - Some(HitResult) if the position is within the node but doesn't match any children
711    ///    - The result of recursively calling child.hit() on the the child element that is
712    ///      positioned at that position if there is one.
713    ///
714    /// TODO: z-index
715    /// (If multiple children are positioned at the position then a random one will be recursed into)
716    pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
717        let mut x = x - self.final_layout.location.x + self.scroll_offset.x as f32;
718        let mut y = y - self.final_layout.location.y + self.scroll_offset.y as f32;
719
720        let size = self.final_layout.size;
721        let matches_self = !(x < 0.0
722            || x > size.width + self.scroll_offset.x as f32
723            || y < 0.0
724            || y > size.height + self.scroll_offset.y as f32);
725
726        let content_size = self.final_layout.content_size;
727        let matches_content = !(x < 0.0
728            || x > content_size.width + self.scroll_offset.x as f32
729            || y < 0.0
730            || y > content_size.height + self.scroll_offset.y as f32);
731
732        if !matches_self && !matches_content {
733            return None;
734        }
735
736        if self.flags.is_inline_root() {
737            let content_box_offset = taffy::Point {
738                x: self.final_layout.padding.left + self.final_layout.border.left,
739                y: self.final_layout.padding.top + self.final_layout.border.top,
740            };
741            x -= content_box_offset.x;
742            y -= content_box_offset.y;
743        }
744
745        // Call `.hit()` on each child in turn. If any return `Some` then return that value. Else return `Some(self.id).
746        self.paint_children
747            .borrow()
748            .iter()
749            .flatten()
750            .rev()
751            .find_map(|&i| self.with(i).hit(x, y))
752            .or_else(|| {
753                if self.flags.is_inline_root() {
754                    let element_data = &self.element_data().unwrap();
755                    let layout = &element_data.inline_layout_data.as_ref().unwrap().layout;
756                    let scale = layout.scale();
757
758                    Cluster::from_point(layout, x * scale, y * scale).and_then(|(cluster, _)| {
759                        let style_index = cluster.glyphs().next()?.style_index();
760                        let node_id = layout.styles()[style_index].brush.id;
761                        Some(HitResult { node_id, x, y })
762                    })
763                } else {
764                    None
765                }
766            })
767            .or(Some(HitResult {
768                node_id: self.id,
769                x,
770                y,
771            })
772            .filter(|_| matches_self))
773    }
774
775    /// Computes the Document-relative coordinates of the Node
776    pub fn absolute_position(&self, x: f32, y: f32) -> taffy::Point<f32> {
777        let x = x + self.final_layout.location.x - self.scroll_offset.x as f32;
778        let y = y + self.final_layout.location.y - self.scroll_offset.y as f32;
779
780        // Recurse up the layout hierarchy
781        self.layout_parent
782            .get()
783            .map(|i| self.with(i).absolute_position(x, y))
784            .unwrap_or(taffy::Point { x, y })
785    }
786
787    /// Creates a synthetic click event
788    pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
789        DomEventData::Click(self.synthetic_click_event_data(mods))
790    }
791
792    pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzMouseButtonEvent {
793        let absolute_position = self.absolute_position(0.0, 0.0);
794        let x = absolute_position.x + (self.final_layout.size.width / 2.0);
795        let y = absolute_position.y + (self.final_layout.size.height / 2.0);
796
797        BlitzMouseButtonEvent {
798            x,
799            y,
800            mods,
801            button: Default::default(),
802            buttons: Default::default(),
803        }
804    }
805}
806
807/// It might be wrong to expose this since what does *equality* mean outside the dom?
808impl PartialEq for Node {
809    fn eq(&self, other: &Self) -> bool {
810        self.id == other.id
811    }
812}
813
814impl Eq for Node {}
815
816impl std::fmt::Debug for Node {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        // FIXME: update to reflect changes to fields
819        f.debug_struct("NodeData")
820            .field("parent", &self.parent)
821            .field("id", &self.id)
822            .field("is_inline_root", &self.flags.is_inline_root())
823            .field("children", &self.children)
824            .field("layout_children", &self.layout_children.borrow())
825            // .field("style", &self.style)
826            .field("node", &self.data)
827            .field("stylo_element_data", &self.stylo_element_data)
828            // .field("unrounded_layout", &self.unrounded_layout)
829            // .field("final_layout", &self.final_layout)
830            .finish()
831    }
832}