Skip to main content

kozan_core/layout/
document_layout.rs

1//! Layout integration — Taffy traits implemented against Document.
2//!
3//! The DOM IS the layout tree. Taffy reads/writes `LayoutNodeData` directly
4//! from Document's parallel storage. No separate `LayoutTree` needed.
5//!
6//! # Architecture
7//!
8//! ```text
9//! Document                       DocumentLayoutView
10//! ────────                       ──────────────────
11//! Storage<LayoutNodeData>  ←──   TraversePartialTree (child iteration)
12//!   .style                 ←──   LayoutPartialTree   (style access)
13//!   .cache                 ←──   CacheTree           (layout caching)
14//!   .unrounded_layout      ←──   set_unrounded_layout
15//!   .layout_children       ←──   child_ids / child_count
16//! ```
17//!
18//! # Entry point
19//!
20//! ```ignore
21//! // In FrameWidget:
22//! doc.resolve_layout(root, Some(vw), Some(vh), &ctx)
23//! ```
24
25use core::any::TypeId;
26
27use style::Atom;
28use style::properties::ComputedValues;
29use taffy::tree::{Cache, Layout, LayoutInput, LayoutOutput, NodeId};
30use taffy::{
31    CacheTree, LayoutBlockContainer, LayoutFlexboxContainer, LayoutGridContainer,
32    LayoutPartialTree, TraversePartialTree, compute_block_layout, compute_cached_layout,
33    compute_flexbox_layout, compute_grid_layout, compute_hidden_layout, compute_leaf_layout,
34    compute_root_layout,
35};
36
37use kozan_primitives::geometry::{Point, Size};
38
39use kozan_primitives::arena::Storage;
40
41use crate::TextData;
42use crate::data_storage::DataStorage;
43use crate::dom::document::Document;
44use crate::dom::element_data::ElementData;
45use crate::dom::node::{NodeMeta, NodeType};
46use crate::layout::algo::shared;
47use crate::layout::box_model::is_stacking_context;
48use crate::layout::context::LayoutContext;
49use crate::layout::fragment::{
50    BoxFragmentData, ChildFragment, Fragment, OverflowClip, PhysicalInsets, TextFragmentData,
51};
52use crate::layout::node_data::LayoutNodeData;
53use crate::layout::result::{EscapedMargins, LayoutResult};
54use crate::tree::{self, TreeData};
55
56// ============================================================
57// Document layout methods
58// ============================================================
59
60impl Document {
61    /// Is this node a text node?
62    #[inline]
63    pub(crate) fn is_text_node(&self, index: u32) -> bool {
64        self.meta
65            .get(index)
66            .is_some_and(|m| m.flags.node_type() == NodeType::Text)
67    }
68
69    /// Get text content by reference (no clone).
70    pub(crate) fn text_content_ref(&self, index: u32) -> Option<&str> {
71        let meta = self.meta.get(index)?;
72        if meta.data_type_id != TypeId::of::<TextData>() {
73            return None;
74        }
75        let data = unsafe { self.data.get::<TextData>(index) };
76        Some(&data.content)
77    }
78
79    /// Flush `ComputedValues` → `taffy::Style` for all nodes.
80    ///
81    /// Incremental: `taffy::Style` IS the damage system.
82    /// - Style changed → layout-affecting → clear cache + ancestors
83    /// - Style unchanged → paint-only (hover color) → skip → 0ms
84    /// - Root always cleared (no `ComputedValues` for comparison)
85    /// - `force_clear`: true on tree rebuild / resize → clear everything
86    pub(crate) fn flush_styles_to_layout(&mut self, index: u32, force_clear: bool) {
87        if !self.is_text_node(index) {
88            if let Some(cv) = self.computed_style(index) {
89                let new_style = shared::computed_to_taffy_item_style(&cv);
90                if let Some(data) = self.layout.get_mut(index) {
91                    if force_clear {
92                        data.style = new_style;
93                        data.clear_cache();
94                    } else if data.style != new_style {
95                        // Layout-affecting change → clear node + ancestors.
96                        data.style = new_style;
97                        data.clear_cache();
98                        self.clear_ancestor_caches(index);
99                    }
100                    // else: paint-only (e.g., hover color) → skip → 0ms
101                }
102            } else {
103                // Root/document node: always clear (no ComputedValues).
104                if let Some(data) = self.layout.get_mut(index) {
105                    data.clear_cache();
106                }
107            }
108        } else if force_clear {
109            // Text nodes: clear on force (tree rebuild).
110            // Normal text changes handled by mark_layout_dirty.
111            if let Some(data) = self.layout.get_mut(index) {
112                data.clear_cache();
113            }
114        }
115
116        let dom_children = unsafe { tree::ops::children(&self.tree, index) };
117        for child in dom_children {
118            self.flush_styles_to_layout(child, force_clear);
119        }
120    }
121
122    /// Clear Taffy caches up the DOM ancestor chain.
123    fn clear_ancestor_caches(&mut self, index: u32) {
124        let mut current = index;
125        loop {
126            let parent = match self.tree.get(current) {
127                Some(td) if td.parent != crate::id::INVALID => td.parent,
128                _ => break,
129            };
130            if let Some(pd) = self.layout.get_mut(parent) {
131                pd.clear_cache();
132            }
133            current = parent;
134        }
135    }
136
137    /// Build `layout_children` from DOM children, filtering `display:none`.
138    ///
139    /// Sets `layout_parent` on each child for cache propagation.
140    /// Text nodes are always included (they're measured, never hidden).
141    pub(crate) fn resolve_layout_children(&mut self, index: u32) {
142        if self.is_text_node(index) {
143            if let Some(data) = self.layout.get_mut(index) {
144                data.layout_children = Some(Vec::new());
145            }
146            return;
147        }
148
149        let dom_children = unsafe { tree::ops::children(&self.tree, index) };
150        let mut layout_children = Vec::new();
151
152        for child in dom_children {
153            // Text nodes always participate in layout.
154            if !self.is_text_node(child) {
155                let display = self
156                    .layout
157                    .get(child)
158                    .map_or(taffy::Display::None, |d| d.style.display);
159                if display == taffy::Display::None {
160                    continue;
161                }
162            }
163
164            if let Some(data) = self.layout.get_mut(child) {
165                data.layout_parent = Some(index);
166            }
167            layout_children.push(child);
168            self.resolve_layout_children(child);
169        }
170
171        if let Some(data) = self.layout.get_mut(index) {
172            data.layout_children = Some(layout_children);
173        }
174    }
175
176    /// Set root to fill viewport (100% width + height) — Chrome's ICB.
177    ///
178    /// Root is a Block container (not Flex) — matches Chrome's initial
179    /// containing block. Block children with `width: auto` stretch to
180    /// fill the parent (normal block flow). Flex would not stretch them.
181    pub(crate) fn set_root_viewport_style(&mut self, root: u32) {
182        let lp = crate::styling::taffy_bridge::convert::length_percentage(
183            &style::values::computed::LengthPercentage::new_percent(
184                style::values::computed::Percentage(1.0),
185            ),
186        );
187        let full: taffy::Dimension = lp.into();
188        if let Some(data) = self.layout.get_mut(root) {
189            data.style.display = taffy::Display::Block;
190            data.style.size.width = full;
191            data.style.size.height = full;
192        }
193    }
194
195    /// Swap left↔right insets for absolute children of RTL parents.
196    fn apply_rtl_swap_recursive(&mut self, index: u32) {
197        let children = self
198            .layout
199            .get(index)
200            .and_then(|d| d.layout_children.clone())
201            .unwrap_or_default();
202
203        for &child in &children {
204            let is_abs = self
205                .layout
206                .get(child)
207                .is_some_and(|d| d.style.position == taffy::Position::Absolute);
208
209            if is_abs {
210                if let Some(parent_cv) = self.computed_style(index) {
211                    let dir = shared::InlineDirection::from_style(&parent_cv);
212                    if let Some(data) = self.layout.get_mut(child) {
213                        dir.swap_absolute_insets(&mut data.style);
214                    }
215                }
216            }
217        }
218
219        for child in children {
220            self.apply_rtl_swap_recursive(child);
221        }
222    }
223
224    /// Full layout resolve: flush styles → build children → compute → fragments.
225    ///
226    /// Single entry point that replaces the old 4-step pipeline:
227    /// 1. `build_layout_tree_from_doc()` → `resolve_layout_children()`
228    /// 2. `sync_all_styles()` → `flush_styles_to_layout()`
229    /// 3. `compute_layout()` → Taffy runs against Document directly
230    /// 4. `build_fragment_recursive()` → reads from Document
231    ///
232    /// Convenience wrapper — always force-clears caches.
233    pub fn resolve_layout(
234        &mut self,
235        root: u32,
236        available_width: Option<f32>,
237        available_height: Option<f32>,
238        ctx: &LayoutContext,
239    ) -> LayoutResult {
240        self.resolve_layout_dirty(root, available_width, available_height, ctx, true)
241    }
242
243    /// Full layout resolve with dirty flag control.
244    ///
245    /// `layout_dirty`: true = something changed, clear all Taffy caches.
246    /// false = nothing changed, Taffy hits cache → ~0ms layout.
247    pub fn resolve_layout_dirty(
248        &mut self,
249        root: u32,
250        available_width: Option<f32>,
251        available_height: Option<f32>,
252        ctx: &LayoutContext,
253        layout_dirty: bool,
254    ) -> LayoutResult {
255        // Step 1: Sync ComputedValues → taffy::Style
256        self.flush_styles_to_layout(root, layout_dirty);
257
258        // Step 2: Build layout_children from DOM
259        self.resolve_layout_children(root);
260
261        // Step 3: Root fills viewport
262        self.set_root_viewport_style(root);
263
264        // Step 4: RTL fixup
265        self.apply_rtl_swap_recursive(root);
266
267        // Step 5: Compute layout via Taffy
268        let available_space = taffy::Size {
269            width: available_width.map_or(
270                taffy::AvailableSpace::MaxContent,
271                taffy::AvailableSpace::Definite,
272            ),
273            height: available_height.map_or(
274                taffy::AvailableSpace::MaxContent,
275                taffy::AvailableSpace::Definite,
276            ),
277        };
278        {
279            let mut view = DocumentLayoutView::from_document(self, ctx);
280            view.compute_layout(root, available_space);
281        }
282
283        // Step 6: Build Fragment tree
284        build_fragment_from_document(self, ctx, root)
285    }
286}
287
288// ============================================================
289// DocumentLayoutView — Taffy trait implementations
290// ============================================================
291
292/// Layout view — implements Taffy's trait-based API via split borrows.
293///
294/// Borrows `layout` mutably (Taffy writes results + cache here) and
295/// everything else immutably. This allows other systems (scroll, animation)
296/// to read Document state concurrently during layout in the future.
297///
298/// Chrome equivalent: layout algorithms take read-only constraint space;
299/// writes go to a separate output buffer (`NGLayoutResult`).
300pub(crate) struct DocumentLayoutView<'a> {
301    layout: &'a mut Storage<LayoutNodeData>,
302    meta: &'a Storage<NodeMeta>,
303    tree: &'a Storage<TreeData>,
304    element_data: &'a Storage<ElementData>,
305    data: &'a DataStorage,
306    ctx: &'a LayoutContext<'a>,
307}
308
309impl<'a> DocumentLayoutView<'a> {
310    fn from_document(doc: &'a mut Document, ctx: &'a LayoutContext<'a>) -> Self {
311        Self {
312            layout: &mut doc.layout,
313            meta: &doc.meta,
314            tree: &doc.tree,
315            element_data: &doc.element_data,
316            data: &doc.data,
317            ctx,
318        }
319    }
320
321    fn compute_layout(&mut self, root: u32, available_space: taffy::Size<taffy::AvailableSpace>) {
322        let root_node = NodeId::from(root as u64);
323        compute_root_layout(self, root_node, available_space);
324    }
325
326    #[inline]
327    fn is_text_node(&self, index: u32) -> bool {
328        self.meta
329            .get(index)
330            .is_some_and(|m| m.flags.node_type() == NodeType::Text)
331    }
332
333    fn text_content_ref(&self, index: u32) -> Option<&str> {
334        let meta = self.meta.get(index)?;
335        if meta.data_type_id != std::any::TypeId::of::<TextData>() {
336            return None;
337        }
338        let data = unsafe { self.data.get::<TextData>(index) };
339        Some(&data.content)
340    }
341
342    fn computed_style(&self, index: u32) -> Option<servo_arc::Arc<ComputedValues>> {
343        let ed = self.element_data.get(index)?;
344        let data = ed.stylo_data.borrow();
345        if data.has_styles() {
346            Some(data.styles.primary().clone())
347        } else {
348            None
349        }
350    }
351}
352
353// ============================================================
354// TraversePartialTree — child iteration via layout_children
355// ============================================================
356
357pub(crate) struct ChildIter {
358    ids: Vec<u32>,
359    pos: usize,
360}
361
362impl Iterator for ChildIter {
363    type Item = NodeId;
364    fn next(&mut self) -> Option<Self::Item> {
365        if self.pos < self.ids.len() {
366            let id = self.ids[self.pos];
367            self.pos += 1;
368            Some(NodeId::from(id as u64))
369        } else {
370            None
371        }
372    }
373}
374
375impl TraversePartialTree for DocumentLayoutView<'_> {
376    type ChildIter<'c>
377        = ChildIter
378    where
379        Self: 'c;
380
381    fn child_ids(&self, parent: NodeId) -> Self::ChildIter<'_> {
382        let idx = u64::from(parent) as u32;
383        let ids = self
384            .layout
385            .get(idx)
386            .and_then(|d| d.layout_children.as_ref())
387            .cloned()
388            .unwrap_or_default();
389        ChildIter { ids, pos: 0 }
390    }
391
392    fn child_count(&self, parent: NodeId) -> usize {
393        let idx = u64::from(parent) as u32;
394        self.layout
395            .get(idx)
396            .and_then(|d| d.layout_children.as_ref())
397            .map_or(0, |c| c.len())
398    }
399
400    fn get_child_id(&self, parent: NodeId, index: usize) -> NodeId {
401        let idx = u64::from(parent) as u32;
402        let child = self
403            .layout
404            .get(idx)
405            .and_then(|d| d.layout_children.as_ref())
406            .and_then(|c| c.get(index))
407            .copied()
408            .expect("child index out of bounds");
409        NodeId::from(child as u64)
410    }
411}
412
413// ============================================================
414// LayoutPartialTree — core layout dispatch
415// ============================================================
416
417impl LayoutPartialTree for DocumentLayoutView<'_> {
418    type CoreContainerStyle<'a>
419        = &'a taffy::Style<Atom>
420    where
421        Self: 'a;
422    type CustomIdent = Atom;
423
424    fn get_core_container_style(&self, node: NodeId) -> Self::CoreContainerStyle<'_> {
425        let idx = u64::from(node) as u32;
426        &self.layout.get(idx).expect("missing layout data").style
427    }
428
429    fn set_unrounded_layout(&mut self, node: NodeId, layout: &Layout) {
430        let idx = u64::from(node) as u32;
431        if let Some(data) = self.layout.get_mut(idx) {
432            data.unrounded_layout = *layout;
433        }
434    }
435
436    fn compute_child_layout(&mut self, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
437        let idx = u64::from(node) as u32;
438        let display = self
439            .layout
440            .get(idx)
441            .map_or(taffy::Display::None, |d| d.style.display);
442
443        if display == taffy::Display::None {
444            return compute_hidden_layout(self, node);
445        }
446
447        compute_cached_layout(self, node, inputs, |view, node, inputs| {
448            view.compute_uncached_child_layout(node, inputs, display)
449        })
450    }
451}
452
453impl DocumentLayoutView<'_> {
454    /// Compute layout for a node that missed the cache.
455    ///
456    /// `display` is the display value already read by the caller to decide
457    /// between hidden/cached paths — passed in to avoid re-reading storage.
458    fn compute_uncached_child_layout(
459        &mut self,
460        node: NodeId,
461        inputs: LayoutInput,
462        display: taffy::Display,
463    ) -> LayoutOutput {
464        let idx = u64::from(node) as u32;
465        let is_text = self.is_text_node(idx);
466        let has_children = self
467            .layout
468            .get(idx)
469            .and_then(|d| d.layout_children.as_ref())
470            .is_some_and(|c: &Vec<u32>| !c.is_empty());
471
472        if is_text && !has_children {
473            return self.layout_text_leaf(node, inputs, idx);
474        }
475
476        let is_replaced = self
477            .layout
478            .get(idx)
479            .is_some_and(|d| d.style.item_is_replaced);
480
481        if is_replaced {
482            return self.layout_replaced(node, inputs, idx);
483        }
484
485        self.layout_container(node, inputs, display, idx)
486    }
487
488    fn layout_text_leaf(&mut self, _node: NodeId, inputs: LayoutInput, idx: u32) -> LayoutOutput {
489        let parent_idx = self.tree.get(idx).map_or(crate::id::INVALID, |t| t.parent);
490        let parent_style = if parent_idx != crate::id::INVALID {
491            self.computed_style(parent_idx)
492        } else {
493            None
494        }
495        .unwrap_or_else(|| crate::styling::initial_values_arc().clone());
496
497        let text = self.text_content_ref(idx).map(str::to_string);
498        let style = &self.layout.get(idx).expect("missing layout data").style;
499        let measurer = self.ctx.text_measurer;
500
501        compute_leaf_layout(
502            inputs,
503            style,
504            |_val, _basis| 0.0,
505            |known_dimensions, available_space| {
506                compute_text_measure(
507                    known_dimensions,
508                    available_space,
509                    text.as_deref(),
510                    measurer,
511                    &parent_style,
512                )
513            },
514        )
515    }
516
517    fn layout_replaced(&mut self, _node: NodeId, inputs: LayoutInput, idx: u32) -> LayoutOutput {
518        let style = &self.layout.get(idx).expect("missing layout data").style;
519        compute_leaf_layout(
520            inputs,
521            style,
522            |_val, _basis| 0.0,
523            |known_dimensions, _available_space| taffy::Size {
524                width: known_dimensions.width.unwrap_or(0.0),
525                height: known_dimensions.height.unwrap_or(0.0),
526            },
527        )
528    }
529
530    fn layout_container(
531        &mut self,
532        node: NodeId,
533        inputs: LayoutInput,
534        display: taffy::Display,
535        idx: u32,
536    ) -> LayoutOutput {
537        match display {
538            taffy::Display::Flex => compute_flexbox_layout(self, node, inputs),
539            taffy::Display::Grid => compute_grid_layout(self, node, inputs),
540            taffy::Display::Block => compute_block_layout(self, node, inputs),
541            _ => {
542                let style = &self.layout.get(idx).expect("missing layout data").style;
543                compute_leaf_layout(
544                    inputs,
545                    style,
546                    |_val, _basis| 0.0,
547                    |known, _avail| taffy::Size {
548                        width: known.width.unwrap_or(0.0),
549                        height: known.height.unwrap_or(0.0),
550                    },
551                )
552            }
553        }
554    }
555}
556
557// ============================================================
558// Text measurement
559// ============================================================
560
561/// Measure a text node's size given known/available constraints.
562///
563/// Called by the Taffy leaf-layout measure callback. `text` is `None` for
564/// nodes whose content was unexpectedly empty — yields zero size.
565fn compute_text_measure(
566    known_dimensions: taffy::Size<Option<f32>>,
567    available_space: taffy::Size<taffy::AvailableSpace>,
568    text: Option<&str>,
569    measurer: &dyn crate::layout::inline::measurer::TextMeasurer,
570    parent_style: &ComputedValues,
571) -> taffy::Size<f32> {
572    let Some(text) = text else {
573        return taffy::Size {
574            width: known_dimensions.width.unwrap_or(0.0),
575            height: known_dimensions.height.unwrap_or(0.0),
576        };
577    };
578
579    use crate::layout::inline::font_system::FontQuery;
580    let query = FontQuery::from_computed(parent_style);
581
582    let max_w = match available_space.width {
583        taffy::AvailableSpace::Definite(w) => Some(w),
584        taffy::AvailableSpace::MaxContent => None,
585        taffy::AvailableSpace::MinContent => Some(0.0),
586    };
587
588    let text_metrics = measurer.shape_text(text, &query);
589    let fm = measurer.query_metrics(&query);
590
591    let text_width = if let Some(mw) = max_w {
592        text_metrics.width.min(mw)
593    } else {
594        text_metrics.width
595    };
596
597    let lh = crate::layout::inline::measurer::resolve_line_height(
598        &parent_style.clone_line_height(),
599        query.font_size,
600        &fm,
601    );
602
603    taffy::Size {
604        width: known_dimensions.width.unwrap_or(text_width),
605        height: known_dimensions.height.unwrap_or(lh),
606    }
607}
608
609// ============================================================
610// CacheTree — layout caching
611// ============================================================
612
613impl CacheTree for DocumentLayoutView<'_> {
614    fn cache_get(
615        &self,
616        node: NodeId,
617        known_dimensions: taffy::Size<Option<f32>>,
618        available_space: taffy::Size<taffy::AvailableSpace>,
619        run_mode: taffy::tree::RunMode,
620    ) -> Option<LayoutOutput> {
621        let idx = u64::from(node) as u32;
622        self.layout
623            .get(idx)?
624            .cache
625            .get(known_dimensions, available_space, run_mode)
626    }
627
628    fn cache_store(
629        &mut self,
630        node: NodeId,
631        known_dimensions: taffy::Size<Option<f32>>,
632        available_space: taffy::Size<taffy::AvailableSpace>,
633        run_mode: taffy::tree::RunMode,
634        layout_output: LayoutOutput,
635    ) {
636        let idx = u64::from(node) as u32;
637        if let Some(data) = self.layout.get_mut(idx) {
638            data.cache
639                .store(known_dimensions, available_space, run_mode, layout_output);
640        }
641    }
642
643    fn cache_clear(&mut self, node: NodeId) {
644        let idx = u64::from(node) as u32;
645        if let Some(data) = self.layout.get_mut(idx) {
646            data.cache = Cache::new();
647        }
648    }
649}
650
651// ============================================================
652// Container style traits (flex, grid, block)
653// ============================================================
654
655impl LayoutFlexboxContainer for DocumentLayoutView<'_> {
656    type FlexboxContainerStyle<'a>
657        = &'a taffy::Style<Atom>
658    where
659        Self: 'a;
660    type FlexboxItemStyle<'a>
661        = &'a taffy::Style<Atom>
662    where
663        Self: 'a;
664
665    fn get_flexbox_container_style(&self, node: NodeId) -> Self::FlexboxContainerStyle<'_> {
666        let idx = u64::from(node) as u32;
667        &self.layout.get(idx).expect("missing layout data").style
668    }
669
670    fn get_flexbox_child_style(&self, child: NodeId) -> Self::FlexboxItemStyle<'_> {
671        let idx = u64::from(child) as u32;
672        &self.layout.get(idx).expect("missing layout data").style
673    }
674}
675
676impl LayoutGridContainer for DocumentLayoutView<'_> {
677    type GridContainerStyle<'a>
678        = &'a taffy::Style<Atom>
679    where
680        Self: 'a;
681    type GridItemStyle<'a>
682        = &'a taffy::Style<Atom>
683    where
684        Self: 'a;
685
686    fn get_grid_container_style(&self, node: NodeId) -> Self::GridContainerStyle<'_> {
687        let idx = u64::from(node) as u32;
688        &self.layout.get(idx).expect("missing layout data").style
689    }
690
691    fn get_grid_child_style(&self, child: NodeId) -> Self::GridItemStyle<'_> {
692        let idx = u64::from(child) as u32;
693        &self.layout.get(idx).expect("missing layout data").style
694    }
695}
696
697impl LayoutBlockContainer for DocumentLayoutView<'_> {
698    type BlockContainerStyle<'a>
699        = &'a taffy::Style<Atom>
700    where
701        Self: 'a;
702    type BlockItemStyle<'a>
703        = &'a taffy::Style<Atom>
704    where
705        Self: 'a;
706
707    fn get_block_container_style(&self, node: NodeId) -> Self::BlockContainerStyle<'_> {
708        let idx = u64::from(node) as u32;
709        &self.layout.get(idx).expect("missing layout data").style
710    }
711
712    fn get_block_child_style(&self, child: NodeId) -> Self::BlockItemStyle<'_> {
713        let idx = u64::from(child) as u32;
714        &self.layout.get(idx).expect("missing layout data").style
715    }
716}
717
718// ============================================================
719// Fragment building from Document layout results
720// ============================================================
721
722/// Build the immutable Fragment tree from Document's layout results.
723///
724/// Reads `LayoutNodeData.unrounded_layout` for positions/sizes,
725/// `ComputedValues` for paint properties, and shapes text.
726pub(crate) fn build_fragment_from_document(
727    doc: &Document,
728    ctx: &LayoutContext,
729    root: u32,
730) -> LayoutResult {
731    build_fragment_recursive(doc, ctx, root)
732}
733
734/// Map Stylo's computed overflow value to our fragment-level `OverflowClip`.
735fn overflow_clip_from_style(overflow: style::computed_values::overflow_x::T) -> OverflowClip {
736    use style::computed_values::overflow_x::T;
737    match overflow {
738        T::Visible => OverflowClip::Visible,
739        T::Hidden | T::Clip => OverflowClip::Hidden,
740        T::Scroll => OverflowClip::Scroll,
741        T::Auto => OverflowClip::Auto,
742    }
743}
744
745fn build_fragment_recursive(doc: &Document, ctx: &LayoutContext, index: u32) -> LayoutResult {
746    let layout_data = doc.layout.get(index).expect("missing layout data");
747    let layout = &layout_data.unrounded_layout;
748    let is_text = doc.is_text_node(index);
749
750    // Style: text nodes use parent's, elements use their own.
751    let style = if is_text {
752        let parent_idx = doc.tree.get(index).map_or(crate::id::INVALID, |t| t.parent);
753        if parent_idx != crate::id::INVALID {
754            doc.computed_style(parent_idx)
755                .unwrap_or_else(|| crate::styling::initial_values_arc().clone())
756        } else {
757            crate::styling::initial_values_arc().clone()
758        }
759    } else {
760        doc.computed_style(index)
761            .unwrap_or_else(|| crate::styling::initial_values_arc().clone())
762    };
763
764    let dom_node = Some(index);
765
766    let border = PhysicalInsets {
767        top: layout.border.top,
768        right: layout.border.right,
769        bottom: layout.border.bottom,
770        left: layout.border.left,
771    };
772    let padding = PhysicalInsets {
773        top: layout.padding.top,
774        right: layout.padding.right,
775        bottom: layout.padding.bottom,
776        left: layout.padding.left,
777    };
778
779    // Build children fragments recursively.
780    let children_ids = layout_data.layout_children.clone().unwrap_or_default();
781    let mut children = Vec::with_capacity(children_ids.len());
782
783    for &child_id in &children_ids {
784        let child_layout = &doc
785            .layout
786            .get(child_id)
787            .expect("missing layout data")
788            .unrounded_layout;
789        let child_result = build_fragment_recursive(doc, ctx, child_id);
790
791        children.push(ChildFragment {
792            offset: Point::new(child_layout.location.x, child_layout.location.y),
793            fragment: child_result.fragment,
794        });
795    }
796
797    let size = Size::new(layout.size.width, layout.size.height);
798
799    // Scrollable overflow = bounding box of all children's positioned extents.
800    // Chrome: `NGPhysicalBoxFragment::ComputeScrollableOverflow()`.
801    let scrollable_overflow = {
802        let mut max_w = 0.0_f32;
803        let mut max_h = 0.0_f32;
804        for child in &children {
805            max_w = max_w.max(child.offset.x + child.fragment.size.width);
806            max_h = max_h.max(child.offset.y + child.fragment.size.height);
807        }
808        Size::new(max_w, max_h)
809    };
810
811    // RTL post-layout fixup.
812    let display = layout_data.style.display;
813    let dir = shared::InlineDirection::from_style(&style);
814    let child_positions: Vec<_> = children_ids
815        .iter()
816        .map(|&id| {
817            doc.layout
818                .get(id)
819                .expect("missing layout data for child during RTL fixup")
820                .style
821                .position
822        })
823        .collect();
824    dir.fixup_children(
825        display,
826        &mut children,
827        &child_positions,
828        border.left,
829        border.right,
830        padding.left,
831        padding.right,
832        size.width,
833    );
834
835    // Text → TextFragment, Box → BoxFragment.
836    let has_children = !children.is_empty();
837
838    let fragment = if is_text && !has_children {
839        let text: Option<std::sync::Arc<str>> = doc.text_content_ref(index).map(Into::into);
840        let text_len = text.as_ref().map_or(0, |t| t.len());
841
842        use crate::layout::inline::font_system::FontQuery;
843        let query = FontQuery::from_computed(&style);
844        let color_abs = style.clone_color();
845        let color_rgba = [
846            (color_abs.components.0 * 255.0).round().clamp(0.0, 255.0) as u8,
847            (color_abs.components.1 * 255.0).round().clamp(0.0, 255.0) as u8,
848            (color_abs.components.2 * 255.0).round().clamp(0.0, 255.0) as u8,
849            (color_abs.alpha * 255.0).round().clamp(0.0, 255.0) as u8,
850        ];
851        let shaped_runs = if let Some(ref t) = text {
852            ctx.text_measurer.shape_glyphs(t, &query, color_rgba)
853        } else {
854            Vec::new()
855        };
856
857        let metrics = ctx.text_measurer.font_metrics(query.font_size);
858        let lh = crate::layout::inline::measurer::resolve_line_height(
859            &style.clone_line_height(),
860            query.font_size,
861            &metrics,
862        );
863        let font_height = metrics.ascent + metrics.descent;
864        let half_leading = ((lh - font_height) / 2.0).max(0.0);
865        let baseline = metrics.ascent + half_leading;
866
867        Fragment::new_text_styled(
868            size,
869            TextFragmentData {
870                text_range: 0..text_len,
871                baseline,
872                text,
873                shaped_runs,
874            },
875            style,
876            dom_node,
877        )
878    } else {
879        Fragment::new_box_styled(
880            size,
881            BoxFragmentData {
882                children,
883                padding,
884                border,
885                scrollable_overflow,
886                is_stacking_context: is_stacking_context(&style),
887                overflow_x: overflow_clip_from_style(style.clone_overflow_x()),
888                overflow_y: overflow_clip_from_style(style.clone_overflow_y()),
889            },
890            style,
891            dom_node,
892        )
893    };
894
895    LayoutResult {
896        fragment,
897        intrinsic_sizes: None,
898        escaped_margins: EscapedMargins::default(),
899    }
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use crate::dom::traits::{Element, HasHandle};
906    use crate::layout::inline::FontSystem;
907
908    #[test]
909    fn resolve_layout_produces_fragment() {
910        let mut doc = Document::new();
911        doc.init_body();
912        doc.set_viewport(800.0, 600.0);
913        doc.recalc_styles();
914
915        let measurer = FontSystem::new();
916        let ctx = LayoutContext {
917            text_measurer: &measurer,
918        };
919
920        let root = doc.root_index();
921        let result = doc.resolve_layout(root, Some(800.0), Some(600.0), &ctx);
922        assert!(result.fragment.size.width > 0.0);
923        assert!(result.fragment.size.height > 0.0);
924    }
925
926    #[test]
927    fn text_node_gets_layout() {
928        let mut doc = Document::new();
929        doc.init_body();
930        doc.set_viewport(800.0, 600.0);
931
932        let text = doc.create_text("Hello");
933        doc.body().append(text.handle());
934        doc.recalc_styles();
935
936        let measurer = FontSystem::new();
937        let ctx = LayoutContext {
938            text_measurer: &measurer,
939        };
940
941        let root = doc.root_index();
942        let result = doc.resolve_layout(root, Some(800.0), Some(600.0), &ctx);
943        assert!(result.fragment.size.width > 0.0);
944    }
945
946    #[test]
947    fn display_none_excluded_from_layout() {
948        let mut doc = Document::new();
949        doc.init_body();
950        doc.set_viewport(800.0, 600.0);
951
952        // Add stylesheet that hides elements with class "hidden"
953        doc.add_stylesheet(".hidden { display: none; }");
954
955        let div = doc.create::<crate::HtmlDivElement>();
956        div.set_class_name("hidden");
957        doc.body().append(div.handle());
958        doc.recalc_styles();
959
960        let body_idx = doc.body;
961        doc.flush_styles_to_layout(doc.root_index(), false);
962        doc.resolve_layout_children(doc.root_index());
963
964        // Body's layout_children should not include the hidden div.
965        let body_children = doc
966            .layout
967            .get(body_idx)
968            .and_then(|d| d.layout_children.as_ref())
969            .map(|c| c.len())
970            .unwrap_or(0);
971        assert_eq!(body_children, 0);
972    }
973
974    #[test]
975    fn is_text_node_works() {
976        let doc = Document::new();
977        // Root is not a text node.
978        assert!(!doc.is_text_node(doc.root_index()));
979    }
980
981    #[test]
982    fn layout_children_set_for_text() {
983        let mut doc = Document::new();
984        doc.init_body();
985        doc.set_viewport(800.0, 600.0);
986
987        let text = doc.create_text("Hello");
988        let text_idx = text.handle().raw().index();
989        doc.body().append(text.handle());
990        doc.recalc_styles();
991
992        doc.flush_styles_to_layout(doc.root_index(), false);
993        doc.resolve_layout_children(doc.root_index());
994
995        // Text nodes are leaves — empty layout_children.
996        let text_children = doc
997            .layout
998            .get(text_idx)
999            .and_then(|d| d.layout_children.as_ref())
1000            .map(|c| c.len());
1001        assert_eq!(text_children, Some(0));
1002    }
1003}