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