Skip to main content

kozan_core/layout/
node_data.rs

1//! Per-node layout data — stored in Document's parallel storage.
2//!
3//! The DOM IS the layout tree. Each node stores its own Taffy style, cache,
4//! and layout results. No separate `LayoutTree` needed.
5//!
6//! Chrome equivalent: `LayoutObject`'s per-node layout state, but stored
7//! as a parallel column rather than a separate tree.
8//!
9//! # Lifecycle
10//!
11//! - Created in `Document::alloc_node()` with defaults.
12//! - Style synced from `ComputedValues` → `taffy::Style` after restyle.
13//! - Cache cleared on style/content changes, propagated to ancestors.
14//! - Cleared in `Document::destroy_node()`.
15
16use style::Atom;
17use taffy::tree::{Cache, Layout};
18
19/// Per-node layout data stored directly on the DOM node.
20///
21/// This replaces the separate `LayoutTree` + `TaffyLayoutData` with data
22/// co-located on each DOM node. Taffy's trait implementations read/write
23/// this data directly through Document.
24///
25/// Chrome equivalent: `LayoutObject`'s inline style/cache/geometry fields.
26#[non_exhaustive]
27pub struct LayoutNodeData {
28    /// Taffy style — converted from Stylo's `ComputedValues`.
29    ///
30    /// Updated when `RestyleDamage` indicates layout-affecting changes.
31    /// Conversion: `ComputedValues` → `taffy::Style` via `computed_to_taffy_item_style()`.
32    pub(crate) style: taffy::Style<Atom>,
33
34    /// Taffy's layout cache — persistent across layout passes.
35    ///
36    /// Cleared when: style changes, content changes, or ancestor propagation.
37    /// Taffy's `compute_cached_layout()` checks this before recomputing.
38    pub(crate) cache: Cache,
39
40    /// Raw layout output from Taffy (sub-pixel positions).
41    ///
42    /// Written by `set_unrounded_layout()` during Taffy's tree traversal.
43    pub(crate) unrounded_layout: Layout,
44
45    /// Layout children — may differ from DOM children.
46    ///
47    /// Reasons for divergence:
48    /// - `display: none` nodes are excluded
49    /// - Anonymous blocks inserted for inline/block mixing
50    /// - `display: contents` promotes children to grandparent
51    ///
52    /// `None` = not yet constructed (needs rebuild).
53    /// `Some(vec)` = constructed (may be empty for leaf nodes).
54    pub(crate) layout_children: Option<Vec<u32>>,
55
56    /// Layout parent — may differ from DOM parent.
57    ///
58    /// Set during layout tree construction. Used for cache invalidation
59    /// propagation (clear ancestor caches when a child changes).
60    pub(crate) layout_parent: Option<u32>,
61}
62
63impl LayoutNodeData {
64    /// Create default layout data for a new node.
65    #[must_use]
66    pub fn new() -> Self {
67        Self {
68            style: taffy::Style::<Atom>::default(),
69            cache: Cache::new(),
70            unrounded_layout: Layout::new(),
71            layout_children: None,
72            layout_parent: None,
73        }
74    }
75
76    /// Clear the Taffy cache, forcing re-layout on next pass.
77    #[inline]
78    pub fn clear_cache(&mut self) {
79        self.cache = Cache::new();
80    }
81
82    /// Returns the layout parent index for ancestor propagation.
83    #[inline]
84    #[must_use]
85    pub fn layout_parent(&self) -> Option<u32> {
86        self.layout_parent
87    }
88
89    /// Mark layout children as needing reconstruction.
90    #[inline]
91    pub fn invalidate_layout_children(&mut self) {
92        self.layout_children = None;
93    }
94
95    /// Whether layout children have been constructed.
96    #[inline]
97    #[must_use]
98    pub fn has_layout_children(&self) -> bool {
99        self.layout_children.is_some()
100    }
101}
102
103impl Default for LayoutNodeData {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn new_has_default_values() {
115        let data = LayoutNodeData::new();
116        assert!(data.layout_children.is_none());
117        assert!(data.layout_parent.is_none());
118        assert_eq!(data.unrounded_layout.size.width, 0.0);
119        assert_eq!(data.unrounded_layout.size.height, 0.0);
120    }
121
122    #[test]
123    fn default_matches_new() {
124        let a = LayoutNodeData::new();
125        let b = LayoutNodeData::default();
126        assert_eq!(a.unrounded_layout.size.width, b.unrounded_layout.size.width);
127        assert_eq!(a.layout_children.is_none(), b.layout_children.is_none());
128    }
129
130    #[test]
131    fn clear_cache_resets() {
132        let mut data = LayoutNodeData::new();
133        // After clear, cache should have no entries.
134        data.clear_cache();
135        let result = data.cache.get(
136            taffy::Size {
137                width: None,
138                height: None,
139            },
140            taffy::Size {
141                width: taffy::AvailableSpace::MaxContent,
142                height: taffy::AvailableSpace::MaxContent,
143            },
144            taffy::tree::RunMode::PerformLayout,
145        );
146        assert!(result.is_none());
147    }
148
149    #[test]
150    fn layout_parent_returns_parent() {
151        let mut data = LayoutNodeData::new();
152        data.layout_parent = Some(5);
153        assert_eq!(data.layout_parent(), Some(5));
154    }
155
156    #[test]
157    fn layout_parent_returns_none_when_root() {
158        let data = LayoutNodeData::new();
159        assert_eq!(data.layout_parent(), None);
160    }
161
162    #[test]
163    fn invalidate_layout_children_clears() {
164        let mut data = LayoutNodeData::new();
165        data.layout_children = Some(vec![1, 2, 3]);
166        assert!(data.has_layout_children());
167
168        data.invalidate_layout_children();
169        assert!(!data.has_layout_children());
170        assert!(data.layout_children.is_none());
171    }
172
173    #[test]
174    fn empty_children_is_constructed() {
175        let mut data = LayoutNodeData::new();
176        data.layout_children = Some(Vec::new());
177        assert!(data.has_layout_children());
178    }
179
180    #[test]
181    fn style_starts_as_default() {
182        let data = LayoutNodeData::new();
183        assert_eq!(data.style.display, taffy::Display::default());
184    }
185
186    #[test]
187    fn layout_starts_at_zero() {
188        let data = LayoutNodeData::new();
189        assert_eq!(data.unrounded_layout.location.x, 0.0);
190        assert_eq!(data.unrounded_layout.location.y, 0.0);
191        assert_eq!(data.unrounded_layout.size.width, 0.0);
192        assert_eq!(data.unrounded_layout.size.height, 0.0);
193    }
194}