blitz_dom/node/
node.rs

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