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
107580
pub fn pos_set(positions: &mut PositionVec, idx: usize, pos: LogicalPosition) {
162
107580
    if idx >= positions.len() {
163
86372
        positions.resize(idx + 1, POSITION_UNSET);
164
86372
    }
165
107580
    positions[idx] = pos;
166
107580
}
167

            
168
/// Check if position has been set for node index.
169
#[inline(always)]
170
55748
pub fn pos_contains(positions: &PositionVec, idx: usize) -> bool {
171
55748
    positions.get(idx).map_or(false, |p| p.x != f32::MIN)
172
55748
}
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
105967
    pub fn debug_log_inner(&mut self, message: String) {
270
105967
        if let Some(messages) = self.debug_messages.as_mut() {
271
104383
            messages.push(LayoutDebugMessage {
272
104383
                message: message.into(),
273
104383
                location: "solver3".into(),
274
104383
                message_type: Default::default(),
275
104383
            });
276
104383
        }
277
105967
    }
278

            
279
    /// Internal method - called by debug_info! macro after checking has_debug()
280
    #[inline]
281
1416415
    pub fn debug_info_inner(&mut self, message: String) {
282
1416415
        if let Some(messages) = self.debug_messages.as_mut() {
283
1416415
            messages.push(LayoutDebugMessage::info(message));
284
1416415
        }
285
1416415
    }
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
465756
    pub fn debug_ifc_layout_inner(&mut self, message: String) {
330
465756
        if let Some(messages) = self.debug_messages.as_mut() {
331
465756
            messages.push(LayoutDebugMessage::ifc_layout(message));
332
465756
        }
333
465756
    }
334

            
335
    /// Internal method - called by debug_table_layout! macro after checking has_debug()
336
    #[inline]
337
57816
    pub fn debug_table_layout_inner(&mut self, message: String) {
338
57816
        if let Some(messages) = self.debug_messages.as_mut() {
339
57816
            messages.push(LayoutDebugMessage::table_layout(message));
340
57816
        }
341
57816
    }
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
94908
    pub fn debug_log(&mut self, message: &str) {
367
94908
        self.debug_log_inner(message.to_string());
368
94908
    }
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
44
pub fn set_skip_display_list(skip: bool) {
425
44
    SKIP_DISPLAY_LIST.store(skip, core::sync::atomic::Ordering::Relaxed);
426
44
}
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
4708
pub fn layout_document<T: ParsedFontTrait + Sync + 'static>(
436
4708
    cache: &mut LayoutCache,
437
4708
    text_cache: &mut TextLayoutCache,
438
4708
    new_dom: &StyledDom,
439
4708
    viewport: LogicalRect,
440
4708
    font_manager: &crate::font_traits::FontManager<T>,
441
4708
    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
442
4708
    text_selections: &BTreeMap<DomId, TextSelection>,
443
4708
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
444
4708
    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
445
4708
    renderer_resources: &azul_core::resources::RendererResources,
446
4708
    id_namespace: azul_core::resources::IdNamespace,
447
4708
    dom_id: azul_core::dom::DomId,
448
4708
    cursor_is_visible: bool,
449
4708
    cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
450
4708
    preedit_text: Option<String>,
451
4708
    image_cache: &azul_core::resources::ImageCache,
452
4708
    system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
453
4708
    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
454
4708
) -> 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
4708
    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
4708
    { let _ = (0xDD00_0001u32); }
461
    // If 0 here → the LogicalRect HFA arg was lost across the lifted call.
462

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

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

            
495
4708
    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
4708
    let dom_ptr = new_dom as *const StyledDom as usize;
514
4708
    cache.prev_dom_ptr = dom_ptr;
515
4708
    cache.prev_viewport = viewport;
516

            
517
    // --- Step 1: Reconciliation & Invalidation ---
518
4708
    crate::probe::reset_peak();
519
4708
    let (new_tree_val, mut recon_result) =
520
4708
        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?;
521
    // [g56 FIX] Box the LayoutTree onto the HEAP. The lifted `&mut new_tree` passed to
522
    // calculate_intrinsic_sizes was mis-lifted (callee saw nodes.len()=0 while the caller saw 2)
523
    // because a stack/SROA'd `new_tree`'s address doesn't survive the cross-function lifted call
524
    // (taking `&new_tree` lifted to 0x0). A heap allocation has a stable absolute wasm address
525
    // that lifts reliably (cf. M8.4 "heap allocations work fine"). Deref coercion handles the
526
    // `&new_tree`/`&mut new_tree`/`new_tree.field` sites unchanged.
527
4708
    let mut new_tree = Box::new(new_tree_val);
528
4708
    { let _ = (0xDD00_0002u32); }
529
    // [az-diag g51 REVERT] 0x71 = reconcile_and_invalidate returned OK (no InvalidTree in reconcile).
530
4708
    unsafe { crate::az_mark((0x60704) as u32, (0x71u32) as u32); }
531
    // [az-diag g54 REVERT] 0x40740 = new_tree.nodes.len() RIGHT AFTER reconcile. If 0 → reconcile
532
    // built an empty LayoutTree (the bug is in reconcile_recursive/create_node_from_dom). If 2 →
533
    // reconcile is fine and the tree gets emptied/mis-lifted downstream (check 0x40744 at the loop).
534
4708
    unsafe { crate::az_mark((0x60740) as u32, (new_tree.nodes.len() as u32) as u32); }
535
4708
    crate::probe::sample_peak_rss("rss:after_reconcile");
536
4708
    crate::probe::sample_phase_peak("rss:peak_during_reconcile");
537

            
538
    // --- Step 1.1: Structural-identity display-list cache ---
539
    //
540
    // If the reconciled root subtree_hash matches the cached one AND
541
    // the viewport is unchanged, nothing structural has moved — skip
542
    // layout, positioning, AND display-list generation and return
543
    // the cached display list verbatim.
544
    //
545
    // This fires on re-renders of an unchanged DOM: the reconcile
546
    // pass still walks and hashes the tree, but that's ~600 µs vs
547
    // the ~4 ms it would otherwise cost to re-emit the display list.
548
4708
    if let Some((cached_hash, cached_viewport, cached_dl)) = &cache.cached_display_list {
549
176
        let new_root_hash = new_tree
550
176
            .cold(new_tree.root)
551
176
            .map(|c| c.subtree_hash);
552
176
        if new_root_hash == Some(*cached_hash) && *cached_viewport == viewport {
553
44
            let _p = crate::probe::Probe::span("display_list_cache_hit");
554
44
            return Ok(cached_dl.clone());
555
132
        }
556
4532
    }
557

            
558
    // Step 1.2: Clear Taffy Caches for Dirty Nodes
559
47124
    for &node_idx in &recon_result.intrinsic_dirty {
560
42460
        if let Some(warm) = new_tree.warm_mut(node_idx) {
561
42460
            warm.taffy_cache.clear();
562
42460
        }
563
    }
564

            
565
    // Step 1.3: Compute CSS Counters
566
    // This must be done after tree generation but before layout,
567
    // as list markers need counter values during formatting context layout
568
4664
    {
569
4664
        let _p = crate::probe::Probe::span("compute_counters");
570
4664
        cache::compute_counters(&new_dom, &new_tree, &mut counter_values);
571
4664
    }
572
    // [az-diag g51 REVERT] 0x72 = compute_counters done (InvalidTree, if any, is after here).
573
4664
    unsafe { crate::az_mark((0x60704) as u32, (0x72u32) as u32); }
574

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

            
596
        // Primary mapping: DOM id → layout idx on both sides. This
597
        // covers every node that has a corresponding DOM node.
598
2332
        for (dom_id, new_indices) in new_tree.dom_to_layout.iter() {
599
2332
            let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
600
                continue;
601
            };
602
2332
            for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
603
2332
                let Some(&old_layout_idx) = old_indices.get(pair_idx) else {
604
                    continue;
605
                };
606
2332
                if old_layout_idx >= cache_map.entries.len()
607
2332
                    || new_layout_idx >= remapped.entries.len()
608
                {
609
                    continue;
610
2332
                }
611
2332
                remapped.entries[new_layout_idx] =
612
2332
                    core::mem::take(&mut cache_map.entries[old_layout_idx]);
613
            }
614
        }
615

            
616
        // Secondary mapping: anonymous wrappers (dom_node_id == None)
617
        // by (parent_new_idx, ordinal-among-anon-siblings). An
618
        // unchanged DOM produces the same anon wrappers in the same
619
        // order under the same parent — matching by position here
620
        // preserves their cache slots too. Without this, anon
621
        // wrappers re-allocate empty every reconcile and invalidate
622
        // their ancestors via `mark_dirty`.
623
264
        fn collect_anon_children_by_parent(
624
264
            tree: &LayoutTree,
625
264
        ) -> std::collections::HashMap<usize, Vec<usize>> {
626
264
            let mut map: std::collections::HashMap<usize, Vec<usize>> =
627
264
                std::collections::HashMap::new();
628
4664
            for (idx, node) in tree.nodes.iter().enumerate() {
629
4664
                if node.dom_node_id.is_some() {
630
4664
                    continue;
631
                }
632
                if let Some(parent) = node.parent {
633
                    map.entry(parent).or_default().push(idx);
634
                }
635
            }
636
264
            map
637
264
        }
638

            
639
        // Build old-parent → [old_anon_indices] and
640
        // new-parent → [new_anon_indices]; match by pair position.
641
132
        let old_anon_by_parent = collect_anon_children_by_parent(old_tree);
642
132
        let new_anon_by_parent = collect_anon_children_by_parent(&new_tree);
643

            
644
        // For each new parent we know: look up its old twin by the
645
        // dom-id mapping we just populated, then match anon children
646
        // positionally within that parent.
647
        // Build a new→old layout-idx lookup from the primary pass.
648
132
        let mut new_to_old_layout_idx: std::collections::HashMap<usize, usize> =
649
132
            std::collections::HashMap::new();
650
2332
        for (dom_id, new_indices) in new_tree.dom_to_layout.iter() {
651
2332
            let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
652
                continue;
653
            };
654
2332
            for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
655
2332
                if let Some(&old_layout_idx) = old_indices.get(pair_idx) {
656
2332
                    new_to_old_layout_idx.insert(new_layout_idx, old_layout_idx);
657
2332
                }
658
            }
659
        }
660

            
661
132
        for (new_parent_idx, new_anon_children) in new_anon_by_parent {
662
            let Some(&old_parent_idx) = new_to_old_layout_idx.get(&new_parent_idx) else {
663
                continue;
664
            };
665
            let Some(old_anon_children) = old_anon_by_parent.get(&old_parent_idx) else {
666
                continue;
667
            };
668
            for (ord, &new_anon_idx) in new_anon_children.iter().enumerate() {
669
                let Some(&old_anon_idx) = old_anon_children.get(ord) else {
670
                    continue;
671
                };
672
                if old_anon_idx >= cache_map.entries.len()
673
                    || new_anon_idx >= remapped.entries.len()
674
                {
675
                    continue;
676
                }
677
                remapped.entries[new_anon_idx] =
678
                    core::mem::take(&mut cache_map.entries[old_anon_idx]);
679
            }
680
        }
681

            
682
132
        cache_map = remapped;
683
4532
    } else {
684
4532
        cache_map.resize_to_tree(new_tree.nodes.len());
685
4532
    }
686
4664
    drop(_probe_cache_remap);
687
4664
    crate::probe::sample_peak_rss("rss:after_cache_remap");
688
47124
    for &node_idx in &recon_result.intrinsic_dirty {
689
42460
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
690
42460
    }
691
9328
    for &node_idx in &recon_result.layout_roots {
692
4664
        cache_map.mark_dirty(node_idx, &new_tree.nodes);
693
4664
    }
694

            
695
    // Now create the real context with computed counters
696
4664
    let mut ctx = LayoutContext {
697
4664
        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
698
4664
        styled_dom: &new_dom,
699
4664
        font_manager,
700
4664
        text_selections,
701
4664
        debug_messages,
702
4664
        counters: &mut counter_values,
703
4664
        viewport_size: viewport.size,
704
4664
        fragmentation_context: None,
705
4664
        cursor_is_visible,
706
4664
        cursor_locations,
707
4664
        preedit_text,
708
4664
        dirty_text_overrides: BTreeMap::new(),
709
4664
        cache_map, // Moved from LayoutCache; will be moved back after layout
710
4664
        image_cache,
711
4664
        system_style,
712
4664
        get_system_time_fn,
713
4664
    };
714

            
715
    // --- Step 1.5: Early Exit Optimization ---
716
    // M12.7: `&& cache.tree.is_some()` — this "nothing changed, reuse cached
717
    // layout" fast path REQUIRES a cached tree; on COLD layout cache.tree is
718
    // None, so entering here would hit `ok_or(InvalidTree)`. recon_result must
719
    // be dirty on cold (the viewport-resize dirties the root), but if
720
    // is_clean() mis-evaluates we'd wrongly early-exit → InvalidTree. Guarding
721
    // on a cached tree is both correct (can't reuse what isn't there) and
722
    // robust. (rc=5 post-reconcile, step=2: this was the failing `?`.)
723
4664
    if recon_result.is_clean() && cache.tree.is_some() {
724
        debug_log!(ctx, "No changes, returning existing display list");
725
        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
726

            
727
        // Use cached scroll IDs if available, otherwise compute them
728
        let scroll_ids = if cache.scroll_ids.is_empty() {
729
            use crate::window::LayoutWindow;
730
            let (scroll_ids, scroll_id_to_node_id) =
731
                LayoutWindow::compute_scroll_ids(tree, &new_dom);
732
            cache.scroll_ids = scroll_ids.clone();
733
            cache.scroll_id_to_node_id = scroll_id_to_node_id;
734
            scroll_ids
735
        } else {
736
            cache.scroll_ids.clone()
737
        };
738

            
739
        if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
740
            return Ok(DisplayList::default());
741
        }
742
        return generate_display_list(
743
            &mut ctx,
744
            tree,
745
            &cache.calculated_positions,
746
            scroll_offsets,
747
            &scroll_ids,
748
            gpu_value_cache,
749
            renderer_resources,
750
            id_namespace,
751
            dom_id,
752
        );
753
4664
    }
754

            
755
4664
    { let _ = (0xDD00_0003u32); }
756
    // [az-diag g51 REVERT] 0x80 = reached the incremental layout loop (past early-exit + remap + dirty loops).
757
4664
    unsafe { crate::az_mark((0x60704) as u32, (0x80u32) as u32); }
758
    // [az-diag g65 PATH-B VALIDATION] new_tree is still valid here (=2). Clone it into the HEAP-backed
759
    // cache.tree (set AFTER the remap+early-exit which read the OLD cache.tree). cache is the stable
760
    // &mut arg (read correctly throughout), so cache.tree is NOT a deep-SP-relative stack local. At the
761
    // sizing call we read BOTH: stack new_tree (expect 0=corrupted) vs heap cache.tree (expect 2 if
762
    // path B sidesteps the SP-drift/wild-store). If heap=2, the full cache.tree refactor will fix it.
763
4664
    cache.tree = Some((*new_tree).clone());
764
    // [az-diag g66] disambiguate the g65 heap=1: read BOTH right after the clone. 0x407C0 = stack
765
    // new_tree.nodes.len() (source), 0x407C4 = clone cache.tree.nodes.len(). If src=2 & clone=1 →
766
    // Vec::clone MIS-LIFTS (drops a node) → the full MOVE-based cache.tree refactor avoids it (do it).
767
    // If src=1=clone → corruption already reached line 758 (heisenbug) → move won't help.
768
    unsafe {
769
4664
        crate::az_mark((0x607C0) as u32, (new_tree.nodes.len() as u32) as u32);
770
4664
        crate::az_mark((0x607C4) as u32, (cache.tree.as_ref().map(|t| t.nodes.len()).unwrap_or(999) as u32) as u32);
771
    }
772

            
773
    // --- Step 2: Incremental Layout Loop (handles scrollbar-induced reflows) ---
774
4664
    let mut calculated_positions = cache.calculated_positions.clone();
775
4664
    let mut loop_count = 0;
776
    loop {
777
4664
        loop_count += 1;
778
4664
        if loop_count > MAX_SCROLLBAR_REFLOW_ITERATIONS {
779
            debug_warning!(ctx, "Scrollbar reflow loop hit limit of {} iterations, breaking to avoid infinite loop", MAX_SCROLLBAR_REFLOW_ITERATIONS);
780
            break;
781
4664
        }
782

            
783
4664
        calculated_positions = {
784
4664
            let _p = crate::probe::Probe::span("clone_calculated_positions");
785
4664
            cache.calculated_positions.clone()
786
        };
787
        // [az-diag g70 RELIABLE free-band] 0x60780 = nodes.len AFTER the in-loop calculated_positions.clone().
788
4664
        unsafe { crate::az_mark((0x60780) as u32, (new_tree.nodes.len() as u32) as u32); }
789
4664
        let mut reflow_needed_for_scrollbars = false;
790

            
791
        {
792
4664
            crate::probe::reset_peak();
793
            // [az-diag g70 RELIABLE free-band] 0x60784 = nodes.len AFTER reset_peak (before the calc Span).
794
4664
            unsafe { crate::az_mark((0x60784) as u32, (new_tree.nodes.len() as u32) as u32); }
795
4664
            let _p = crate::probe::Probe::span("calc_intrinsic_sizes");
796
            // [az-diag g70 RELIABLE free-band] 0x60788 = nodes.len AFTER the calc_intrinsic_sizes Span.
797
4664
            unsafe { crate::az_mark((0x60788) as u32, (new_tree.nodes.len() as u32) as u32); }
798
            // [az-diag g72 FIX] REMOVED the g48 `#[cfg(feature="web_lift")] panic!(...)` that lived
799
            // here. web-transpiler => azul-layout?/web_lift IS enabled (dll/Cargo.toml:651), so this
800
            // panic WAS compiled in, and with `-Z build-std-features=panic_immediate_abort` it lowered
801
            // to a bare `brk #0x1` right after the 0x90 marker — aborting BEFORE calculate_intrinsic_sizes.
802
            // The whole-session "new_tree 2→0 corruption" was a MIRAGE: the beforeCall marker store was
803
            // dead-code-eliminated (after the abort), so the harness read uninitialized 0, not a corrupted
804
            // tree. Native disasm of layout_document proved it: 0x90 marker store → `brk #0x1` → no `bl
805
            // calculate_intrinsic_sizes` anywhere. (The prior "string absent ⇒ web_lift off" check was
806
            // wrong — panic_immediate_abort strips the message string.)
807
            // [az-diag g65 PATH-B VALIDATION] 0x40748 = stack new_tree.nodes.len() (expect 0),
808
            // 0x4074C = HEAP cache.tree.nodes.len() (expect 2 if path B sidesteps the corruption).
809
            unsafe {
810
4664
                crate::az_mark((0x60748) as u32, (new_tree.nodes.len() as u32) as u32);
811
4664
                crate::az_mark((0x6074C) as u32, (cache.tree.as_ref().map(|t| t.nodes.len()).unwrap_or(999) as u32) as u32);
812
            }
813
4664
            calculate_intrinsic_sizes(
814
4664
                &mut ctx,
815
4664
                &mut new_tree,
816
4664
                text_cache,
817
4664
                &recon_result.intrinsic_dirty,
818
            )?;
819
        }
820
4664
        crate::probe::sample_peak_rss("rss:after_calc_intrinsic");
821
4664
        crate::probe::sample_phase_peak("rss:peak_during_intrinsic");
822
        // divergence is inside calculate_intrinsic_sizes (the SIMD/text intrinsic pass).
823
4664
        { let _ = (0xDD00_0005u32); }
824

            
825
9328
        for &root_idx in &recon_result.layout_roots {
826
4664
            let (cb_pos, cb_size) = get_containing_block_for_node(
827
4664
                &new_tree,
828
4664
                &new_dom,
829
4664
                root_idx,
830
4664
                &calculated_positions,
831
4664
                viewport,
832
4664
            );
833
            // 0x05, the divergence is INSIDE get_containing_block_for_node (or the for-loop
834
            // entry); if 0x53 but not 0x55, it's the margin logic / box_props.unpack below.
835
4664
            { let _ = (0xDD00_0053u32); }
836
            // get_containing_block_for_node(viewport)). 800 here but viewport=800 ⟹ OK;
837
            // 0 here with viewport=800 ⟹ get_containing_block_for_node lost it (HFA return).
838

            
839
            // For ROOT nodes (no parent), we need to account for their margin.
840
            // The containing block position from viewport is (0, 0), but the root's
841
            // content starts at (margin + border + padding, margin + border + padding).
842
            // We pass margin-adjusted position so calculate_content_box_pos works correctly.
843
4664
            let root_node = &new_tree.nodes[root_idx];
844
4664
            let root_bp = root_node.box_props.unpack();
845
4664
            { let _ = (0xDD00_0054u32); }
846

            
847
4664
            let is_root_with_margin = root_node.parent.is_none()
848
4664
                && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
849

            
850
4664
            let adjusted_cb_pos = if is_root_with_margin {
851
616
                LogicalPosition::new(
852
616
                    cb_pos.x + root_bp.margin.left,
853
616
                    cb_pos.y + root_bp.margin.top,
854
                )
855
            } else {
856
4048
                cb_pos
857
            };
858
4664
            { let _ = (0xDD00_0056u32); }
859

            
860
            // DEBUG: Log containing block info for this root
861
4664
            if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
862
3872
                let dom_name = root_node
863
3872
                    .dom_node_id
864
3872
                    .and_then(|id| new_dom.node_data.as_container().internal.get(id.index()))
865
3872
                    .map(|n| format!("{:?}", n.node_type))
866
3872
                    .unwrap_or_else(|| "Unknown".to_string());
867

            
868
3872
                debug_msgs.push(LayoutDebugMessage::new(
869
3872
                    LayoutDebugMessageType::PositionCalculation,
870
3872
                    format!(
871
3872
                        "[LAYOUT ROOT {}] {} - CB pos=({:.2}, {:.2}), adjusted=({:.2}, {:.2}), \
872
3872
                         CB size=({:.2}x{:.2}), viewport=({:.2}x{:.2}), margin=({:.2}, {:.2})",
873
                        root_idx,
874
                        dom_name,
875
                        cb_pos.x,
876
                        cb_pos.y,
877
                        adjusted_cb_pos.x,
878
                        adjusted_cb_pos.y,
879
                        cb_size.width,
880
                        cb_size.height,
881
                        viewport.size.width,
882
                        viewport.size.height,
883
                        root_bp.margin.left,
884
                        root_bp.margin.top
885
                    ),
886
                ));
887
792
            }
888

            
889
            // Purge after intrinsic sizing — frees child_intrinsics Vecs,
890
            // IntrinsicSizeCalculator temporaries, text measurement caches.
891
4664
            crate::probe::hint_purge_allocator();
892
4664
            crate::probe::sample_peak_rss("rss:before_root_layout");
893
4664
            crate::probe::reset_peak();
894
            // 0x57 = it RETURNED. If step stays 0x55, calculate_layout_for_subtree diverges.
895
4664
            { let _ = (0xDD00_0055u32); }
896
            // This is exactly what calc_used_size reads as `viewport`. 0 here pinpoints the
897
            // loss to the ctx build (viewport.size → ctx.viewport_size copy).
898
            // 0x5E = Err. Do NOT propagate (continue to the cache store) so layout-real can
899
            // see whether the geometry was computed regardless of a (possibly spurious,
900
            // niche-Result-mis-discriminated) Err.
901
4664
            let _clr = {
902
4664
                let _p = crate::probe::Probe::span("root_layout_pass");
903
4664
                cache::calculate_layout_for_subtree(
904
4664
                    &mut ctx,
905
4664
                    &mut new_tree,
906
4664
                    text_cache,
907
4664
                    root_idx,
908
4664
                    adjusted_cb_pos,
909
4664
                    cb_size,
910
4664
                    &mut calculated_positions,
911
4664
                    &mut reflow_needed_for_scrollbars,
912
4664
                    &mut cache.float_cache,
913
4664
                    cache::ComputeMode::PerformLayout,
914
                )
915
            };
916
4664
            { let _ = (if _clr.is_ok() { 0xDD00_0057u32 } else { 0xDD00_005Eu32 }); }
917
4664
            crate::probe::sample_peak_rss("rss:after_root_layout");
918
4664
            crate::probe::sample_phase_peak("rss:peak_during_root_layout");
919

            
920
            // CRITICAL: Insert the root node's own position into calculated_positions
921
            // This is necessary because calculate_layout_for_subtree only inserts
922
            // positions for children, not for the root itself.
923
            //
924
            // For root nodes, the position should be at (margin.left, margin.top) relative
925
            // to the viewport origin, because the margin creates space between the viewport
926
            // edge and the element's border-box.
927
4664
            if !pos_contains(&calculated_positions, root_idx) {
928
4532
                let root_node = &new_tree.nodes[root_idx];
929
4532
                let root_bp2 = root_node.box_props.unpack();
930

            
931
                // Calculate the root's border-box position by adding margins to viewport origin
932
                // This is different from non-root nodes which inherit their position from
933
                // their containing block.
934
4532
                let root_position = LogicalPosition::new(
935
4532
                    cb_pos.x + root_bp2.margin.left,
936
4532
                    cb_pos.y + root_bp2.margin.top,
937
                );
938

            
939
                // DEBUG: Log root positioning
940
4532
                if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
941
3740
                    let dom_name = root_node
942
3740
                        .dom_node_id
943
3740
                        .and_then(|id| new_dom.node_data.as_container().internal.get(id.index()))
944
3740
                        .map(|n| format!("{:?}", n.node_type))
945
3740
                        .unwrap_or_else(|| "Unknown".to_string());
946

            
947
3740
                    debug_msgs.push(LayoutDebugMessage::new(
948
3740
                        LayoutDebugMessageType::PositionCalculation,
949
3740
                        format!(
950
3740
                            "[ROOT POSITION {}] {} - Inserting position=({:.2}, {:.2}) (viewport origin + margin), \
951
3740
                             margin=({:.2}, {:.2}, {:.2}, {:.2})",
952
                            root_idx,
953
                            dom_name,
954
                            root_position.x,
955
                            root_position.y,
956
                            root_bp2.margin.top,
957
                            root_bp2.margin.right,
958
                            root_bp2.margin.bottom,
959
                            root_bp2.margin.left
960
                        ),
961
                    ));
962
792
                }
963

            
964
4532
                pos_set(&mut calculated_positions, root_idx, root_position);
965
132
            }
966
        }
967
        // (step 6). If step stays 5, the divergence is in calculate_layout_for_subtree.
968
4664
        { let _ = (0xDD00_0006u32); }
969

            
970
4664
        {
971
4664
            let _p = crate::probe::Probe::span("reposition_clean_subtrees");
972
4664
            cache::reposition_clean_subtrees(
973
4664
                &new_dom,
974
4664
                &new_tree,
975
4664
                &recon_result.layout_roots,
976
4664
                &mut calculated_positions,
977
4664
            );
978
4664
        }
979

            
980
4664
        if reflow_needed_for_scrollbars {
981
            debug_log!(ctx,
982
                "Scrollbars changed container size, starting full reflow (loop {})",
983
                loop_count
984
            );
985
            recon_result.layout_roots.clear();
986
            recon_result.layout_roots.insert(new_tree.root);
987
            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
988
            continue;
989
4664
        }
990

            
991
4664
        break;
992
    }
993

            
994
    // +spec:positioning:8d1286 - normal flow, relative, float, absolute positioning dispatch
995
    // +spec:positioning:bdfc81 - Layout divided into sizing (Step 2) then positioning (Step 3)
996
    // --- Step 3: Adjust Relatively Positioned Elements ---
997
    // +spec:positioning:a831e8 - inline content width uses pre-relative-offset positions (satisfied by post-layout relative adjustment)
998
    // +spec:positioning:e2647b - Relative positioning applied after line height calculation, so line height is not adjusted for relative offsets
999
    // +spec:positioning:77a2d2 - Relatively positioned boxes considered without their offset during auto height
    // +spec:positioning:b47ac2 - Relatively positioned boxes considered without their offset for block auto height
    // Relative offsets applied AFTER layout, so auto-height calculation sees normal-flow positions.
    // This must be done BEFORE positioning out-of-flow elements, because
    // relatively positioned elements establish containing blocks for their
    // absolutely positioned descendants. If we adjust relative positions after
    // positioning absolute elements, the absolute elements will be positioned
    // relative to the wrong (pre-adjustment) position of their containing block.
    // Pass the viewport to correctly resolve percentage offsets for the root element.
4664
    {
4664
        let _p = crate::probe::Probe::span("adjust_relative_positions");
4664
        positioning::adjust_relative_positions(
4664
            &mut ctx,
4664
            &new_tree,
4664
            &mut calculated_positions,
4664
            viewport,
4664
        );
4664
    }
    // --- Step 3.25: Adjust Sticky Positioned Elements ---
    // Sticky elements are laid out in normal flow, then their visual position
    // is clamped based on scroll offset and inset properties relative to the
    // nearest scrollport. Must happen after relative positioning but before
    // absolute positioning (sticky elements establish containing blocks).
4664
    {
4664
        let _p = crate::probe::Probe::span("adjust_sticky_positions");
4664
        positioning::adjust_sticky_positions(
4664
            &mut ctx,
4664
            &new_tree,
4664
            &mut calculated_positions,
4664
            scroll_offsets,
4664
            viewport,
4664
        );
4664
    }
    // --- Step 3.5: Position Out-of-Flow Elements ---
    // This must be done AFTER adjusting relative positions, so that absolutely
    // positioned elements are positioned relative to the final (post-adjustment)
    // position of their relatively positioned containing blocks.
4664
    {
4664
        let _p = crate::probe::Probe::span("position_out_of_flow");
4664
        positioning::position_out_of_flow_elements(
4664
            &mut ctx,
4664
            &mut new_tree,
4664
            text_cache,
4664
            &mut calculated_positions,
4664
            viewport,
4664
        );
4664
    }
    // --- Step 3.75: Compute Stable Scroll IDs ---
    // This must be done AFTER layout but BEFORE display list generation
    use crate::window::LayoutWindow;
4664
    let (scroll_ids, scroll_id_to_node_id) = {
4664
        let _p = crate::probe::Probe::span("compute_scroll_ids");
4664
        LayoutWindow::compute_scroll_ids(&new_tree, &new_dom)
4664
    };
4664
    crate::probe::sample_peak_rss("rss:before_display_list");
4664
    crate::probe::reset_peak();
    // --- Step 4: Generate Display List & Update Cache ---
4664
    let display_list = if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
        // Web backend: positions are done; the painter is dead weight.
44
        DisplayList::default()
    } else {
4620
        let _p = crate::probe::Probe::span("generate_display_list");
4620
        generate_display_list(
4620
            &mut ctx,
4620
            &new_tree,
4620
            &calculated_positions,
4620
            scroll_offsets,
4620
            &scroll_ids,
4620
            gpu_value_cache,
4620
            renderer_resources,
4620
            id_namespace,
4620
            dom_id,
        )?
    };
4664
    crate::probe::sample_phase_peak("rss:peak_during_display_list");
    // Move cache_map back into LayoutCache before dropping ctx
4664
    let _p_writeback = crate::probe::Probe::span("cache_writeback");
4664
    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.
4664
    let root_subtree_hash = new_tree
4664
        .cold(new_tree.root)
4664
        .map(|c| c.subtree_hash)
4664
        .unwrap_or(crate::solver3::layout_tree::SubtreeHash(0));
4664
    cache.cached_display_list = Some((root_subtree_hash, viewport, display_list.clone()));
4664
    cache.tree = Some(*new_tree); // [g56] unbox the heap LayoutTree back into the cache
4664
    cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
4664
    cache.viewport = Some(viewport);
4664
    cache.scroll_ids = scroll_ids;
4664
    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.
4664
    { let _ = (0xDD00_0004u32 | ((cache.calculated_positions.len() as u32 & 0xfff) << 4)); }
4664
    cache.counters = counter_values;
4664
    cache.cache_map = cache_map_back;
4664
    crate::probe::sample_peak_rss("rss:after_layout_document");
4664
    Ok(display_list)
4708
}
// +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
4664
fn get_containing_block_for_node(
4664
    tree: &LayoutTree,
4664
    styled_dom: &StyledDom,
4664
    node_idx: usize,
4664
    calculated_positions: &PositionVec,
4664
    viewport: LogicalRect,
4664
) -> (LogicalPosition, LogicalSize) {
4664
    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);
        }
4664
    }
    // +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.
4664
    (viewport.origin, viewport.size)
4664
}
// [g119 az-web-lift FIX] `#[repr(C, u8)]` (was repr(Rust)): the `Text(font_traits::LayoutError)`
// variant's String/FontSelector pointer gives `Result<T, LayoutError>` a POINTER-niche disc, which
// the web lift MIS-READS → every solver3 `?`/Result return flips Ok→Err (heisenbug; g118 = collect's
// Result<(),LayoutError> arrived as Err → rc=5 InvalidTree though the out-param content was correct).
// An explicit u8 tag (0..=4) moves the Result niche to unused tag values (5..) = a simple u8 compare
// the lift handles. Same disc-mis-lift class as InlineContent/LogicalItem/ShapedItem (g117/g118).
#[derive(Debug)]
#[repr(C, u8)]
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>;