blitz_dom/node/
node.rs

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