Skip to main content

azul_layout/solver3/
mod.rs

1//! solver3/mod.rs
2//!
3//! Next-generation CSS layout engine with proper formatting context separation
4
5pub mod cache;
6pub mod counters;
7pub mod display_list;
8pub mod fc;
9pub mod geometry;
10pub mod getters;
11pub mod layout_tree;
12pub mod paged_layout;
13pub mod pagination;
14pub mod positioning;
15pub mod scrollbar;
16pub mod sizing;
17pub mod taffy_bridge;
18
19/// Lazy debug_info macro - only evaluates format args when debug_messages is Some
20#[macro_export]
21macro_rules! debug_info {
22    ($ctx:expr, $($arg:tt)*) => {
23        if $ctx.debug_messages.is_some() {
24            $ctx.debug_info_inner(format!($($arg)*));
25        }
26    };
27}
28
29/// Lazy debug_warning macro - only evaluates format args when debug_messages is Some
30#[macro_export]
31macro_rules! debug_warning {
32    ($ctx:expr, $($arg:tt)*) => {
33        if $ctx.debug_messages.is_some() {
34            $ctx.debug_warning_inner(format!($($arg)*));
35        }
36    };
37}
38
39/// Lazy debug_error macro - only evaluates format args when debug_messages is Some
40#[macro_export]
41macro_rules! debug_error {
42    ($ctx:expr, $($arg:tt)*) => {
43        if $ctx.debug_messages.is_some() {
44            $ctx.debug_error_inner(format!($($arg)*));
45        }
46    };
47}
48
49/// Lazy debug_log macro - only evaluates format args when debug_messages is Some
50#[macro_export]
51macro_rules! debug_log {
52    ($ctx:expr, $($arg:tt)*) => {
53        if $ctx.debug_messages.is_some() {
54            $ctx.debug_log_inner(format!($($arg)*));
55        }
56    };
57}
58
59/// Lazy debug_box_props macro - only evaluates format args when debug_messages is Some
60#[macro_export]
61macro_rules! debug_box_props {
62    ($ctx:expr, $($arg:tt)*) => {
63        if $ctx.debug_messages.is_some() {
64            $ctx.debug_box_props_inner(format!($($arg)*));
65        }
66    };
67}
68
69/// Lazy debug_css_getter macro - only evaluates format args when debug_messages is Some
70#[macro_export]
71macro_rules! debug_css_getter {
72    ($ctx:expr, $($arg:tt)*) => {
73        if $ctx.debug_messages.is_some() {
74            $ctx.debug_css_getter_inner(format!($($arg)*));
75        }
76    };
77}
78
79/// Lazy debug_bfc_layout macro - only evaluates format args when debug_messages is Some
80#[macro_export]
81macro_rules! debug_bfc_layout {
82    ($ctx:expr, $($arg:tt)*) => {
83        if $ctx.debug_messages.is_some() {
84            $ctx.debug_bfc_layout_inner(format!($($arg)*));
85        }
86    };
87}
88
89/// Lazy debug_ifc_layout macro - only evaluates format args when debug_messages is Some
90#[macro_export]
91macro_rules! debug_ifc_layout {
92    ($ctx:expr, $($arg:tt)*) => {
93        if $ctx.debug_messages.is_some() {
94            $ctx.debug_ifc_layout_inner(format!($($arg)*));
95        }
96    };
97}
98
99/// Lazy debug_table_layout macro - only evaluates format args when debug_messages is Some
100#[macro_export]
101macro_rules! debug_table_layout {
102    ($ctx:expr, $($arg:tt)*) => {
103        if $ctx.debug_messages.is_some() {
104            $ctx.debug_table_layout_inner(format!($($arg)*));
105        }
106    };
107}
108
109/// Lazy debug_display_type macro - only evaluates format args when debug_messages is Some
110#[macro_export]
111macro_rules! debug_display_type {
112    ($ctx:expr, $($arg:tt)*) => {
113        if $ctx.debug_messages.is_some() {
114            $ctx.debug_display_type_inner(format!($($arg)*));
115        }
116    };
117}
118
119// Test modules commented out until they are implemented
120// #[cfg(test)]
121// mod tests;
122// #[cfg(test)]
123// mod tests_arabic;
124
125use std::{collections::BTreeMap, sync::Arc};
126
127use azul_core::{
128    dom::{DomId, NodeId},
129    geom::{LogicalPosition, LogicalRect, LogicalSize},
130    hit_test::{DocumentId, ScrollPosition},
131    resources::RendererResources,
132    selection::{SelectionState, TextCursor, TextSelection},
133    styled_dom::StyledDom,
134};
135use azul_css::{
136    props::property::{CssProperty, CssPropertyCategory},
137    LayoutDebugMessage, LayoutDebugMessageType,
138};
139
140use self::{
141    display_list::generate_display_list,
142    geometry::IntrinsicSizes,
143    getters::get_writing_mode,
144    layout_tree::{generate_layout_tree, LayoutTree},
145    sizing::calculate_intrinsic_sizes,
146};
147#[cfg(feature = "text_layout")]
148pub use crate::font_traits::TextLayoutCache;
149use crate::{
150    font_traits::ParsedFontTrait,
151    solver3::{
152        cache::LayoutCache,
153        display_list::DisplayList,
154        fc::{check_scrollbar_necessity, LayoutConstraints, LayoutResult},
155        layout_tree::DirtyFlag,
156    },
157};
158
159/// A map of hashes for each node to detect changes in content like text.
160pub type NodeHashMap = BTreeMap<usize, u64>;
161
162/// Central context for a single layout pass.
163pub struct LayoutContext<'a, T: ParsedFontTrait> {
164    pub styled_dom: &'a StyledDom,
165    #[cfg(feature = "text_layout")]
166    pub font_manager: &'a crate::font_traits::FontManager<T>,
167    #[cfg(not(feature = "text_layout"))]
168    pub font_manager: core::marker::PhantomData<&'a T>,
169    /// Legacy per-node selection state (for backward compatibility)
170    pub selections: &'a BTreeMap<DomId, SelectionState>,
171    /// New multi-node text selection with anchor/focus model
172    pub text_selections: &'a BTreeMap<DomId, TextSelection>,
173    pub debug_messages: &'a mut Option<Vec<LayoutDebugMessage>>,
174    pub counters: &'a mut BTreeMap<(usize, String), i32>,
175    pub viewport_size: LogicalSize,
176    /// Fragmentation context for CSS Paged Media (PDF generation)
177    /// When Some, layout respects page boundaries and generates one DisplayList per page
178    pub fragmentation_context: Option<&'a mut crate::paged::FragmentationContext>,
179    /// Whether the text cursor should be drawn (managed by CursorManager blink timer)
180    /// When false, the cursor is in the "off" phase of blinking and should not be rendered.
181    /// When true (default), the cursor is visible.
182    pub cursor_is_visible: bool,
183    /// Current cursor location from CursorManager (dom_id, node_id, cursor)
184    /// This is separate from selections - the cursor represents the text insertion point
185    /// in a contenteditable element and should be painted independently.
186    pub cursor_location: Option<(DomId, NodeId, TextCursor)>,
187}
188
189impl<'a, T: ParsedFontTrait> LayoutContext<'a, T> {
190    /// Check if debug messages are enabled (for use with lazy macros)
191    #[inline]
192    pub fn has_debug(&self) -> bool {
193        self.debug_messages.is_some()
194    }
195
196    /// Internal method - called by debug_log! macro after checking has_debug()
197    #[inline]
198    pub fn debug_log_inner(&mut self, message: String) {
199        if let Some(messages) = self.debug_messages.as_mut() {
200            messages.push(LayoutDebugMessage {
201                message: message.into(),
202                location: "solver3".into(),
203                message_type: Default::default(),
204            });
205        }
206    }
207
208    /// Internal method - called by debug_info! macro after checking has_debug()
209    #[inline]
210    pub fn debug_info_inner(&mut self, message: String) {
211        if let Some(messages) = self.debug_messages.as_mut() {
212            messages.push(LayoutDebugMessage::info(message));
213        }
214    }
215
216    /// Internal method - called by debug_warning! macro after checking has_debug()
217    #[inline]
218    pub fn debug_warning_inner(&mut self, message: String) {
219        if let Some(messages) = self.debug_messages.as_mut() {
220            messages.push(LayoutDebugMessage::warning(message));
221        }
222    }
223
224    /// Internal method - called by debug_error! macro after checking has_debug()
225    #[inline]
226    pub fn debug_error_inner(&mut self, message: String) {
227        if let Some(messages) = self.debug_messages.as_mut() {
228            messages.push(LayoutDebugMessage::error(message));
229        }
230    }
231
232    /// Internal method - called by debug_box_props! macro after checking has_debug()
233    #[inline]
234    pub fn debug_box_props_inner(&mut self, message: String) {
235        if let Some(messages) = self.debug_messages.as_mut() {
236            messages.push(LayoutDebugMessage::box_props(message));
237        }
238    }
239
240    /// Internal method - called by debug_css_getter! macro after checking has_debug()
241    #[inline]
242    pub fn debug_css_getter_inner(&mut self, message: String) {
243        if let Some(messages) = self.debug_messages.as_mut() {
244            messages.push(LayoutDebugMessage::css_getter(message));
245        }
246    }
247
248    /// Internal method - called by debug_bfc_layout! macro after checking has_debug()
249    #[inline]
250    pub fn debug_bfc_layout_inner(&mut self, message: String) {
251        if let Some(messages) = self.debug_messages.as_mut() {
252            messages.push(LayoutDebugMessage::bfc_layout(message));
253        }
254    }
255
256    /// Internal method - called by debug_ifc_layout! macro after checking has_debug()
257    #[inline]
258    pub fn debug_ifc_layout_inner(&mut self, message: String) {
259        if let Some(messages) = self.debug_messages.as_mut() {
260            messages.push(LayoutDebugMessage::ifc_layout(message));
261        }
262    }
263
264    /// Internal method - called by debug_table_layout! macro after checking has_debug()
265    #[inline]
266    pub fn debug_table_layout_inner(&mut self, message: String) {
267        if let Some(messages) = self.debug_messages.as_mut() {
268            messages.push(LayoutDebugMessage::table_layout(message));
269        }
270    }
271
272    /// Internal method - called by debug_display_type! macro after checking has_debug()
273    #[inline]
274    pub fn debug_display_type_inner(&mut self, message: String) {
275        if let Some(messages) = self.debug_messages.as_mut() {
276            messages.push(LayoutDebugMessage::display_type(message));
277        }
278    }
279
280    // DEPRECATED: Use debug_*!() macros instead for lazy evaluation
281    // These methods always evaluate format!() arguments even when debug is disabled
282
283    #[inline]
284    #[deprecated(note = "Use debug_info! macro for lazy evaluation")]
285    #[allow(deprecated)]
286    pub fn debug_info(&mut self, message: impl Into<String>) {
287        self.debug_info_inner(message.into());
288    }
289
290    #[inline]
291    #[deprecated(note = "Use debug_warning! macro for lazy evaluation")]
292    #[allow(deprecated)]
293    pub fn debug_warning(&mut self, message: impl Into<String>) {
294        self.debug_warning_inner(message.into());
295    }
296
297    #[inline]
298    #[deprecated(note = "Use debug_error! macro for lazy evaluation")]
299    #[allow(deprecated)]
300    pub fn debug_error(&mut self, message: impl Into<String>) {
301        self.debug_error_inner(message.into());
302    }
303
304    #[inline]
305    #[deprecated(note = "Use debug_log! macro for lazy evaluation")]
306    #[allow(deprecated)]
307    pub fn debug_log(&mut self, message: &str) {
308        self.debug_log_inner(message.to_string());
309    }
310
311    #[inline]
312    #[deprecated(note = "Use debug_box_props! macro for lazy evaluation")]
313    #[allow(deprecated)]
314    pub fn debug_box_props(&mut self, message: impl Into<String>) {
315        self.debug_box_props_inner(message.into());
316    }
317
318    #[inline]
319    #[deprecated(note = "Use debug_css_getter! macro for lazy evaluation")]
320    #[allow(deprecated)]
321    pub fn debug_css_getter(&mut self, message: impl Into<String>) {
322        self.debug_css_getter_inner(message.into());
323    }
324
325    #[inline]
326    #[deprecated(note = "Use debug_bfc_layout! macro for lazy evaluation")]
327    #[allow(deprecated)]
328    pub fn debug_bfc_layout(&mut self, message: impl Into<String>) {
329        self.debug_bfc_layout_inner(message.into());
330    }
331
332    #[inline]
333    #[deprecated(note = "Use debug_ifc_layout! macro for lazy evaluation")]
334    #[allow(deprecated)]
335    pub fn debug_ifc_layout(&mut self, message: impl Into<String>) {
336        self.debug_ifc_layout_inner(message.into());
337    }
338
339    #[inline]
340    #[deprecated(note = "Use debug_table_layout! macro for lazy evaluation")]
341    #[allow(deprecated)]
342    pub fn debug_table_layout(&mut self, message: impl Into<String>) {
343        self.debug_table_layout_inner(message.into());
344    }
345
346    #[inline]
347    #[deprecated(note = "Use debug_display_type! macro for lazy evaluation")]
348    #[allow(deprecated)]
349    pub fn debug_display_type(&mut self, message: impl Into<String>) {
350        self.debug_display_type_inner(message.into());
351    }
352}
353
354/// Main entry point for the incremental, cached layout engine
355#[cfg(feature = "text_layout")]
356pub fn layout_document<T: ParsedFontTrait + Sync + 'static>(
357    cache: &mut LayoutCache,
358    text_cache: &mut TextLayoutCache,
359    new_dom: StyledDom,
360    viewport: LogicalRect,
361    font_manager: &crate::font_traits::FontManager<T>,
362    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
363    selections: &BTreeMap<DomId, SelectionState>,
364    text_selections: &BTreeMap<DomId, TextSelection>,
365    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
366    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
367    renderer_resources: &azul_core::resources::RendererResources,
368    id_namespace: azul_core::resources::IdNamespace,
369    dom_id: azul_core::dom::DomId,
370    cursor_is_visible: bool,
371    cursor_location: Option<(DomId, NodeId, TextCursor)>,
372) -> Result<DisplayList> {
373    // Reset IFC ID counter at the start of each layout pass
374    // This ensures IFCs get consistent IDs across frames when the DOM structure is stable
375    crate::solver3::layout_tree::IfcId::reset_counter();
376
377    if let Some(msgs) = debug_messages.as_mut() {
378        msgs.push(LayoutDebugMessage::info(format!(
379            "[Layout] layout_document called - viewport: ({:.1}, {:.1}) size ({:.1}x{:.1})",
380            viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height
381        )));
382        msgs.push(LayoutDebugMessage::info(format!(
383            "[Layout] DOM has {} nodes",
384            new_dom.node_data.len()
385        )));
386    }
387
388    // Create temporary context without counters for tree generation
389    let mut counter_values = BTreeMap::new();
390    let mut ctx_temp = LayoutContext {
391        styled_dom: &new_dom,
392        font_manager,
393        selections,
394        text_selections,
395        debug_messages,
396        counters: &mut counter_values,
397        viewport_size: viewport.size,
398        fragmentation_context: None,
399        cursor_is_visible,
400        cursor_location: cursor_location.clone(),
401    };
402
403    // --- Step 1: Reconciliation & Invalidation ---
404    let (mut new_tree, mut recon_result) =
405        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?;
406
407    // Step 1.2: Clear Taffy Caches for Dirty Nodes
408    for &node_idx in &recon_result.intrinsic_dirty {
409        if let Some(node) = new_tree.get_mut(node_idx) {
410            node.taffy_cache.clear();
411        }
412    }
413
414    // Step 1.3: Compute CSS Counters
415    // This must be done after tree generation but before layout,
416    // as list markers need counter values during formatting context layout
417    cache::compute_counters(&new_dom, &new_tree, &mut counter_values);
418
419    // Now create the real context with computed counters
420    let mut ctx = LayoutContext {
421        styled_dom: &new_dom,
422        font_manager,
423        selections,
424        text_selections,
425        debug_messages,
426        counters: &mut counter_values,
427        viewport_size: viewport.size,
428        fragmentation_context: None,
429        cursor_is_visible,
430        cursor_location,
431    };
432
433    // --- Step 1.5: Early Exit Optimization ---
434    if recon_result.is_clean() {
435        ctx.debug_log("No changes, returning existing display list");
436        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
437
438        // Use cached scroll IDs if available, otherwise compute them
439        let scroll_ids = if cache.scroll_ids.is_empty() {
440            use crate::window::LayoutWindow;
441            let (scroll_ids, scroll_id_to_node_id) =
442                LayoutWindow::compute_scroll_ids(tree, &new_dom);
443            cache.scroll_ids = scroll_ids.clone();
444            cache.scroll_id_to_node_id = scroll_id_to_node_id;
445            scroll_ids
446        } else {
447            cache.scroll_ids.clone()
448        };
449
450        return generate_display_list(
451            &mut ctx,
452            tree,
453            &cache.calculated_positions,
454            scroll_offsets,
455            &scroll_ids,
456            gpu_value_cache,
457            renderer_resources,
458            id_namespace,
459            dom_id,
460        );
461    }
462
463    // --- Step 2: Incremental Layout Loop (handles scrollbar-induced reflows) ---
464    let mut calculated_positions = cache.calculated_positions.clone();
465    let mut loop_count = 0;
466    loop {
467        loop_count += 1;
468        if loop_count > 10 {
469            // Safety limit to prevent infinite loops
470            break;
471        }
472
473        calculated_positions = cache.calculated_positions.clone();
474        let mut reflow_needed_for_scrollbars = false;
475
476        calculate_intrinsic_sizes(&mut ctx, &mut new_tree, &recon_result.intrinsic_dirty)?;
477
478        for &root_idx in &recon_result.layout_roots {
479            let (cb_pos, cb_size) = get_containing_block_for_node(
480                &new_tree,
481                &new_dom,
482                root_idx,
483                &calculated_positions,
484                viewport,
485            );
486
487            // For ROOT nodes (no parent), we need to account for their margin.
488            // The containing block position from viewport is (0, 0), but the root's
489            // content starts at (margin + border + padding, margin + border + padding).
490            // We pass margin-adjusted position so calculate_content_box_pos works correctly.
491            let root_node = &new_tree.nodes[root_idx];
492            let is_root_with_margin = root_node.parent.is_none()
493                && (root_node.box_props.margin.left != 0.0 || root_node.box_props.margin.top != 0.0);
494
495            let adjusted_cb_pos = if is_root_with_margin {
496                LogicalPosition::new(
497                    cb_pos.x + root_node.box_props.margin.left,
498                    cb_pos.y + root_node.box_props.margin.top,
499                )
500            } else {
501                cb_pos
502            };
503
504            // DEBUG: Log containing block info for this root
505            if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
506                let dom_name = root_node
507                    .dom_node_id
508                    .and_then(|id| new_dom.node_data.as_container().internal.get(id.index()))
509                    .map(|n| format!("{:?}", n.node_type))
510                    .unwrap_or_else(|| "Unknown".to_string());
511
512                debug_msgs.push(LayoutDebugMessage::new(
513                    LayoutDebugMessageType::PositionCalculation,
514                    format!(
515                        "[LAYOUT ROOT {}] {} - CB pos=({:.2}, {:.2}), adjusted=({:.2}, {:.2}), \
516                         CB size=({:.2}x{:.2}), viewport=({:.2}x{:.2}), margin=({:.2}, {:.2})",
517                        root_idx,
518                        dom_name,
519                        cb_pos.x,
520                        cb_pos.y,
521                        adjusted_cb_pos.x,
522                        adjusted_cb_pos.y,
523                        cb_size.width,
524                        cb_size.height,
525                        viewport.size.width,
526                        viewport.size.height,
527                        root_node.box_props.margin.left,
528                        root_node.box_props.margin.top
529                    ),
530                ));
531            }
532
533            cache::calculate_layout_for_subtree(
534                &mut ctx,
535                &mut new_tree,
536                text_cache,
537                root_idx,
538                adjusted_cb_pos,
539                cb_size,
540                &mut calculated_positions,
541                &mut reflow_needed_for_scrollbars,
542                &mut cache.float_cache,
543            )?;
544
545            // CRITICAL: Insert the root node's own position into calculated_positions
546            // This is necessary because calculate_layout_for_subtree only inserts
547            // positions for children, not for the root itself.
548            //
549            // For root nodes, the position should be at (margin.left, margin.top) relative
550            // to the viewport origin, because the margin creates space between the viewport
551            // edge and the element's border-box.
552            if !calculated_positions.contains_key(&root_idx) {
553                let root_node = &new_tree.nodes[root_idx];
554
555                // Calculate the root's border-box position by adding margins to viewport origin
556                // This is different from non-root nodes which inherit their position from
557                // their containing block.
558                let root_position = LogicalPosition::new(
559                    cb_pos.x + root_node.box_props.margin.left,
560                    cb_pos.y + root_node.box_props.margin.top,
561                );
562
563                // DEBUG: Log root positioning
564                if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
565                    let dom_name = root_node
566                        .dom_node_id
567                        .and_then(|id| new_dom.node_data.as_container().internal.get(id.index()))
568                        .map(|n| format!("{:?}", n.node_type))
569                        .unwrap_or_else(|| "Unknown".to_string());
570
571                    debug_msgs.push(LayoutDebugMessage::new(
572                        LayoutDebugMessageType::PositionCalculation,
573                        format!(
574                            "[ROOT POSITION {}] {} - Inserting position=({:.2}, {:.2}) (viewport origin + margin), \
575                             margin=({:.2}, {:.2}, {:.2}, {:.2})",
576                            root_idx,
577                            dom_name,
578                            root_position.x,
579                            root_position.y,
580                            root_node.box_props.margin.top,
581                            root_node.box_props.margin.right,
582                            root_node.box_props.margin.bottom,
583                            root_node.box_props.margin.left
584                        ),
585                    ));
586                }
587
588                calculated_positions.insert(root_idx, root_position);
589            }
590        }
591
592        cache::reposition_clean_subtrees(
593            &new_dom,
594            &new_tree,
595            &recon_result.layout_roots,
596            &mut calculated_positions,
597        );
598
599        if reflow_needed_for_scrollbars {
600            ctx.debug_log(&format!(
601                "Scrollbars changed container size, starting full reflow (loop {})",
602                loop_count
603            ));
604            recon_result.layout_roots.clear();
605            recon_result.layout_roots.insert(new_tree.root);
606            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
607            continue;
608        }
609
610        break;
611    }
612
613    // --- Step 3: Adjust Relatively Positioned Elements ---
614    // This must be done BEFORE positioning out-of-flow elements, because
615    // relatively positioned elements establish containing blocks for their
616    // absolutely positioned descendants. If we adjust relative positions after
617    // positioning absolute elements, the absolute elements will be positioned
618    // relative to the wrong (pre-adjustment) position of their containing block.
619    // Pass the viewport to correctly resolve percentage offsets for the root element.
620    positioning::adjust_relative_positions(
621        &mut ctx,
622        &new_tree,
623        &mut calculated_positions,
624        viewport,
625    )?;
626
627    // --- Step 3.5: Position Out-of-Flow Elements ---
628    // This must be done AFTER adjusting relative positions, so that absolutely
629    // positioned elements are positioned relative to the final (post-adjustment)
630    // position of their relatively positioned containing blocks.
631    positioning::position_out_of_flow_elements(
632        &mut ctx,
633        &mut new_tree,
634        &mut calculated_positions,
635        viewport,
636    )?;
637
638    // --- Step 3.75: Compute Stable Scroll IDs ---
639    // This must be done AFTER layout but BEFORE display list generation
640    use crate::window::LayoutWindow;
641    let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(&new_tree, &new_dom);
642
643    // --- Step 4: Generate Display List & Update Cache ---
644    let display_list = generate_display_list(
645        &mut ctx,
646        &new_tree,
647        &calculated_positions,
648        scroll_offsets,
649        &scroll_ids,
650        gpu_value_cache,
651        renderer_resources,
652        id_namespace,
653        dom_id,
654    )?;
655
656    cache.tree = Some(new_tree);
657    cache.calculated_positions = calculated_positions;
658    cache.viewport = Some(viewport);
659    cache.scroll_ids = scroll_ids;
660    cache.scroll_id_to_node_id = scroll_id_to_node_id;
661    cache.counters = counter_values;
662
663    Ok(display_list)
664}
665
666// STUB: This helper is required by the main loop
667fn get_containing_block_for_node(
668    tree: &LayoutTree,
669    styled_dom: &StyledDom,
670    node_idx: usize,
671    calculated_positions: &BTreeMap<usize, LogicalPosition>,
672    viewport: LogicalRect,
673) -> (LogicalPosition, LogicalSize) {
674    if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
675        if let Some(parent_node) = tree.get(parent_idx) {
676            let pos = calculated_positions
677                .get(&parent_idx)
678                .copied()
679                .unwrap_or_default();
680            let size = parent_node.used_size.unwrap_or_default();
681            // Position in calculated_positions is the margin-box position
682            // To get content-box, add: border + padding (NOT margin, that's already in pos)
683            let content_pos = LogicalPosition::new(
684                pos.x + parent_node.box_props.border.left + parent_node.box_props.padding.left,
685                pos.y + parent_node.box_props.border.top + parent_node.box_props.padding.top,
686            );
687
688            if let Some(dom_id) = parent_node.dom_node_id {
689                let styled_node_state = &styled_dom
690                    .styled_nodes
691                    .as_container()
692                    .get(dom_id)
693                    .map(|n| &n.styled_node_state)
694                    .cloned()
695                    .unwrap_or_default();
696                let writing_mode =
697                    get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
698                let content_size = parent_node.box_props.inner_size(size, writing_mode);
699                return (content_pos, content_size);
700            }
701
702            return (content_pos, size);
703        }
704    }
705    (viewport.origin, viewport.size)
706}
707
708#[derive(Debug)]
709pub enum LayoutError {
710    InvalidTree,
711    SizingFailed,
712    PositioningFailed,
713    DisplayListFailed,
714    Text(crate::font_traits::LayoutError),
715}
716
717impl std::fmt::Display for LayoutError {
718    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
719        match self {
720            LayoutError::InvalidTree => write!(f, "Invalid layout tree"),
721            LayoutError::SizingFailed => write!(f, "Sizing calculation failed"),
722            LayoutError::PositioningFailed => write!(f, "Position calculation failed"),
723            LayoutError::DisplayListFailed => write!(f, "Display list generation failed"),
724            LayoutError::Text(e) => write!(f, "Text layout error: {:?}", e),
725        }
726    }
727}
728
729impl From<crate::font_traits::LayoutError> for LayoutError {
730    fn from(err: crate::font_traits::LayoutError) -> Self {
731        LayoutError::Text(err)
732    }
733}
734
735impl std::error::Error for LayoutError {}
736
737pub type Result<T> = std::result::Result<T, LayoutError>;