azul_layout/solver3/mod.rs
1//! solver3/mod.rs
2//!
3//! Next-generation CSS layout engine with proper formatting context separation
4
5pub mod cache;
6pub mod calc;
7pub mod counters;
8pub mod display_list;
9pub mod fc;
10pub mod geometry;
11pub mod getters;
12pub mod layout_tree;
13pub mod paged_layout;
14pub mod pagination;
15pub mod positioning;
16pub mod scrollbar;
17pub mod sizing;
18pub mod taffy_bridge;
19
20/// Lazy `debug_info` macro - only evaluates format args when `debug_messages` is Some
21#[macro_export]
22macro_rules! debug_info {
23 ($ctx:expr, $($arg:tt)*) => {
24 if $ctx.debug_messages.is_some() {
25 $ctx.debug_info_inner(format!($($arg)*));
26 }
27 };
28}
29
30/// Lazy `debug_warning` macro - only evaluates format args when `debug_messages` is Some
31#[macro_export]
32macro_rules! debug_warning {
33 ($ctx:expr, $($arg:tt)*) => {
34 if $ctx.debug_messages.is_some() {
35 $ctx.debug_warning_inner(format!($($arg)*));
36 }
37 };
38}
39
40/// Lazy `debug_error` macro - only evaluates format args when `debug_messages` is Some
41#[macro_export]
42macro_rules! debug_error {
43 ($ctx:expr, $($arg:tt)*) => {
44 if $ctx.debug_messages.is_some() {
45 $ctx.debug_error_inner(format!($($arg)*));
46 }
47 };
48}
49
50/// Lazy `debug_log` macro - only evaluates format args when `debug_messages` is Some
51#[macro_export]
52macro_rules! debug_log {
53 ($ctx:expr, $($arg:tt)*) => {
54 if $ctx.debug_messages.is_some() {
55 $ctx.debug_log_inner(format!($($arg)*));
56 }
57 };
58}
59
60/// Lazy `debug_box_props` macro - only evaluates format args when `debug_messages` is Some
61#[macro_export]
62macro_rules! debug_box_props {
63 ($ctx:expr, $($arg:tt)*) => {
64 if $ctx.debug_messages.is_some() {
65 $ctx.debug_box_props_inner(format!($($arg)*));
66 }
67 };
68}
69
70/// Lazy `debug_css_getter` macro - only evaluates format args when `debug_messages` is Some
71#[macro_export]
72macro_rules! debug_css_getter {
73 ($ctx:expr, $($arg:tt)*) => {
74 if $ctx.debug_messages.is_some() {
75 $ctx.debug_css_getter_inner(format!($($arg)*));
76 }
77 };
78}
79
80/// Lazy `debug_bfc_layout` macro - only evaluates format args when `debug_messages` is Some
81#[macro_export]
82macro_rules! debug_bfc_layout {
83 ($ctx:expr, $($arg:tt)*) => {
84 if $ctx.debug_messages.is_some() {
85 $ctx.debug_bfc_layout_inner(format!($($arg)*));
86 }
87 };
88}
89
90/// Lazy `debug_ifc_layout` macro - only evaluates format args when `debug_messages` is Some
91#[macro_export]
92macro_rules! debug_ifc_layout {
93 ($ctx:expr, $($arg:tt)*) => {
94 if $ctx.debug_messages.is_some() {
95 $ctx.debug_ifc_layout_inner(format!($($arg)*));
96 }
97 };
98}
99
100/// Lazy `debug_table_layout` macro - only evaluates format args when `debug_messages` is Some
101#[macro_export]
102macro_rules! debug_table_layout {
103 ($ctx:expr, $($arg:tt)*) => {
104 if $ctx.debug_messages.is_some() {
105 $ctx.debug_table_layout_inner(format!($($arg)*));
106 }
107 };
108}
109
110/// Lazy `debug_display_type` macro - only evaluates format args when `debug_messages` is Some
111#[macro_export]
112macro_rules! debug_display_type {
113 ($ctx:expr, $($arg:tt)*) => {
114 if $ctx.debug_messages.is_some() {
115 $ctx.debug_display_type_inner(format!($($arg)*));
116 }
117 };
118}
119
120use std::{collections::{BTreeMap, HashMap}, sync::Arc};
121
122use 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`.
132pub(crate) 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.
137const 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.
141pub type PositionVec = Vec<LogicalPosition>;
142
143/// Get position for node index, returning None if unset.
144///
145/// Note: only the `x` component is checked against the sentinel. This is sufficient
146/// because `POSITION_UNSET` always sets both `x` and `y` to `f32::MIN`, and `pos_set`
147/// always writes both components together.
148#[inline]
149#[must_use] pub fn pos_get(positions: &PositionVec, idx: usize) -> Option<LogicalPosition> {
150 positions.get(idx).copied().filter(|p| p.x != f32::MIN)
151}
152
153/// Set position for node index. Grows the vec if needed.
154#[inline]
155pub fn pos_set(positions: &mut PositionVec, idx: usize, pos: LogicalPosition) {
156 if idx >= positions.len() {
157 positions.resize(idx + 1, POSITION_UNSET);
158 }
159 positions[idx] = pos;
160}
161
162/// Check if position has been set for node index.
163#[inline]
164#[must_use] pub fn pos_contains(positions: &PositionVec, idx: usize) -> bool {
165 positions.get(idx).is_some_and(|p| p.x != f32::MIN)
166}
167use azul_css::{
168 props::property::{CssProperty, CssPropertyCategory},
169 LayoutDebugMessage, LayoutDebugMessageType,
170};
171
172use self::{
173 display_list::generate_display_list,
174 geometry::IntrinsicSizes,
175 getters::get_writing_mode,
176 layout_tree::{generate_layout_tree, LayoutTree},
177 sizing::calculate_intrinsic_sizes,
178};
179#[cfg(feature = "text_layout")]
180pub use crate::font_traits::TextLayoutCache;
181use crate::{
182 font_traits::ParsedFontTrait,
183 solver3::{
184 cache::LayoutCache,
185 display_list::DisplayList,
186 fc::LayoutConstraints,
187 layout_tree::DirtyFlag,
188 },
189};
190
191/// Central context for a single layout pass.
192#[derive(Debug)]
193pub struct LayoutContext<'a, T: ParsedFontTrait> {
194 pub styled_dom: &'a StyledDom,
195 #[cfg(feature = "text_layout")]
196 pub font_manager: &'a crate::font_traits::FontManager<T>,
197 #[cfg(not(feature = "text_layout"))]
198 pub font_manager: core::marker::PhantomData<&'a T>,
199 /// Text selections for rendering highlights. Populated from `MultiCursorState`.
200 pub text_selections: &'a BTreeMap<DomId, TextSelection>,
201 pub debug_messages: &'a mut Option<Vec<LayoutDebugMessage>>,
202 pub counters: &'a mut HashMap<(usize, String), i32>,
203 pub viewport_size: LogicalSize,
204 /// Fragmentation context for CSS Paged Media (PDF generation)
205 /// When Some, layout respects page boundaries and generates one `DisplayList` per page
206 pub fragmentation_context: Option<&'a mut crate::paged::FragmentationContext>,
207 /// Whether the text cursor should be drawn (managed by `CursorManager` blink timer)
208 /// When false, the cursor is in the "off" phase of blinking and should not be rendered.
209 /// When true (default), the cursor is visible.
210 pub cursor_is_visible: bool,
211 /// All active cursor locations from `MultiCursorState` / `CursorManager`.
212 /// Each entry is (`dom_id`, `node_id`, cursor). Multiple entries = multi-cursor mode.
213 /// Empty = no active cursor. The last entry is the primary cursor.
214 pub cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
215 /// IME preedit (composition) text to render inline at the cursor position.
216 /// When Some, the text should be rendered with an underline decoration.
217 pub preedit_text: Option<String>,
218 /// Text content overrides from in-progress edits (`dirty_text_nodes`).
219 /// When a text node has been edited but not yet committed to the DOM,
220 /// the layout pipeline should read from here instead of `StyledDom`.
221 /// Key: (`DomId`, `NodeId` of the text node), Value: the edited text string.
222 pub dirty_text_overrides: BTreeMap<(DomId, NodeId), String>,
223 /// Per-node multi-slot cache (Taffy-inspired 9+1 architecture).
224 /// Moved out of `LayoutCache` via `std::mem::take` for the duration of layout,
225 /// then moved back after the layout pass completes.
226 pub cache_map: cache::LayoutCacheMap,
227 /// Image cache for resolving `background-image: url(...)` references.
228 pub image_cache: &'a azul_core::resources::ImageCache,
229 /// System style containing colors, fonts, metrics, and theme information.
230 /// Used for selection colors, caret styling, and other system-themed elements.
231 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
232 /// Callback to get the current system time. Used for profiling inside layout.
233 /// On WASM targets (where `std::time::Instant::now()` panics), callers should
234 /// supply a no-op or platform-specific implementation.
235 pub get_system_time_fn: azul_core::task::GetSystemTimeCallback,
236 /// Memoised `get_scrollbar_style` results, keyed by DOM node id.
237 /// `compute_scrollbar_info_core` is called many times per node
238 /// per layout pass (BFC path + Taffy flex/grid path + display
239 /// list), and each call previously did 9 cascade walks. Once
240 /// populated, subsequent callers in the same `LayoutContext`
241 /// (a single render) return a clone.
242 ///
243 /// Uses `RefCell` so shared `&self` borrows (e.g. in the Taffy
244 /// bridge's `get_core_container_style`) can mutate the cache
245 /// without lifting the ctx to `&mut`. Keyed by `NodeId` so
246 /// entries span DOMs in iframe-style nested documents if that
247 /// ever becomes a thing.
248 pub scrollbar_style_cache:
249 core::cell::RefCell<HashMap<NodeId, getters::ComputedScrollbarStyle>>,
250}
251
252impl<T: ParsedFontTrait> LayoutContext<'_, T> {
253 /// Internal method - called by `debug_log`! macro after checking `debug_messages.is_some()`
254 #[inline]
255 pub fn debug_log_inner(&mut self, message: String) {
256 if let Some(messages) = self.debug_messages.as_mut() {
257 messages.push(LayoutDebugMessage {
258 message: message.into(),
259 location: "solver3".into(),
260 message_type: LayoutDebugMessageType::default(),
261 });
262 }
263 }
264
265 /// Internal method - called by `debug_info`! macro after checking `debug_messages.is_some()`
266 #[inline]
267 pub fn debug_info_inner(&mut self, message: String) {
268 if let Some(messages) = self.debug_messages.as_mut() {
269 messages.push(LayoutDebugMessage::info(message));
270 }
271 }
272
273 /// Internal method - called by `debug_warning`! macro after checking `debug_messages.is_some()`
274 #[inline]
275 pub fn debug_warning_inner(&mut self, message: String) {
276 if let Some(messages) = self.debug_messages.as_mut() {
277 messages.push(LayoutDebugMessage::warning(message));
278 }
279 }
280
281 /// Internal method - called by `debug_error`! macro after checking `debug_messages.is_some()`
282 #[inline]
283 pub fn debug_error_inner(&mut self, message: String) {
284 if let Some(messages) = self.debug_messages.as_mut() {
285 messages.push(LayoutDebugMessage::error(message));
286 }
287 }
288
289 /// Internal method - called by `debug_box_props`! macro after checking `debug_messages.is_some()`
290 #[inline]
291 pub fn debug_box_props_inner(&mut self, message: String) {
292 if let Some(messages) = self.debug_messages.as_mut() {
293 messages.push(LayoutDebugMessage::box_props(message));
294 }
295 }
296
297 /// Internal method - called by `debug_css_getter`! macro after checking `debug_messages.is_some()`
298 #[inline]
299 pub fn debug_css_getter_inner(&mut self, message: String) {
300 if let Some(messages) = self.debug_messages.as_mut() {
301 messages.push(LayoutDebugMessage::css_getter(message));
302 }
303 }
304
305 /// Internal method - called by `debug_bfc_layout`! macro after checking `debug_messages.is_some()`
306 #[inline]
307 pub fn debug_bfc_layout_inner(&mut self, message: String) {
308 if let Some(messages) = self.debug_messages.as_mut() {
309 messages.push(LayoutDebugMessage::bfc_layout(message));
310 }
311 }
312
313 /// Internal method - called by `debug_ifc_layout`! macro after checking `debug_messages.is_some()`
314 #[inline]
315 pub fn debug_ifc_layout_inner(&mut self, message: String) {
316 if let Some(messages) = self.debug_messages.as_mut() {
317 messages.push(LayoutDebugMessage::ifc_layout(message));
318 }
319 }
320
321 /// Internal method - called by `debug_table_layout`! macro after checking `debug_messages.is_some()`
322 #[inline]
323 pub fn debug_table_layout_inner(&mut self, message: String) {
324 if let Some(messages) = self.debug_messages.as_mut() {
325 messages.push(LayoutDebugMessage::table_layout(message));
326 }
327 }
328
329 /// Internal method - called by `debug_display_type`! macro after checking `debug_messages.is_some()`
330 #[inline]
331 pub fn debug_display_type_inner(&mut self, message: String) {
332 if let Some(messages) = self.debug_messages.as_mut() {
333 messages.push(LayoutDebugMessage::display_type(message));
334 }
335 }
336}
337
338/// Main entry point for the incremental, cached layout engine.
339///
340/// `new_dom` is borrowed, not owned — every use inside is `&new_dom`,
341/// so taking ownership was a pure formality that forced every caller
342/// to `styled_dom.clone()` the DOM before calling. The clone was
343/// ~2 MiB per render on excel.html; kept at the borrow now.
344#[cfg(feature = "text_layout")]
345/// Web-backend opt-out for display-list generation.
346///
347/// When set, [`layout_document`] runs the full positioning pipeline
348/// (intrinsic sizing, taffy block/flex/grid, relative/sticky/absolute
349/// adjustment → `calculated_positions`) but **skips
350/// `generate_display_list`**, returning an empty [`DisplayList`]. The
351/// web backend emits TLV DOM patches, not a display list, so it needs
352/// the geometry in `calculated_positions` but nothing the painter
353/// produces. This also lets the AArch64→wasm lift drop the entire
354/// `display_list` painter surface (those symbols are classified `Leaf`
355/// in `dll/src/web/symbol_table.rs::classify_for_name`, so the
356/// transitive lifter never descends into them). Defaults `false` →
357/// desktop/native behaviour is unchanged.
358pub static SKIP_DISPLAY_LIST: core::sync::atomic::AtomicBool =
359 core::sync::atomic::AtomicBool::new(false);
360
361/// Set [`SKIP_DISPLAY_LIST`].
362///
363/// Provided as a function (rather than the
364/// caller touching the static directly) so the web backend's
365/// `dll`-crate caller reaches it through a normal `bl` into this
366/// `azul_layout` function — keeping the static's address computation
367/// intra-crate (direct `adrp+add`) instead of a cross-crate GOT load,
368/// which the AArch64→wasm lift mirrors more reliably.
369pub fn set_skip_display_list(skip: bool) {
370 SKIP_DISPLAY_LIST.store(skip, core::sync::atomic::Ordering::Relaxed);
371}
372
373// M12.7: keep this out-of-line so the web lift sees it as its own wasm fn
374// (not inlined into layout_dom_recursive). An opt-folded infinite loop in the
375// solver (a mis-lifted loop exit) is otherwise hidden inside the giant inlined
376// layout_dom_recursive; de-inlining lets AZ_FUEL/AZ_WASM_DEBUG name the actual
377// source fn — and may itself prevent the inlining-induced fold. No perf cost on
378// desktop (called once per layout).
379#[inline(never)]
380// node counts / indices / tree-len values fed to az_mark debug markers as u32; bounded.
381#[allow(clippy::cast_possible_truncation)]
382#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
383/// # Errors
384///
385/// Returns a `LayoutError` if document layout fails.
386pub fn layout_document<T: ParsedFontTrait + Sync + 'static>(
387 cache: &mut LayoutCache,
388 text_cache: &mut TextLayoutCache,
389 new_dom: &StyledDom,
390 viewport: LogicalRect,
391 font_manager: &crate::font_traits::FontManager<T>,
392 scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
393 text_selections: &BTreeMap<DomId, TextSelection>,
394 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
395 gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
396 renderer_resources: &azul_core::resources::RendererResources,
397 id_namespace: azul_core::resources::IdNamespace,
398 dom_id: DomId,
399 cursor_is_visible: bool,
400 cursor_locations: Vec<(DomId, NodeId, TextCursor)>,
401 preedit_text: Option<String>,
402 image_cache: &azul_core::resources::ImageCache,
403 system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
404 get_system_time_fn: azul_core::task::GetSystemTimeCallback,
405) -> Result<DisplayList> {
406 use crate::window::LayoutWindow;
407
408 // Secondary mapping: anonymous wrappers (dom_node_id == None)
409 // by (parent_new_idx, ordinal-among-anon-siblings). An
410 // unchanged DOM produces the same anon wrappers in the same
411 // order under the same parent — matching by position here
412 // preserves their cache slots too. Without this, anon
413 // wrappers re-allocate empty every reconcile and invalidate
414 // their ancestors via `mark_dirty`.
415 fn collect_anon_children_by_parent(
416 tree: &LayoutTree,
417 ) -> HashMap<usize, Vec<usize>> {
418 let mut map: HashMap<usize, Vec<usize>> =
419 HashMap::new();
420 for (idx, node) in tree.nodes.iter().enumerate() {
421 if node.dom_node_id.is_some() {
422 continue;
423 }
424 if let Some(parent) = node.parent {
425 map.entry(parent).or_default().push(idx);
426 }
427 }
428 map
429 }
430
431 // Reset IFC ID counter at the start of each layout pass
432 // This ensures IFCs get consistent IDs across frames when the DOM structure is stable
433 layout_tree::IfcId::reset_counter();
434 // in layout_document returns the rc=5 Err (the error enum can't be captured
435 // reliably in the lift). The last value seen = the step that errored next.
436 { let _ = (0xDD00_0001u32); }
437 // If 0 here → the LogicalRect HFA arg was lost across the lifted call.
438
439 if let Some(msgs) = debug_messages.as_mut() {
440 msgs.push(LayoutDebugMessage::info(format!(
441 "[Layout] layout_document called - viewport: ({:.1}, {:.1}) size ({:.1}x{:.1})",
442 viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height
443 )));
444 msgs.push(LayoutDebugMessage::info(format!(
445 "[Layout] DOM has {} nodes",
446 new_dom.node_data.len()
447 )));
448 }
449
450 // Create temporary context without counters for tree generation
451 let mut counter_values = HashMap::new();
452 let mut ctx_temp = LayoutContext {
453 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
454 styled_dom: new_dom,
455 font_manager,
456 text_selections,
457 debug_messages,
458 counters: &mut counter_values,
459 viewport_size: viewport.size,
460 fragmentation_context: None,
461 cursor_is_visible,
462 cursor_locations: cursor_locations.clone(),
463 preedit_text: preedit_text.clone(),
464 dirty_text_overrides: BTreeMap::new(),
465 cache_map: cache::LayoutCacheMap::default(), // temp context doesn't need real cache
466 image_cache,
467 system_style: system_style.clone(),
468 get_system_time_fn,
469 };
470
471 crate::probe::sample_peak_rss("rss:enter_layout_document");
472
473 // --- Step 0: record DOM pointer / viewport for diagnostics only ---
474 //
475 // NOTE: there is intentionally NO pointer-identity fast path here.
476 // Comparing `new_dom as *const StyledDom as usize` against a stored
477 // pointer is UNSOUND across layout passes: each `regenerate_layout`
478 // builds a fresh `StyledDom`, and after the previous one is dropped
479 // (e.g. `layout_and_generate_display_list` calls `layout_results.clear()`
480 // before re-laying out), the allocator/stack frequently hands the new,
481 // *different* StyledDom the SAME address. A pointer match therefore does
482 // NOT prove the content is unchanged — it would return the previous
483 // frame's display list for a structurally different DOM (e.g. an image
484 // removed from the tree would still appear in `scan_used_images`,
485 // breaking resource GC). The Step 1.1 structural-identity cache below
486 // (root `subtree_hash` + viewport) is the correct, content-based skip;
487 // it costs one ~600 µs reconcile pass but cannot be fooled by address
488 // reuse.
489 let dom_ptr = std::ptr::from_ref::<StyledDom>(new_dom) as usize;
490 cache.prev_dom_ptr = dom_ptr;
491 cache.prev_viewport = viewport;
492
493 // --- Step 1: Reconciliation & Invalidation ---
494 crate::probe::reset_peak();
495 let (new_tree_val, mut recon_result) =
496 cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?;
497 // [g56 FIX] Box the LayoutTree onto the HEAP. The lifted `&mut new_tree` passed to
498 // calculate_intrinsic_sizes was mis-lifted (callee saw nodes.len()=0 while the caller saw 2)
499 // because a stack/SROA'd `new_tree`'s address doesn't survive the cross-function lifted call
500 // (taking `&new_tree` lifted to 0x0). A heap allocation has a stable absolute wasm address
501 // that lifts reliably (cf. M8.4 "heap allocations work fine"). Deref coercion handles the
502 // `&new_tree`/`&mut new_tree`/`new_tree.field` sites unchanged.
503 let mut new_tree = Box::new(new_tree_val);
504 { let _ = (0xDD00_0002u32); }
505 // [az-diag g51 REVERT] 0x71 = reconcile_and_invalidate returned OK (no InvalidTree in reconcile).
506 unsafe { crate::az_mark(0x60704_u32, (0x71u32)); }
507 // [az-diag g54 REVERT] 0x40740 = new_tree.nodes.len() RIGHT AFTER reconcile. If 0 → reconcile
508 // built an empty LayoutTree (the bug is in reconcile_recursive/create_node_from_dom). If 2 →
509 // reconcile is fine and the tree gets emptied/mis-lifted downstream (check 0x40744 at the loop).
510 unsafe { crate::az_mark(0x60740_u32, (new_tree.nodes.len() as u32)); }
511 crate::probe::sample_peak_rss("rss:after_reconcile");
512 crate::probe::sample_phase_peak("rss:peak_during_reconcile");
513
514 // --- Step 1.1: Structural-identity display-list cache ---
515 //
516 // If the reconciled root subtree_hash matches the cached one AND
517 // the viewport is unchanged, nothing structural has moved — skip
518 // layout, positioning, AND display-list generation and return
519 // the cached display list verbatim.
520 //
521 // This fires on re-renders of an unchanged DOM: the reconcile
522 // pass still walks and hashes the tree, but that's ~600 µs vs
523 // the ~4 ms it would otherwise cost to re-emit the display list.
524 if let Some((cached_hash, cached_viewport, cached_dl)) = &cache.cached_display_list {
525 let new_root_hash = new_tree
526 .cold(new_tree.root)
527 .map(|c| c.subtree_hash);
528 if new_root_hash == Some(*cached_hash) && *cached_viewport == viewport {
529 let _p = crate::probe::Probe::span("display_list_cache_hit");
530 return Ok(cached_dl.clone());
531 }
532 }
533
534 // Step 1.2: Clear Taffy Caches for Dirty Nodes
535 for &node_idx in &recon_result.intrinsic_dirty {
536 if let Some(warm) = new_tree.warm_mut(node_idx) {
537 warm.taffy_cache.clear();
538 }
539 }
540
541 // Step 1.3: Compute CSS Counters
542 // This must be done after tree generation but before layout,
543 // as list markers need counter values during formatting context layout
544 {
545 let _p = crate::probe::Probe::span("compute_counters");
546 cache::compute_counters(new_dom, &new_tree, &mut counter_values);
547 }
548 // [az-diag g51 REVERT] 0x72 = compute_counters done (InvalidTree, if any, is after here).
549 unsafe { crate::az_mark(0x60704_u32, (0x72u32)); }
550
551 // Step 1.4: Resize and invalidate per-node cache (Taffy-inspired 9+1 slot cache)
552 // Move cache_map out of LayoutCache for the duration of layout (avoids borrow conflicts).
553 // It will be moved back after the layout pass completes.
554 //
555 // Critically: the old `cache_map.entries` is indexed by OLD
556 // layout-tree positions. The NEW tree may have re-ordered
557 // indices (anonymous wrapper slots shifted, whitespace nodes
558 // dropped, etc.). A plain `resize_with(default)` would silently
559 // serve the wrong node's cached result for any shifted index.
560 //
561 // Re-map by stable identity: build `old_layout_idx → new_layout_idx`
562 // via the `(dom_node_id → layout_idx)` tables on both trees,
563 // then move each surviving cache entry into its new slot. Nodes
564 // without a matching DOM id (pure anonymous wrappers) fall
565 // through to the default (empty, i.e. dirty) entry.
566 let mut cache_map = std::mem::take(&mut cache.cache_map);
567 let probe_cache_remap = Some(crate::probe::Probe::span("cache_map_remap"));
568 if let Some(old_tree) = cache.tree.as_ref() {
569 let mut remapped = cache::LayoutCacheMap::default();
570 remapped.entries.resize_with(new_tree.nodes.len(), Default::default);
571
572 // Primary mapping: DOM id → layout idx on both sides. This
573 // covers every node that has a corresponding DOM node.
574 for (dom_id, new_indices) in &new_tree.dom_to_layout {
575 let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
576 continue;
577 };
578 for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
579 let Some(&old_layout_idx) = old_indices.get(pair_idx) else {
580 continue;
581 };
582 if old_layout_idx >= cache_map.entries.len()
583 || new_layout_idx >= remapped.entries.len()
584 {
585 continue;
586 }
587 remapped.entries[new_layout_idx] =
588 core::mem::take(&mut cache_map.entries[old_layout_idx]);
589 }
590 }
591
592 // Build old-parent → [old_anon_indices] and
593 // new-parent → [new_anon_indices]; match by pair position.
594 let old_anon_by_parent = collect_anon_children_by_parent(old_tree);
595 let new_anon_by_parent = collect_anon_children_by_parent(&new_tree);
596
597 // For each new parent we know: look up its old twin by the
598 // dom-id mapping we just populated, then match anon children
599 // positionally within that parent.
600 // Build a new→old layout-idx lookup from the primary pass.
601 let mut new_to_old_layout_idx: HashMap<usize, usize> =
602 HashMap::new();
603 for (dom_id, new_indices) in &new_tree.dom_to_layout {
604 let Some(old_indices) = old_tree.dom_to_layout.get(dom_id) else {
605 continue;
606 };
607 for (pair_idx, &new_layout_idx) in new_indices.iter().enumerate() {
608 if let Some(&old_layout_idx) = old_indices.get(pair_idx) {
609 new_to_old_layout_idx.insert(new_layout_idx, old_layout_idx);
610 }
611 }
612 }
613
614 for (new_parent_idx, new_anon_children) in new_anon_by_parent {
615 let Some(&old_parent_idx) = new_to_old_layout_idx.get(&new_parent_idx) else {
616 continue;
617 };
618 let Some(old_anon_children) = old_anon_by_parent.get(&old_parent_idx) else {
619 continue;
620 };
621 for (ord, &new_anon_idx) in new_anon_children.iter().enumerate() {
622 let Some(&old_anon_idx) = old_anon_children.get(ord) else {
623 continue;
624 };
625 if old_anon_idx >= cache_map.entries.len()
626 || new_anon_idx >= remapped.entries.len()
627 {
628 continue;
629 }
630 remapped.entries[new_anon_idx] =
631 core::mem::take(&mut cache_map.entries[old_anon_idx]);
632 }
633 }
634
635 cache_map = remapped;
636 } else {
637 cache_map.resize_to_tree(new_tree.nodes.len());
638 }
639 drop(probe_cache_remap);
640 crate::probe::sample_peak_rss("rss:after_cache_remap");
641 for &node_idx in &recon_result.intrinsic_dirty {
642 cache_map.mark_dirty(node_idx, &new_tree.nodes);
643 }
644 for &node_idx in &recon_result.layout_roots {
645 cache_map.mark_dirty(node_idx, &new_tree.nodes);
646 }
647
648 // Now create the real context with computed counters
649 let mut ctx = LayoutContext {
650 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
651 styled_dom: new_dom,
652 font_manager,
653 text_selections,
654 debug_messages,
655 counters: &mut counter_values,
656 viewport_size: viewport.size,
657 fragmentation_context: None,
658 cursor_is_visible,
659 cursor_locations,
660 preedit_text,
661 dirty_text_overrides: BTreeMap::new(),
662 cache_map, // Moved from LayoutCache; will be moved back after layout
663 image_cache,
664 system_style,
665 get_system_time_fn,
666 };
667
668 // --- Step 1.5: Early Exit Optimization ---
669 // M12.7: `&& cache.tree.is_some()` — this "nothing changed, reuse cached
670 // layout" fast path REQUIRES a cached tree; on COLD layout cache.tree is
671 // None, so entering here would hit `ok_or(InvalidTree)`. recon_result must
672 // be dirty on cold (the viewport-resize dirties the root), but if
673 // is_clean() mis-evaluates we'd wrongly early-exit → InvalidTree. Guarding
674 // on a cached tree is both correct (can't reuse what isn't there) and
675 // robust. (rc=5 post-reconcile, step=2: this was the failing `?`.)
676 if recon_result.is_clean() && cache.tree.is_some() {
677 debug_log!(ctx, "No changes, returning existing display list");
678 let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
679
680 // Use cached scroll IDs if available, otherwise compute them
681 let scroll_ids = if cache.scroll_ids.is_empty() {
682 use crate::window::LayoutWindow;
683 let (scroll_ids, scroll_id_to_node_id) =
684 LayoutWindow::compute_scroll_ids(tree, new_dom);
685 cache.scroll_ids.clone_from(&scroll_ids);
686 cache.scroll_id_to_node_id = scroll_id_to_node_id;
687 scroll_ids
688 } else {
689 cache.scroll_ids.clone()
690 };
691
692 if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
693 return Ok(DisplayList::default());
694 }
695 return generate_display_list(
696 &mut ctx,
697 tree,
698 &cache.calculated_positions,
699 scroll_offsets,
700 &scroll_ids,
701 gpu_value_cache,
702 renderer_resources,
703 id_namespace,
704 dom_id,
705 );
706 }
707
708 { let _ = (0xDD00_0003u32); }
709 // [az-diag g51 REVERT] 0x80 = reached the incremental layout loop (past early-exit + remap + dirty loops).
710 unsafe { crate::az_mark(0x60704_u32, (0x80u32)); }
711 // [az-diag g65 PATH-B VALIDATION] new_tree is still valid here (=2). Clone it into the HEAP-backed
712 // cache.tree (set AFTER the remap+early-exit which read the OLD cache.tree). cache is the stable
713 // &mut arg (read correctly throughout), so cache.tree is NOT a deep-SP-relative stack local. At the
714 // sizing call we read BOTH: stack new_tree (expect 0=corrupted) vs heap cache.tree (expect 2 if
715 // path B sidesteps the SP-drift/wild-store). If heap=2, the full cache.tree refactor will fix it.
716 cache.tree = Some((*new_tree).clone());
717 // [az-diag g66] disambiguate the g65 heap=1: read BOTH right after the clone. 0x407C0 = stack
718 // new_tree.nodes.len() (source), 0x407C4 = clone cache.tree.nodes.len(). If src=2 & clone=1 →
719 // Vec::clone MIS-LIFTS (drops a node) → the full MOVE-based cache.tree refactor avoids it (do it).
720 // If src=1=clone → corruption already reached line 758 (heisenbug) → move won't help.
721 unsafe {
722 crate::az_mark(0x607C0_u32, (new_tree.nodes.len() as u32));
723 crate::az_mark(0x607C4_u32, (cache.tree.as_ref().map_or(999, |t| t.nodes.len()) as u32));
724 }
725
726 // --- Step 2: Incremental Layout Loop (handles scrollbar-induced reflows) ---
727 let mut calculated_positions = cache.calculated_positions.clone();
728 let mut loop_count = 0;
729 loop {
730 loop_count += 1;
731 if loop_count > MAX_SCROLLBAR_REFLOW_ITERATIONS {
732 debug_warning!(ctx, "Scrollbar reflow loop hit limit of {} iterations, breaking to avoid infinite loop", MAX_SCROLLBAR_REFLOW_ITERATIONS);
733 break;
734 }
735
736 calculated_positions = {
737 let _p = crate::probe::Probe::span("clone_calculated_positions");
738 cache.calculated_positions.clone()
739 };
740 // [az-diag g70 RELIABLE free-band] 0x60780 = nodes.len AFTER the in-loop calculated_positions.clone().
741 unsafe { crate::az_mark(0x60780_u32, (new_tree.nodes.len() as u32)); }
742 let mut reflow_needed_for_scrollbars = false;
743
744 {
745 crate::probe::reset_peak();
746 // [az-diag g70 RELIABLE free-band] 0x60784 = nodes.len AFTER reset_peak (before the calc Span).
747 unsafe { crate::az_mark(0x60784_u32, (new_tree.nodes.len() as u32)); }
748 let _p = crate::probe::Probe::span("calc_intrinsic_sizes");
749 // [az-diag g70 RELIABLE free-band] 0x60788 = nodes.len AFTER the calc_intrinsic_sizes Span.
750 unsafe { crate::az_mark(0x60788_u32, (new_tree.nodes.len() as u32)); }
751 // [az-diag g72 FIX] REMOVED the g48 `#[cfg(feature="web_lift")] panic!(...)` that lived
752 // here. web-transpiler => azul-layout?/web_lift IS enabled (dll/Cargo.toml:651), so this
753 // panic WAS compiled in, and with `-Z build-std-features=panic_immediate_abort` it lowered
754 // to a bare `brk #0x1` right after the 0x90 marker — aborting BEFORE calculate_intrinsic_sizes.
755 // The whole-session "new_tree 2→0 corruption" was a MIRAGE: the beforeCall marker store was
756 // dead-code-eliminated (after the abort), so the harness read uninitialized 0, not a corrupted
757 // tree. Native disasm of layout_document proved it: 0x90 marker store → `brk #0x1` → no `bl
758 // calculate_intrinsic_sizes` anywhere. (The prior "string absent ⇒ web_lift off" check was
759 // wrong — panic_immediate_abort strips the message string.)
760 // [az-diag g65 PATH-B VALIDATION] 0x40748 = stack new_tree.nodes.len() (expect 0),
761 // 0x4074C = HEAP cache.tree.nodes.len() (expect 2 if path B sidesteps the corruption).
762 unsafe {
763 crate::az_mark(0x60748_u32, (new_tree.nodes.len() as u32));
764 crate::az_mark(0x6074C_u32, (cache.tree.as_ref().map_or(999, |t| t.nodes.len()) as u32));
765 }
766 calculate_intrinsic_sizes(
767 &mut ctx,
768 &mut new_tree,
769 text_cache,
770 &recon_result.intrinsic_dirty,
771 )?;
772 }
773 crate::probe::sample_peak_rss("rss:after_calc_intrinsic");
774 crate::probe::sample_phase_peak("rss:peak_during_intrinsic");
775 // divergence is inside calculate_intrinsic_sizes (the SIMD/text intrinsic pass).
776 { let _ = (0xDD00_0005u32); }
777
778 for &root_idx in &recon_result.layout_roots {
779 let (cb_pos, cb_size) = get_containing_block_for_node(
780 &new_tree,
781 new_dom,
782 root_idx,
783 &calculated_positions,
784 viewport,
785 );
786 // 0x05, the divergence is INSIDE get_containing_block_for_node (or the for-loop
787 // entry); if 0x53 but not 0x55, it's the margin logic / box_props.unpack below.
788 { let _ = (0xDD00_0053u32); }
789 // get_containing_block_for_node(viewport)). 800 here but viewport=800 ⟹ OK;
790 // 0 here with viewport=800 ⟹ get_containing_block_for_node lost it (HFA return).
791
792 // For ROOT nodes (no parent), we need to account for their margin.
793 // The containing block position from viewport is (0, 0), but the root's
794 // content starts at (margin + border + padding, margin + border + padding).
795 // We pass margin-adjusted position so calculate_content_box_pos works correctly.
796 let root_node = &new_tree.nodes[root_idx];
797 let root_bp = root_node.box_props.unpack();
798 { let _ = (0xDD00_0054u32); }
799
800 let is_root_with_margin = root_node.parent.is_none()
801 && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
802
803 let adjusted_cb_pos = if is_root_with_margin {
804 LogicalPosition::new(
805 cb_pos.x + root_bp.margin.left,
806 cb_pos.y + root_bp.margin.top,
807 )
808 } else {
809 cb_pos
810 };
811 { let _ = (0xDD00_0056u32); }
812
813 // DEBUG: Log containing block info for this root
814 if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
815 let dom_name = root_node
816 .dom_node_id
817 .and_then(|id| new_dom.node_data.as_container().internal.get(id.index())).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
818
819 debug_msgs.push(LayoutDebugMessage::new(
820 LayoutDebugMessageType::PositionCalculation,
821 format!(
822 "[LAYOUT ROOT {}] {} - CB pos=({:.2}, {:.2}), adjusted=({:.2}, {:.2}), \
823 CB size=({:.2}x{:.2}), viewport=({:.2}x{:.2}), margin=({:.2}, {:.2})",
824 root_idx,
825 dom_name,
826 cb_pos.x,
827 cb_pos.y,
828 adjusted_cb_pos.x,
829 adjusted_cb_pos.y,
830 cb_size.width,
831 cb_size.height,
832 viewport.size.width,
833 viewport.size.height,
834 root_bp.margin.left,
835 root_bp.margin.top
836 ),
837 ));
838 }
839
840 // Purge after intrinsic sizing — frees child_intrinsics Vecs,
841 // IntrinsicSizeCalculator temporaries, text measurement caches.
842 crate::probe::hint_purge_allocator();
843 crate::probe::sample_peak_rss("rss:before_root_layout");
844 crate::probe::reset_peak();
845 // 0x57 = it RETURNED. If step stays 0x55, calculate_layout_for_subtree diverges.
846 { let _ = (0xDD00_0055u32); }
847 // This is exactly what calc_used_size reads as `viewport`. 0 here pinpoints the
848 // loss to the ctx build (viewport.size → ctx.viewport_size copy).
849 // 0x5E = Err. Do NOT propagate (continue to the cache store) so layout-real can
850 // see whether the geometry was computed regardless of a (possibly spurious,
851 // niche-Result-mis-discriminated) Err.
852 let clr = {
853 let _p = crate::probe::Probe::span("root_layout_pass");
854 cache::calculate_layout_for_subtree(
855 &mut ctx,
856 &mut new_tree,
857 text_cache,
858 root_idx,
859 adjusted_cb_pos,
860 cb_size,
861 &mut calculated_positions,
862 &mut reflow_needed_for_scrollbars,
863 &mut cache.float_cache,
864 cache::ComputeMode::PerformLayout,
865 )
866 };
867 { let _ = (if clr.is_ok() { 0xDD00_0057u32 } else { 0xDD00_005Eu32 }); }
868 crate::probe::sample_peak_rss("rss:after_root_layout");
869 crate::probe::sample_phase_peak("rss:peak_during_root_layout");
870
871 // CRITICAL: Insert the root node's own position into calculated_positions
872 // This is necessary because calculate_layout_for_subtree only inserts
873 // positions for children, not for the root itself.
874 //
875 // For root nodes, the position should be at (margin.left, margin.top) relative
876 // to the viewport origin, because the margin creates space between the viewport
877 // edge and the element's border-box.
878 if !pos_contains(&calculated_positions, root_idx) {
879 let root_node = &new_tree.nodes[root_idx];
880 let root_bp2 = root_node.box_props.unpack();
881
882 // Calculate the root's border-box position by adding margins to viewport origin
883 // This is different from non-root nodes which inherit their position from
884 // their containing block.
885 let root_position = LogicalPosition::new(
886 cb_pos.x + root_bp2.margin.left,
887 cb_pos.y + root_bp2.margin.top,
888 );
889
890 // DEBUG: Log root positioning
891 if let Some(debug_msgs) = ctx.debug_messages.as_mut() {
892 let dom_name = root_node
893 .dom_node_id
894 .and_then(|id| new_dom.node_data.as_container().internal.get(id.index())).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
895
896 debug_msgs.push(LayoutDebugMessage::new(
897 LayoutDebugMessageType::PositionCalculation,
898 format!(
899 "[ROOT POSITION {}] {} - Inserting position=({:.2}, {:.2}) (viewport origin + margin), \
900 margin=({:.2}, {:.2}, {:.2}, {:.2})",
901 root_idx,
902 dom_name,
903 root_position.x,
904 root_position.y,
905 root_bp2.margin.top,
906 root_bp2.margin.right,
907 root_bp2.margin.bottom,
908 root_bp2.margin.left
909 ),
910 ));
911 }
912
913 pos_set(&mut calculated_positions, root_idx, root_position);
914 }
915 }
916 // (step 6). If step stays 5, the divergence is in calculate_layout_for_subtree.
917 { let _ = (0xDD00_0006u32); }
918
919 {
920 let _p = crate::probe::Probe::span("reposition_clean_subtrees");
921 cache::reposition_clean_subtrees(
922 new_dom,
923 &new_tree,
924 &recon_result.layout_roots,
925 &mut calculated_positions,
926 );
927 }
928
929 if reflow_needed_for_scrollbars {
930 debug_log!(ctx,
931 "Scrollbars changed container size, starting full reflow (loop {})",
932 loop_count
933 );
934 recon_result.layout_roots.clear();
935 recon_result.layout_roots.insert(new_tree.root);
936 recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
937 continue;
938 }
939
940 break;
941 }
942
943 // +spec:positioning:8d1286 - normal flow, relative, float, absolute positioning dispatch
944 // +spec:positioning:bdfc81 - Layout divided into sizing (Step 2) then positioning (Step 3)
945 // --- Step 3: Adjust Relatively Positioned Elements ---
946 // +spec:positioning:a831e8 - inline content width uses pre-relative-offset positions (satisfied by post-layout relative adjustment)
947 // +spec:positioning:e2647b - Relative positioning applied after line height calculation, so line height is not adjusted for relative offsets
948 // +spec:positioning:77a2d2 - Relatively positioned boxes considered without their offset during auto height
949 // +spec:positioning:b47ac2 - Relatively positioned boxes considered without their offset for block auto height
950 // Relative offsets applied AFTER layout, so auto-height calculation sees normal-flow positions.
951 // This must be done BEFORE positioning out-of-flow elements, because
952 // relatively positioned elements establish containing blocks for their
953 // absolutely positioned descendants. If we adjust relative positions after
954 // positioning absolute elements, the absolute elements will be positioned
955 // relative to the wrong (pre-adjustment) position of their containing block.
956 // Pass the viewport to correctly resolve percentage offsets for the root element.
957 {
958 let _p = crate::probe::Probe::span("adjust_relative_positions");
959 positioning::adjust_relative_positions(
960 &mut ctx,
961 &new_tree,
962 &mut calculated_positions,
963 viewport,
964 );
965 }
966
967 // --- Step 3.25: Adjust Sticky Positioned Elements ---
968 // Sticky elements are laid out in normal flow, then their visual position
969 // is clamped based on scroll offset and inset properties relative to the
970 // nearest scrollport. Must happen after relative positioning but before
971 // absolute positioning (sticky elements establish containing blocks).
972 {
973 let _p = crate::probe::Probe::span("adjust_sticky_positions");
974 positioning::adjust_sticky_positions(
975 &mut ctx,
976 &new_tree,
977 &mut calculated_positions,
978 scroll_offsets,
979 viewport,
980 );
981 }
982
983 // --- Step 3.5: Position Out-of-Flow Elements ---
984 // This must be done AFTER adjusting relative positions, so that absolutely
985 // positioned elements are positioned relative to the final (post-adjustment)
986 // position of their relatively positioned containing blocks.
987 {
988 let _p = crate::probe::Probe::span("position_out_of_flow");
989 positioning::position_out_of_flow_elements(
990 &mut ctx,
991 &mut new_tree,
992 text_cache,
993 &mut calculated_positions,
994 viewport,
995 );
996 }
997
998 // --- Step 3.75: Compute Stable Scroll IDs ---
999 // This must be done AFTER layout but BEFORE display list generation
1000 let (scroll_ids, scroll_id_to_node_id) = {
1001 let _p = crate::probe::Probe::span("compute_scroll_ids");
1002 LayoutWindow::compute_scroll_ids(&new_tree, new_dom)
1003 };
1004
1005 crate::probe::sample_peak_rss("rss:before_display_list");
1006 crate::probe::reset_peak();
1007 // --- Step 4: Generate Display List & Update Cache ---
1008 let display_list = if SKIP_DISPLAY_LIST.load(core::sync::atomic::Ordering::Relaxed) {
1009 // Web backend: positions are done; the painter is dead weight.
1010 DisplayList::default()
1011 } else {
1012 let _p = crate::probe::Probe::span("generate_display_list");
1013 generate_display_list(
1014 &mut ctx,
1015 &new_tree,
1016 &calculated_positions,
1017 scroll_offsets,
1018 &scroll_ids,
1019 gpu_value_cache,
1020 renderer_resources,
1021 id_namespace,
1022 dom_id,
1023 )?
1024 };
1025 crate::probe::sample_phase_peak("rss:peak_during_display_list");
1026
1027 // Move cache_map back into LayoutCache before dropping ctx
1028 let _p_writeback = crate::probe::Probe::span("cache_writeback");
1029 let cache_map_back = std::mem::take(&mut ctx.cache_map);
1030
1031 // Cache the freshly-generated display list keyed on the root's
1032 // subtree_hash + viewport. If the next `layout_document` call
1033 // sees matching values after reconcile, it returns this clone
1034 // directly and skips all downstream work.
1035 let root_subtree_hash = new_tree
1036 .cold(new_tree.root)
1037 .map_or(layout_tree::SubtreeHash(0), |c| c.subtree_hash);
1038 cache.cached_display_list = Some((root_subtree_hash, viewport, display_list.clone()));
1039
1040 cache.tree = Some(*new_tree); // [g56] unbox the heap LayoutTree back into the cache
1041 cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
1042 cache.viewport = Some(viewport);
1043 cache.scroll_ids = scroll_ids;
1044 cache.scroll_id_to_node_id = scroll_id_to_node_id;
1045 // + calculated_positions.len in the low bits. If step stays 3, it diverged earlier.
1046 { let _ = (0xDD00_0004u32 | ((cache.calculated_positions.len() as u32 & 0xfff) << 4)); }
1047 cache.counters = counter_values;
1048 cache.cache_map = cache_map_back;
1049 crate::probe::sample_peak_rss("rss:after_layout_document");
1050
1051 Ok(display_list)
1052}
1053
1054// +spec:containing-block:159830 - Containing block chain: parent content-box for in-flow, viewport for initial containing block
1055// +spec:containing-block:22fbaa - computes the element's original containing block (before positioning effects)
1056// +spec:containing-block:238fc5 - containing block dimensions calculated here (CSS 2.2 §9.1.2 forward ref to §10)
1057// +spec:containing-block:263629 - block element's content-box establishes the containing block for its line boxes
1058// +spec:containing-block:2a5280 - boxes act as containing blocks for descendants; CB = parent's content box
1059// +spec:containing-block:6776cb - boxes positioned w.r.t. containing block but not confined; overflow allowed
1060// +spec:containing-block:718894 - CB derived from parent content-box edges; root uses initial CB (viewport)
1061// +spec:containing-block:a2aa37 - box edges act as containing block for descendants; initial containing block = viewport
1062// +spec:containing-block:e23b3f - CSS 2.2 §10.1: initial containing block = viewport; static/relative = parent content-box; fixed = viewport
1063// +spec:containing-block:e8fdb2 - Containing block resolution (CSS2 §9.1.2, §10.1)
1064// +spec:overflow:9a2b11 - containing block is content-box of parent; boxes may overflow it
1065// +spec:positioning:acc663 - containing block definition: element boxes positioned relative to containing block
1066pub(super) fn get_containing_block_for_node(
1067 tree: &LayoutTree,
1068 styled_dom: &StyledDom,
1069 node_idx: usize,
1070 calculated_positions: &PositionVec,
1071 viewport: LogicalRect,
1072) -> (LogicalPosition, LogicalSize) {
1073 if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
1074 if let Some(parent_node) = tree.get(parent_idx) {
1075 let pos = pos_get(calculated_positions, parent_idx)
1076 .unwrap_or(viewport.origin);
1077 let size = parent_node.used_size.unwrap_or_default();
1078 // Position in calculated_positions is the margin-box position
1079 // To get content-box, add: border + padding (NOT margin, that's already in pos)
1080 let pbp = parent_node.box_props.unpack();
1081 let content_pos = LogicalPosition::new(
1082 pos.x + pbp.border.left + pbp.padding.left,
1083 pos.y + pbp.border.top + pbp.padding.top,
1084 );
1085
1086 if let Some(dom_id) = parent_node.dom_node_id {
1087 let styled_node_state = &styled_dom
1088 .styled_nodes
1089 .as_container()
1090 .get(dom_id)
1091 .map(|n| &n.styled_node_state)
1092 .copied()
1093 .unwrap_or_default();
1094 // +spec:containing-block:c205e5 - writing mode of containing block used for inner_size (orthogonal flow awareness)
1095 let writing_mode =
1096 get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
1097 let content_size = pbp.inner_size(size, writing_mode);
1098 return (content_pos, content_size);
1099 }
1100
1101 return (content_pos, size);
1102 }
1103 }
1104
1105 // +spec:containing-block:41bdfc - ICB equals viewport; overflow:hidden on root clips to ICB
1106 // +spec:containing-block:1eed60 - Initial containing block establishes a BFC; viewport is the ICB
1107 // +spec:containing-block:99866f - Containing block is a rectangle for sizing/positioning; ICB from viewport
1108 // +spec:containing-block:22f09b - viewport serves as initial containing block for root element
1109 // Root element's containing block is the initial containing block (CSS 2.2 §10.1, CSS Display 3 §2.8).
1110 // +spec:containing-block:2fd7b1 - ICB equals viewport; principal writing mode propagated to ICB
1111 // Root element's containing block is the initial containing block (CSS 2.2 §10.1, CSS Display 3 §2.8).
1112 // The principal writing mode is propagated to the ICB and viewport (css-writing-modes-4 §8.1).
1113 // +spec:containing-block:5efb84 - Root element's containing block is the initial containing block
1114 // +spec:containing-block:6278fb - initial containing block is the viewport; also serves as initial fixed containing block
1115 // Root element's containing block is the initial containing block (CSS 2.2 §10.1, CSS Display 3 §2.8).
1116 // For ROOT nodes: the containing block is the viewport (initial containing block).
1117 // Do NOT subtract margin here - margins are handled in calculate_used_size().
1118 // The margin creates space between viewport edge and element's border-box,
1119 // but the available space for calculating width/height percentages
1120 // is still the full viewport size.
1121 (viewport.origin, viewport.size)
1122}
1123
1124// [g119 az-web-lift FIX] `#[repr(C, u8)]` (was repr(Rust)): the `Text(font_traits::LayoutError)`
1125// variant's String/FontSelector pointer gives `Result<T, LayoutError>` a POINTER-niche disc, which
1126// the web lift MIS-READS → every solver3 `?`/Result return flips Ok→Err (heisenbug; g118 = collect's
1127// Result<(),LayoutError> arrived as Err → rc=5 InvalidTree though the out-param content was correct).
1128// An explicit u8 tag (0..=4) moves the Result niche to unused tag values (5..) = a simple u8 compare
1129// the lift handles. Same disc-mis-lift class as InlineContent/LogicalItem/ShapedItem (g117/g118).
1130#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1131#[derive(Debug)]
1132#[repr(C, u8)]
1133pub enum LayoutError {
1134 InvalidTree,
1135 SizingFailed,
1136 PositioningFailed,
1137 DisplayListFailed,
1138 Text(crate::font_traits::LayoutError),
1139}
1140
1141impl std::fmt::Display for LayoutError {
1142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1143 match self {
1144 Self::InvalidTree => write!(f, "Invalid layout tree"),
1145 Self::SizingFailed => write!(f, "Sizing calculation failed"),
1146 Self::PositioningFailed => write!(f, "Position calculation failed"),
1147 Self::DisplayListFailed => write!(f, "Display list generation failed"),
1148 Self::Text(e) => write!(f, "Text layout error: {e:?}"),
1149 }
1150 }
1151}
1152
1153impl From<crate::font_traits::LayoutError> for LayoutError {
1154 fn from(err: crate::font_traits::LayoutError) -> Self {
1155 Self::Text(err)
1156 }
1157}
1158
1159impl std::error::Error for LayoutError {}
1160
1161pub type Result<T> = std::result::Result<T, LayoutError>;