1
//! solver3/mod.rs
2
//!
3
//! Next-generation CSS layout engine with proper formatting context separation
4

            
5
pub mod cache;
6
pub mod calc;
7
pub mod counters;
8
pub mod display_list;
9
pub mod fc;
10
pub mod geometry;
11
pub mod getters;
12
pub mod layout_tree;
13
pub mod paged_layout;
14
pub mod pagination;
15
pub mod positioning;
16
pub mod scrollbar;
17
pub mod sizing;
18
pub mod taffy_bridge;
19

            
20
/// Lazy debug_info macro - only evaluates format args when debug_messages is Some
21
#[macro_export]
22
macro_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]
32
macro_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]
42
macro_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]
52
macro_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]
62
macro_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]
72
macro_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]
82
macro_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]
92
macro_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]
102
macro_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]
112
macro_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
use std::{collections::{BTreeMap, HashMap}, sync::Arc};
121

            
122
use azul_core::{
123
    dom::{DomId, NodeId},
124
    geom::{LogicalPosition, LogicalRect, LogicalSize},
125
    hit_test::{DocumentId, ScrollPosition},
126
    resources::RendererResources,
127
    selection::{TextCursor, TextSelection},
128
    styled_dom::StyledDom,
129
};
130

            
131
/// Sentinel value for "position not yet computed". No real position is ever f32::MIN.
132
pub const POSITION_UNSET: LogicalPosition = LogicalPosition { x: f32::MIN, y: f32::MIN };
133

            
134
/// Maximum number of scrollbar-induced reflow iterations before layout gives up.
135
/// Scrollbar appearance can change container size, which may trigger further scrollbar
136
/// changes. This limit prevents infinite loops in pathological layouts.
137
const MAX_SCROLLBAR_REFLOW_ITERATIONS: usize = 10;
138

            
139
/// Vec-based position storage indexed by layout-tree node index.
140
/// Replaces `BTreeMap<usize, LogicalPosition>` for O(1) access and cache-friendly iteration.
141
pub type PositionVec = Vec<LogicalPosition>;
142

            
143
/// Create a new PositionVec pre-sized for the given number of nodes.
144
#[inline]
145
pub fn new_position_vec(num_nodes: usize) -> PositionVec {
146
    vec![POSITION_UNSET; num_nodes]
147
}
148

            
149
/// Get position for node index, returning None if unset.
150
///
151
/// Note: only the `x` component is checked against the sentinel. This is sufficient
152
/// because `POSITION_UNSET` always sets both `x` and `y` to `f32::MIN`, and `pos_set`
153
/// always writes both components together.
154
#[inline(always)]
155
pub fn pos_get(positions: &PositionVec, idx: usize) -> Option<LogicalPosition> {
156
    positions.get(idx).copied().filter(|p| p.x != f32::MIN)
157
}
158

            
159
/// Set position for node index. Grows the vec if needed.
160
#[inline(always)]
161
51800
pub fn pos_set(positions: &mut PositionVec, idx: usize, pos: LogicalPosition) {
162
51800
    if idx >= positions.len() {
163
47740
        positions.resize(idx + 1, POSITION_UNSET);
164
47740
    }
165
51800
    positions[idx] = pos;
166
51800
}
167

            
168
/// Check if position has been set for node index.
169
#[inline(always)]
170
29540
pub fn pos_contains(positions: &PositionVec, idx: usize) -> bool {
171
29540
    positions.get(idx).map_or(false, |p| p.x != f32::MIN)
172
29540
}
173
use azul_css::{
174
    props::property::{CssProperty, CssPropertyCategory},
175
    LayoutDebugMessage, LayoutDebugMessageType,
176
};
177

            
178
use self::{
179
    display_list::generate_display_list,
180
    geometry::IntrinsicSizes,
181
    getters::get_writing_mode,
182
    layout_tree::{generate_layout_tree, LayoutTree},
183
    sizing::calculate_intrinsic_sizes,
184
};
185
#[cfg(feature = "text_layout")]
186
pub use crate::font_traits::TextLayoutCache;
187
use crate::{
188
    font_traits::ParsedFontTrait,
189
    solver3::{
190
        cache::LayoutCache,
191
        display_list::DisplayList,
192
        fc::{LayoutConstraints, LayoutResult},
193
        layout_tree::DirtyFlag,
194
    },
195
};
196

            
197
/// A map of hashes for each node to detect changes in content like text.
198
pub type NodeHashMap = BTreeMap<usize, u64>;
199

            
200
/// Central context for a single layout pass.
201
pub struct LayoutContext<'a, T: ParsedFontTrait> {
202
    pub styled_dom: &'a StyledDom,
203
    #[cfg(feature = "text_layout")]
204
    pub font_manager: &'a crate::font_traits::FontManager<T>,
205
    #[cfg(not(feature = "text_layout"))]
206
    pub font_manager: core::marker::PhantomData<&'a T>,
207
    /// Text selections for rendering highlights. Populated from MultiCursorState.
208
    pub text_selections: &'a BTreeMap<DomId, TextSelection>,
209
    pub debug_messages: &'a mut Option<Vec<LayoutDebugMessage>>,
210
    pub counters: &'a mut HashMap<(usize, String), i32>,
211
    pub viewport_size: LogicalSize,
212
    /// Fragmentation context for CSS Paged Media (PDF generation)
213
    /// When Some, layout respects page boundaries and generates one DisplayList per page
214
    pub fragmentation_context: Option<&'a mut crate::paged::FragmentationContext>,
215
    /// Whether the text cursor should be drawn (managed by CursorManager blink timer)
216
    /// When false, the cursor is in the "off" phase of blinking and should not be rendered.
217
    /// When true (default), the cursor is visible.
218
    pub cursor_is_visible: bool,
219
    /// All active cursor locations from MultiCursorState / CursorManager.
220
    /// Each entry is (dom_id, node_id, cursor). Multiple entries = multi-cursor mode.
221
    /// Empty = no active cursor. The last entry is the primary cursor.
222
    pub cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
223
    /// IME preedit (composition) text to render inline at the cursor position.
224
    /// When Some, the text should be rendered with an underline decoration.
225
    pub preedit_text: Option<String>,
226
    /// Text content overrides from in-progress edits (dirty_text_nodes).
227
    /// When a text node has been edited but not yet committed to the DOM,
228
    /// the layout pipeline should read from here instead of StyledDom.
229
    /// Key: (DomId, NodeId of the text node), Value: the edited text string.
230
    pub dirty_text_overrides: BTreeMap<(DomId, NodeId), String>,
231
    /// Per-node multi-slot cache (Taffy-inspired 9+1 architecture).
232
    /// Moved out of LayoutCache via std::mem::take for the duration of layout,
233
    /// then moved back after the layout pass completes.
234
    pub cache_map: cache::LayoutCacheMap,
235
    /// Image cache for resolving `background-image: url(...)` references.
236
    pub image_cache: &'a azul_core::resources::ImageCache,
237
    /// System style containing colors, fonts, metrics, and theme information.
238
    /// Used for selection colors, caret styling, and other system-themed elements.
239
    pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
240
    /// Callback to get the current system time. Used for profiling inside layout.
241
    /// On WASM targets (where `std::time::Instant::now()` panics), callers should
242
    /// supply a no-op or platform-specific implementation.
243
    pub get_system_time_fn: azul_core::task::GetSystemTimeCallback,
244
    /// Memoised `get_scrollbar_style` results, keyed by DOM node id.
245
    /// `compute_scrollbar_info_core` is called many times per node
246
    /// per layout pass (BFC path + Taffy flex/grid path + display
247
    /// list), and each call previously did 9 cascade walks. Once
248
    /// populated, subsequent callers in the same LayoutContext
249
    /// (a single render) return a clone.
250
    ///
251
    /// Uses `RefCell` so shared `&self` borrows (e.g. in the Taffy
252
    /// bridge's `get_core_container_style`) can mutate the cache
253
    /// without lifting the ctx to `&mut`. Keyed by `NodeId` so
254
    /// entries span DOMs in iframe-style nested documents if that
255
    /// ever becomes a thing.
256
    pub scrollbar_style_cache:
257
        core::cell::RefCell<std::collections::HashMap<NodeId, crate::solver3::getters::ComputedScrollbarStyle>>,
258
}
259

            
260
impl<'a, T: ParsedFontTrait> LayoutContext<'a, T> {
261
    /// Check if debug messages are enabled (for use with lazy macros)
262
    #[inline]
263
    pub fn has_debug(&self) -> bool {
264
        self.debug_messages.is_some()
265
    }
266

            
267
    /// Internal method - called by debug_log! macro after checking has_debug()
268
    #[inline]
269
59518
    pub fn debug_log_inner(&mut self, message: String) {
270
59518
        if let Some(messages) = self.debug_messages.as_mut() {
271
58538
            messages.push(LayoutDebugMessage {
272
58538
                message: message.into(),
273
58538
                location: "solver3".into(),
274
58538
                message_type: Default::default(),
275
58538
            });
276
58538
        }
277
59518
    }
278

            
279
    /// Internal method - called by debug_info! macro after checking has_debug()
280
    #[inline]
281
486262
    pub fn debug_info_inner(&mut self, message: String) {
282
486262
        if let Some(messages) = self.debug_messages.as_mut() {
283
486262
            messages.push(LayoutDebugMessage::info(message));
284
486262
        }
285
486262
    }
286

            
287
    /// Internal method - called by debug_warning! macro after checking has_debug()
288
    #[inline]
289
    pub fn debug_warning_inner(&mut self, message: String) {
290
        if let Some(messages) = self.debug_messages.as_mut() {
291
            messages.push(LayoutDebugMessage::warning(message));
292
        }
293
    }
294

            
295
    /// Internal method - called by debug_error! macro after checking has_debug()
296
    #[inline]
297
    pub fn debug_error_inner(&mut self, message: String) {
298
        if let Some(messages) = self.debug_messages.as_mut() {
299
            messages.push(LayoutDebugMessage::error(message));
300
        }
301
    }
302

            
303
    /// Internal method - called by debug_box_props! macro after checking has_debug()
304
    #[inline]
305
    pub fn debug_box_props_inner(&mut self, message: String) {
306
        if let Some(messages) = self.debug_messages.as_mut() {
307
            messages.push(LayoutDebugMessage::box_props(message));
308
        }
309
    }
310

            
311
    /// Internal method - called by debug_css_getter! macro after checking has_debug()
312
    #[inline]
313
    pub fn debug_css_getter_inner(&mut self, message: String) {
314
        if let Some(messages) = self.debug_messages.as_mut() {
315
            messages.push(LayoutDebugMessage::css_getter(message));
316
        }
317
    }
318

            
319
    /// Internal method - called by debug_bfc_layout! macro after checking has_debug()
320
    #[inline]
321
    pub fn debug_bfc_layout_inner(&mut self, message: String) {
322
        if let Some(messages) = self.debug_messages.as_mut() {
323
            messages.push(LayoutDebugMessage::bfc_layout(message));
324
        }
325
    }
326

            
327
    /// Internal method - called by debug_ifc_layout! macro after checking has_debug()
328
    #[inline]
329
105191
    pub fn debug_ifc_layout_inner(&mut self, message: String) {
330
105191
        if let Some(messages) = self.debug_messages.as_mut() {
331
105191
            messages.push(LayoutDebugMessage::ifc_layout(message));
332
105191
        }
333
105191
    }
334

            
335
    /// Internal method - called by debug_table_layout! macro after checking has_debug()
336
    #[inline]
337
45990
    pub fn debug_table_layout_inner(&mut self, message: String) {
338
45990
        if let Some(messages) = self.debug_messages.as_mut() {
339
45990
            messages.push(LayoutDebugMessage::table_layout(message));
340
45990
        }
341
45990
    }
342

            
343
    /// Internal method - called by debug_display_type! macro after checking has_debug()
344
    #[inline]
345
    pub fn debug_display_type_inner(&mut self, message: String) {
346
        if let Some(messages) = self.debug_messages.as_mut() {
347
            messages.push(LayoutDebugMessage::display_type(message));
348
        }
349
    }
350

            
351
    // Convenience wrappers (prefer debug_*! macros for lazy evaluation)
352

            
353
    #[inline]
354
    pub fn debug_info(&mut self, message: impl Into<String>) {
355
        self.debug_info_inner(message.into());
356
    }
357
    #[inline]
358
    pub fn debug_warning(&mut self, message: impl Into<String>) {
359
        self.debug_warning_inner(message.into());
360
    }
361
    #[inline]
362
    pub fn debug_error(&mut self, message: impl Into<String>) {
363
        self.debug_error_inner(message.into());
364
    }
365
    #[inline]
366
50713
    pub fn debug_log(&mut self, message: &str) {
367
50713
        self.debug_log_inner(message.to_string());
368
50713
    }
369
    #[inline]
370
    pub fn debug_box_props(&mut self, message: impl Into<String>) {
371
        self.debug_box_props_inner(message.into());
372
    }
373
    #[inline]
374
    pub fn debug_css_getter(&mut self, message: impl Into<String>) {
375
        self.debug_css_getter_inner(message.into());
376
    }
377
    #[inline]
378
    pub fn debug_bfc_layout(&mut self, message: impl Into<String>) {
379
        self.debug_bfc_layout_inner(message.into());
380
    }
381
    #[inline]
382
    pub fn debug_ifc_layout(&mut self, message: impl Into<String>) {
383
        self.debug_ifc_layout_inner(message.into());
384
    }
385
    #[inline]
386
    pub fn debug_table_layout(&mut self, message: impl Into<String>) {
387
        self.debug_table_layout_inner(message.into());
388
    }
389
    #[inline]
390
    pub fn debug_display_type(&mut self, message: impl Into<String>) {
391
        self.debug_display_type_inner(message.into());
392
    }
393
}
394

            
395
/// Main entry point for the incremental, cached layout engine.
396
///
397
/// `new_dom` is borrowed, not owned — every use inside is `&new_dom`,
398
/// so taking ownership was a pure formality that forced every caller
399
/// to `styled_dom.clone()` the DOM before calling. The clone was
400
/// ~2 MiB per render on excel.html; kept at the borrow now.
401
#[cfg(feature = "text_layout")]
402
/// Web-backend opt-out for display-list generation.
403
///
404
/// When set, [`layout_document`] runs the full positioning pipeline
405
/// (intrinsic sizing, taffy block/flex/grid, relative/sticky/absolute
406
/// adjustment → `calculated_positions`) but **skips
407
/// `generate_display_list`**, returning an empty [`DisplayList`]. The
408
/// web backend emits TLV DOM patches, not a display list, so it needs
409
/// the geometry in `calculated_positions` but nothing the painter
410
/// produces. This also lets the AArch64→wasm lift drop the entire
411
/// `display_list` painter surface (those symbols are classified `Leaf`
412
/// in `dll/src/web/symbol_table.rs::classify_for_name`, so the
413
/// transitive lifter never descends into them). Defaults `false` →
414
/// desktop/native behaviour is unchanged.
415
pub static SKIP_DISPLAY_LIST: core::sync::atomic::AtomicBool =
416
    core::sync::atomic::AtomicBool::new(false);
417

            
418
/// Set [`SKIP_DISPLAY_LIST`]. Provided as a function (rather than the
419
/// caller touching the static directly) so the web backend's
420
/// `dll`-crate caller reaches it through a normal `bl` into this
421
/// `azul_layout` function — keeping the static's address computation
422
/// intra-crate (direct `adrp+add`) instead of a cross-crate GOT load,
423
/// which the AArch64→wasm lift mirrors more reliably.
424
pub fn set_skip_display_list(skip: bool) {
425
    SKIP_DISPLAY_LIST.store(skip, core::sync::atomic::Ordering::Relaxed);
426
}
427

            
428
// M12.7: keep this out-of-line so the web lift sees it as its own wasm fn
429
// (not inlined into layout_dom_recursive). An opt-folded infinite loop in the
430
// solver (a mis-lifted loop exit) is otherwise hidden inside the giant inlined
431
// layout_dom_recursive; de-inlining lets AZ_FUEL/AZ_WASM_DEBUG name the actual
432
// source fn — and may itself prevent the inlining-induced fold. No perf cost on
433
// desktop (called once per layout).
434
#[inline(never)]
435
2765
pub fn layout_document<T: ParsedFontTrait + Sync + 'static>(
436
2765
    cache: &mut LayoutCache,
437
2765
    text_cache: &mut TextLayoutCache,
438
2765
    new_dom: &StyledDom,
439
2765
    viewport: LogicalRect,
440
2765
    font_manager: &crate::font_traits::FontManager<T>,
441
2765
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
442
2765
    text_selections: &BTreeMap<DomId, TextSelection>,
443
2765
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
444
2765
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
445
2765
    renderer_resources: &azul_core::resources::RendererResources,
446
2765
    id_namespace: azul_core::resources::IdNamespace,
447
2765
    dom_id: azul_core::dom::DomId,
448
2765
    cursor_is_visible: bool,
449
2765
    cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
450
2765
    preedit_text: Option<String>,
451
2765
    image_cache: &azul_core::resources::ImageCache,
452
2765
    system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
453
2765
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
454
2765
) -> Result<DisplayList> {
455
    // Reset IFC ID counter at the start of each layout pass
456
    // This ensures IFCs get consistent IDs across frames when the DOM structure is stable
457
2765
    crate::solver3::layout_tree::IfcId::reset_counter();
458
    // in layout_document returns the rc=5 Err (the error enum can't be captured
459
    // reliably in the lift). The last value seen = the step that errored next.
460
2765
    { let _ = (0xDD00_0001u32); }
461
    // If 0 here → the LogicalRect HFA arg was lost across the lifted call.
462

            
463
2765
    if let Some(msgs) = debug_messages.as_mut() {
464
2275
        msgs.push(LayoutDebugMessage::info(format!(
465
2275
            "[Layout] layout_document called - viewport: ({:.1}, {:.1}) size ({:.1}x{:.1})",
466
2275
            viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height
467
2275
        )));
468
2275
        msgs.push(LayoutDebugMessage::info(format!(
469
2275
            "[Layout] DOM has {} nodes",
470
2275
            new_dom.node_data.len()
471
2275
        )));
472
2275
    }
473

            
474
    // Create temporary context without counters for tree generation
475
2765
    let mut counter_values = HashMap::new();
476
2765
    let mut ctx_temp = LayoutContext {
477
2765
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
478
2765
        styled_dom: new_dom,
479
2765
        font_manager,
480
2765
        text_selections,
481
2765
        debug_messages,
482
2765
        counters: &mut counter_values,
483
2765
        viewport_size: viewport.size,
484
2765
        fragmentation_context: None,
485
2765
        cursor_is_visible,
486
2765
        cursor_locations: cursor_locations.clone(),
487
2765
        preedit_text: preedit_text.clone(),
488
2765
        dirty_text_overrides: BTreeMap::new(),
489
2765
        cache_map: cache::LayoutCacheMap::default(), // temp context doesn't need real cache
490
2765
        image_cache,
491
2765
        system_style: system_style.clone(),
492
2765
        get_system_time_fn,
493
2765
    };
494

            
495
2765
    crate::probe::sample_peak_rss("rss:enter_layout_document");
496

            
497
    // --- Step 0: record DOM pointer / viewport for diagnostics only ---
498
    //
499
    // NOTE: there is intentionally NO pointer-identity fast path here.
500
    // Comparing `new_dom as *const StyledDom as usize` against a stored
501
    // pointer is UNSOUND across layout passes: each `regenerate_layout`
502
    // builds a fresh `StyledDom`, and after the previous one is dropped
503
    // (e.g. `layout_and_generate_display_list` calls `layout_results.clear()`
504
    // before re-laying out), the allocator/stack frequently hands the new,
505
    // *different* StyledDom the SAME address. A pointer match therefore does
506
    // NOT prove the content is unchanged — it would return the previous
507
    // frame's display list for a structurally different DOM (e.g. an image
508
    // removed from the tree would still appear in `scan_used_images`,
509
    // breaking resource GC). The Step 1.1 structural-identity cache below
510
    // (root `subtree_hash` + viewport) is the correct, content-based skip;
511
    // it costs one ~600 µs reconcile pass but cannot be fooled by address
512
    // reuse.
513
2765
    let dom_ptr = new_dom as *const StyledDom as usize;
514
2765
    cache.prev_dom_ptr = dom_ptr;
515
2765
    cache.prev_viewport = viewport;
516

            
517
    // --- Step 1: Reconciliation & Invalidation ---
518
2765
    crate::probe::reset_peak();
519
2765
    let (mut new_tree, mut recon_result) =
520
2765
        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?;
521
2765
    { let _ = (0xDD00_0002u32); }
522
2765
    crate::probe::sample_peak_rss("rss:after_reconcile");
523
2765
    crate::probe::sample_phase_peak("rss:peak_during_reconcile");
524

            
525
    // --- Step 1.1: Structural-identity display-list cache ---
526
    //
527
    // If the reconciled root subtree_hash matches the cached one AND
528
    // the viewport is unchanged, nothing structural has moved — skip
529
    // layout, positioning, AND display-list generation and return
530
    // the cached display list verbatim.
531
    //
532
    // This fires on re-renders of an unchanged DOM: the reconcile
533
    // pass still walks and hashes the tree, but that's ~600 µs vs
534
    // the ~4 ms it would otherwise cost to re-emit the display list.
535
2765
    if let Some((cached_hash, cached_viewport, cached_dl)) = &cache.cached_display_list {
536
35
        let new_root_hash = new_tree
537
35
            .cold(new_tree.root)
538
35
            .map(|c| c.subtree_hash);
539
35
        if new_root_hash == Some(*cached_hash) && *cached_viewport == viewport {
540
35
            let _p = crate::probe::Probe::span("display_list_cache_hit");
541
35
            return Ok(cached_dl.clone());
542
        }
543
2730
    }
544

            
545
    // Step 1.2: Clear Taffy Caches for Dirty Nodes
546
18550
    for &node_idx in &recon_result.intrinsic_dirty {
547
15820
        if let Some(warm) = new_tree.warm_mut(node_idx) {
548
15820
            warm.taffy_cache.clear();
549
15820
        }
550
    }
551

            
552
    // Step 1.3: Compute CSS Counters
553
    // This must be done after tree generation but before layout,
554
    // as list markers need counter values during formatting context layout
555
2730
    {
556
2730
        let _p = crate::probe::Probe::span("compute_counters");
557
2730
        cache::compute_counters(&new_dom, &new_tree, &mut counter_values);
558
2730
    }
559

            
560
    // Step 1.4: Resize and invalidate per-node cache (Taffy-inspired 9+1 slot cache)
561
    // Move cache_map out of LayoutCache for the duration of layout (avoids borrow conflicts).
562
    // It will be moved back after the layout pass completes.
563
    //
564
    // Critically: the old `cache_map.entries` is indexed by OLD
565
    // layout-tree positions. The NEW tree may have re-ordered
566
    // indices (anonymous wrapper slots shifted, whitespace nodes
567
    // dropped, etc.). A plain `resize_with(default)` would silently
568
    // serve the wrong node's cached result for any shifted index.
569
    //
570
    // Re-map by stable identity: build `old_layout_idx → new_layout_idx`
571
    // via the `(dom_node_id → layout_idx)` tables on both trees,
572
    // then move each surviving cache entry into its new slot. Nodes
573
    // without a matching DOM id (pure anonymous wrappers) fall
574
    // through to the default (empty, i.e. dirty) entry.
575
2730
    let mut cache_map = std::mem::take(&mut cache.cache_map);
576
2730
    let _probe_cache_remap = Some(crate::probe::Probe::span("cache_map_remap"));
577
2730
    if let Some(old_tree) = cache.tree.as_ref() {
578
        let mut remapped = cache::LayoutCacheMap::default();
579
        remapped.entries.resize_with(new_tree.nodes.len(), Default::default);
580

            
581
        // Primary mapping: DOM id → layout idx on both sides. This
582
        // covers every node that has a corresponding DOM node.
583
        for (dom_id, new_indices) in new_tree.dom_to_layout.iter() {
584
            let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
585
                continue;
586
            };
587
            for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
588
                let Some(&old_layout_idx) = old_indices.get(pair_idx) else {
589
                    continue;
590
                };
591
                if old_layout_idx >= cache_map.entries.len()
592
                    || new_layout_idx >= remapped.entries.len()
593
                {
594
                    continue;
595
                }
596
                remapped.entries[new_layout_idx] =
597
                    core::mem::take(&mut cache_map.entries[old_layout_idx]);
598
            }
599
        }
600

            
601
        // Secondary mapping: anonymous wrappers (dom_node_id == None)
602
        // by (parent_new_idx, ordinal-among-anon-siblings). An
603
        // unchanged DOM produces the same anon wrappers in the same
604
        // order under the same parent — matching by position here
605
        // preserves their cache slots too. Without this, anon
606
        // wrappers re-allocate empty every reconcile and invalidate
607
        // their ancestors via `mark_dirty`.
608
        fn collect_anon_children_by_parent(
609
            tree: &LayoutTree,
610
        ) -> std::collections::HashMap<usize, Vec<usize>> {
611
            let mut map: std::collections::HashMap<usize, Vec<usize>> =
612
                std::collections::HashMap::new();
613
            for (idx, node) in tree.nodes.iter().enumerate() {
614
                if node.dom_node_id.is_some() {
615
                    continue;
616
                }
617
                if let Some(parent) = node.parent {
618
                    map.entry(parent).or_default().push(idx);
619
                }
620
            }
621
            map
622
        }
623

            
624
        // Build old-parent → [old_anon_indices] and
625
        // new-parent → [new_anon_indices]; match by pair position.
626
        let old_anon_by_parent = collect_anon_children_by_parent(old_tree);
627
        let new_anon_by_parent = collect_anon_children_by_parent(&new_tree);
628

            
629
        // For each new parent we know: look up its old twin by the
630
        // dom-id mapping we just populated, then match anon children
631
        // positionally within that parent.
632
        // Build a new→old layout-idx lookup from the primary pass.
633
        let mut new_to_old_layout_idx: std::collections::HashMap<usize, usize> =
634
            std::collections::HashMap::new();
635
        for (dom_id, new_indices) in new_tree.dom_to_layout.iter() {
636
            let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
637
                continue;
638
            };
639
            for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
640
                if let Some(&old_layout_idx) = old_indices.get(pair_idx) {
641
                    new_to_old_layout_idx.insert(new_layout_idx, old_layout_idx);
642
                }
643
            }
644
        }
645

            
646
        for (new_parent_idx, new_anon_children) in new_anon_by_parent {
647
            let Some(&old_parent_idx) = new_to_old_layout_idx.get(&new_parent_idx) else {
648
                continue;
649
            };
650
            let Some(old_anon_children) = old_anon_by_parent.get(&old_parent_idx) else {
651
                continue;
652
            };
653
            for (ord, &new_anon_idx) in new_anon_children.iter().enumerate() {
654
                let Some(&old_anon_idx) = old_anon_children.get(ord) else {
655
                    continue;
656
                };
657
                if old_anon_idx >= cache_map.entries.len()
658
                    || new_anon_idx >= remapped.entries.len()
659
                {
660
                    continue;
661
                }
662
                remapped.entries[new_anon_idx] =
663
                    core::mem::take(&mut cache_map.entries[old_anon_idx]);
664
            }
665
        }
666

            
667
        cache_map = remapped;
668
2730
    } else {
669
2730
        cache_map.resize_to_tree(new_tree.nodes.len());
670
2730
    }
671
2730
    drop(_probe_cache_remap);
672
2730
    crate::probe::sample_peak_rss("rss:after_cache_remap");
673
18550
    for &node_idx in &recon_result.intrinsic_dirty {
674
15820
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
675
15820
    }
676
5460
    for &node_idx in &recon_result.layout_roots {
677
2730
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
678
2730
    }
679

            
680
    // Now create the real context with computed counters
681
2730
    let mut ctx = LayoutContext {
682
2730
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
683
2730
        styled_dom: &new_dom,
684
2730
        font_manager,
685
2730
        text_selections,
686
2730
        debug_messages,
687
2730
        counters: &mut counter_values,
688
2730
        viewport_size: viewport.size,
689
2730
        fragmentation_context: None,
690
2730
        cursor_is_visible,
691
2730
        cursor_locations,
692
2730
        preedit_text,
693
2730
        dirty_text_overrides: BTreeMap::new(),
694
2730
        cache_map, // Moved from LayoutCache; will be moved back after layout
695
2730
        image_cache,
696
2730
        system_style,
697
2730
        get_system_time_fn,
698
2730
    };
699

            
700
    // --- Step 1.5: Early Exit Optimization ---
701
    // M12.7: `&& cache.tree.is_some()` — this "nothing changed, reuse cached
702
    // layout" fast path REQUIRES a cached tree; on COLD layout cache.tree is
703
    // None, so entering here would hit `ok_or(InvalidTree)`. recon_result must
704
    // be dirty on cold (the viewport-resize dirties the root), but if
705
    // is_clean() mis-evaluates we'd wrongly early-exit → InvalidTree. Guarding
706
    // on a cached tree is both correct (can't reuse what isn't there) and
707
    // robust. (rc=5 post-reconcile, step=2: this was the failing `?`.)
708
2730
    if recon_result.is_clean() && cache.tree.is_some() {
709
        debug_log!(ctx, "No changes, returning existing display list");
710
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
711

            
712
        // Use cached scroll IDs if available, otherwise compute them
713
        let scroll_ids = if cache.scroll_ids.is_empty() {
714
            use crate::window::LayoutWindow;
715
            let (scroll_ids, scroll_id_to_node_id) =
716
                LayoutWindow::compute_scroll_ids(tree, &new_dom);
717
            cache.scroll_ids = scroll_ids.clone();
718
            cache.scroll_id_to_node_id = scroll_id_to_node_id;
719
            scroll_ids
720
        } else {
721
            cache.scroll_ids.clone()
722
        };
723

            
724
        if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
725
            return Ok(DisplayList::default());
726
        }
727
        return generate_display_list(
728
            &mut ctx,
729
            tree,
730
            &cache.calculated_positions,
731
            scroll_offsets,
732
            &scroll_ids,
733
            gpu_value_cache,
734
            renderer_resources,
735
            id_namespace,
736
            dom_id,
737
        );
738
2730
    }
739

            
740
2730
    { let _ = (0xDD00_0003u32); }
741

            
742
    // --- Step 2: Incremental Layout Loop (handles scrollbar-induced reflows) ---
743
2730
    let mut calculated_positions = cache.calculated_positions.clone();
744
2730
    let mut loop_count = 0;
745
    loop {
746
2730
        loop_count += 1;
747
2730
        if loop_count > MAX_SCROLLBAR_REFLOW_ITERATIONS {
748
            debug_warning!(ctx, "Scrollbar reflow loop hit limit of {} iterations, breaking to avoid infinite loop", MAX_SCROLLBAR_REFLOW_ITERATIONS);
749
            break;
750
2730
        }
751

            
752
2730
        calculated_positions = {
753
2730
            let _p = crate::probe::Probe::span("clone_calculated_positions");
754
2730
            cache.calculated_positions.clone()
755
        };
756
2730
        let mut reflow_needed_for_scrollbars = false;
757

            
758
        {
759
2730
            crate::probe::reset_peak();
760
2730
            let _p = crate::probe::Probe::span("calc_intrinsic_sizes");
761
2730
            calculate_intrinsic_sizes(
762
2730
                &mut ctx,
763
2730
                &mut new_tree,
764
2730
                text_cache,
765
2730
                &recon_result.intrinsic_dirty,
766
            )?;
767
        }
768
2730
        crate::probe::sample_peak_rss("rss:after_calc_intrinsic");
769
2730
        crate::probe::sample_phase_peak("rss:peak_during_intrinsic");
770
        // divergence is inside calculate_intrinsic_sizes (the SIMD/text intrinsic pass).
771
2730
        { let _ = (0xDD00_0005u32); }
772

            
773
5460
        for &root_idx in &recon_result.layout_roots {
774
2730
            let (cb_pos, cb_size) = get_containing_block_for_node(
775
2730
                &new_tree,
776
2730
                &new_dom,
777
2730
                root_idx,
778
2730
                &calculated_positions,
779
2730
                viewport,
780
2730
            );
781
            // 0x05, the divergence is INSIDE get_containing_block_for_node (or the for-loop
782
            // entry); if 0x53 but not 0x55, it's the margin logic / box_props.unpack below.
783
2730
            { let _ = (0xDD00_0053u32); }
784
            // get_containing_block_for_node(viewport)). 800 here but viewport=800 ⟹ OK;
785
            // 0 here with viewport=800 ⟹ get_containing_block_for_node lost it (HFA return).
786

            
787
            // For ROOT nodes (no parent), we need to account for their margin.
788
            // The containing block position from viewport is (0, 0), but the root's
789
            // content starts at (margin + border + padding, margin + border + padding).
790
            // We pass margin-adjusted position so calculate_content_box_pos works correctly.
791
2730
            let root_node = &new_tree.nodes[root_idx];
792
2730
            let root_bp = root_node.box_props.unpack();
793
2730
            { let _ = (0xDD00_0054u32); }
794

            
795
2730
            let is_root_with_margin = root_node.parent.is_none()
796
2730
                && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
797

            
798
2730
            let adjusted_cb_pos = if is_root_with_margin {
799
105
                LogicalPosition::new(
800
105
                    cb_pos.x + root_bp.margin.left,
801
105
                    cb_pos.y + root_bp.margin.top,
802
                )
803
            } else {
804
2625
                cb_pos
805
            };
806
2730
            { let _ = (0xDD00_0056u32); }
807

            
808
            // DEBUG: Log containing block info for this root
809
2730
            if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
810
2240
                let dom_name = root_node
811
2240
                    .dom_node_id
812
2240
                    .and_then(|id| new_dom.node_data.as_container().internal.get(id.index()))
813
2240
                    .map(|n| format!("{:?}", n.node_type))
814
2240
                    .unwrap_or_else(|| "Unknown".to_string());
815

            
816
2240
                debug_msgs.push(LayoutDebugMessage::new(
817
2240
                    LayoutDebugMessageType::PositionCalculation,
818
2240
                    format!(
819
2240
                        "[LAYOUT ROOT {}] {} - CB pos=({:.2}, {:.2}), adjusted=({:.2}, {:.2}), \
820
2240
                         CB size=({:.2}x{:.2}), viewport=({:.2}x{:.2}), margin=({:.2}, {:.2})",
821
                        root_idx,
822
                        dom_name,
823
                        cb_pos.x,
824
                        cb_pos.y,
825
                        adjusted_cb_pos.x,
826
                        adjusted_cb_pos.y,
827
                        cb_size.width,
828
                        cb_size.height,
829
                        viewport.size.width,
830
                        viewport.size.height,
831
                        root_bp.margin.left,
832
                        root_bp.margin.top
833
                    ),
834
                ));
835
490
            }
836

            
837
            // Purge after intrinsic sizing — frees child_intrinsics Vecs,
838
            // IntrinsicSizeCalculator temporaries, text measurement caches.
839
2730
            crate::probe::hint_purge_allocator();
840
2730
            crate::probe::sample_peak_rss("rss:before_root_layout");
841
2730
            crate::probe::reset_peak();
842
            // 0x57 = it RETURNED. If step stays 0x55, calculate_layout_for_subtree diverges.
843
2730
            { let _ = (0xDD00_0055u32); }
844
            // This is exactly what calc_used_size reads as `viewport`. 0 here pinpoints the
845
            // loss to the ctx build (viewport.size → ctx.viewport_size copy).
846
            // 0x5E = Err. Do NOT propagate (continue to the cache store) so layout-real can
847
            // see whether the geometry was computed regardless of a (possibly spurious,
848
            // niche-Result-mis-discriminated) Err.
849
2730
            let _clr = {
850
2730
                let _p = crate::probe::Probe::span("root_layout_pass");
851
2730
                cache::calculate_layout_for_subtree(
852
2730
                    &mut ctx,
853
2730
                    &mut new_tree,
854
2730
                    text_cache,
855
2730
                    root_idx,
856
2730
                    adjusted_cb_pos,
857
2730
                    cb_size,
858
2730
                    &mut calculated_positions,
859
2730
                    &mut reflow_needed_for_scrollbars,
860
2730
                    &mut cache.float_cache,
861
2730
                    cache::ComputeMode::PerformLayout,
862
                )
863
            };
864
2730
            { let _ = (if _clr.is_ok() { 0xDD00_0057u32 } else { 0xDD00_005Eu32 }); }
865
2730
            crate::probe::sample_peak_rss("rss:after_root_layout");
866
2730
            crate::probe::sample_phase_peak("rss:peak_during_root_layout");
867

            
868
            // CRITICAL: Insert the root node's own position into calculated_positions
869
            // This is necessary because calculate_layout_for_subtree only inserts
870
            // positions for children, not for the root itself.
871
            //
872
            // For root nodes, the position should be at (margin.left, margin.top) relative
873
            // to the viewport origin, because the margin creates space between the viewport
874
            // edge and the element's border-box.
875
2730
            if !pos_contains(&calculated_positions, root_idx) {
876
2730
                let root_node = &new_tree.nodes[root_idx];
877
2730
                let root_bp2 = root_node.box_props.unpack();
878

            
879
                // Calculate the root's border-box position by adding margins to viewport origin
880
                // This is different from non-root nodes which inherit their position from
881
                // their containing block.
882
2730
                let root_position = LogicalPosition::new(
883
2730
                    cb_pos.x + root_bp2.margin.left,
884
2730
                    cb_pos.y + root_bp2.margin.top,
885
                );
886

            
887
                // DEBUG: Log root positioning
888
2730
                if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
889
2240
                    let dom_name = root_node
890
2240
                        .dom_node_id
891
2240
                        .and_then(|id| new_dom.node_data.as_container().internal.get(id.index()))
892
2240
                        .map(|n| format!("{:?}", n.node_type))
893
2240
                        .unwrap_or_else(|| "Unknown".to_string());
894

            
895
2240
                    debug_msgs.push(LayoutDebugMessage::new(
896
2240
                        LayoutDebugMessageType::PositionCalculation,
897
2240
                        format!(
898
2240
                            "[ROOT POSITION {}] {} - Inserting position=({:.2}, {:.2}) (viewport origin + margin), \
899
2240
                             margin=({:.2}, {:.2}, {:.2}, {:.2})",
900
                            root_idx,
901
                            dom_name,
902
                            root_position.x,
903
                            root_position.y,
904
                            root_bp2.margin.top,
905
                            root_bp2.margin.right,
906
                            root_bp2.margin.bottom,
907
                            root_bp2.margin.left
908
                        ),
909
                    ));
910
490
                }
911

            
912
2730
                pos_set(&mut calculated_positions, root_idx, root_position);
913
            }
914
        }
915
        // (step 6). If step stays 5, the divergence is in calculate_layout_for_subtree.
916
2730
        { let _ = (0xDD00_0006u32); }
917

            
918
2730
        {
919
2730
            let _p = crate::probe::Probe::span("reposition_clean_subtrees");
920
2730
            cache::reposition_clean_subtrees(
921
2730
                &new_dom,
922
2730
                &new_tree,
923
2730
                &recon_result.layout_roots,
924
2730
                &mut calculated_positions,
925
2730
            );
926
2730
        }
927

            
928
2730
        if reflow_needed_for_scrollbars {
929
            debug_log!(ctx,
930
                "Scrollbars changed container size, starting full reflow (loop {})",
931
                loop_count
932
            );
933
            recon_result.layout_roots.clear();
934
            recon_result.layout_roots.insert(new_tree.root);
935
            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
936
            continue;
937
2730
        }
938

            
939
2730
        break;
940
    }
941

            
942
    // +spec:positioning:8d1286 - normal flow, relative, float, absolute positioning dispatch
943
    // +spec:positioning:bdfc81 - Layout divided into sizing (Step 2) then positioning (Step 3)
944
    // --- Step 3: Adjust Relatively Positioned Elements ---
945
    // +spec:positioning:a831e8 - inline content width uses pre-relative-offset positions (satisfied by post-layout relative adjustment)
946
    // +spec:positioning:e2647b - Relative positioning applied after line height calculation, so line height is not adjusted for relative offsets
947
    // +spec:positioning:77a2d2 - Relatively positioned boxes considered without their offset during auto height
948
    // +spec:positioning:b47ac2 - Relatively positioned boxes considered without their offset for block auto height
949
    // Relative offsets applied AFTER layout, so auto-height calculation sees normal-flow positions.
950
    // This must be done BEFORE positioning out-of-flow elements, because
951
    // relatively positioned elements establish containing blocks for their
952
    // absolutely positioned descendants. If we adjust relative positions after
953
    // positioning absolute elements, the absolute elements will be positioned
954
    // relative to the wrong (pre-adjustment) position of their containing block.
955
    // Pass the viewport to correctly resolve percentage offsets for the root element.
956
    {
957
2730
        let _p = crate::probe::Probe::span("adjust_relative_positions");
958
2730
        positioning::adjust_relative_positions(
959
2730
            &mut ctx,
960
2730
            &new_tree,
961
2730
            &mut calculated_positions,
962
2730
            viewport,
963
        )?;
964
    }
965

            
966
    // --- Step 3.25: Adjust Sticky Positioned Elements ---
967
    // Sticky elements are laid out in normal flow, then their visual position
968
    // is clamped based on scroll offset and inset properties relative to the
969
    // nearest scrollport. Must happen after relative positioning but before
970
    // absolute positioning (sticky elements establish containing blocks).
971
    {
972
2730
        let _p = crate::probe::Probe::span("adjust_sticky_positions");
973
2730
        positioning::adjust_sticky_positions(
974
2730
            &mut ctx,
975
2730
            &new_tree,
976
2730
            &mut calculated_positions,
977
2730
            scroll_offsets,
978
2730
            viewport,
979
        )?;
980
    }
981

            
982
    // --- Step 3.5: Position Out-of-Flow Elements ---
983
    // This must be done AFTER adjusting relative positions, so that absolutely
984
    // positioned elements are positioned relative to the final (post-adjustment)
985
    // position of their relatively positioned containing blocks.
986
    {
987
2730
        let _p = crate::probe::Probe::span("position_out_of_flow");
988
2730
        positioning::position_out_of_flow_elements(
989
2730
            &mut ctx,
990
2730
            &mut new_tree,
991
2730
            &mut calculated_positions,
992
2730
            viewport,
993
        )?;
994
    }
995

            
996
    // --- Step 3.75: Compute Stable Scroll IDs ---
997
    // This must be done AFTER layout but BEFORE display list generation
998
    use crate::window::LayoutWindow;
999
2730
    let (scroll_ids, scroll_id_to_node_id) = {
2730
        let _p = crate::probe::Probe::span("compute_scroll_ids");
2730
        LayoutWindow::compute_scroll_ids(&new_tree, &new_dom)
2730
    };
2730
    crate::probe::sample_peak_rss("rss:before_display_list");
2730
    crate::probe::reset_peak();
    // --- Step 4: Generate Display List & Update Cache ---
2730
    let display_list = if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
        // Web backend: positions are done; the painter is dead weight.
        DisplayList::default()
    } else {
2730
        let _p = crate::probe::Probe::span("generate_display_list");
2730
        generate_display_list(
2730
            &mut ctx,
2730
            &new_tree,
2730
            &calculated_positions,
2730
            scroll_offsets,
2730
            &scroll_ids,
2730
            gpu_value_cache,
2730
            renderer_resources,
2730
            id_namespace,
2730
            dom_id,
        )?
    };
2730
    crate::probe::sample_phase_peak("rss:peak_during_display_list");
    // Move cache_map back into LayoutCache before dropping ctx
2730
    let _p_writeback = crate::probe::Probe::span("cache_writeback");
2730
    let cache_map_back = std::mem::take(&mut ctx.cache_map);
    // Cache the freshly-generated display list keyed on the root's
    // subtree_hash + viewport. If the next `layout_document` call
    // sees matching values after reconcile, it returns this clone
    // directly and skips all downstream work.
2730
    let root_subtree_hash = new_tree
2730
        .cold(new_tree.root)
2730
        .map(|c| c.subtree_hash)
2730
        .unwrap_or(crate::solver3::layout_tree::SubtreeHash(0));
2730
    cache.cached_display_list = Some((root_subtree_hash, viewport, display_list.clone()));
2730
    cache.tree = Some(new_tree);
2730
    cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
2730
    cache.viewport = Some(viewport);
2730
    cache.scroll_ids = scroll_ids;
2730
    cache.scroll_id_to_node_id = scroll_id_to_node_id;
    // + calculated_positions.len in the low bits. If step stays 3, it diverged earlier.
2730
    { let _ = (0xDD00_0004u32 | ((cache.calculated_positions.len() as u32 & 0xfff) << 4)); }
2730
    cache.counters = counter_values;
2730
    cache.cache_map = cache_map_back;
2730
    crate::probe::sample_peak_rss("rss:after_layout_document");
2730
    Ok(display_list)
2765
}
// +spec:containing-block:159830 - Containing block chain: parent content-box for in-flow, viewport for initial containing block
// +spec:containing-block:22fbaa - computes the element's original containing block (before positioning effects)
// +spec:containing-block:238fc5 - containing block dimensions calculated here (CSS 2.2 §9.1.2 forward ref to §10)
// +spec:containing-block:263629 - block element's content-box establishes the containing block for its line boxes
// +spec:containing-block:2a5280 - boxes act as containing blocks for descendants; CB = parent's content box
// +spec:containing-block:6776cb - boxes positioned w.r.t. containing block but not confined; overflow allowed
// +spec:containing-block:718894 - CB derived from parent content-box edges; root uses initial CB (viewport)
// +spec:containing-block:a2aa37 - box edges act as containing block for descendants; initial containing block = viewport
// +spec:containing-block:e23b3f - CSS 2.2 §10.1: initial containing block = viewport; static/relative = parent content-box; fixed = viewport
// +spec:containing-block:e8fdb2 - Containing block resolution (CSS2 §9.1.2, §10.1)
// +spec:overflow:9a2b11 - containing block is content-box of parent; boxes may overflow it
// +spec:positioning:acc663 - containing block definition: element boxes positioned relative to containing block
2730
fn get_containing_block_for_node(
2730
    tree: &LayoutTree,
2730
    styled_dom: &StyledDom,
2730
    node_idx: usize,
2730
    calculated_positions: &PositionVec,
2730
    viewport: LogicalRect,
2730
) -> (LogicalPosition, LogicalSize) {
2730
    if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
        if let Some(parent_node) = tree.get(parent_idx) {
            let pos = calculated_positions
                .get(parent_idx)
                .copied()
                .unwrap_or_default();
            let size = parent_node.used_size.unwrap_or_default();
            // Position in calculated_positions is the margin-box position
            // To get content-box, add: border + padding (NOT margin, that's already in pos)
            let pbp = parent_node.box_props.unpack();
            let content_pos = LogicalPosition::new(
                pos.x + pbp.border.left + pbp.padding.left,
                pos.y + pbp.border.top + pbp.padding.top,
            );
            if let Some(dom_id) = parent_node.dom_node_id {
                let styled_node_state = &styled_dom
                    .styled_nodes
                    .as_container()
                    .get(dom_id)
                    .map(|n| &n.styled_node_state)
                    .cloned()
                    .unwrap_or_default();
                // +spec:containing-block:c205e5 - writing mode of containing block used for inner_size (orthogonal flow awareness)
                let writing_mode =
                    get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
                let content_size = pbp.inner_size(size, writing_mode);
                return (content_pos, content_size);
            }
            return (content_pos, size);
        }
2730
    }
    // +spec:containing-block:41bdfc - ICB equals viewport; overflow:hidden on root clips to ICB
    // +spec:containing-block:1eed60 - Initial containing block establishes a BFC; viewport is the ICB
    // +spec:containing-block:99866f - Containing block is a rectangle for sizing/positioning; ICB from viewport
    // +spec:containing-block:22f09b - viewport serves as initial containing block for root element
    // Root element's containing block is the initial containing block (CSS 2.2 §10.1, CSS Display 3 §2.8).
    // +spec:containing-block:2fd7b1 - ICB equals viewport; principal writing mode propagated to ICB
    // Root element's containing block is the initial containing block (CSS 2.2 §10.1, CSS Display 3 §2.8).
    // The principal writing mode is propagated to the ICB and viewport (css-writing-modes-4 §8.1).
    // +spec:containing-block:5efb84 - Root element's containing block is the initial containing block
    // +spec:containing-block:6278fb - initial containing block is the viewport; also serves as initial fixed containing block
    // Root element's containing block is the initial containing block (CSS 2.2 §10.1, CSS Display 3 §2.8).
    // For ROOT nodes: the containing block is the viewport (initial containing block).
    // Do NOT subtract margin here - margins are handled in calculate_used_size().
    // The margin creates space between viewport edge and element's border-box,
    // but the available space for calculating width/height percentages
    // is still the full viewport size.
2730
    (viewport.origin, viewport.size)
2730
}
#[derive(Debug)]
pub enum LayoutError {
    InvalidTree,
    SizingFailed,
    PositioningFailed,
    DisplayListFailed,
    Text(crate::font_traits::LayoutError),
}
impl std::fmt::Display for LayoutError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LayoutError::InvalidTree => write!(f, "Invalid layout tree"),
            LayoutError::SizingFailed => write!(f, "Sizing calculation failed"),
            LayoutError::PositioningFailed => write!(f, "Position calculation failed"),
            LayoutError::DisplayListFailed => write!(f, "Display list generation failed"),
            LayoutError::Text(e) => write!(f, "Text layout error: {:?}", e),
        }
    }
}
impl From<crate::font_traits::LayoutError> for LayoutError {
    fn from(err: crate::font_traits::LayoutError) -> Self {
        LayoutError::Text(err)
    }
}
impl std::error::Error for LayoutError {}
pub type Result<T> = std::result::Result<T, LayoutError>;