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