1
//! Bridge between Azul's CSS style system and the Taffy layout engine.
2
//!
3
//! This module translates Azul CSS properties into Taffy's `Style` struct and
4
//! implements Taffy's `TraversePartialTree`, `LayoutPartialTree`, `CacheTree`,
5
//! `LayoutFlexboxContainer`, and `LayoutGridContainer` traits via the
6
//! [`TaffyBridge`] struct. The main entry point is [`layout_taffy_subtree`],
7
//! which is called from `fc.rs` when a flex or grid formatting context is
8
//! encountered during layout.
9

            
10
use crate::solver3::layout_tree::LayoutNodeId;
11
use crate::solver3::calc::CalcResolveContext;
12
use crate::solver3::getters::{get_overflow_x, get_overflow_y};
13
use azul_core::dom::FormattingContext;
14
use azul_css::{
15
    css::CssPropertyValue,
16
    props::{
17
        basic::{
18
            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
19
            PixelValue, SizeMetric,
20
        },
21
        layout::{
22
            dimensions::CalcAstItemVec,
23
            flex::LayoutFlexBasis,
24
            grid::{GridAutoTracks, GridTemplate, GridTrackSizing},
25
            LayoutAlignContent, LayoutAlignItems, LayoutAlignSelf, LayoutDisplay,
26
            LayoutFlexDirection, LayoutFlexWrap, LayoutGridAutoFlow, LayoutJustifyContent,
27
            LayoutPosition, LayoutWritingMode,
28
        },
29
        property::{
30
            LayoutAlignContentValue, LayoutAlignItemsValue, LayoutAlignSelfValue,
31
            LayoutDisplayValue, LayoutFlexDirectionValue, LayoutFlexWrapValue,
32
            LayoutGridAutoColumnsValue, LayoutGridAutoFlowValue, LayoutGridAutoRowsValue,
33
            LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue, LayoutJustifyContentValue,
34
            LayoutPositionValue,
35
        },
36
    },
37
};
38
use taffy::style::{MaxTrackSizingFunction, MinTrackSizingFunction, TrackSizingFunction};
39

            
40
/// CSS reference pixels per inch (96 dpi per CSS Values spec).
41
const CSS_PX_PER_INCH: f32 = 96.0;
42

            
43
/// Convert `PixelValue` to pixels, only for absolute units (no %, and em/rem use fallback)
44
/// Used where proper resolution context is not available (grid tracks, etc.)
45
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
46
1418815
fn pixel_value_to_pixels_fallback(pv: &PixelValue) -> Option<f32> {
47
1418815
    match pv.metric {
48
1362107
        SizeMetric::Px => Some(pv.number.get()),
49
10
        SizeMetric::Pt => Some(pv.number.get() * PT_TO_PX),
50
7
        SizeMetric::In => Some(pv.number.get() * CSS_PX_PER_INCH),
51
7
        SizeMetric::Cm => Some(pv.number.get() * CSS_PX_PER_INCH / 2.54),
52
7
        SizeMetric::Mm => Some(pv.number.get() * CSS_PX_PER_INCH / 25.4),
53
        // For em/rem, use DEFAULT_FONT_SIZE as fallback (not ideal but needed without context)
54
55905
        SizeMetric::Em | SizeMetric::Rem => Some(pv.number.get() * DEFAULT_FONT_SIZE),
55
745
        SizeMetric::Percent => None, // Cannot resolve without containing block
56
        // Viewport units: Cannot resolve without viewport context
57
27
        SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => None,
58
    }
59
1418815
}
60

            
61
/// Converts an Azul `grid-template-rows` value into Taffy grid template components.
62
35
fn grid_template_rows_to_taffy(
63
35
    val: LayoutGridTemplateRowsValue,
64
35
) -> Vec<GridTemplateComponent<String>> {
65
35
    let auto_tracks = val.get_property_or_default().unwrap_or_default();
66
35
    auto_tracks
67
35
        .tracks
68
35
        .iter()
69
53
        .map(|track| GridTemplateComponent::Single(translate_track(track)))
70
35
        .collect()
71
35
}
72

            
73
/// Converts an Azul `grid-template-columns` value into Taffy grid template components.
74
35
fn grid_template_columns_to_taffy(
75
35
    val: LayoutGridTemplateColumnsValue,
76
35
) -> Vec<GridTemplateComponent<String>> {
77
35
    let auto_tracks = val.get_property_or_default().unwrap_or_default();
78
35
    auto_tracks
79
35
        .tracks
80
35
        .iter()
81
2132
        .map(|track| GridTemplateComponent::Single(translate_track(track)))
82
35
        .collect()
83
35
}
84

            
85
/// Converts an Azul `grid-auto-rows` value into Taffy min/max track sizing pairs.
86
7
fn grid_auto_rows_to_taffy(
87
7
    val: LayoutGridAutoRowsValue,
88
7
) -> Vec<taffy::MinMax<MinTrackSizingFunction, MaxTrackSizingFunction>> {
89
7
    let auto_tracks = val.get_property_or_default().unwrap_or_default();
90
7
    let tracks = auto_tracks.tracks;
91
7
    tracks
92
7
        .iter()
93
7
        .map(|track| taffy::MinMax {
94
4
            min: translate_track(track).min,
95
4
            max: translate_track(track).max,
96
4
        })
97
7
        .collect()
98
7
}
99

            
100
/// Converts an Azul `grid-auto-columns` value into Taffy track sizing functions.
101
7
fn grid_auto_columns_to_taffy(
102
7
    val: LayoutGridAutoColumnsValue,
103
7
) -> Vec<taffy::TrackSizingFunction> {
104
7
    let auto_tracks = val.get_property_or_default().unwrap_or_default();
105
7
    auto_tracks.tracks.iter().map(translate_track).collect()
106
7
}
107

            
108
#[allow(clippy::cast_precision_loss)] // bounded layout/render numeric cast
109
4554
fn translate_track(track: &GridTrackSizing) -> taffy::TrackSizingFunction {
110
    // Helper to resolve PixelValue to absolute pixels (handles em, rem, but not %)
111
    // Grid track sizing in Taffy doesn't support % - only absolute values
112
4554
    let px_to_float = |pv: PixelValue| -> f32 {
113
1157
        pixel_value_to_pixels_fallback(&pv).unwrap_or(0.0)
114
1157
    };
115

            
116
4554
    match track {
117
6
        GridTrackSizing::MinContent => minmax(
118
6
            MinTrackSizingFunction::min_content(),
119
6
            MaxTrackSizingFunction::min_content(),
120
        ),
121
70
        GridTrackSizing::MaxContent => minmax(
122
70
            MinTrackSizingFunction::max_content(),
123
70
            MaxTrackSizingFunction::max_content(),
124
        ),
125
1165
        GridTrackSizing::MinMax(minmax_box) => minmax(
126
1165
            translate_track(&minmax_box.min).min,
127
1165
            translate_track(&minmax_box.max).max,
128
        ),
129
1147
        GridTrackSizing::Fixed(px) => {
130
            // Fixed tracks: resolve em/rem to pixels
131
            // Note: % is not supported in grid track sizing (CSS Grid spec)
132
1147
            let pixels = px_to_float(*px);
133
1147
            minmax(
134
1147
                MinTrackSizingFunction::length(pixels),
135
1147
                MaxTrackSizingFunction::length(pixels),
136
            )
137
        }
138
2153
        GridTrackSizing::Fr(fr) => {
139
            // Fr units: minmax(auto, Xfr) per CSS Grid spec
140
            // The min is auto, max is the fractional value
141
            // fr is stored as i32 * 100 (e.g., 1fr = 100, 2fr = 200)
142
2153
            minmax(
143
2153
                MinTrackSizingFunction::auto(),
144
2153
                MaxTrackSizingFunction::fr(*fr as f32 / 100.0),
145
            )
146
        }
147
3
        GridTrackSizing::Auto => minmax(
148
3
            MinTrackSizingFunction::min_content(),
149
3
            MaxTrackSizingFunction::max_content(),
150
        ),
151
10
        GridTrackSizing::FitContent(px) => {
152
            // fit-content: resolve em/rem to pixels
153
10
            let pixels = px_to_float(*px);
154
10
            minmax(
155
10
                MinTrackSizingFunction::length(pixels),
156
10
                MaxTrackSizingFunction::max_content(),
157
            )
158
        }
159
    }
160
4554
}
161

            
162
4582
const fn minmax(min: MinTrackSizingFunction, max: MaxTrackSizingFunction) -> taffy::TrackSizingFunction {
163
4582
    TrackSizingFunction { min, max }
164
4582
}
165

            
166
111693
fn layout_display_to_taffy(val: LayoutDisplayValue) -> Display {
167
111693
    match val.get_property_or_default().unwrap_or_default() {
168
2
        LayoutDisplay::None => Display::None,
169
57351
        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => Display::Flex,
170
29
        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => Display::Grid,
171
54311
        _ => Display::Block,
172
    }
173
111693
}
174

            
175
// to determine their CB; Taffy's Position::Absolute handles this for both flex and grid
176
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
177
7
fn layout_position_to_taffy(val: LayoutPositionValue) -> Position {
178
7
    match val.get_property_or_default().unwrap_or_default() {
179
1
        LayoutPosition::Absolute => Position::Absolute,
180
1
        LayoutPosition::Fixed => Position::Absolute, // Taffy has no Fixed variant
181
1
        LayoutPosition::Relative => Position::Relative,
182
3
        LayoutPosition::Static => Position::Relative,
183
1
        LayoutPosition::Sticky => Position::Relative, // Sticky treated as Relative
184
    }
185
7
}
186

            
187
#[allow(clippy::cast_sign_loss)] // bounded layout/render numeric cast
188
10
fn decode_compact_grid_line(v: i16) -> GridPlacement<String> {
189
10
    if v == azul_css::compact_cache::I16_AUTO || v == azul_css::compact_cache::I16_SENTINEL {
190
3
        GridPlacement::Auto
191
7
    } else if v < 0 {
192
4
        GridPlacement::<String>::from_span((-v) as u16)
193
    } else {
194
3
        GridPlacement::<String>::from_line_index(v)
195
    }
196
10
}
197

            
198
37
fn grid_auto_flow_to_taffy(val: LayoutGridAutoFlowValue) -> GridAutoFlow {
199
37
    match val.get_property_or_default().unwrap_or_default() {
200
34
        LayoutGridAutoFlow::Row => GridAutoFlow::Row,
201
1
        LayoutGridAutoFlow::Column => GridAutoFlow::Column,
202
1
        LayoutGridAutoFlow::RowDense => GridAutoFlow::RowDense,
203
1
        LayoutGridAutoFlow::ColumnDense => GridAutoFlow::ColumnDense,
204
    }
205
37
}
206

            
207
/// Convert an azul `GridLine` (single start or end) to a Taffy `GridPlacement`.
208
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // bounded layout/render numeric cast
209
202
fn grid_line_to_taffy(
210
202
    line: &azul_css::props::layout::grid::GridLine,
211
202
) -> GridPlacement<String> {
212
    use azul_css::props::layout::grid::GridLine as AzGridLine;
213
    use taffy::style_helpers::{TaffyGridLine, TaffyGridSpan};
214
202
    match line {
215
3
        AzGridLine::Auto => GridPlacement::Auto,
216
7
        AzGridLine::Line(n) => {
217
7
            GridPlacement::<String>::from_line_index(*n as i16)
218
        }
219
6
        AzGridLine::Span(n) => GridPlacement::<String>::from_span(*n as u16),
220
186
        AzGridLine::Named(named) => {
221
            // Named lines: use the name with optional span
222
186
            let name = named.grid_line_name.as_str().to_string();
223
186
            if named.span_count > 0 {
224
3
                GridPlacement::NamedSpan(name, named.span_count as u16)
225
            } else {
226
183
                GridPlacement::NamedLine(name, 0)
227
            }
228
        }
229
    }
230
202
}
231

            
232
/// Convert an azul `GridPlacement` (grid-column / grid-row) to a Taffy `Line<GridPlacement>`.
233
92
fn grid_placement_to_taffy(
234
92
    placement: &azul_css::props::layout::grid::GridPlacement,
235
92
) -> Line<GridPlacement<String>> {
236
92
    Line {
237
92
        start: grid_line_to_taffy(&placement.grid_start),
238
92
        end: grid_line_to_taffy(&placement.grid_end),
239
92
    }
240
92
}
241

            
242
111677
fn layout_flex_direction_to_taffy(val: LayoutFlexDirectionValue) -> FlexDirection {
243
111677
    match val.get_property_or_default().unwrap_or_default() {
244
94146
        LayoutFlexDirection::Row => FlexDirection::Row,
245
1
        LayoutFlexDirection::RowReverse => FlexDirection::RowReverse,
246
17529
        LayoutFlexDirection::Column => FlexDirection::Column,
247
1
        LayoutFlexDirection::ColumnReverse => FlexDirection::ColumnReverse,
248
    }
249
111677
}
250

            
251
111676
fn layout_flex_wrap_to_taffy(val: LayoutFlexWrapValue) -> FlexWrap {
252
111676
    match val.get_property_or_default().unwrap_or_default() {
253
111665
        LayoutFlexWrap::NoWrap => FlexWrap::NoWrap,
254
10
        LayoutFlexWrap::Wrap => FlexWrap::Wrap,
255
1
        LayoutFlexWrap::WrapReverse => FlexWrap::WrapReverse,
256
    }
257
111676
}
258

            
259
111679
fn layout_align_items_to_taffy(val: LayoutAlignItemsValue) -> AlignItems {
260
111679
    match val.get_property_or_default().unwrap_or_default() {
261
68555
        LayoutAlignItems::Stretch => AlignItems::Stretch,
262
35020
        LayoutAlignItems::Center => AlignItems::Center,
263
8102
        LayoutAlignItems::Start => AlignItems::FlexStart,
264
1
        LayoutAlignItems::End => AlignItems::FlexEnd,
265
1
        LayoutAlignItems::Baseline => AlignItems::Baseline,
266
    }
267
111679
}
268

            
269
8
fn layout_align_self_to_taffy(val: LayoutAlignSelfValue) -> Option<AlignSelf> {
270
8
    match val.get_property_or_default().unwrap_or_default() {
271
3
        LayoutAlignSelf::Auto => None, // Auto means inherit from parent's align-items (for non-abspos; abspos auto computes to itself per spec)
272
1
        LayoutAlignSelf::Start => Some(AlignSelf::FlexStart),
273
1
        LayoutAlignSelf::End => Some(AlignSelf::FlexEnd),
274
1
        LayoutAlignSelf::Center => Some(AlignSelf::Center),
275
1
        LayoutAlignSelf::Baseline => Some(AlignSelf::Baseline),
276
1
        LayoutAlignSelf::Stretch => Some(AlignSelf::Stretch),
277
    }
278
8
}
279

            
280
111679
fn layout_align_content_to_taffy(val: LayoutAlignContentValue) -> AlignContent {
281
111679
    match val.get_property_or_default().unwrap_or_default() {
282
1
        LayoutAlignContent::Start => AlignContent::FlexStart,
283
1
        LayoutAlignContent::End => AlignContent::FlexEnd,
284
1
        LayoutAlignContent::Center => AlignContent::Center,
285
111674
        LayoutAlignContent::Stretch => AlignContent::Stretch,
286
1
        LayoutAlignContent::SpaceBetween => AlignContent::SpaceBetween,
287
1
        LayoutAlignContent::SpaceAround => AlignContent::SpaceAround,
288
    }
289
111679
}
290

            
291
9
fn layout_justify_content_to_taffy(val: LayoutJustifyContentValue) -> JustifyContent {
292
9
    match val.get_property_or_default().unwrap_or_default() {
293
1
        LayoutJustifyContent::FlexStart => JustifyContent::FlexStart,
294
1
        LayoutJustifyContent::FlexEnd => JustifyContent::FlexEnd,
295
2
        LayoutJustifyContent::Start => JustifyContent::Start,
296
1
        LayoutJustifyContent::End => JustifyContent::End,
297
1
        LayoutJustifyContent::Center => JustifyContent::Center,
298
1
        LayoutJustifyContent::SpaceBetween => JustifyContent::SpaceBetween,
299
1
        LayoutJustifyContent::SpaceAround => JustifyContent::SpaceAround,
300
1
        LayoutJustifyContent::SpaceEvenly => JustifyContent::SpaceEvenly,
301
    }
302
9
}
303

            
304
6
fn layout_justify_items_to_taffy(
305
6
    val: azul_css::props::property::LayoutJustifyItemsValue,
306
6
) -> AlignItems {
307
    use azul_css::props::layout::grid::LayoutJustifyItems;
308
6
    match val.get_property_or_default().unwrap_or_default() {
309
2
        LayoutJustifyItems::Start => AlignItems::Start,
310
1
        LayoutJustifyItems::End => AlignItems::End,
311
1
        LayoutJustifyItems::Center => AlignItems::Center,
312
2
        LayoutJustifyItems::Stretch => AlignItems::Stretch,
313
    }
314
6
}
315

            
316
// TODO: visibility, z_index still missing
317
// --- CSS <-> Taffy conversion functions ---
318

            
319
use std::{collections::{BTreeMap, HashMap}, sync::Arc};
320

            
321
use azul_core::{dom::NodeId, geom::LogicalSize, styled_dom::StyledDom};
322
use azul_css::props::{
323
    layout::{LayoutHeight, LayoutWidth},
324
    property::{CssProperty, CssPropertyType},
325
};
326
use taffy::{
327
    compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_leaf_layout,
328
    prelude::*, CacheTree, LayoutFlexboxContainer, LayoutGridContainer, LayoutInput, LayoutOutput,
329
    RunMode,
330
};
331

            
332
use crate::{
333
    font_traits::{FontLoaderTrait, ParsedFontTrait},
334
    solver3::{
335
        fc::{
336
            translate_taffy_point_back, translate_taffy_size_back, FloatingContext,
337
            LayoutConstraints, TextAlign as FcTextAlign,
338
        },
339
        getters::{
340
            get_align_content, get_align_items, get_css_border_bottom_width,
341
            get_css_border_left_width, get_css_border_right_width,
342
            get_css_border_top_width, get_css_box_sizing, get_css_bottom, get_css_height, get_css_left,
343
            get_css_margin_bottom, get_css_margin_left, get_css_margin_right, get_css_margin_top,
344
            get_css_max_height, get_css_max_width, get_css_min_height, get_css_min_width,
345
            get_css_padding_bottom, get_css_padding_left, get_css_padding_right,
346
            get_css_padding_top, get_css_right, get_css_top, get_css_width, get_flex_direction,
347
            get_position, MultiValue,
348
        },
349
        layout_tree::{get_display_type, LayoutTree},
350
        sizing, LayoutContext,
351
    },
352
};
353

            
354
/// Coordinate space the `taffy_content_*` arguments of
355
/// [`compute_taffy_scrollbar_info`] are measured in.
356
#[derive(Copy, Clone, PartialEq, Eq)]
357
enum ContentSizeOrigin {
358
    /// Taffy's `LayoutOutput::content_size`: child extents measured from the
359
    /// BORDER-BOX origin, so the leading border + padding ride along as an
360
    /// offset and the flex path adds the trailing padding on top.
361
    BorderBox,
362
    /// Our own BFC/IFC `overflow_size`, already relative to the content box.
363
    ContentBox,
364
}
365

            
366
/// Shared scrollbar detection for Taffy-managed flex/grid nodes.
367
///
368
/// When Taffy lays out a flex/grid container, it may expand the container
369
/// beyond the CSS-specified size (Taffy doesn't know about `overflow`).
370
/// This function resolves the CSS-constrained container size, computes
371
/// content vs. container overflow, and returns the resulting `ScrollbarRequirements`
372
/// plus the effective content size (for `overflow_content_size`).
373
///
374
/// Returns `(scrollbar_info, effective_content_width, effective_content_height)`.
375
318518
fn compute_taffy_scrollbar_info<T: ParsedFontTrait>(
376
318518
    ctx: &LayoutContext<'_, T>,
377
318518
    tree: &LayoutTree,
378
318518
    node_idx: usize,
379
318518
    result_width: f32,
380
318518
    result_height: f32,
381
318518
    taffy_content_width: f32,
382
318518
    taffy_content_height: f32,
383
318518
    content_origin: ContentSizeOrigin,
384
318518
) -> (crate::solver3::scrollbar::ScrollbarRequirements, f32, f32) {
385
    use crate::solver3::scrollbar::ScrollbarRequirements;
386

            
387
318518
    let node = tree.get(LayoutNodeId::new(node_idx));
388
318518
    let dom_id = node.and_then(|n| n.dom_node_id);
389

            
390
318518
    let Some(dom_id) = dom_id else {
391
3
        return (ScrollbarRequirements::default(), 0.0, 0.0);
392
    };
393

            
394
318515
    let styled_node_state = ctx
395
318515
        .styled_dom
396
318515
        .styled_nodes
397
318515
        .as_container()
398
318515
        .get(dom_id)
399
318515
        .map(|s| s.styled_node_state)
400
318515
        .unwrap_or_default();
401

            
402
    // Compute padding + border from the node's box_props
403
318515
    let (padding_width, padding_height, border_width, border_height, border_left, border_top) = tree
404
318515
        .get(LayoutNodeId::new(node_idx))
405
318515
        .map_or((0.0, 0.0, 0.0, 0.0, 0.0, 0.0), |node| {
406
318515
            let bp = node.box_props.unpack();
407
318515
            (
408
318515
                bp.padding.left + bp.padding.right,
409
318515
                bp.padding.top + bp.padding.bottom,
410
318515
                bp.border.left + bp.border.right,
411
318515
                bp.border.top + bp.border.bottom,
412
318515
                bp.border.left,
413
318515
                bp.border.top,
414
318515
            )
415
318515
        });
416

            
417
    // Use CSS-specified dimensions as the container constraint.
418
    // Taffy may have expanded the box beyond these, but the CSS spec says
419
    // the container clips at the specified size.
420
318515
    let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
421
318515
    let css_width = get_css_width(ctx.styled_dom, dom_id, &styled_node_state);
422

            
423
318515
    let result_content_w = result_width - padding_width - border_width;
424
318515
    let result_content_h = result_height - padding_height - border_height;
425

            
426
318515
    let css_container_w = css_width
427
318515
        .exact()
428
318515
        .and_then(|w| css_width_to_px(&w))
429
318515
        .unwrap_or(result_content_w)
430
318515
        .max(0.0);
431

            
432
318515
    let css_container_h = css_height
433
318515
        .exact()
434
318515
        .and_then(|h| css_height_to_px(&h))
435
318515
        .unwrap_or(result_content_h)
436
318515
        .max(0.0);
437

            
438
    // Content size: use the caller's content_size if non-zero,
439
    // else result size minus padding/border (Taffy expanded to fit).
440
    //
441
    // IMPORTANT: Taffy's content_size is measured from (0,0) of the BORDER box.
442
    // Child positions therefore carry border.left/top AND the leading padding as
443
    // an offset, and the flex path adds the TRAILING padding on top
444
    // (`content_size.height += content_box_inset.bottom - border.bottom`).
445
    // container_size below is a content-box size, so the whole inset has to come
446
    // off to align the coordinate spaces: subtracting only the border left a
447
    // `padding-top: 18px` container reporting content = viewport + 18px, i.e. a
448
    // phantom `overflow: auto` scrollbar whose thumb spans ~98% of the track and
449
    // whose max_scroll is 18px. Subtracting the padding SUM (leading + trailing)
450
    // leaves the pure child extent, so a container whose children fit reports
451
    // content == container (no bar) and a genuinely overflowing one keeps
452
    // max_scroll = children − content box.
453
    //
454
    // Our own BFC/IFC overflow_size is already content-box relative and must not
455
    // be adjusted at all — hence `content_origin`.
456
318515
    let (inset_w, inset_h) = match content_origin {
457
51727
        ContentSizeOrigin::BorderBox => (border_left + padding_width, border_top + padding_height),
458
266788
        ContentSizeOrigin::ContentBox => (0.0, 0.0),
459
    };
460
318515
    let content_w = if taffy_content_width > 0.0 {
461
242834
        (taffy_content_width - inset_w).max(0.0)
462
    } else {
463
75681
        result_content_w.max(0.0)
464
    };
465
318515
    let content_h = if taffy_content_height > 0.0 {
466
243108
        (taffy_content_height - inset_h).max(0.0)
467
    } else {
468
75407
        result_content_h.max(0.0)
469
    };
470

            
471
318515
    let content_size = LogicalSize::new(content_w, content_h);
472
318515
    let container_size = LogicalSize::new(css_container_w, css_container_h);
473

            
474
318515
    let scrollbar_info =
475
318515
        crate::solver3::cache::compute_scrollbar_info_core(ctx, dom_id, &styled_node_state, content_size, container_size);
476

            
477
318515
    (scrollbar_info, content_w, content_h)
478
318518
}
479

            
480
/// Convert `LayoutWidth::Px(…)` to `f32`, returning None for non-px units.
481
7968
fn css_width_to_px(w: &LayoutWidth) -> Option<f32> {
482
7968
    match w {
483
7963
        LayoutWidth::Px(px) => pixel_value_to_pixels_fallback(px),
484
5
        _ => None,
485
    }
486
7968
}
487

            
488
/// Convert `LayoutHeight::Px(…)` to `f32`, returning None for non-px units.
489
26811
fn css_height_to_px(h: &LayoutHeight) -> Option<f32> {
490
26811
    match h {
491
26806
        LayoutHeight::Px(px) => pixel_value_to_pixels_fallback(px),
492
5
        _ => None,
493
    }
494
26811
}
495

            
496
// Helper function to convert MultiValue<PixelValue> to LengthPercentageAuto
497
446712
fn multi_value_to_lpa(mv: MultiValue<PixelValue>) -> LengthPercentageAuto {
498
446712
    match mv {
499
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
500
446668
            LengthPercentageAuto::auto()
501
        }
502
44
        MultiValue::Exact(pv) => pixel_value_to_pixels_fallback(&pv)
503
44
            .map(LengthPercentageAuto::length)
504
44
            .or_else(|| {
505
5
                pv.to_percent()
506
5
                    .map(|p| LengthPercentageAuto::percent(p.get()))
507
5
            })
508
44
            .unwrap_or_else(LengthPercentageAuto::auto),
509
    }
510
446712
}
511

            
512
// Helper function to convert MultiValue<PixelValue> to LengthPercentageAuto for margins
513
// CSS spec: margin initial value is 0, but `auto` has special centering meaning in flexbox
514
446707
fn multi_value_to_lpa_margin(mv: MultiValue<PixelValue>) -> LengthPercentageAuto {
515
446707
    match mv {
516
        MultiValue::Auto => {
517
37
            LengthPercentageAuto::auto() // Preserve auto for flexbox centering
518
        }
519
        MultiValue::Initial | MultiValue::Inherit => {
520
14
            LengthPercentageAuto::length(0.0) // Margins' initial value is 0
521
        }
522
446656
        MultiValue::Exact(pv) => {
523
446656
            pixel_value_to_pixels_fallback(&pv)
524
446656
                .map(LengthPercentageAuto::length)
525
446656
                .or_else(|| {
526
2
                    pv.to_percent()
527
2
                        .map(|p| LengthPercentageAuto::percent(p.get()))
528
2
                })
529
446656
                .unwrap_or_else(|| LengthPercentageAuto::length(0.0)) // Fallback to 0 for
530
                                                                             // margins
531
        }
532
    }
533
446707
}
534

            
535
// Helper function to convert MultiValue<PixelValue> to LengthPercentage
536
893420
fn multi_value_to_lp(mv: MultiValue<PixelValue>) -> LengthPercentage {
537
893420
    match mv {
538
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
539
27
            LengthPercentage::ZERO
540
        }
541
893393
        MultiValue::Exact(pv) => pixel_value_to_pixels_fallback(&pv)
542
893393
            .map(LengthPercentage::length)
543
893393
            .or_else(|| {
544
8
                pv.to_percent()
545
8
                    .map(|p| LengthPercentage::percent(p.get()))
546
8
            })
547
893393
            .unwrap_or(LengthPercentage::ZERO),
548
    }
549
893420
}
550

            
551
// Helper function to convert plain PixelValue to LengthPercentage
552
/// Converts Azul's CSS overflow value to Taffy's Overflow enum.
553
///
554
/// Taffy only has Visible, Clip, Hidden, Scroll (no Auto).
555
/// CSS `auto` behaves like `scroll` from a layout perspective —
556
/// it constrains the container and enables scrolling.
557
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
558
223358
const fn azul_overflow_to_taffy(ov: MultiValue<azul_css::props::layout::LayoutOverflow>) -> taffy::Overflow {
559
    use azul_css::props::layout::LayoutOverflow;
560
223349
    match ov {
561
217982
        MultiValue::Exact(LayoutOverflow::Visible) => taffy::Overflow::Visible,
562
5257
        MultiValue::Exact(LayoutOverflow::Hidden) => taffy::Overflow::Hidden,
563
90
        MultiValue::Exact(LayoutOverflow::Scroll) => taffy::Overflow::Scroll,
564
19
        MultiValue::Exact(LayoutOverflow::Auto) => taffy::Overflow::Scroll, // Auto acts like scroll for layout
565
1
        MultiValue::Exact(LayoutOverflow::Clip) => taffy::Overflow::Clip,
566
9
        _ => taffy::Overflow::Visible, // default
567
    }
568
223358
}
569

            
570
5106
fn pixel_to_lp(pv: PixelValue) -> LengthPercentage {
571
5106
    pixel_value_to_pixels_fallback(&pv)
572
5106
        .map(LengthPercentage::length)
573
5106
        .or_else(|| {
574
4
            pv.to_percent()
575
4
                .map(|p| LengthPercentage::percent(p.get()))
576
4
        })
577
5106
        .unwrap_or(LengthPercentage::ZERO)
578
5106
}
579

            
580
/// Slow path for flex-basis: full property cache lookup + decode.
581
/// Extracted to avoid duplicating the logic in the compact fast-path fallback.
582
#[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
583
3
fn flex_basis_slow_path(
584
3
    cache: &azul_core::prop_cache::CssPropertyCache,
585
3
    node_data: &azul_core::dom::NodeData,
586
3
    id: &NodeId,
587
3
    node_state: &azul_core::styled_dom::StyledNodeState,
588
3
    taffy_style: &mut Style,
589
3
) -> Dimension {
590
3
    cache
591
3
        .get_property(node_data, id, node_state, &CssPropertyType::FlexBasis)
592
3
        .and_then(|p| {
593
            if let CssProperty::FlexBasis(v) = p {
594
                let basis = match v.get_property_or_default().unwrap_or_default() {
595
                    LayoutFlexBasis::Auto => Dimension::auto(),
596
                    LayoutFlexBasis::Exact(pv) => pixel_value_to_pixels_fallback(&pv)
597
                        .map(Dimension::length)
598
                        .or_else(|| pv.to_percent().map(|p| Dimension::percent(p.get())))
599
                        .unwrap_or_else(Dimension::auto),
600
                };
601
                // WORKAROUND: If flex-basis is set and not auto, clear width to let flex-basis
602
                // take precedence. Workaround for Taffy not properly prioritizing flex-basis over width
603
                if !matches!(basis, auto if auto == Dimension::auto()) {
604
                    taffy_style.size.width = Dimension::auto();
605
                }
606
                Some(basis)
607
            } else {
608
                None
609
            }
610
        })
611
3
        .unwrap_or_else(Dimension::auto)
612
3
}
613

            
614
/// The bridge struct that implements Taffy's traits.
615
/// It holds mutable references to the solver's data structures, allowing Taffy
616
/// to read styles and write layout results back into our `LayoutTree`.
617
struct TaffyBridge<'a, 'b, T: ParsedFontTrait> {
618
    ctx: &'a mut LayoutContext<'b, T>,
619
    tree: &'a mut LayoutTree,
620
    /// Raw pointer to text cache - needed because we can't have multiple &mut references
621
    /// SAFETY: This pointer is only valid for the lifetime of the `TaffyBridge`
622
    /// and must only be used within `compute_child_layout` callbacks
623
    text_cache: *mut crate::font_traits::TextLayoutCache,
624
    /// Heap-pinned `CalcResolveContext`s whose addresses are passed into taffy
625
    /// `Dimension::calc(ptr)`. Kept alive for the duration of the layout pass.
626
    /// Uses `RefCell` because `get_core_container_style` takes `&self`.
627
    // Box gives each CalcResolveContext a stable heap address for the `*const` handed to
628
    // taffy `Dimension::calc()`; a plain Vec<T> would invalidate those pointers on realloc.
629
    #[allow(clippy::vec_box)]
630
    calc_storage: std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
631
    /// Memoised `translate_style_to_taffy` results, keyed by DOM node id
632
    /// (`usize` = `NodeId::index`). Taffy calls
633
    /// `get_core_container_style` and `should_suppress_cross_intrinsic`
634
    /// many times per node during a single layout pass; each call
635
    /// triggers ~13 `cache.get_property` cascade walks for grid/flex
636
    /// props. Caching the built `Style` cuts this to one build per node.
637
    style_memo: std::cell::RefCell<HashMap<usize, Style>>,
638
}
639

            
640
impl<'a, 'b, T: ParsedFontTrait> TaffyBridge<'a, 'b, T> {
641
1937
    fn new(
642
1937
        ctx: &'a mut LayoutContext<'b, T>,
643
1937
        tree: &'a mut LayoutTree,
644
1937
        text_cache: *mut crate::font_traits::TextLayoutCache,
645
1937
    ) -> Self {
646
1937
        Self {
647
1937
            ctx,
648
1937
            tree,
649
1937
            text_cache,
650
1937
            calc_storage: std::cell::RefCell::new(Vec::new()),
651
1937
            style_memo: std::cell::RefCell::new(HashMap::new()),
652
1937
        }
653
1937
    }
654

            
655
    /// Cache-backed wrapper for `translate_style_to_taffy`. Returns a
656
    /// clone of the memoised `Style` on cache hit, builds + inserts on
657
    /// miss. Keyed by DOM node index (not tree index) because the
658
    /// result depends only on the styled DOM, not on the transient
659
    /// layout tree.
660
9549816
    fn translate_style_to_taffy_cached(&self, dom_id: Option<NodeId>) -> Style {
661
9549816
        let Some(id) = dom_id else {
662
            return Style::default();
663
        };
664
9549816
        let key = id.index();
665
9549816
        if let Some(style) = self.style_memo.borrow().get(&key) {
666
9438429
            return style.clone();
667
111387
        }
668
111387
        let style = self.translate_style_to_taffy(dom_id);
669
111387
        self.style_memo.borrow_mut().insert(key, style.clone());
670
111387
        style
671
9549816
    }
672

            
673
    /// Translates CSS properties from the `StyledDom` into a `taffy::Style` struct.
674
    /// This is the core of the integration, mapping one style system to another.
675
    #[allow(clippy::field_reassign_with_default)] // struct built incrementally / test setup; a struct literal is not clearer here
676
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
677
111387
    fn translate_style_to_taffy(&self, dom_id: Option<NodeId>) -> Style {
678
111387
        let Some(id) = dom_id else {
679
            return Style::default();
680
        };
681
111387
        let styled_dom = &self.ctx.styled_dom;
682
111387
        let node_data = &styled_dom.node_data.as_ref()[id.index()];
683
111387
        let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
684
111387
        let cache = &styled_dom.css_property_cache.ptr;
685
111387
        let mut taffy_style = Style::default();
686

            
687
        // Box Sizing — CSS default is content-box, but Taffy defaults to border-box
688
111387
        taffy_style.box_sizing = match get_css_box_sizing(styled_dom, id, node_state).unwrap_or_default() {
689
43092
            azul_css::props::layout::LayoutBoxSizing::BorderBox => BoxSizing::BorderBox,
690
68295
            azul_css::props::layout::LayoutBoxSizing::ContentBox => BoxSizing::ContentBox,
691
        };
692

            
693
        // Display Mode
694
111387
        taffy_style.display =
695
111387
            layout_display_to_taffy(CssPropertyValue::Exact(get_display_type(styled_dom, id)));
696

            
697
        // Position
698
111387
        taffy_style.position =
699
111387
            from_layout_position(get_position(styled_dom, id, node_state).unwrap_or_default());
700

            
701
        // Inset (top, left, bottom, right)
702
111387
        taffy_style.inset = Rect {
703
111387
            left: multi_value_to_lpa(get_css_left(styled_dom, id, node_state)),
704
111387
            right: multi_value_to_lpa(get_css_right(styled_dom, id, node_state)),
705
111387
            top: multi_value_to_lpa(get_css_top(styled_dom, id, node_state)),
706
111387
            bottom: multi_value_to_lpa(get_css_bottom(styled_dom, id, node_state)),
707
111387
        };
708

            
709
        // Size
710
111387
        let width = get_css_width(self.ctx.styled_dom, id, node_state);
711
111387
        let height = get_css_height(self.ctx.styled_dom, id, node_state);
712

            
713
        // Resolve node-local font sizes for calc() em/rem resolution
714
111387
        let em_size = crate::solver3::getters::get_element_font_size(styled_dom, id, node_state);
715
111387
        let rem_size = {
716
111387
            let root_id = NodeId::new(0);
717
111387
            let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
718
111387
            crate::solver3::getters::get_element_font_size(styled_dom, root_id, root_state)
719
        };
720

            
721
111387
        let taffy_width = from_layout_width(width.unwrap_or_default(), &self.calc_storage, em_size, rem_size);
722
111387
        let taffy_height = from_layout_height(height.unwrap_or_default(), &self.calc_storage, em_size, rem_size);
723

            
724
111387
        taffy_style.size = Size {
725
111387
            width: taffy_width,
726
111387
            height: taffy_height,
727
111387
        };
728

            
729
        // Overflow — CRITICAL for scroll containers.
730
        // Without this, Taffy's flexbox algorithm uses content size as automatic
731
        // minimum size, causing flex containers with overflow:auto/scroll to
732
        // expand to fit all content instead of clipping at the explicit size.
733
        // With overflow: Hidden/Scroll, Taffy sets automatic min size to 0 and
734
        // constrains the container.
735
111387
        let overflow_x = get_overflow_x(styled_dom, id, node_state);
736
111387
        let overflow_y = get_overflow_y(styled_dom, id, node_state);
737
111387
        taffy_style.overflow = taffy::Point {
738
111387
            x: azul_overflow_to_taffy(overflow_x),
739
111387
            y: azul_overflow_to_taffy(overflow_y),
740
111387
        };
741

            
742
        // Forward CSS aspect-ratio to taffy so flex/grid items honor it (and taffy's
743
        // transferred min-size suggestion works). AspectRatioValue stores width and
744
        // height ×1000, so the preferred ratio is simply width/height.
745
        #[allow(clippy::cast_precision_loss)] // small integer aspect-ratio components (e.g. 2000/1000)
746
        {
747
111387
            taffy_style.aspect_ratio = match crate::solver3::getters::get_aspect_ratio_property(
748
111387
                styled_dom, id, node_state,
749
            ) {
750
                MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(ar))
751
                    if ar.height != 0 =>
752
                {
753
                    Some(ar.width as f32 / ar.height as f32)
754
                }
755
111387
                _ => None,
756
            };
757
        }
758

            
759
        // Min/Max Size
760
        // min-size:auto enables Taffy's auto minimum size algorithm which computes the
761
        // content size suggestion (min-content in main axis) and transferred size suggestion
762
        // (cross size converted through aspect ratio, if any). NOTE: aspect_ratio is not yet
763
        // forwarded to Taffy, so the transferred size suggestion path is incomplete.
764
        // NOTE: In CSS, the default min-width/min-height for flex items is `auto`
765
        // (which resolves to `min-content`), preventing them from shrinking below
766
        // their content size. We must map Auto to Dimension::Auto, NOT to 0px.
767
111387
        let min_width_css = get_css_min_width(styled_dom, id, node_state);
768
111387
        let min_height_css = get_css_min_height(styled_dom, id, node_state);
769

            
770
        taffy_style.min_size = Size {
771
111387
            width: match min_width_css {
772
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
773
106374
                    Dimension::auto()
774
                }
775
5013
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
776
            },
777
111387
            height: match min_height_css {
778
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
779
111315
                    Dimension::auto()
780
                }
781
72
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
782
            },
783
        };
784

            
785
        // For max-size, we need to handle Auto specially - it should translate to Taffy's auto, not
786
        // a concrete value This is CRITICAL for flexbox stretch to work: items with
787
        // max-height: auto CAN be stretched
788
111387
        let max_width_css = get_css_max_width(styled_dom, id, node_state);
789
111387
        let max_height_css = get_css_max_height(styled_dom, id, node_state);
790

            
791
        taffy_style.max_size = Size {
792
111387
            width: match max_width_css {
793
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
794
111378
                    Dimension::auto()
795
                }
796
9
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
797
            },
798
111387
            height: match max_height_css {
799
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
800
111387
                    Dimension::auto()
801
                }
802
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
803
            },
804
        };
805

            
806
        // Box Model (margin, padding, border)
807
111387
        let margin_left_css = get_css_margin_left(styled_dom, id, node_state);
808
111387
        let margin_right_css = get_css_margin_right(styled_dom, id, node_state);
809
111387
        let margin_top_css = get_css_margin_top(styled_dom, id, node_state);
810
111387
        let margin_bottom_css = get_css_margin_bottom(styled_dom, id, node_state);
811

            
812
111387
        taffy_style.margin = Rect {
813
111387
            left: multi_value_to_lpa_margin(margin_left_css),
814
111387
            right: multi_value_to_lpa_margin(margin_right_css),
815
111387
            top: multi_value_to_lpa_margin(margin_top_css),
816
111387
            bottom: multi_value_to_lpa_margin(margin_bottom_css),
817
111387
        };
818

            
819
111387
        taffy_style.padding = Rect {
820
111387
            left: multi_value_to_lp(get_css_padding_left(styled_dom, id, node_state)),
821
111387
            right: multi_value_to_lp(get_css_padding_right(styled_dom, id, node_state)),
822
111387
            top: multi_value_to_lp(get_css_padding_top(styled_dom, id, node_state)),
823
111387
            bottom: multi_value_to_lp(get_css_padding_bottom(styled_dom, id, node_state)),
824
111387
        };
825

            
826
111387
        taffy_style.border = Rect {
827
111387
            left: multi_value_to_lp(get_css_border_left_width(styled_dom, id, node_state)),
828
111387
            right: multi_value_to_lp(get_css_border_right_width(styled_dom, id, node_state)),
829
111387
            top: multi_value_to_lp(get_css_border_top_width(styled_dom, id, node_state)),
830
111387
            bottom: multi_value_to_lp(get_css_border_bottom_width(styled_dom, id, node_state)),
831
111387
        };
832

            
833
        // Grid & gap properties — COMPACT FAST PATH: row_gap/column_gap are
834
        // i16 px × 10 in tier2_dims. The slow-path lookup would walk the
835
        // cascade for every node even though the answer is already encoded.
836
111387
        taffy_style.gap = cache.compact_cache.as_ref().map_or_else(|| cache
837
                .get_property(node_data, &id, node_state, &CssPropertyType::Gap)
838
                .and_then(|p| if let CssProperty::Gap(v) = p { Some(v) } else { None })
839
                .map_or_else(Size::zero, |v| {
840
                    let val = v.get_property_or_default().unwrap_or_default().inner;
841
                    let gap_lp = pixel_to_lp(val);
842
                    Size { width: gap_lp, height: gap_lp }
843
111387
                }), |cc| {
844
111387
            let row = cc.tier2_dims[id.index()].row_gap;
845
111387
            let col = cc.tier2_dims[id.index()].column_gap;
846
222774
            let decode = |raw: i16| -> LengthPercentage {
847
222774
                if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
848
                    LengthPercentage::length(0.0)
849
                } else {
850
222774
                    LengthPercentage::length(f32::from(raw) / 10.0)
851
                }
852
222774
            };
853
111387
            Size {
854
111387
                width: decode(col),
855
111387
                height: decode(row),
856
111387
            }
857
111387
        });
858

            
859
        // Skip grid properties when not in a grid context.
860
        // Grid container props: only if this node has display:grid.
861
        // Grid item props: only if parent has display:grid.
862
111387
        let (self_is_grid, parent_is_grid) = cache.compact_cache.as_ref().map_or((false, false), |cc| {
863
            #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
864
            use azul_css::compact_cache::*;
865
111387
            let self_t1 = cc.tier1_enums[id.index()];
866
111387
            let self_display = ((self_t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8;
867
111387
            let grid_val = layout_display_to_u8(LayoutDisplay::Grid);
868
111387
            let self_grid = self_display == grid_val;
869

            
870
111387
            let parent_idx = styled_dom.node_hierarchy.as_ref()[id.index()].parent_id()
871
111387
                .map_or(0, |p| p.index());
872
111387
            let parent_t1 = cc.tier1_enums[parent_idx];
873
111387
            let parent_display = ((parent_t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8;
874
111387
            let parent_grid = parent_display == grid_val;
875
111387
            (self_grid, parent_grid)
876
111387
        });
877

            
878
111387
        if self_is_grid {
879
27
        taffy_style.grid_template_rows = cache
880
27
            .get_property(
881
27
                node_data,
882
27
                &id,
883
27
                node_state,
884
27
                &CssPropertyType::GridTemplateRows,
885
27
            )
886
27
            .and_then(|p| {
887
27
                if let CssProperty::GridTemplateRows(v) = p {
888
27
                    Some(v.clone())
889
                } else {
890
                    None
891
                }
892
27
            })
893
27
            .map(grid_template_rows_to_taffy)
894
27
            .unwrap_or_default();
895

            
896
        // Grid template columns - convert GridTemplate to Vec<GridTemplateComponent>
897
27
        taffy_style.grid_template_columns = cache
898
27
            .get_property(
899
27
                node_data,
900
27
                &id,
901
27
                node_state,
902
27
                &CssPropertyType::GridTemplateColumns,
903
27
            )
904
27
            .and_then(|p| {
905
27
                if let CssProperty::GridTemplateColumns(v) = p {
906
27
                    Some(v.clone())
907
                } else {
908
                    None
909
                }
910
27
            })
911
27
            .map(grid_template_columns_to_taffy)
912
27
            .unwrap_or_default();
913

            
914
        // Grid template areas - convert GridTemplateAreas to Vec<taffy::GridTemplateArea<String>>
915
27
        taffy_style.grid_template_areas = cache
916
27
            .get_property(
917
27
                node_data,
918
27
                &id,
919
27
                node_state,
920
27
                &CssPropertyType::GridTemplateAreas,
921
27
            )
922
27
            .and_then(|p| {
923
9
                if let CssProperty::GridTemplateAreas(v) = p {
924
9
                    v.get_property().cloned()
925
                } else {
926
                    None
927
                }
928
9
            })
929
27
            .map(|areas| {
930
9
                areas
931
9
                    .areas
932
9
                    .as_ref()
933
9
                    .iter()
934
9
                    .map(|a| taffy::GridTemplateArea {
935
45
                        name: a.name.as_str().to_string(),
936
45
                        row_start: a.row_start,
937
45
                        row_end: a.row_end,
938
45
                        column_start: a.column_start,
939
45
                        column_end: a.column_end,
940
45
                    })
941
9
                    .collect::<Vec<_>>()
942
9
            })
943
27
            .unwrap_or_default();
944

            
945
27
        taffy_style.grid_auto_rows = cache
946
27
            .get_property(node_data, &id, node_state, &CssPropertyType::GridAutoRows)
947
27
            .and_then(|p| {
948
                if let CssProperty::GridAutoRows(v) = p {
949
                    Some(v.clone())
950
                } else {
951
                    None
952
                }
953
            })
954
27
            .map(grid_auto_rows_to_taffy)
955
27
            .unwrap_or_default();
956

            
957
27
        taffy_style.grid_auto_columns = cache
958
27
            .get_property(
959
27
                node_data,
960
27
                &id,
961
27
                node_state,
962
27
                &CssPropertyType::GridAutoColumns,
963
27
            )
964
27
            .and_then(|p| {
965
                if let CssProperty::GridAutoColumns(v) = p {
966
                    Some(v.clone())
967
                } else {
968
                    None
969
                }
970
            })
971
27
            .map(grid_auto_columns_to_taffy)
972
27
            .unwrap_or_default();
973

            
974
27
        taffy_style.grid_auto_flow = cache.compact_cache.as_ref().map_or_else(|| cache
975
                .get_property(node_data, &id, node_state, &CssPropertyType::GridAutoFlow)
976
                .and_then(|p| if let CssProperty::GridAutoFlow(v) = p { Some(*v) } else { None })
977
                .map(grid_auto_flow_to_taffy)
978
27
                .unwrap_or_default(), |cc| {
979
            #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
980
            use azul_css::compact_cache::*;
981
27
            let bits = ((cc.tier1_enums[id.index()] >> GRID_AUTO_FLOW_SHIFT) & GRID_AUTO_FLOW_MASK) as u8;
982
27
            let val = layout_grid_auto_flow_from_u8(bits);
983
27
            grid_auto_flow_to_taffy(CssPropertyValue::Exact(val))
984
27
        });
985

            
986
111360
        } // end if self_is_grid
987

            
988
111387
        if parent_is_grid {
989
        // Grid item placement. The compact cold cache holds Auto/Line/Span as
990
        // an i16; NAMED lines and areas (`grid-area: header`) cannot be
991
        // encoded there and are stored as I16_SENTINEL — for those the REAL
992
        // property must be consulted, per axis. Decoding the sentinel as
993
        // Auto silently discarded every named placement, which auto-flowed
994
        // all grid-template-areas layouts (grid-template-areas-001).
995
99
        let slow_grid_column = || {
996
45
            cache
997
45
                .get_property(node_data, &id, node_state, &CssPropertyType::GridColumn)
998
45
                .and_then(|p| if let CssProperty::GridColumn(v) = p { v.get_property().cloned() } else { None })
999
45
        };
99
        let slow_grid_row = || {
45
            cache
45
                .get_property(node_data, &id, node_state, &CssPropertyType::GridRow)
45
                .and_then(|p| if let CssProperty::GridRow(v) = p { v.get_property().cloned() } else { None })
45
        };
99
        if let Some(cc) = cache.compact_cache.as_ref() {
            use azul_css::compact_cache::{I16_AUTO, I16_SENTINEL};
99
            let cold = &cc.tier2_cold[id.index()];
99
            let (cs, ce) = (cold.grid_col_start, cold.grid_col_end);
99
            if cs == I16_SENTINEL || ce == I16_SENTINEL {
45
                if let Some(grid_col) = slow_grid_column() {
45
                    taffy_style.grid_column = grid_placement_to_taffy(&grid_col);
45
                }
54
            } else if cs != I16_AUTO || ce != I16_AUTO {
                taffy_style.grid_column = Line { start: decode_compact_grid_line(cs), end: decode_compact_grid_line(ce) };
54
            }
99
            let (rs, re) = (cold.grid_row_start, cold.grid_row_end);
99
            if rs == I16_SENTINEL || re == I16_SENTINEL {
45
                if let Some(grid_row) = slow_grid_row() {
45
                    taffy_style.grid_row = grid_placement_to_taffy(&grid_row);
45
                }
54
            } else if rs != I16_AUTO || re != I16_AUTO {
                taffy_style.grid_row = Line { start: decode_compact_grid_line(rs), end: decode_compact_grid_line(re) };
54
            }
        } else {
            if let Some(grid_col) = slow_grid_column() {
                taffy_style.grid_column = grid_placement_to_taffy(&grid_col);
            }
            if let Some(grid_row) = slow_grid_row() {
                taffy_style.grid_row = grid_placement_to_taffy(&grid_row);
            }
        }
111288
        } // end if parent_is_grid
        // Flexbox
111387
        taffy_style.flex_direction = match get_flex_direction(styled_dom, id, node_state) {
111384
            MultiValue::Exact(v) => layout_flex_direction_to_taffy(CssPropertyValue::Exact(v)),
3
            _ => FlexDirection::Row,
        };
        // COMPACT FAST PATH: flex_wrap is Tier 1 enum
        taffy_style.flex_wrap = {
111387
            let compact = if node_state.is_normal() {
111384
                cache.compact_cache.as_ref().map(|cc| {
111384
                    layout_flex_wrap_to_taffy(CssPropertyValue::Exact(cc.get_flex_wrap(id.index())))
111384
                })
            } else {
3
                None
            };
111387
            compact.unwrap_or_else(|| {
3
                cache
3
                    .get_property(node_data, &id, node_state, &CssPropertyType::FlexWrap)
3
                    .and_then(|p| if let CssProperty::FlexWrap(v) = p { Some(*v) } else { None })
3
                    .map_or(FlexWrap::NoWrap, layout_flex_wrap_to_taffy)
3
            })
        };
111387
        taffy_style.align_items = match get_align_items(styled_dom, id, node_state) {
111384
            MultiValue::Exact(v) => Some(layout_align_items_to_taffy(CssPropertyValue::Exact(v))),
3
            _ => None,
        };
                // CSS spec: default align-items is "normal" which acts like "stretch"
                // for non-replaced grid/flex items. Taffy handles this internally when
                // align_items is None, so we should NOT force a default here.
111387
        taffy_style.justify_items = cache.compact_cache.as_ref().map_or_else(|| cache
                .get_property(node_data, &id, node_state, &CssPropertyType::JustifyItems)
                .and_then(|p| if let CssProperty::JustifyItems(v) = p { Some(*v) } else { None })
111387
                .map(layout_justify_items_to_taffy), |cc| {
            #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
            use azul_css::compact_cache::*;
            use azul_css::props::layout::grid::LayoutJustifyItems;
111387
            let bits = ((cc.tier1_enums[id.index()] >> JUSTIFY_ITEMS_SHIFT) & JUSTIFY_ITEMS_MASK) as u8;
111387
            let val = layout_justify_items_from_u8(bits);
111387
            Some(match val {
                LayoutJustifyItems::Start => AlignItems::Start,
                LayoutJustifyItems::End => AlignItems::End,
                LayoutJustifyItems::Center => AlignItems::Center,
111387
                LayoutJustifyItems::Stretch => AlignItems::Stretch,
            })
111387
        });
        // COMPACT FAST PATH: justify-content is in tier1 bits 21-23.
111387
        taffy_style.justify_content = cache.compact_cache.as_ref().map_or_else(|| cache
                .get_property(node_data, &id, node_state, &CssPropertyType::JustifyContent)
                .and_then(|p| if let CssProperty::JustifyContent(v) = p { Some(v) } else { None })
111387
                .map(|v| layout_justify_content_to_taffy(*v)), |cc| {
            #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
            use azul_css::compact_cache::*;
            use azul_css::props::layout::LayoutJustifyContent;
111387
            let bits = ((cc.tier1_enums[id.index()] >> JUSTIFY_CONTENT_SHIFT) & JUSTIFY_MASK) as u8;
111387
            Some(match layout_justify_content_from_u8(bits) {
102585
                LayoutJustifyContent::FlexStart => JustifyContent::FlexStart,
                LayoutJustifyContent::FlexEnd => JustifyContent::FlexEnd,
                LayoutJustifyContent::Start => JustifyContent::Start,
                LayoutJustifyContent::End => JustifyContent::End,
8739
                LayoutJustifyContent::Center => JustifyContent::Center,
63
                LayoutJustifyContent::SpaceBetween => JustifyContent::SpaceBetween,
                LayoutJustifyContent::SpaceAround => JustifyContent::SpaceAround,
                LayoutJustifyContent::SpaceEvenly => JustifyContent::SpaceEvenly,
            })
111387
        });
                // CSS spec: default justify-content is "normal". Taffy handles
                // this internally when justify_content is None.
        // COMPACT FAST PATH: flex_grow stored as u16 × 100
        taffy_style.flex_grow = {
111387
            let compact = if node_state.is_normal() {
111384
                cache.compact_cache.as_ref().and_then(|cc| cc.get_flex_grow(id.index()))
            } else {
3
                None
            };
111387
            compact.unwrap_or_else(|| {
3
                cache
3
                    .get_property(node_data, &id, node_state, &CssPropertyType::FlexGrow)
3
                    .and_then(|p| if let CssProperty::FlexGrow(v) = p {
                        Some(v.get_property_or_default().unwrap_or_default().inner.get())
                    } else { None })
3
                    .unwrap_or(0.0)
3
            })
        };
        // COMPACT FAST PATH: flex_shrink stored as u16 × 100
        taffy_style.flex_shrink = {
111387
            let compact = if node_state.is_normal() {
111384
                cache.compact_cache.as_ref().and_then(|cc| cc.get_flex_shrink(id.index()))
            } else {
3
                None
            };
111387
            compact.unwrap_or_else(|| {
3
                cache
3
                    .get_property(node_data, &id, node_state, &CssPropertyType::FlexShrink)
3
                    .and_then(|p| if let CssProperty::FlexShrink(v) = p {
                        Some(v.get_property_or_default().unwrap_or_default().inner.get())
                    } else { None })
3
                    .unwrap_or(1.0)
3
            })
        };
        // COMPACT FAST PATH: flex_basis stored as u32 with PixelValue encoding
        taffy_style.flex_basis = {
111387
            let compact = if node_state.is_normal() {
111384
                cache.compact_cache.as_ref().and_then(|cc| {
111384
                    let raw = cc.get_flex_basis_raw(id.index());
111384
                    match raw {
                        azul_css::compact_cache::U32_AUTO
                        | azul_css::compact_cache::U32_NONE
111354
                        | azul_css::compact_cache::U32_INITIAL => Some(Dimension::auto()),
                        azul_css::compact_cache::U32_SENTINEL
                        | azul_css::compact_cache::U32_INHERIT => None,
                        _ => {
30
                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
30
                                let basis = pixel_value_to_pixels_fallback(&pv)
30
                                    .map(Dimension::length)
30
                                    .or_else(|| pv.to_percent().map(|p| Dimension::percent(p.get())))
30
                                    .unwrap_or_else(Dimension::auto);
30
                                if !matches!(basis, auto if auto == Dimension::auto()) {
30
                                    taffy_style.size.width = Dimension::auto();
30
                                }
30
                                Some(basis)
                            } else {
                                Some(Dimension::auto())
                            }
                        }
                    }
111384
                })
            } else {
3
                None
            };
111387
            compact.unwrap_or_else(|| {
3
                flex_basis_slow_path(cache, node_data, &id, node_state, &mut taffy_style)
3
            })
        };
111387
        taffy_style.align_self = cache.compact_cache.as_ref().map_or_else(|| cache
                .get_property(node_data, &id, node_state, &CssPropertyType::AlignSelf)
111387
                .and_then(|p| if let CssProperty::AlignSelf(v) = p { layout_align_self_to_taffy(*v) } else { None }), |cc| {
            #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
            use azul_css::compact_cache::*;
111387
            let bits = ((cc.tier1_enums[id.index()] >> ALIGN_SELF_SHIFT) & ALIGN_SELF_MASK) as u8;
111387
            let val = layout_align_self_from_u8(bits);
111387
            match val {
111377
                LayoutAlignSelf::Auto => None,
                LayoutAlignSelf::Start => Some(AlignSelf::FlexStart),
9
                LayoutAlignSelf::End => Some(AlignSelf::FlexEnd),
                LayoutAlignSelf::Center => Some(AlignSelf::Center),
                LayoutAlignSelf::Baseline => Some(AlignSelf::Baseline),
1
                LayoutAlignSelf::Stretch => Some(AlignSelf::Stretch),
            }
111387
        });
111387
        taffy_style.justify_self = cache.compact_cache.as_ref().map_or_else(|| cache
                .get_property(node_data, &id, node_state, &CssPropertyType::JustifySelf)
                .and_then(|p| if let CssProperty::JustifySelf(v) = p {
                    use azul_css::props::layout::grid::LayoutJustifySelf;
                    match v.get_property_or_default().unwrap_or_default() {
                        LayoutJustifySelf::Auto => None,
                        LayoutJustifySelf::Start => Some(AlignSelf::Start),
                        LayoutJustifySelf::End => Some(AlignSelf::End),
                        LayoutJustifySelf::Center => Some(AlignSelf::Center),
                        LayoutJustifySelf::Stretch => Some(AlignSelf::Stretch),
                    }
111387
                } else { None }), |cc| {
            #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
            use azul_css::compact_cache::*;
            use azul_css::props::layout::grid::LayoutJustifySelf;
111387
            let bits = ((cc.tier1_enums[id.index()] >> JUSTIFY_SELF_SHIFT) & JUSTIFY_SELF_MASK) as u8;
111387
            let val = layout_justify_self_from_u8(bits);
111387
            match val {
111387
                LayoutJustifySelf::Auto => None,
                LayoutJustifySelf::Start => Some(AlignSelf::Start),
                LayoutJustifySelf::End => Some(AlignSelf::End),
                LayoutJustifySelf::Center => Some(AlignSelf::Center),
                LayoutJustifySelf::Stretch => Some(AlignSelf::Stretch),
            }
111387
        });
111387
        taffy_style.align_content = match get_align_content(styled_dom, id, node_state) {
111384
            MultiValue::Exact(v) => Some(layout_align_content_to_taffy(CssPropertyValue::Exact(v))),
3
            _ => None,
        };
                // CSS spec: default align-content is "normal". Taffy handles
                // this internally when align_content is None.
111387
        taffy_style
111387
    }
    /// Gets or computes the Taffy style for a given node index.
4778009
    fn get_taffy_style(&self, node_idx: usize) -> Style {
4778009
        let dom_id = self.tree.get(LayoutNodeId::new(node_idx)).and_then(|n| n.dom_node_id);
4778009
        let mut style = self.translate_style_to_taffy_cached(dom_id);
        // CSS 2.1 § 10.3.3: Root element margin handling for Flex/Grid.
        //
        // The root element's margin is already resolved and subtracted from
        // available_size by calculate_used_size_for_node() (sizing.rs). The
        // resulting margin-adjusted size is passed to Taffy as known_dimensions.
        //
        // Taffy's layout algorithm reads margin from the style and subtracts it
        // from known_dimensions internally. If we also pass the margin through
        // the style, it gets subtracted twice:
        //   1. calculate_used_size_for_node: viewport - margin → available_size
        //   2. Taffy: known_dimensions - style.margin → content_area
        //
        // Zeroing the style margin for root nodes prevents this double-subtraction.
        // This is NOT a hack — it's the correct integration point between Azul's
        // BFC-level sizing and Taffy's Flex/Grid algorithm.
4778009
        let is_root = self.tree.get(LayoutNodeId::new(node_idx)).is_some_and(|n| n.parent.is_none());
4778009
        if is_root {
1183
            style.margin = Rect::zero();
4776826
        }
        // FIX: Apply cross-axis intrinsic size suppression for stretch alignment.
        // This enables align-self: stretch to work correctly by ensuring Taffy
        // sees the cross-axis size as Auto (allowing stretch) rather than a definite value.
4778009
        let (suppress_width, suppress_height) = self.should_suppress_cross_intrinsic(node_idx, &style);
4778009
        if suppress_width {
687372
            // Force width to Auto and set min-width to 0 to allow stretching.
687372
            // Taffy treats Auto size + Stretch alignment as a signal to fill the container.
687372
            style.size.width = Dimension::auto(); 
687372
            style.min_size.width = Dimension::length(0.0);
4090637
        }
4778009
        if suppress_height {
855885
            style.size.height = Dimension::auto();
855885
            style.min_size.height = Dimension::length(0.0);
3922124
        }
4778009
        style
4778009
    }
    /// Determines if cross-axis intrinsic size should be suppressed for stretching.
    ///
    /// Per CSS Flexbox spec, align-items: stretch makes items fill the cross-axis
    /// ONLY if the item's cross-size is 'auto' AND the item has no intrinsic cross-size.
    ///
    /// Returns (`suppress_width`, `suppress_height`) booleans.
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
4778009
    fn should_suppress_cross_intrinsic(&self, node_idx: usize, style: &Style) -> (bool, bool) {
4778009
        let Some(node) = self.tree.get(LayoutNodeId::new(node_idx)) else {
            return (false, false);
        };
        // Check if parent is a flex or grid container
4778009
        let Some(parent_fc) = self.tree.warm(LayoutNodeId::new(node_idx)).and_then(|w| w.parent_formatting_context) else {
1183
            return (false, false);
        };
4776826
        match parent_fc {
            FormattingContext::Flex => {
                // Get parent node to check its flex-direction and align-items
4771807
                let Some(parent_idx) = node.parent else {
                    return (false, false);
                };
4771807
                let parent_dom_id = self.tree.get(LayoutNodeId::new(parent_idx)).and_then(|n| n.dom_node_id);
4771807
                let parent_style = self.translate_style_to_taffy_cached(parent_dom_id);
                // Determine if flex container is row or column
4771807
                let is_row = matches!(
4771807
                    parent_style.flex_direction,
                    FlexDirection::Row | FlexDirection::RowReverse
                );
                // Get effective align value for this item
                // align-self overrides parent's align-items
4771807
                let align = style
4771807
                    .align_self
4771807
                    .or(parent_style.align_items)
4771807
                    .unwrap_or(AlignSelf::Stretch);
4771807
                let should_stretch = matches!(align, AlignSelf::Stretch);
4771807
                if !should_stretch {
3065751
                    return (false, false);
1706056
                }
                // Check if cross-axis size is auto
                // For row flex: cross-axis is height
                // For column flex: cross-axis is width
1706056
                let cross_size_is_auto = if is_row {
929141
                    style.size.height == Dimension::auto()
                } else {
776915
                    style.size.width == Dimension::auto()
                };
1706056
                if !cross_size_is_auto {
162799
                    return (false, false);
1543257
                }
                // All conditions met: suppress intrinsic cross-size
1543257
                if is_row {
855885
                    (false, true) // Suppress height for row flex
                } else {
687372
                    (true, false) // Suppress width for column flex
                }
            }
            FormattingContext::Grid => {
                // TODO: Implement grid stretch detection
                // Grid is more complex because:
                // 1. Default align-items is 'start', not 'stretch'
                // 2. Items can stretch in both axes simultaneously
                // 3. Need to check grid-auto-flow and track sizing
1044
                (false, false)
            }
3975
            _ => (false, false),
        }
4778009
    }
    /// Helper to get children that participate in layout (i.e., not `display: none`).
695789
    fn get_layout_children(&self, node_idx: usize) -> Vec<usize> {
        use crate::solver3::getters::{get_display_property, MultiValue};
695789
        let Some(node) = self.tree.get(LayoutNodeId::new(node_idx)) else {
            return Vec::new();
        };
695789
        self.tree.children(node_idx)
695789
            .iter()
1478115
            .filter(|&&child_idx| {
1478115
                let Some(child_node) = self.tree.get(LayoutNodeId::new(child_idx)) else {
                    return false;
                };
1478115
                let Some(child_dom_id) = child_node.dom_node_id else {
                    return true;
                };
                // Check if child has display: none
1478115
                let display = get_display_property(self.ctx.styled_dom, Some(child_dom_id));
1478115
                let is_display_none = matches!(display, MultiValue::Exact(LayoutDisplay::None));
1478115
                !is_display_none
1478115
            })
695789
            .copied()
695789
            .collect()
695789
    }
}
/// Main entry point for laying out a Flexbox or Grid container using Taffy.
///
/// This function now accepts a `text_cache` parameter so that IFC layout can be
/// performed inline during Taffy's measure callbacks, rather than as a post-processing step.
/// # Panics
///
/// Panics if `node_idx` is not present in the layout tree.
1937
pub fn layout_taffy_subtree<T: ParsedFontTrait>(
1937
    ctx: &mut LayoutContext<'_, T>,
1937
    tree: &mut LayoutTree,
1937
    text_cache: &mut crate::font_traits::TextLayoutCache,
1937
    node_idx: usize,
1937
    inputs: LayoutInput,
1937
) -> LayoutOutput {
1937
    let children: Vec<usize> = tree.children(node_idx).to_vec();
    // DEBUG: Log Taffy inputs
1937
    if ctx.debug_messages.is_some() {
1900
        ctx.debug_info_inner(format!(
1900
            "[TAFFY INPUT] node_idx={} known_dims=({:?}, {:?}) available=({:?}, {:?}) \
1900
             parent_size=({:?}, {:?}) children={:?}",
1900
            node_idx,
1900
            inputs.known_dimensions.width,
1900
            inputs.known_dimensions.height,
1900
            inputs.available_space.width,
1900
            inputs.available_space.height,
1900
            inputs.parent_size.width,
1900
            inputs.parent_size.height,
1900
            children
1900
        ));
1900
    }
    // NOTE (2026-08-08): the unconditional "clear every child's taffy_cache
    // to force re-measure" that used to live here (Nov 2025, pre-reconcile)
    // is GONE. Dirty-driven invalidation now happens once per pass in
    // layout_document Step 1.2 over the ancestor closure of
    // `intrinsic_dirty`; everything else is covered by taffy's own
    // (known_dimensions, available_space, run_mode) cache key. The hammer
    // was re-running full min/max-content subtree layouts for every flex
    // child on every pass — 312 of them per steady-state resize on big.md,
    // each re-breaking every IFC line in the subtree — and, worse, each
    // measure STORE evicted the Definite entry in the single-slot IFC cache
    // (`should_replace_with`: width type changed), so the final pass
    // re-flowed the text AGAIN. Measured: root_layout_pass 25.5 ms → see
    // scripts/ for the after numbers.
    // SAFETY: We pass text_cache as a raw pointer because TaffyBridge needs to call
    // layout_ifc from within compute_child_layout, but we already have &mut ctx and &mut tree.
    // The pointer is only valid for the duration of this function call.
1937
    let text_cache_ptr = core::ptr::from_mut::<crate::font_traits::TextLayoutCache>(text_cache);
1937
    let mut bridge = TaffyBridge::new(ctx, tree, text_cache_ptr);
1937
    let node = bridge.tree.get(LayoutNodeId::new(node_idx)).unwrap();
1937
    let output = match node.formatting_context {
1910
        FormattingContext::Flex => compute_flexbox_layout(&mut bridge, node_idx.into(), inputs),
27
        FormattingContext::Grid => compute_grid_layout(&mut bridge, node_idx.into(), inputs),
        _ => LayoutOutput::HIDDEN,
    };
    // DEBUG: Log Taffy output
1937
    if bridge.ctx.debug_messages.is_some() {
1900
        bridge.ctx.debug_info_inner(format!(
1900
            "[TAFFY OUTPUT] node_idx={} output_size=({:?}, {:?})",
            node_idx, output.size.width, output.size.height
        ));
        // Log child layout results
5597
        for &child_idx in &children {
3697
            if let Some(child) = bridge.tree.get(LayoutNodeId::new(child_idx)) {
3697
                bridge.ctx.debug_info_inner(format!(
3697
                    "[TAFFY CHILD RESULT] child_idx={} used_size={:?} relative_pos={:?}",
3697
                    child_idx, child.used_size, bridge.tree.warm(LayoutNodeId::new(child_idx)).and_then(|w| w.relative_position)
                ));
            }
        }
37
    }
1937
    output
1937
}
// --- Trait Implementations for the Bridge ---
impl<T: ParsedFontTrait> TraversePartialTree for TaffyBridge<'_, '_, T> {
    type ChildIter<'c>
        = std::vec::IntoIter<taffy::NodeId>
    where
        Self: 'c;
362524
    fn child_ids(&self, node_id: taffy::NodeId) -> Self::ChildIter<'_> {
362524
        let node_idx: usize = node_id.into();
362524
        let children = self.get_layout_children(node_idx);
362524
        children
362524
            .into_iter()
362524
            .map(Into::into)
362524
            .collect::<Vec<taffy::NodeId>>()
362524
            .into_iter()
362524
    }
114464
    fn child_count(&self, node_id: taffy::NodeId) -> usize {
114464
        let node_idx: usize = node_id.into();
114464
        self.get_layout_children(node_idx).len()
114464
    }
218801
    fn get_child_id(&self, node_id: taffy::NodeId, index: usize) -> taffy::NodeId {
218801
        self.get_layout_children(node_id.into())[index].into()
218801
    }
}
impl<T: ParsedFontTrait> LayoutPartialTree for TaffyBridge<'_, '_, T> {
    type CoreContainerStyle<'c>
        = Style
    where
        Self: 'c;
    type CustomIdent = String;
2990339
    fn get_core_container_style(&self, node_id: taffy::NodeId) -> Self::CoreContainerStyle<'_> {
2990339
        let node_idx: usize = node_id.into();
        // Use get_taffy_style instead of translate_style_to_taffy to apply
        // cross-axis intrinsic suppression for stretch alignment
2990339
        self.get_taffy_style(node_idx)
2990339
    }
109450
    fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &Layout) {
109450
        let node_idx: usize = node_id.into();
        // FIX: Retrieve parent border/padding to adjust position.
        // Taffy positions are relative to the parent's Border Box origin.
        // Azul expects positions relative to the parent's Content Box origin.
        // We must subtract the parent's border and padding from the Taffy-returned position.
109450
        let (parent_border_left, parent_border_top, parent_padding_left, parent_padding_top) = {
109450
            if let Some(child) = self.tree.get(LayoutNodeId::new(node_idx)) {
109450
                if let Some(parent_idx) = child.parent {
109450
                    self.tree.get(LayoutNodeId::new(parent_idx)).map_or((0.0, 0.0, 0.0, 0.0), |parent| {
109450
                        let pbp = parent.box_props.unpack();
109450
                        (
109450
                            pbp.border.left,
109450
                            pbp.border.top,
109450
                            pbp.padding.left,
109450
                            pbp.padding.top,
109450
                        )
109450
                    })
                } else {
                    (0.0, 0.0, 0.0, 0.0)
                }
            } else {
                (0.0, 0.0, 0.0, 0.0)
            }
        };
109450
        if let Some(node) = self.tree.get_mut(LayoutNodeId::new(node_idx)) {
109450
            let size = translate_taffy_size_back(layout.size);
109450
            let mut pos = translate_taffy_point_back(layout.location);
            // DEBUG: Log Taffy's raw layout result before adjustment
109450
            if self.ctx.debug_messages.is_some() {
109367
                self.ctx.debug_info_inner(format!(
109367
                    "[TAFFY set_unrounded_layout] node_idx={} taffy_size=({:.2}, {:.2}) \
109367
                     taffy_pos=({:.2}, {:.2}) parent_border=({:.2}, {:.2}) parent_padding=({:.2}, \
109367
                     {:.2})",
109367
                    node_idx,
109367
                    layout.size.width,
109367
                    layout.size.height,
109367
                    layout.location.x,
109367
                    layout.location.y,
109367
                    parent_border_left,
109367
                    parent_border_top,
109367
                    parent_padding_left,
109367
                    parent_padding_top
109367
                ));
109367
            }
            // Subtract parent's border and padding offset to convert
            // from border-box-relative to content-box-relative position
109450
            pos.x -= parent_border_left + parent_padding_left;
109450
            pos.y -= parent_border_top + parent_padding_top;
109450
            node.used_size = Some(size);
        }
109450
        if let Some(warm) = self.tree.warm_mut(LayoutNodeId::new(node_idx)) {
109450
            let mut pos = translate_taffy_point_back(layout.location);
109450
            pos.x -= parent_border_left + parent_padding_left;
109450
            pos.y -= parent_border_top + parent_padding_top;
109450
            warm.relative_position = Some(pos);
109450
        }
109450
    }
    fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 {
        // SAFETY: `val` came from `store_calc_and_make_dimension` which stored
        // a `Box<CalcResolveContext>` in `self.calc_storage`. The Box is alive for
        // the lifetime of this TaffyBridge, and taffy only clears the low 3 bits.
        let ctx = unsafe { &*val.cast::<CalcResolveContext>() };
        crate::solver3::calc::evaluate_calc(ctx, basis)
    }
1787936
    fn compute_child_layout(
1787936
        &mut self,
1787936
        node_id: taffy::NodeId,
1787936
        inputs: LayoutInput,
1787936
    ) -> LayoutOutput {
1787936
        let node_idx: usize = node_id.into();
        // DEBUG: Log the style being used for this child
1787936
        if self.ctx.debug_messages.is_some() {
1787670
            let style = self.get_taffy_style(node_idx);
1787670
            self.ctx.debug_info_inner(format!(
1787670
                "[TAFFY compute_child_layout] node_idx={} flex_grow={} flex_shrink={} \
1787670
                 flex_basis={:?} size=({:?}, {:?}) inputs.known_dims=({:?}, {:?})",
1787670
                node_idx,
1787670
                style.flex_grow,
1787670
                style.flex_shrink,
1787670
                style.flex_basis,
1787670
                style.size.width,
1787670
                style.size.height,
1787670
                inputs.known_dimensions.width,
1787670
                inputs.known_dimensions.height
1787670
            ));
1787670
        }
        // Get formatting context
1787936
        let fc = self
1787936
            .tree
1787936
            .get(LayoutNodeId::new(node_idx))
1787936
            .map(|s| s.formatting_context)
1787936
            .unwrap_or_default();
        // PURE CONTENT MEASURE cache (min-/max-content, no known dimensions).
        // These answers are viewport-independent, but taffy's own cache
        // cannot keep them alive: the flex algorithm's candidate-width
        // probes share their slot class and evict them within every pass.
        // Serve them from the warm node instead — invalidated with
        // `taffy_cache` by the intrinsic-dirty ancestor closure. Gated off
        // for documents that use viewport units (vw/vh in font sizes make
        // "content size" viewport-dependent), mirroring the collect gate.
1787936
        let pure_measure_slot = if inputs.run_mode == RunMode::ComputeSize
1678486
            && inputs.known_dimensions.width.is_none()
1182174
            && inputs.known_dimensions.height.is_none()
842601
            && !self
842601
                .ctx
842601
                .styled_dom
842601
                .css_property_cache
842601
                .ptr
842601
                .compact_cache
842601
                .as_ref()
842601
                .is_none_or(|cc| cc.uses_viewport_units)
        {
842601
            match inputs.available_space.width {
446334
                AvailableSpace::MinContent => Some(0usize),
323979
                AvailableSpace::MaxContent => Some(1usize),
72288
                AvailableSpace::Definite(_) => None,
            }
        } else {
945335
            None
        };
1787936
        if let Some(slot) = pure_measure_slot {
770313
            if let Some(warm) = self.tree.warm(LayoutNodeId::new(node_idx)) {
770313
                let cached = match slot {
446334
                    0 => warm.measured_content_sizes.0,
323979
                    _ => warm.measured_content_sizes.1,
                };
770313
                if let Some(out) = cached {
639081
                    drop(crate::probe::Probe::span("taffy_pure_measure_hit"));
639081
                    return out;
131232
                }
            }
131232
            drop(crate::probe::Probe::span("taffy_pure_measure_miss"));
1017623
        }
1148855
        let mut result = compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
627294
            let node_idx: usize = node_id.into();
627294
            let fc = tree
627294
                .tree
627294
                .get(LayoutNodeId::new(node_idx))
627294
                .map(|s| s.formatting_context)
627294
                .unwrap_or_default();
627294
            match fc {
360506
                FormattingContext::Flex => compute_flexbox_layout(tree, node_id, inputs),
                FormattingContext::Grid => compute_grid_layout(tree, node_id, inputs),
                // For Block, Inline, Table, InlineBlock - delegate to layout_formatting_context
                // This ensures proper recursive layout of all formatting contexts
266788
                _ => tree.compute_non_flex_layout(node_idx, inputs),
            }
627294
        });
        // Populate the pure-measure cache from the result we just computed.
1148855
        if let Some(slot) = pure_measure_slot {
131232
            if let Some(warm) = self.tree.warm_mut(LayoutNodeId::new(node_idx)) {
131232
                match slot {
65814
                    0 => warm.measured_content_sizes.0 = Some(result),
65418
                    _ => warm.measured_content_sizes.1 = Some(result),
                }
            }
1017623
        }
        // Store layout for container nodes - Taffy only calls set_unrounded_layout for leaf nodes
1148855
        if let Some(node) = self.tree.get_mut(LayoutNodeId::new(node_idx)) {
1148855
            let size = translate_taffy_size_back(result.size);
1148855
            node.used_size = Some(size);
1148855
        }
        // CRITICAL FIX: For Flex/Grid children with overflow:auto/scroll,
        // compute scrollbar_info by comparing Taffy's content_size against the
        // CSS-specified container size.
        //
        // We skip when content_size is (0,0) because that's the sizing pass
        // where Taffy hasn't determined actual content size yet. The final
        // layout pass always has non-zero content_size for nodes that need
        // scroll. This avoids 2/3 of the compute_taffy_scrollbar_info calls
        // (one sizing pass per axis) while still getting correct final values.
1148855
        if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
597512
            let taffy_content_width = result.content_size.width;
597512
            let taffy_content_height = result.content_size.height;
            // Skip on sizing pass where content_size is still zero:
            // scrollbar_info computed from zero content would be wrong anyway.
597512
            if taffy_content_width <= 0.0 && taffy_content_height <= 0.0 {
545835
                return result;
51677
            }
51677
            let (scrollbar_info, eff_content_w, eff_content_h) =
51677
                compute_taffy_scrollbar_info(
51677
                    self.ctx,
51677
                    self.tree,
51677
                    node_idx,
51677
                    result.size.width,
51677
                    result.size.height,
51677
                    taffy_content_width,
51677
                    taffy_content_height,
51677
                    ContentSizeOrigin::BorderBox,
51677
                );
51677
            if let Some(warm) = self.tree.warm_mut(LayoutNodeId::new(node_idx)) {
51677
                warm.scrollbar_info = Some(scrollbar_info);
51677
                // eff_content_w/h are already in content-box coordinates
51677
                // (the border+padding inset is subtracted in
51677
                // compute_taffy_scrollbar_info), so store directly without
51677
                // further subtraction.
51677
                warm.overflow_content_size = Some(LogicalSize::new(
51677
                    eff_content_w,
51677
                    eff_content_h,
51677
                ));
51677
            }
551343
        }
603020
        result
1787936
    }
}
impl<T: ParsedFontTrait> TaffyBridge<'_, '_, T> {
    /// Compute layout for non-flex/grid nodes by delegating to `layout_formatting_context`.
    /// This handles Block, Inline, Table, `InlineBlock` formatting contexts recursively.
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
266788
    fn compute_non_flex_layout(&mut self, node_idx: usize, inputs: LayoutInput) -> LayoutOutput {
        // Taffy's known_dimensions are BORDER-BOX sizes (the child's outer size
        // as determined by the parent flex/grid algorithm, e.g. via stretch alignment).
        // Our BFC/IFC layout expects the available_size to be the CONTENT-BOX width
        // (i.e. the space available for the child's own content, excluding the child's
        // own padding and border).
        //
        // Get padding/border early so we can convert border-box → content-box.
266788
        let (node_padding_width, node_padding_height, node_border_width, node_border_height) = self
266788
            .tree
266788
            .get(LayoutNodeId::new(node_idx))
266788
            .map_or((0.0, 0.0, 0.0, 0.0), |node| {
266788
                let bp = node.box_props.unpack();
266788
                (
266788
                    bp.padding.left + bp.padding.right,
266788
                    bp.padding.top + bp.padding.bottom,
266788
                    bp.border.left + bp.border.right,
266788
                    bp.border.top + bp.border.bottom,
266788
                )
266788
            });
        // Determine available size from Taffy's inputs.
        // When known_dimensions is set (e.g. flex stretch), subtract the child's own
        // padding+border to convert from border-box to content-box available space.
        // For MinContent/MaxContent, use INFINITY and let the text layout calculate
        // its actual intrinsic width.
266788
        let available_width = inputs
266788
            .known_dimensions
266788
            .width
266788
            .map(|kw| (kw - node_padding_width - node_border_width).max(0.0))
266788
            .or(match inputs.available_space.width {
165142
                AvailableSpace::Definite(w) => Some(w),
56223
                AvailableSpace::MinContent => None, // Use infinity, return intrinsic min-content
45423
                AvailableSpace::MaxContent => None, // Use infinity for max-content
            })
266788
            .unwrap_or(f32::INFINITY);
266788
        let available_height = inputs
266788
            .known_dimensions
266788
            .height
266788
            .map(|kh| (kh - node_padding_height - node_border_height).max(0.0))
266788
            .or(match inputs.available_space.height {
143685
                AvailableSpace::Definite(h) => Some(h),
85462
                AvailableSpace::MinContent => None, // Use infinity, return intrinsic min-content
37641
                AvailableSpace::MaxContent => None,
            })
266788
            .unwrap_or(f32::INFINITY);
266788
        let mut available_size = LogicalSize {
266788
            width: available_width,
266788
            height: available_height,
266788
        };
        // NOTE: Scrollbar reservation is handled inside layout_bfc() where it subtracts
        // scrollbar width from children_containing_block_size. We do NOT subtract here
        // to avoid double-subtraction when compute_non_flex_layout delegates to
        // layout_formatting_context → layout_bfc.
        // Convert Taffy's AvailableSpace to our Text3AvailableSpace for caching.
        // When the child has known_dimensions.width (from flex/grid layout), use that
        // instead of the parent's available_space — otherwise text centers/wraps in
        // the wrong width (e.g., 404px parent instead of 120px child).
266788
        let available_width_type = if inputs.known_dimensions.width.is_some() {
138679
            crate::text3::cache::AvailableSpace::Definite(available_width)
        } else {
128109
            match inputs.available_space.width {
26463
                AvailableSpace::Definite(w) => crate::text3::cache::AvailableSpace::Definite(w),
56223
                AvailableSpace::MinContent => crate::text3::cache::AvailableSpace::MinContent,
45423
                AvailableSpace::MaxContent => crate::text3::cache::AvailableSpace::MaxContent,
            }
        };
        // Get text-align from CSS for this node (important for centering content in flex items)
266788
        let text_align = self
266788
            .tree
266788
            .get(LayoutNodeId::new(node_idx))
266788
            .and_then(|node| node.dom_node_id)
266788
            .map(|dom_id| {
266788
                let node_state =
266788
                    &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
266788
                crate::solver3::getters::get_text_align(self.ctx.styled_dom, dom_id, node_state)
266788
                    .unwrap_or_default()
266788
            })
266788
            .unwrap_or_default();
        // Convert CSS text-align to our internal TextAlign enum
266788
        let fc_text_align = match text_align {
            azul_css::props::style::StyleTextAlign::Left => FcTextAlign::Start,
            azul_css::props::style::StyleTextAlign::Right => FcTextAlign::End,
38727
            azul_css::props::style::StyleTextAlign::Center => FcTextAlign::Center,
            azul_css::props::style::StyleTextAlign::Justify => FcTextAlign::Justify,
228061
            azul_css::props::style::StyleTextAlign::Start => FcTextAlign::Start,
            azul_css::props::style::StyleTextAlign::End => FcTextAlign::End,
        };
        // SAFETY: `self.text_cache` was derived from `&mut TextLayoutCache` in
        // `layout_taffy_subtree` and no other reference to it exists at this point.
        // The raw pointer is necessary because we already hold `&mut self` (which
        // borrows `ctx` and `tree`), and Rust's borrow checker cannot express the
        // disjointness of text_cache from ctx/tree.
266788
        let text_cache = unsafe { &mut *self.text_cache };
266788
        let constraints = LayoutConstraints {
266788
            available_size,
266788
            writing_mode: LayoutWritingMode::HorizontalTb,
266788
            writing_mode_ctx: super::geometry::WritingModeContext::default(),
266788
            bfc_state: None,
266788
            text_align: fc_text_align,
266788
            containing_block_size: available_size,
266788
            available_width_type,
266788
            fragmentainer: None,
266788
        };
        // A prior Taffy measurement pass (e.g. the min-content pass Taffy runs to
        // find a flex item's intrinsic width) stores its result in `node.used_size`
        // at the end of this function. layout_bfc then reads `used_size` as the
        // children's containing-block width. When the subsequent definite-width
        // cross-sizing pass re-enters here, that STALE min-content width (not this
        // pass's `known_dimensions.width`) drives child wrapping — so a flex item
        // with long text wraps at min-content and reports an over-tall cross size,
        // over-sizing the container (invoice `.head` measured 125px for ~45px of
        // content). Reset `used_size` to the border-box dims Taffy fixed for THIS
        // measure. When width is unknown (an intrinsic pass), clear it so layout_bfc
        // falls back to `constraints.available_size` (INFINITY → true intrinsic).
266788
        if let Some(n) = self.tree.get_mut(LayoutNodeId::new(node_idx)) {
266788
            n.used_size = match (inputs.known_dimensions.width, inputs.known_dimensions.height) {
54155
                (Some(w), Some(h)) => Some(LogicalSize {
54155
                    width: w,
54155
                    height: h,
54155
                }),
84524
                (Some(w), None) if available_height.is_finite() => Some(LogicalSize {
61249
                    width: w,
61249
                    height: available_height + node_padding_height + node_border_height,
61249
                }),
                // Height genuinely unknown with INFINITE available height —
                // the main-axis content-measure pass of a COLUMN flex
                // container (cross width is known via stretch, height is
                // what taffy is asking us to compute). The old arm
                // fabricated height 0.0 here, and layout_bfc then used that
                // used_size as the children's containing block: every
                // descendant laid out inside a 0-HEIGHT box, the results
                // were cached, and auto-height text content inside a
                // fixed-height column flex item rendered as NOTHING (row
                // containers dodged it because the UNKNOWN axis there is
                // the width, which maps to None). Clear instead — layout_bfc
                // falls back to constraints.available_size (the true
                // content-measure request), same as the width-unknown case.
151384
                _ => None,
            };
        }
        // Use a temporary float cache for this subtree
266788
        let mut float_cache = HashMap::new();
        // Call layout_formatting_context - this handles ALL formatting context types
        // including nested flex/grid, tables, BFC, and IFC
266788
        let fc_result = crate::solver3::fc::layout_formatting_context(
266788
            self.ctx,
266788
            self.tree,
266788
            text_cache,
266788
            node_idx,
266788
            &constraints,
266788
            &mut float_cache,
        );
266788
        match fc_result {
266788
            Ok(bfc_result) => {
266788
                let output = bfc_result.output;
266788
                let content_width = output.overflow_size.width;
266788
                let content_height = output.overflow_size.height;
                // Padding/border already computed at start of function
266788
                let padding_width = node_padding_width;
266788
                let padding_height = node_padding_height;
266788
                let border_width = node_border_width;
266788
                let border_height = node_border_height;
                // Get intrinsic sizes for min/max-content queries
266788
                let intrinsic = self
266788
                    .tree
266788
                    .warm(LayoutNodeId::new(node_idx))
266788
                    .and_then(|w| w.intrinsic_sizes)
266788
                    .unwrap_or_default();
                // min-content size in the main axis; for items with a preferred aspect ratio, it
                // should be clamped by definite min/max cross sizes converted through the ratio.
                // For MinContent/MaxContent queries, use intrinsic sizes instead of layout result.
                // HOWEVER: If intrinsic sizes are 0 but content_width is non-zero, use content_width.
                // This happens for FormattingContext::Inline nodes that are measured by their
                // parent IFC root and don't have their own intrinsic sizes stored.
                //
                // CRITICAL FIX: For InlineBlock elements with width: auto (known_dimensions.width = None),
                // we must use intrinsic max-content width instead of content_width from BFC layout.
                // The BFC layout was done with the full container width, but InlineBlock should
                // shrink-to-fit its content. This is per CSS 2.1 § 10.3.9: "shrink-to-fit width".
266788
                let fc = self
266788
                    .tree
266788
                    .get(LayoutNodeId::new(node_idx))
266788
                    .map(|s| s.formatting_context)
266788
                    .unwrap_or_default();
266788
                let is_shrink_to_fit = matches!(fc, FormattingContext::InlineBlock)
                    && inputs.known_dimensions.width.is_none();
266788
                let effective_content_width = match inputs.available_space.width {
                    AvailableSpace::MinContent => {
56223
                        if intrinsic.min_content_width > 0.0 {
39024
                            intrinsic.min_content_width
                        } else {
17199
                            content_width
                        }
                    }
                    AvailableSpace::MaxContent => {
45423
                        if intrinsic.max_content_width > 0.0 {
28260
                            intrinsic.max_content_width
                        } else {
17163
                            content_width
                        }
                    }
                    AvailableSpace::Definite(_) => {
                        // For shrink-to-fit elements (InlineBlock with auto width),
                        // use intrinsic max-content width clamped by available space.
                        // CSS 2.1 § 10.3.9: shrink-to-fit = min(max(preferred minimum, available), preferred)
165142
                        if is_shrink_to_fit && intrinsic.max_content_width > 0.0 {
                            // Use max-content (preferred width) - already clamped by min/max-width in sizing
                            intrinsic.max_content_width
                        } else {
165142
                            content_width
                        }
                    }
                };
                // Replaced elements (image / VirtualView) have NO flow content, so the
                // BFC content_height above is 0 (and shrink-to-fit width may be wrong).
                // Their content size is the CSS/intrinsic-resolved size from
                // calculate_used_size_for_node (border-box) — strip padding+border back
                // to content-box. Fixes blank / 0-height images as flex/grid items.
266788
                let (effective_content_width, content_height) = {
266788
                    let dom_id = self.tree.get(LayoutNodeId::new(node_idx)).and_then(|n| n.dom_node_id);
266788
                    let is_replaced = dom_id
266788
                        .is_some_and(|id| {
266788
                            let nd = &self.ctx.styled_dom.node_data.as_container()[id];
266788
                            matches!(nd.get_node_type(), azul_core::dom::NodeType::Image(_))
266712
                                || nd.is_virtual_view_node()
266788
                        });
266788
                    match (is_replaced, dom_id) {
76
                        (true, Some(id)) => {
76
                            let bp = self.tree.get(LayoutNodeId::new(node_idx)).unwrap().box_props.unpack();
76
                            crate::solver3::sizing::calculate_used_size_for_node(
76
                                self.ctx.styled_dom,
76
                                Some(id),
76
                                &constraints.containing_block_size,
76
                                intrinsic,
76
                                &bp,
76
                                &self.ctx.viewport_size,
76
                            ).map_or((effective_content_width, content_height), |sz| (
76
                                    (sz.width - padding_width - border_width).max(0.0),
76
                                    (sz.height - padding_height - border_height).max(0.0),
                                ))
                        }
266712
                        _ => (effective_content_width, content_height),
                    }
                };
                // Convert content-box size to border-box size (for when we compute our own size)
266788
                let border_box_width = effective_content_width + padding_width + border_width;
266788
                let border_box_height = content_height + padding_height + border_height;
                // CRITICAL: Taffy's known_dimensions is BORDER-BOX (the child's
                // outer size as set by the parent flex/grid algorithm). Our BFC/IFC
                // layout computes content-box sizes, but Taffy expects the returned
                // `size` to be BORDER-BOX for correct positioning of subsequent items.
                //
                // When known_dimensions is set: use it directly (it's already border-box).
                // When it's None: add padding+border to our content-box result.
266788
                let final_width = inputs.known_dimensions.width.map_or(border_box_width, |border_box_w| border_box_w);
                // For grid items: if known_dimensions.height is None but available_space.height
                // is definite, use the available space. This ensures empty grid items stretch
                // to fill their grid cell, per CSS Grid spec behavior.
266788
                let final_height = if let Some(border_box_h) = inputs.known_dimensions.height { border_box_h } else {
                    // Check if parent is a grid container and available_space is definite
203840
                    let parent_is_grid = self
203840
                        .tree
203840
                        .get(LayoutNodeId::new(node_idx))
203840
                        .and_then(|n| n.parent)
203840
                        .and_then(|p| self.tree.get(LayoutNodeId::new(p)))
203840
                        .is_some_and(|p| matches!(p.formatting_context, FormattingContext::Grid));
203840
                    if parent_is_grid {
                        // For grid items, use available space if content is smaller
288
                        match inputs.available_space.height {
                            AvailableSpace::Definite(h) => {
                                // Grid items stretch to fill their cell by default
                                // Use the larger of content size or available space
                                h.max(border_box_height)
                            }
288
                            _ => border_box_height,
                        }
                    } else {
203552
                        border_box_height
                    }
                };
                // CRITICAL: Transfer positions from layout_formatting_context to child nodes.
                // Without this, children of flex items won't have their relative_position set,
                // causing them to all render at (0,0) relative to their parent.
267605
                for (child_idx, child_pos) in &output.positions {
817
                    if let Some(child_warm) = self.tree.warm_mut(LayoutNodeId::new(*child_idx)) {
817
                        child_warm.relative_position = Some(*child_pos);
817
                    }
                }
                // Compute scrollbar_info for this node (it's a child of a Flex/Grid container,
                // so calculate_layout_for_subtree won't be called for it).
                // Uses the unified compute_scrollbar_info_core path.
                //
                // content_width/height come from our own BFC/IFC overflow_size,
                // which is already content-box relative — unlike Taffy's
                // border-box-origin content_size.
266788
                let (scrollbar_info, _, _) = compute_taffy_scrollbar_info(
266788
                    self.ctx,
266788
                    self.tree,
266788
                    node_idx,
266788
                    final_width,
266788
                    final_height,
266788
                    content_width,
266788
                    content_height,
266788
                    ContentSizeOrigin::ContentBox,
266788
                );
                // Store the border-box size and scrollbar_info on the node for display list generation
266788
                if let Some(node) = self.tree.get_mut(LayoutNodeId::new(node_idx)) {
266788
                    node.used_size = Some(LogicalSize {
266788
                        width: final_width,
266788
                        height: final_height,
266788
                    });
266788
                }
266788
                if let Some(warm) = self.tree.warm_mut(LayoutNodeId::new(node_idx)) {
266788
                    warm.scrollbar_info = Some(scrollbar_info);
266788
                    // Store the actual content size for scroll calculations
266788
                    warm.overflow_content_size = Some(LogicalSize {
266788
                        width: content_width,
266788
                        height: content_height,
266788
                    });
266788
                }
                // Return the same size to Taffy for correct positioning
266788
                LayoutOutput {
266788
                    size: Size {
266788
                        width: final_width,
266788
                        height: final_height,
266788
                    },
266788
                    content_size: Size {
266788
                        width: content_width,
266788
                        height: content_height,
266788
                    },
266788
                    first_baselines: taffy::Point {
266788
                        x: None,
266788
                        y: output.baseline,
266788
                    },
266788
                    top_margin: taffy::CollapsibleMarginSet::ZERO,
266788
                    bottom_margin: taffy::CollapsibleMarginSet::ZERO,
266788
                    margins_can_collapse_through: false,
266788
                }
            }
            Err(_e) => {
                // Fallback to intrinsic sizes if layout fails
                let intrinsic = self.tree.warm(LayoutNodeId::new(node_idx)).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
                let width = inputs
                    .known_dimensions
                    .width
                    .unwrap_or(intrinsic.max_content_width);
                let height = inputs
                    .known_dimensions
                    .height
                    .unwrap_or(intrinsic.max_content_height);
                LayoutOutput {
                    size: Size { width, height },
                    content_size: Size { width, height },
                    first_baselines: taffy::Point { x: None, y: None },
                    top_margin: taffy::CollapsibleMarginSet::ZERO,
                    bottom_margin: taffy::CollapsibleMarginSet::ZERO,
                    margins_can_collapse_through: false,
                }
            }
        }
266788
    }
}
impl<T: ParsedFontTrait> CacheTree for TaffyBridge<'_, '_, T> {
1148855
    fn cache_get(
1148855
        &self,
1148855
        node_id: taffy::NodeId,
1148855
        input: &LayoutInput,
1148855
    ) -> Option<LayoutOutput> {
1148855
        let node_idx: usize = node_id.into();
1148855
        let hit = self.tree
1148855
            .warm(LayoutNodeId::new(node_idx))?
            .taffy_cache
1148855
            .get(input);
1148855
        drop(crate::probe::Probe::span(if hit.is_some() {
521561
            "taffy_cache_get_hit"
        } else {
627294
            "taffy_cache_get_miss"
        }));
        // AZ_TAFFY_DEBUG: one line per lookup with the full taffy cache key.
        // Diffing two passes' lines for the same node names WHICH component
        // (known_dimensions / available_space / run_mode) moved and broke
        // the key — aggregates can't tell that.
1148855
        if std::env::var_os("AZ_TAFFY_DEBUG").is_some() {
            eprintln!(
                "[taffy] {} n{} kd=({:?},{:?}) avail=({:?},{:?}) mode={:?}",
                if hit.is_some() { "HIT " } else { "MISS" },
                node_idx,
                input.known_dimensions.width,
                input.known_dimensions.height,
                input.available_space.width,
                input.available_space.height,
                input.run_mode,
            );
1148855
        }
1148855
        hit
1148855
    }
627294
    fn cache_store(
627294
        &mut self,
627294
        node_id: taffy::NodeId,
627294
        input: &LayoutInput,
627294
        layout_output: LayoutOutput,
627294
    ) {
627294
        let node_idx: usize = node_id.into();
627294
        if let Some(warm) = self.tree.warm_mut(LayoutNodeId::new(node_idx)) {
627294
            warm.taffy_cache
627294
                .store(input, layout_output);
627294
        }
627294
    }
    fn cache_clear(&mut self, node_id: taffy::NodeId) {
        drop(crate::probe::Probe::span("taffy_cache_clear"));
        let node_idx: usize = node_id.into();
        if let Some(warm) = self.tree.warm_mut(LayoutNodeId::new(node_idx)) {
            warm.taffy_cache.clear();
            warm.measured_content_sizes = (None, None);
        }
    }
}
impl<T: ParsedFontTrait> LayoutFlexboxContainer for TaffyBridge<'_, '_, T> {
    type FlexboxContainerStyle<'c>
        = Style
    where
        Self: 'c;
    type FlexboxItemStyle<'c>
        = Style
    where
        Self: 'c;
934026
    fn get_flexbox_container_style(
934026
        &self,
934026
        node_id: taffy::NodeId,
934026
    ) -> Self::FlexboxContainerStyle<'_> {
934026
        self.get_core_container_style(node_id)
934026
    }
2055692
    fn get_flexbox_child_style(&self, child_node_id: taffy::NodeId) -> Self::FlexboxItemStyle<'_> {
2055692
        self.get_core_container_style(child_node_id)
2055692
    }
}
impl<T: ParsedFontTrait> LayoutGridContainer for TaffyBridge<'_, '_, T> {
    type GridContainerStyle<'c>
        = Style
    where
        Self: 'c;
    type GridItemStyle<'c>
        = Style
    where
        Self: 'c;
27
    fn get_grid_container_style(&self, node_id: taffy::NodeId) -> Self::GridContainerStyle<'_> {
27
        self.get_core_container_style(node_id)
27
    }
594
    fn get_grid_child_style(&self, child_node_id: taffy::NodeId) -> Self::GridItemStyle<'_> {
594
        self.get_core_container_style(child_node_id)
594
    }
}
// --- Conversion Functions ---
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
#[allow(clippy::vec_box)] // calc_storage Box gives stable addresses for taffy calc() pointers
111692
fn from_layout_width(
111692
    val: LayoutWidth,
111692
    calc_storage: &std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
111692
    em_size: f32,
111692
    rem_size: f32,
111692
) -> Dimension {
111692
    match val {
100425
        LayoutWidth::Auto => Dimension::auto(),
11260
        LayoutWidth::Px(px) => pixel_value_to_pixels_fallback(&px).map_or_else(
215
            || px.to_percent().map_or_else(Dimension::auto, |p| Dimension::percent(p.get())),
            Dimension::length,
        ),
6
        LayoutWidth::MinContent | LayoutWidth::MaxContent | LayoutWidth::FitContent(_) => Dimension::auto(),
1
        LayoutWidth::Calc(items) => store_calc_and_make_dimension(items, calc_storage, em_size, rem_size),
    }
111692
}
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
#[allow(clippy::vec_box)] // calc_storage Box gives stable addresses for taffy calc() pointers
111683
fn from_layout_height(
111683
    val: LayoutHeight,
111683
    calc_storage: &std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
111683
    em_size: f32,
111683
    rem_size: f32,
111683
) -> Dimension {
111683
    match val {
85362
        LayoutHeight::Auto => Dimension::auto(),
26318
        LayoutHeight::Px(px) => pixel_value_to_pixels_fallback(&px).map_or_else(
287
            || px.to_percent().map_or_else(Dimension::auto, |p| Dimension::percent(p.get())),
            Dimension::length,
        ),
3
        LayoutHeight::MinContent | LayoutHeight::MaxContent | LayoutHeight::FitContent(_) => Dimension::auto(),
        LayoutHeight::Calc(items) => store_calc_and_make_dimension(items, calc_storage, em_size, rem_size),
    }
111683
}
/// Stores the calc AST + font-size context in heap-pinned storage and returns
/// a `Dimension::calc(ptr)` with a stable pointer to the `CalcResolveContext`.
///
/// The `Box` ensures the address doesn't move when the outer `Vec` reallocates.
/// The `RefCell<Vec<…>>` keeps all boxes alive for the layout pass duration.
#[allow(clippy::vec_box)] // calc_storage Box gives stable addresses for taffy calc() pointers
260
fn store_calc_and_make_dimension(
260
    items: CalcAstItemVec,
260
    storage: &std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
260
    em_size: f32,
260
    rem_size: f32,
260
) -> Dimension {
260
    let boxed = Box::new(CalcResolveContext { items, em_size, rem_size });
260
    let ptr: *const CalcResolveContext = &raw const *boxed;
260
    storage.borrow_mut().push(boxed);
    // SAFETY: Box gives ≥8-byte-aligned heap pointer; taffy masks low 3 bits.
260
    Dimension::calc(ptr.cast::<()>())
260
}
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
111685
const fn from_layout_position(val: LayoutPosition) -> Position {
111685
    match val {
110345
        LayoutPosition::Static => Position::Relative, // Taffy treats Static as Relative
1325
        LayoutPosition::Relative => Position::Relative,
11
        LayoutPosition::Absolute => Position::Absolute,
2
        LayoutPosition::Fixed => Position::Absolute, // Taffy doesn't distinguish Fixed
2
        LayoutPosition::Sticky => Position::Relative, // Sticky = Relative for Taffy
    }
111685
}
#[cfg(test)]
#[allow(
    clippy::float_cmp,
    clippy::too_many_lines,
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation
)]
mod autotest_generated {
    use azul_css::props::layout::{
        dimensions::CalcAstItem,
        grid::{
            GridLine as AzGridLine, GridMinMax, GridPlacement as AzGridPlacement,
            GridTrackSizingVec, LayoutJustifyItems, NamedGridLine,
        },
        LayoutOverflow,
    };
    use super::*;
    // ==================================================================
    // Fixtures
    // ==================================================================
    fn track_vec(v: Vec<GridTrackSizing>) -> GridTrackSizingVec {
        GridTrackSizingVec::from_vec(v)
    }
    fn template(v: Vec<GridTrackSizing>) -> GridTemplate {
        GridTemplate { tracks: track_vec(v) }
    }
    fn auto_tracks(v: Vec<GridTrackSizing>) -> GridAutoTracks {
        GridAutoTracks { tracks: track_vec(v) }
    }
    /// The absolute metrics `pixel_value_to_pixels_fallback` can resolve.
    const ABSOLUTE_METRICS: [SizeMetric; 7] = [
        SizeMetric::Px,
        SizeMetric::Pt,
        SizeMetric::In,
        SizeMetric::Cm,
        SizeMetric::Mm,
        SizeMetric::Em,
        SizeMetric::Rem,
    ];
    /// The metrics that need a resolution context this function does not have.
    const CONTEXTUAL_METRICS: [SizeMetric; 5] = [
        SizeMetric::Percent,
        SizeMetric::Vw,
        SizeMetric::Vh,
        SizeMetric::Vmin,
        SizeMetric::Vmax,
    ];
    fn approx(actual: f32, expected: f32) {
        assert!(
            (actual - expected).abs() < 1e-3,
            "expected ~{expected}, got {actual}"
        );
    }
    // ==================================================================
    // pixel_value_to_pixels_fallback — numeric
    // ==================================================================
    #[test]
    fn pixel_value_to_pixels_fallback_is_zero_at_zero_for_every_absolute_metric() {
        for m in ABSOLUTE_METRICS {
            assert_eq!(
                pixel_value_to_pixels_fallback(&PixelValue::from_metric(m, 0.0)),
                Some(0.0),
                "{m:?}"
            );
        }
    }
    #[test]
    fn pixel_value_to_pixels_fallback_converts_each_absolute_unit_to_css_px() {
        assert_eq!(
            pixel_value_to_pixels_fallback(&PixelValue::px(10.5)),
            Some(10.5)
        );
        assert_eq!(
            pixel_value_to_pixels_fallback(&PixelValue::inch(1.0)),
            Some(96.0)
        );
        assert_eq!(
            pixel_value_to_pixels_fallback(&PixelValue::em(2.0)),
            Some(2.0 * DEFAULT_FONT_SIZE)
        );
        assert_eq!(
            pixel_value_to_pixels_fallback(&PixelValue::rem(2.0)),
            Some(2.0 * DEFAULT_FONT_SIZE)
        );
        approx(
            pixel_value_to_pixels_fallback(&PixelValue::pt(72.0)).unwrap(),
            96.0,
        );
        approx(
            pixel_value_to_pixels_fallback(&PixelValue::cm(2.54)).unwrap(),
            96.0,
        );
        approx(
            pixel_value_to_pixels_fallback(&PixelValue::mm(25.4)).unwrap(),
            96.0,
        );
        // PT_TO_PX is the documented factor — 1pt = 96/72 px.
        approx(
            pixel_value_to_pixels_fallback(&PixelValue::pt(1.0)).unwrap(),
            PT_TO_PX,
        );
    }
    #[test]
    fn pixel_value_to_pixels_fallback_keeps_the_sign_of_negative_lengths() {
        for m in ABSOLUTE_METRICS {
            let out = pixel_value_to_pixels_fallback(&PixelValue::from_metric(m, -4.0))
                .expect("absolute metric resolves");
            assert!(out < 0.0, "{m:?} produced {out} for -4");
        }
    }
    #[test]
    fn pixel_value_to_pixels_fallback_returns_none_for_context_dependent_metrics() {
        for m in CONTEXTUAL_METRICS {
            assert_eq!(
                pixel_value_to_pixels_fallback(&PixelValue::from_metric(m, 50.0)),
                None,
                "{m:?} must not be resolved without a containing block / viewport"
            );
        }
    }
    #[test]
    fn pixel_value_to_pixels_fallback_sanitises_nan_and_infinite_inputs() {
        // `FloatValue` stores `f32 * 1000` in an `isize` via an `as` cast, which
        // saturates: NaN → 0, ±inf → isize::MIN/MAX. So no NaN and no infinity can
        // reach the layout through a PixelValue, whatever the caller passes in.
        assert_eq!(
            pixel_value_to_pixels_fallback(&PixelValue::px(f32::NAN)),
            Some(0.0),
            "NaN must be flattened to 0, not propagated"
        );
        let pos = pixel_value_to_pixels_fallback(&PixelValue::px(f32::INFINITY))
            .expect("px resolves");
        assert!(pos.is_finite() && pos > 0.0, "+inf px became {pos}");
        let neg = pixel_value_to_pixels_fallback(&PixelValue::px(f32::NEG_INFINITY))
            .expect("px resolves");
        assert!(neg.is_finite() && neg < 0.0, "-inf px became {neg}");
    }
    #[test]
    fn pixel_value_to_pixels_fallback_stays_finite_at_the_f32_extremes() {
        for v in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, -f32::MIN_POSITIVE] {
            for m in ABSOLUTE_METRICS {
                let out = pixel_value_to_pixels_fallback(&PixelValue::from_metric(m, v))
                    .expect("absolute metric resolves");
                assert!(out.is_finite(), "{m:?} at {v} overflowed to {out}");
            }
        }
    }
    // ==================================================================
    // minmax + translate_track
    // ==================================================================
    #[test]
    fn minmax_puts_the_arguments_where_it_says_it_does() {
        let t = minmax(
            MinTrackSizingFunction::length(1.0),
            MaxTrackSizingFunction::fr(2.0),
        );
        assert_eq!(t.min, MinTrackSizingFunction::length(1.0));
        assert_eq!(t.max, MaxTrackSizingFunction::fr(2.0));
    }
    #[test]
    fn translate_track_maps_the_intrinsic_keywords() {
        assert_eq!(
            translate_track(&GridTrackSizing::MinContent),
            minmax(
                MinTrackSizingFunction::min_content(),
                MaxTrackSizingFunction::min_content()
            )
        );
        assert_eq!(
            translate_track(&GridTrackSizing::MaxContent),
            minmax(
                MinTrackSizingFunction::max_content(),
                MaxTrackSizingFunction::max_content()
            )
        );
        // `auto` is minmax(min-content, max-content) per CSS Grid §7.2.
        assert_eq!(
            translate_track(&GridTrackSizing::Auto),
            minmax(
                MinTrackSizingFunction::min_content(),
                MaxTrackSizingFunction::max_content()
            )
        );
    }
    #[test]
    fn translate_track_resolves_fixed_tracks_through_the_absolute_unit_table() {
        assert_eq!(
            translate_track(&GridTrackSizing::Fixed(PixelValue::px(120.0))),
            minmax(
                MinTrackSizingFunction::length(120.0),
                MaxTrackSizingFunction::length(120.0)
            )
        );
        assert_eq!(
            translate_track(&GridTrackSizing::Fixed(PixelValue::em(2.0))),
            minmax(
                MinTrackSizingFunction::length(32.0),
                MaxTrackSizingFunction::length(32.0)
            )
        );
        assert_eq!(
            translate_track(&GridTrackSizing::FitContent(PixelValue::px(50.0))),
            minmax(
                MinTrackSizingFunction::length(50.0),
                MaxTrackSizingFunction::max_content()
            )
        );
    }
    #[test]
    fn translate_track_collapses_unresolvable_track_units_to_zero_px() {
        // % / vw / vh cannot be expressed as a taffy track sizing fn here, and the
        // `.unwrap_or(0.0)` inside translate_track turns them into a 0px track
        // rather than dropping the track. Locking the (lossy) behaviour in.
        for m in CONTEXTUAL_METRICS {
            let pv = PixelValue::from_metric(m, 50.0);
            assert_eq!(
                translate_track(&GridTrackSizing::Fixed(pv)),
                minmax(
                    MinTrackSizingFunction::length(0.0),
                    MaxTrackSizingFunction::length(0.0)
                ),
                "{m:?}"
            );
            assert_eq!(
                translate_track(&GridTrackSizing::FitContent(pv)),
                minmax(
                    MinTrackSizingFunction::length(0.0),
                    MaxTrackSizingFunction::max_content()
                ),
                "{m:?}"
            );
        }
    }
    #[test]
    fn translate_track_divides_fr_by_the_hundredfold_scaling_factor() {
        assert_eq!(
            translate_track(&GridTrackSizing::Fr(100)),
            minmax(
                MinTrackSizingFunction::auto(),
                MaxTrackSizingFunction::fr(1.0)
            )
        );
        assert_eq!(
            translate_track(&GridTrackSizing::Fr(50)),
            minmax(
                MinTrackSizingFunction::auto(),
                MaxTrackSizingFunction::fr(0.5)
            )
        );
        assert_eq!(
            translate_track(&GridTrackSizing::Fr(0)),
            minmax(
                MinTrackSizingFunction::auto(),
                MaxTrackSizingFunction::fr(0.0)
            )
        );
    }
    #[test]
    fn translate_track_does_not_overflow_at_the_fr_integer_bounds() {
        for fr in [i32::MIN, i32::MIN + 1, -100, i32::MAX, i32::MAX - 1] {
            let t = translate_track(&GridTrackSizing::Fr(fr));
            let v = t.max.into_raw().value();
            assert!(v.is_finite(), "Fr({fr}) produced a non-finite fr: {v}");
            assert_eq!(v, fr as f32 / 100.0, "Fr({fr})");
        }
    }
    #[test]
    fn translate_track_minmax_takes_the_min_of_the_min_and_the_max_of_the_max() {
        let t = GridTrackSizing::MinMax(GridMinMax {
            min: Box::new(GridTrackSizing::Fixed(PixelValue::px(10.0))),
            max: Box::new(GridTrackSizing::Fr(200)),
        });
        assert_eq!(
            translate_track(&t),
            minmax(
                MinTrackSizingFunction::length(10.0),
                MaxTrackSizingFunction::fr(2.0)
            )
        );
        // The *other* halves are discarded: only minmax_box.min.min and
        // minmax_box.max.max survive the translation.
        let t = GridTrackSizing::MinMax(GridMinMax {
            min: Box::new(GridTrackSizing::MaxContent),
            max: Box::new(GridTrackSizing::MinContent),
        });
        assert_eq!(
            translate_track(&t),
            minmax(
                MinTrackSizingFunction::max_content(),
                MaxTrackSizingFunction::min_content()
            )
        );
    }
    #[test]
    fn translate_track_terminates_on_a_left_nested_minmax_chain() {
        // Nesting only on the `min` side keeps the recursion linear.
        let mut t = GridTrackSizing::Fixed(PixelValue::px(7.0));
        for _ in 0..64 {
            t = GridTrackSizing::MinMax(GridMinMax {
                min: Box::new(t),
                max: Box::new(GridTrackSizing::MaxContent),
            });
        }
        assert_eq!(
            translate_track(&t),
            minmax(
                MinTrackSizingFunction::length(7.0),
                MaxTrackSizingFunction::max_content()
            )
        );
    }
    #[test]
    fn translate_track_terminates_on_a_doubly_nested_minmax_chain() {
        // NOTE: translate_track calls itself on BOTH halves of a MinMax, so a
        // minmax nested on both sides costs O(2^depth). CSS grammar forbids
        // minmax() inside minmax(), so the parser can't reach this — but the type
        // can express it. Depth 10 = ~1k calls; it must still terminate and pick
        // the leaf on each side.
        let mut t = GridTrackSizing::Fixed(PixelValue::px(3.0));
        for _ in 0..10 {
            t = GridTrackSizing::MinMax(GridMinMax {
                min: Box::new(t.clone()),
                max: Box::new(t),
            });
        }
        assert_eq!(
            translate_track(&t),
            minmax(
                MinTrackSizingFunction::length(3.0),
                MaxTrackSizingFunction::length(3.0)
            )
        );
    }
    // ==================================================================
    // grid-template-* / grid-auto-* → taffy
    // ==================================================================
    #[test]
    fn grid_templates_are_empty_for_every_non_exact_css_value() {
        for v in [
            CssPropertyValue::None,
            CssPropertyValue::Inherit,
            CssPropertyValue::Revert,
            CssPropertyValue::Unset,
            CssPropertyValue::Auto,
            CssPropertyValue::Initial,
        ] {
            assert!(grid_template_rows_to_taffy(v.clone()).is_empty(), "{v:?}");
            assert!(grid_template_columns_to_taffy(v).is_empty());
        }
        assert!(grid_template_rows_to_taffy(CssPropertyValue::Exact(template(Vec::new()))).is_empty());
    }
    #[test]
    fn grid_auto_tracks_are_empty_for_every_non_exact_css_value() {
        for v in [
            CssPropertyValue::None,
            CssPropertyValue::Inherit,
            CssPropertyValue::Revert,
            CssPropertyValue::Unset,
            CssPropertyValue::Auto,
            CssPropertyValue::Initial,
        ] {
            assert!(grid_auto_rows_to_taffy(v.clone()).is_empty(), "{v:?}");
            assert!(grid_auto_columns_to_taffy(v).is_empty());
        }
    }
    #[test]
    fn grid_template_rows_and_columns_translate_each_track_in_order() {
        let t = template(vec![
            GridTrackSizing::Fr(100),
            GridTrackSizing::Fixed(PixelValue::px(20.0)),
            GridTrackSizing::Auto,
        ]);
        let rows = grid_template_rows_to_taffy(CssPropertyValue::Exact(t.clone()));
        let cols = grid_template_columns_to_taffy(CssPropertyValue::Exact(t));
        assert_eq!(rows.len(), 3);
        assert_eq!(rows, cols, "rows and columns share one translation path");
        assert_eq!(
            rows[0],
            GridTemplateComponent::Single(minmax(
                MinTrackSizingFunction::auto(),
                MaxTrackSizingFunction::fr(1.0)
            ))
        );
        assert_eq!(
            rows[1],
            GridTemplateComponent::Single(minmax(
                MinTrackSizingFunction::length(20.0),
                MaxTrackSizingFunction::length(20.0)
            ))
        );
        assert_eq!(
            rows[2],
            GridTemplateComponent::Single(minmax(
                MinTrackSizingFunction::min_content(),
                MaxTrackSizingFunction::max_content()
            ))
        );
    }
    #[test]
    fn grid_auto_rows_and_columns_agree_on_every_track() {
        let tracks = vec![
            GridTrackSizing::Fr(250),
            GridTrackSizing::MinContent,
            GridTrackSizing::FitContent(PixelValue::px(9.0)),
            GridTrackSizing::MinMax(GridMinMax {
                min: Box::new(GridTrackSizing::Fixed(PixelValue::px(1.0))),
                max: Box::new(GridTrackSizing::MaxContent),
            }),
        ];
        let rows = grid_auto_rows_to_taffy(CssPropertyValue::Exact(auto_tracks(tracks.clone())));
        let cols = grid_auto_columns_to_taffy(CssPropertyValue::Exact(auto_tracks(tracks.clone())));
        assert_eq!(rows.len(), tracks.len());
        // grid_auto_rows_to_taffy rebuilds the MinMax by calling translate_track
        // twice; it must land on exactly the same value as the single-call path.
        assert_eq!(rows, cols);
        for (i, track) in tracks.iter().enumerate() {
            assert_eq!(rows[i], translate_track(track), "track #{i}");
        }
    }
    #[test]
    fn grid_templates_handle_a_very_long_track_list() {
        let tracks: Vec<GridTrackSizing> = (0..2048).map(GridTrackSizing::Fr).collect();
        let out = grid_template_columns_to_taffy(CssPropertyValue::Exact(template(tracks)));
        assert_eq!(out.len(), 2048);
        assert_eq!(
            out[2047],
            GridTemplateComponent::Single(minmax(
                MinTrackSizingFunction::auto(),
                MaxTrackSizingFunction::fr(20.47)
            ))
        );
    }
    // ==================================================================
    // decode_compact_grid_line — numeric
    // ==================================================================
    #[test]
    fn decode_compact_grid_line_maps_both_sentinels_to_auto() {
        assert_eq!(
            decode_compact_grid_line(azul_css::compact_cache::I16_AUTO),
            GridPlacement::<String>::Auto
        );
        assert_eq!(
            decode_compact_grid_line(azul_css::compact_cache::I16_SENTINEL),
            GridPlacement::<String>::Auto
        );
        // i16::MAX *is* the sentinel, so the top of the range is Auto, not a line.
        assert_eq!(
            decode_compact_grid_line(i16::MAX),
            GridPlacement::<String>::Auto
        );
    }
    #[test]
    fn decode_compact_grid_line_zero_is_line_zero_not_auto() {
        assert_eq!(
            decode_compact_grid_line(0),
            GridPlacement::<String>::from_line_index(0)
        );
    }
    #[test]
    fn decode_compact_grid_line_positive_is_a_line_and_negative_is_a_span() {
        assert_eq!(
            decode_compact_grid_line(3),
            GridPlacement::<String>::from_line_index(3)
        );
        assert_eq!(
            decode_compact_grid_line(32_765),
            GridPlacement::<String>::from_line_index(32_765),
            "the largest value below I16_SENTINEL_THRESHOLD is still a line"
        );
        assert_eq!(
            decode_compact_grid_line(-1),
            GridPlacement::<String>::from_span(1)
        );
        assert_eq!(
            decode_compact_grid_line(-4),
            GridPlacement::<String>::from_span(4)
        );
        assert_eq!(
            decode_compact_grid_line(-32_767),
            GridPlacement::<String>::from_span(32_767)
        );
    }
    #[test]
    fn decode_compact_grid_line_at_i16_min_overflows_the_negation() {
        // `(-v) as u16` on i16::MIN: -(-32768) is not representable in i16.
        // Debug builds panic ("attempt to negate with overflow"); release wraps
        // back to i16::MIN, whose bit pattern as u16 is 32768. Neither is a
        // sensible span. Accept both so the test is profile-independent, and see
        // the report: this input should be rejected (or the negation widened to
        // i32) inside decode_compact_grid_line.
        let decoded = std::panic::catch_unwind(|| decode_compact_grid_line(i16::MIN));
        match decoded {
            Err(_) => { /* debug: overflow panic */ }
            Ok(p) => assert_eq!(
                p,
                GridPlacement::<String>::from_span(32_768),
                "release build: the negation wraps"
            ),
        }
    }
    // ==================================================================
    // grid_line_to_taffy / grid_placement_to_taffy
    // ==================================================================
    #[test]
    fn grid_line_to_taffy_maps_auto_lines_and_spans() {
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Auto),
            GridPlacement::<String>::Auto
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Line(0)),
            GridPlacement::<String>::from_line_index(0)
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Line(4)),
            GridPlacement::<String>::from_line_index(4)
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Line(-2)),
            GridPlacement::<String>::from_line_index(-2),
            "negative lines count from the end of the explicit grid"
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Span(3)),
            GridPlacement::<String>::from_span(3)
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Span(0)),
            GridPlacement::<String>::from_span(0)
        );
    }
    #[test]
    fn grid_line_to_taffy_truncates_out_of_range_line_numbers_instead_of_clamping() {
        // azul stores grid lines as i32, taffy as i16 — the `as i16` cast wraps.
        // A `grid-column: 70000` therefore silently becomes line 4464 rather than
        // being clamped to i16::MAX. Documented here; see the report.
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Line(70_000)),
            GridPlacement::<String>::from_line_index(70_000_i32 as i16)
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Line(i32::MAX)),
            GridPlacement::<String>::from_line_index(-1),
            "i32::MAX wraps to line -1 — the far end of the grid"
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Line(i32::MIN)),
            GridPlacement::<String>::from_line_index(0)
        );
    }
    #[test]
    fn grid_line_to_taffy_wraps_out_of_range_and_negative_spans() {
        // `span -1` is not valid CSS, but the i32 → u16 cast turns it into the
        // largest possible span rather than rejecting it.
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Span(-1)),
            GridPlacement::<String>::from_span(u16::MAX)
        );
        // `span 65536` wraps to `span 0`.
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Span(65_536)),
            GridPlacement::<String>::from_span(0)
        );
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Span(i32::MIN)),
            GridPlacement::<String>::from_span(0)
        );
    }
    #[test]
    fn grid_line_to_taffy_named_lines_keep_their_name_and_split_on_the_span_count() {
        let named = |name: &str, span: i32| {
            AzGridLine::Named(NamedGridLine {
                grid_line_name: name.into(),
                span_count: span,
            })
        };
        assert_eq!(
            grid_line_to_taffy(&named("sidebar", 0)),
            GridPlacement::NamedLine("sidebar".to_string(), 0)
        );
        assert_eq!(
            grid_line_to_taffy(&named("sidebar", 2)),
            GridPlacement::NamedSpan("sidebar".to_string(), 2)
        );
        // A negative span_count is not > 0, so it falls back to a named *line*.
        assert_eq!(
            grid_line_to_taffy(&named("sidebar", -5)),
            GridPlacement::NamedLine("sidebar".to_string(), 0)
        );
        // Empty and non-ASCII names must survive the AzString → String hop.
        assert_eq!(
            grid_line_to_taffy(&named("", 0)),
            GridPlacement::NamedLine(String::new(), 0)
        );
        assert_eq!(
            grid_line_to_taffy(&named("行 🎉 col", 1)),
            GridPlacement::NamedSpan("行 🎉 col".to_string(), 1)
        );
    }
    #[test]
    fn grid_line_to_taffy_named_span_count_wraps_at_u16() {
        assert_eq!(
            grid_line_to_taffy(&AzGridLine::Named(NamedGridLine {
                grid_line_name: "x".into(),
                span_count: 65_536,
            })),
            GridPlacement::NamedSpan("x".to_string(), 0),
            "span_count 65536 wraps to a 0-track named span"
        );
    }
    #[test]
    fn grid_placement_to_taffy_does_not_swap_start_and_end() {
        let p = AzGridPlacement {
            grid_start: AzGridLine::Line(2),
            grid_end: AzGridLine::Span(3),
        };
        let out = grid_placement_to_taffy(&p);
        assert_eq!(out.start, GridPlacement::<String>::from_line_index(2));
        assert_eq!(out.end, GridPlacement::<String>::from_span(3));
        let both_auto = AzGridPlacement {
            grid_start: AzGridLine::Auto,
            grid_end: AzGridLine::Auto,
        };
        let out = grid_placement_to_taffy(&both_auto);
        assert_eq!(out.start, GridPlacement::<String>::Auto);
        assert_eq!(out.end, GridPlacement::<String>::Auto);
    }
    // ==================================================================
    // enum → taffy mapping tables
    // ==================================================================
    #[test]
    fn layout_display_to_taffy_maps_flex_and_grid_and_folds_the_rest_into_block() {
        assert_eq!(
            layout_display_to_taffy(CssPropertyValue::Exact(LayoutDisplay::None)),
            Display::None
        );
        for d in [LayoutDisplay::Flex, LayoutDisplay::InlineFlex] {
            assert_eq!(
                layout_display_to_taffy(CssPropertyValue::Exact(d)),
                Display::Flex,
                "{d:?}"
            );
        }
        for d in [LayoutDisplay::Grid, LayoutDisplay::InlineGrid] {
            assert_eq!(
                layout_display_to_taffy(CssPropertyValue::Exact(d)),
                Display::Grid,
                "{d:?}"
            );
        }
        // Everything else — including `contents`, `table*` and `list-item` — is
        // handed to taffy as a plain block box.
        for d in [
            LayoutDisplay::Block,
            LayoutDisplay::Inline,
            LayoutDisplay::InlineBlock,
            LayoutDisplay::Table,
            LayoutDisplay::TableCell,
            LayoutDisplay::TableRow,
            LayoutDisplay::FlowRoot,
            LayoutDisplay::ListItem,
            LayoutDisplay::Contents,
        ] {
            assert_eq!(
                layout_display_to_taffy(CssPropertyValue::Exact(d)),
                Display::Block,
                "{d:?}"
            );
        }
    }
    #[test]
    fn layout_display_to_taffy_distinguishes_css_wide_none_from_display_none() {
        // `CssPropertyValue::None` means "the property is absent", NOT `display: none`.
        // It must fall back to the initial value (block), or nothing would render.
        assert_eq!(
            layout_display_to_taffy(CssPropertyValue::None),
            Display::Block
        );
        assert_eq!(
            layout_display_to_taffy(CssPropertyValue::Inherit),
            Display::Block
        );
        assert_eq!(
            layout_display_to_taffy(CssPropertyValue::Auto),
            Display::Block
        );
        assert_eq!(
            layout_display_to_taffy(CssPropertyValue::Exact(LayoutDisplay::None)),
            Display::None,
            "…but an explicit `display: none` still means none"
        );
    }
    #[test]
    fn layout_position_to_taffy_and_from_layout_position_agree_on_every_variant() {
        let all = [
            LayoutPosition::Static,
            LayoutPosition::Relative,
            LayoutPosition::Absolute,
            LayoutPosition::Fixed,
            LayoutPosition::Sticky,
        ];
        for p in all {
            assert_eq!(
                layout_position_to_taffy(CssPropertyValue::Exact(p)),
                from_layout_position(p),
                "the two position paths disagree on {p:?}"
            );
        }
        assert_eq!(from_layout_position(LayoutPosition::Static), Position::Relative);
        assert_eq!(from_layout_position(LayoutPosition::Relative), Position::Relative);
        assert_eq!(from_layout_position(LayoutPosition::Sticky), Position::Relative);
        assert_eq!(from_layout_position(LayoutPosition::Absolute), Position::Absolute);
        assert_eq!(from_layout_position(LayoutPosition::Fixed), Position::Absolute);
        // Absent property → `static` → relative.
        assert_eq!(layout_position_to_taffy(CssPropertyValue::None), Position::Relative);
        assert_eq!(layout_position_to_taffy(CssPropertyValue::Inherit), Position::Relative);
    }
    #[test]
    fn grid_auto_flow_to_taffy_maps_all_four_variants_and_defaults_to_row() {
        assert_eq!(
            grid_auto_flow_to_taffy(CssPropertyValue::Exact(LayoutGridAutoFlow::Row)),
            GridAutoFlow::Row
        );
        assert_eq!(
            grid_auto_flow_to_taffy(CssPropertyValue::Exact(LayoutGridAutoFlow::Column)),
            GridAutoFlow::Column
        );
        assert_eq!(
            grid_auto_flow_to_taffy(CssPropertyValue::Exact(LayoutGridAutoFlow::RowDense)),
            GridAutoFlow::RowDense
        );
        assert_eq!(
            grid_auto_flow_to_taffy(CssPropertyValue::Exact(LayoutGridAutoFlow::ColumnDense)),
            GridAutoFlow::ColumnDense
        );
        for v in [
            CssPropertyValue::None,
            CssPropertyValue::Inherit,
            CssPropertyValue::Revert,
            CssPropertyValue::Unset,
            CssPropertyValue::Auto,
            CssPropertyValue::Initial,
        ] {
            assert_eq!(grid_auto_flow_to_taffy(v), GridAutoFlow::Row, "{v:?}");
        }
    }
    #[test]
    fn layout_flex_direction_to_taffy_maps_all_four_variants_and_defaults_to_row() {
        assert_eq!(
            layout_flex_direction_to_taffy(CssPropertyValue::Exact(LayoutFlexDirection::Row)),
            FlexDirection::Row
        );
        assert_eq!(
            layout_flex_direction_to_taffy(CssPropertyValue::Exact(LayoutFlexDirection::RowReverse)),
            FlexDirection::RowReverse
        );
        assert_eq!(
            layout_flex_direction_to_taffy(CssPropertyValue::Exact(LayoutFlexDirection::Column)),
            FlexDirection::Column
        );
        assert_eq!(
            layout_flex_direction_to_taffy(CssPropertyValue::Exact(
                LayoutFlexDirection::ColumnReverse
            )),
            FlexDirection::ColumnReverse
        );
        assert_eq!(
            layout_flex_direction_to_taffy(CssPropertyValue::Inherit),
            FlexDirection::Row
        );
    }
    #[test]
    fn layout_flex_wrap_to_taffy_maps_all_three_variants_and_defaults_to_nowrap() {
        assert_eq!(
            layout_flex_wrap_to_taffy(CssPropertyValue::Exact(LayoutFlexWrap::NoWrap)),
            FlexWrap::NoWrap
        );
        assert_eq!(
            layout_flex_wrap_to_taffy(CssPropertyValue::Exact(LayoutFlexWrap::Wrap)),
            FlexWrap::Wrap
        );
        assert_eq!(
            layout_flex_wrap_to_taffy(CssPropertyValue::Exact(LayoutFlexWrap::WrapReverse)),
            FlexWrap::WrapReverse
        );
        assert_eq!(
            layout_flex_wrap_to_taffy(CssPropertyValue::Inherit),
            FlexWrap::NoWrap
        );
    }
    #[test]
    fn layout_align_items_to_taffy_maps_start_and_end_onto_the_flex_variants() {
        assert_eq!(
            layout_align_items_to_taffy(CssPropertyValue::Exact(LayoutAlignItems::Stretch)),
            AlignItems::Stretch
        );
        assert_eq!(
            layout_align_items_to_taffy(CssPropertyValue::Exact(LayoutAlignItems::Center)),
            AlignItems::Center
        );
        assert_eq!(
            layout_align_items_to_taffy(CssPropertyValue::Exact(LayoutAlignItems::Start)),
            AlignItems::FlexStart
        );
        assert_eq!(
            layout_align_items_to_taffy(CssPropertyValue::Exact(LayoutAlignItems::End)),
            AlignItems::FlexEnd
        );
        assert_eq!(
            layout_align_items_to_taffy(CssPropertyValue::Exact(LayoutAlignItems::Baseline)),
            AlignItems::Baseline
        );
        // Absent → the CSS initial value, `stretch`.
        assert_eq!(
            layout_align_items_to_taffy(CssPropertyValue::Inherit),
            AlignItems::Stretch
        );
    }
    #[test]
    fn layout_align_self_to_taffy_returns_none_only_for_auto() {
        assert_eq!(
            layout_align_self_to_taffy(CssPropertyValue::Exact(LayoutAlignSelf::Auto)),
            None
        );
        // `auto` is the initial value, so an absent property is None too — that is
        // what lets taffy inherit the parent's align-items.
        assert_eq!(layout_align_self_to_taffy(CssPropertyValue::Inherit), None);
        assert_eq!(layout_align_self_to_taffy(CssPropertyValue::Initial), None);
        assert_eq!(
            layout_align_self_to_taffy(CssPropertyValue::Exact(LayoutAlignSelf::Start)),
            Some(AlignSelf::FlexStart)
        );
        assert_eq!(
            layout_align_self_to_taffy(CssPropertyValue::Exact(LayoutAlignSelf::End)),
            Some(AlignSelf::FlexEnd)
        );
        assert_eq!(
            layout_align_self_to_taffy(CssPropertyValue::Exact(LayoutAlignSelf::Center)),
            Some(AlignSelf::Center)
        );
        assert_eq!(
            layout_align_self_to_taffy(CssPropertyValue::Exact(LayoutAlignSelf::Baseline)),
            Some(AlignSelf::Baseline)
        );
        assert_eq!(
            layout_align_self_to_taffy(CssPropertyValue::Exact(LayoutAlignSelf::Stretch)),
            Some(AlignSelf::Stretch)
        );
    }
    #[test]
    fn layout_align_content_to_taffy_maps_every_variant_and_defaults_to_stretch() {
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Exact(LayoutAlignContent::Start)),
            AlignContent::FlexStart
        );
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Exact(LayoutAlignContent::End)),
            AlignContent::FlexEnd
        );
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Exact(LayoutAlignContent::Center)),
            AlignContent::Center
        );
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Exact(LayoutAlignContent::Stretch)),
            AlignContent::Stretch
        );
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Exact(
                LayoutAlignContent::SpaceBetween
            )),
            AlignContent::SpaceBetween
        );
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Exact(
                LayoutAlignContent::SpaceAround
            )),
            AlignContent::SpaceAround
        );
        assert_eq!(
            layout_align_content_to_taffy(CssPropertyValue::Inherit),
            AlignContent::Stretch
        );
    }
    #[test]
    fn layout_justify_content_to_taffy_keeps_start_distinct_from_flex_start() {
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(
                LayoutJustifyContent::FlexStart
            )),
            JustifyContent::FlexStart
        );
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(LayoutJustifyContent::Start)),
            JustifyContent::Start
        );
        assert_ne!(JustifyContent::Start, JustifyContent::FlexStart);
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(
                LayoutJustifyContent::FlexEnd
            )),
            JustifyContent::FlexEnd
        );
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(LayoutJustifyContent::End)),
            JustifyContent::End
        );
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(LayoutJustifyContent::Center)),
            JustifyContent::Center
        );
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(
                LayoutJustifyContent::SpaceBetween
            )),
            JustifyContent::SpaceBetween
        );
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(
                LayoutJustifyContent::SpaceAround
            )),
            JustifyContent::SpaceAround
        );
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Exact(
                LayoutJustifyContent::SpaceEvenly
            )),
            JustifyContent::SpaceEvenly
        );
        // Initial value of justify-content in this codebase is `start`.
        assert_eq!(
            layout_justify_content_to_taffy(CssPropertyValue::Inherit),
            JustifyContent::Start
        );
    }
    #[test]
    fn layout_justify_items_to_taffy_maps_all_four_variants_and_defaults_to_stretch() {
        assert_eq!(
            layout_justify_items_to_taffy(CssPropertyValue::Exact(LayoutJustifyItems::Start)),
            AlignItems::Start
        );
        assert_eq!(
            layout_justify_items_to_taffy(CssPropertyValue::Exact(LayoutJustifyItems::End)),
            AlignItems::End
        );
        assert_eq!(
            layout_justify_items_to_taffy(CssPropertyValue::Exact(LayoutJustifyItems::Center)),
            AlignItems::Center
        );
        assert_eq!(
            layout_justify_items_to_taffy(CssPropertyValue::Exact(LayoutJustifyItems::Stretch)),
            AlignItems::Stretch
        );
        assert_eq!(
            layout_justify_items_to_taffy(CssPropertyValue::Inherit),
            AlignItems::Stretch
        );
        // justify-items uses the *logical* Start/End, unlike align-items, which is
        // mapped onto FlexStart/FlexEnd. Guard the asymmetry.
        assert_ne!(
            layout_justify_items_to_taffy(CssPropertyValue::Exact(LayoutJustifyItems::Start)),
            layout_align_items_to_taffy(CssPropertyValue::Exact(LayoutAlignItems::Start))
        );
    }
    #[test]
    fn azul_overflow_to_taffy_treats_auto_as_scroll_and_everything_unset_as_visible() {
        assert_eq!(
            azul_overflow_to_taffy(MultiValue::Exact(LayoutOverflow::Visible)),
            taffy::Overflow::Visible
        );
        assert_eq!(
            azul_overflow_to_taffy(MultiValue::Exact(LayoutOverflow::Hidden)),
            taffy::Overflow::Hidden
        );
        assert_eq!(
            azul_overflow_to_taffy(MultiValue::Exact(LayoutOverflow::Scroll)),
            taffy::Overflow::Scroll
        );
        assert_eq!(
            azul_overflow_to_taffy(MultiValue::Exact(LayoutOverflow::Clip)),
            taffy::Overflow::Clip
        );
        // Taffy has no `auto`; `auto` constrains the box exactly like `scroll`.
        assert_eq!(
            azul_overflow_to_taffy(MultiValue::Exact(LayoutOverflow::Auto)),
            taffy::Overflow::Scroll
        );
        for v in [MultiValue::Auto, MultiValue::Initial, MultiValue::Inherit] {
            assert_eq!(azul_overflow_to_taffy(v), taffy::Overflow::Visible);
        }
    }
    // ==================================================================
    // css_width_to_px / css_height_to_px
    // ==================================================================
    #[test]
    fn css_width_and_height_to_px_only_resolve_absolute_px_lengths() {
        assert_eq!(
            css_width_to_px(&LayoutWidth::Px(PixelValue::px(120.0))),
            Some(120.0)
        );
        assert_eq!(
            css_height_to_px(&LayoutHeight::Px(PixelValue::px(120.0))),
            Some(120.0)
        );
        // em/rem go through the 16px fallback rather than returning None.
        assert_eq!(
            css_width_to_px(&LayoutWidth::Px(PixelValue::em(2.0))),
            Some(32.0)
        );
        assert_eq!(
            css_height_to_px(&LayoutHeight::Px(PixelValue::rem(2.0))),
            Some(32.0)
        );
        // …but a percentage width has no containing block here.
        assert_eq!(
            css_width_to_px(&LayoutWidth::Px(PixelValue::percent(50.0))),
            None
        );
        assert_eq!(
            css_height_to_px(&LayoutHeight::Px(PixelValue::percent(50.0))),
            None
        );
    }
    #[test]
    fn css_width_and_height_to_px_return_none_for_every_non_px_variant() {
        let widths = [
            LayoutWidth::Auto,
            LayoutWidth::MinContent,
            LayoutWidth::MaxContent,
            LayoutWidth::FitContent(PixelValue::px(10.0)),
            LayoutWidth::Calc(CalcAstItemVec::from_vec(vec![CalcAstItem::Value(
                PixelValue::px(10.0),
            )])),
        ];
        for w in &widths {
            assert_eq!(css_width_to_px(w), None, "{w:?}");
        }
        let heights = [
            LayoutHeight::Auto,
            LayoutHeight::MinContent,
            LayoutHeight::MaxContent,
            LayoutHeight::FitContent(PixelValue::px(10.0)),
            LayoutHeight::Calc(CalcAstItemVec::from_vec(Vec::new())),
        ];
        for h in &heights {
            assert_eq!(css_height_to_px(h), None, "{h:?}");
        }
    }
    #[test]
    fn css_width_to_px_never_returns_nan_for_a_nan_pixel_value() {
        let w = css_width_to_px(&LayoutWidth::Px(PixelValue::px(f32::NAN)));
        assert_eq!(w, Some(0.0));
        let h = css_height_to_px(&LayoutHeight::Px(PixelValue::px(f32::INFINITY)));
        assert!(h.expect("px resolves").is_finite());
    }
    // ==================================================================
    // MultiValue<PixelValue> → taffy lengths
    // ==================================================================
    #[test]
    fn multi_value_to_lpa_maps_the_css_wide_keywords_to_auto() {
        for mv in [MultiValue::Auto, MultiValue::Initial, MultiValue::Inherit] {
            assert!(
                multi_value_to_lpa(mv).is_auto(),
                "inset keywords must stay auto"
            );
        }
    }
    #[test]
    fn multi_value_to_lpa_resolves_lengths_percentages_and_falls_back_to_auto() {
        assert_eq!(
            multi_value_to_lpa(MultiValue::Exact(PixelValue::px(0.0))),
            LengthPercentageAuto::length(0.0)
        );
        assert_eq!(
            multi_value_to_lpa(MultiValue::Exact(PixelValue::px(-12.5))),
            LengthPercentageAuto::length(-12.5),
            "negative insets are legal and must not be clamped"
        );
        assert_eq!(
            multi_value_to_lpa(MultiValue::Exact(PixelValue::percent(50.0))),
            LengthPercentageAuto::percent(0.5),
            "taffy percentages are 0..1, azul's are 0..100"
        );
        assert_eq!(
            multi_value_to_lpa(MultiValue::Exact(PixelValue::em(2.0))),
            LengthPercentageAuto::length(32.0)
        );
        // Viewport units resolve to neither a length nor a percent → auto.
        for m in [SizeMetric::Vw, SizeMetric::Vh, SizeMetric::Vmin, SizeMetric::Vmax] {
            assert!(
                multi_value_to_lpa(MultiValue::Exact(PixelValue::from_metric(m, 10.0))).is_auto(),
                "{m:?} is silently dropped to auto"
            );
        }
    }
    #[test]
    fn multi_value_to_lpa_margin_keeps_auto_but_zeroes_the_other_keywords() {
        // The whole point of the margin variant: `auto` survives (flex centering),
        // `initial`/`inherit` become 0 (the CSS initial margin).
        assert!(multi_value_to_lpa_margin(MultiValue::Auto).is_auto());
        assert_eq!(
            multi_value_to_lpa_margin(MultiValue::Initial),
            LengthPercentageAuto::length(0.0)
        );
        assert_eq!(
            multi_value_to_lpa_margin(MultiValue::Inherit),
            LengthPercentageAuto::length(0.0)
        );
        // …which is exactly where it differs from the inset variant.
        assert!(multi_value_to_lpa(MultiValue::Initial).is_auto());
    }
    #[test]
    fn multi_value_to_lpa_margin_falls_back_to_zero_not_auto_for_unresolvable_units() {
        assert_eq!(
            multi_value_to_lpa_margin(MultiValue::Exact(PixelValue::from_metric(
                SizeMetric::Vw,
                10.0
            ))),
            LengthPercentageAuto::length(0.0),
            "an unresolvable margin must not turn into `margin: auto` (it would centre the item)"
        );
        assert_eq!(
            multi_value_to_lpa_margin(MultiValue::Exact(PixelValue::percent(25.0))),
            LengthPercentageAuto::percent(0.25)
        );
        assert_eq!(
            multi_value_to_lpa_margin(MultiValue::Exact(PixelValue::px(-8.0))),
            LengthPercentageAuto::length(-8.0),
            "negative margins are legal CSS"
        );
        assert_eq!(
            multi_value_to_lpa_margin(MultiValue::Exact(PixelValue::px(f32::NAN))),
            LengthPercentageAuto::length(0.0)
        );
    }
    #[test]
    fn multi_value_to_lp_maps_every_keyword_and_unresolvable_unit_to_zero() {
        for mv in [MultiValue::Auto, MultiValue::Initial, MultiValue::Inherit] {
            assert_eq!(multi_value_to_lp(mv), LengthPercentage::ZERO);
        }
        for m in CONTEXTUAL_METRICS.iter().filter(|m| **m != SizeMetric::Percent) {
            assert_eq!(
                multi_value_to_lp(MultiValue::Exact(PixelValue::from_metric(*m, 10.0))),
                LengthPercentage::ZERO,
                "{m:?}"
            );
        }
        assert_eq!(
            multi_value_to_lp(MultiValue::Exact(PixelValue::px(4.0))),
            LengthPercentage::length(4.0)
        );
        assert_eq!(
            multi_value_to_lp(MultiValue::Exact(PixelValue::percent(10.0))),
            LengthPercentage::percent(0.1)
        );
    }
    #[test]
    fn pixel_to_lp_agrees_with_multi_value_to_lp_on_every_exact_value() {
        let values = [
            PixelValue::px(0.0),
            PixelValue::px(12.0),
            PixelValue::px(-12.0),
            PixelValue::px(f32::NAN),
            PixelValue::px(f32::INFINITY),
            PixelValue::em(1.5),
            PixelValue::rem(1.5),
            PixelValue::pt(12.0),
            PixelValue::percent(33.0),
            PixelValue::from_metric(SizeMetric::Vw, 100.0),
            PixelValue::from_metric(SizeMetric::Vmax, 100.0),
        ];
        for pv in values {
            assert_eq!(
                pixel_to_lp(pv),
                multi_value_to_lp(MultiValue::Exact(pv)),
                "{pv:?}"
            );
        }
        assert_eq!(
            pixel_to_lp(PixelValue::from_metric(SizeMetric::Vw, 100.0)),
            LengthPercentage::ZERO
        );
    }
    // ==================================================================
    // from_layout_width / from_layout_height / store_calc_and_make_dimension
    // ==================================================================
    #[allow(clippy::vec_box)] // return type must mirror the production calc_storage (Box = stable element addresses)
    fn empty_calc_storage() -> std::cell::RefCell<Vec<Box<CalcResolveContext>>> {
        std::cell::RefCell::new(Vec::new())
    }
    #[test]
    fn from_layout_width_and_height_agree_on_every_shared_variant() {
        let storage = empty_calc_storage();
        let pairs: [(LayoutWidth, LayoutHeight); 6] = [
            (LayoutWidth::Auto, LayoutHeight::Auto),
            (
                LayoutWidth::Px(PixelValue::px(100.0)),
                LayoutHeight::Px(PixelValue::px(100.0)),
            ),
            (
                LayoutWidth::Px(PixelValue::percent(50.0)),
                LayoutHeight::Px(PixelValue::percent(50.0)),
            ),
            (LayoutWidth::MinContent, LayoutHeight::MinContent),
            (LayoutWidth::MaxContent, LayoutHeight::MaxContent),
            (
                LayoutWidth::FitContent(PixelValue::px(10.0)),
                LayoutHeight::FitContent(PixelValue::px(10.0)),
            ),
        ];
        for (w, h) in pairs {
            assert_eq!(
                from_layout_width(w.clone(), &storage, 16.0, 16.0),
                from_layout_height(h, &storage, 16.0, 16.0),
                "{w:?}"
            );
        }
        assert!(storage.borrow().is_empty(), "no calc() → no storage growth");
    }
    #[test]
    fn from_layout_width_maps_the_intrinsic_keywords_to_auto() {
        let storage = empty_calc_storage();
        for v in [
            LayoutWidth::Auto,
            LayoutWidth::MinContent,
            LayoutWidth::MaxContent,
            LayoutWidth::FitContent(PixelValue::px(10.0)),
        ] {
            assert_eq!(
                from_layout_width(v.clone(), &storage, 16.0, 16.0),
                Dimension::auto(),
                "{v:?} is not forwarded to taffy — it becomes auto"
            );
        }
    }
    #[test]
    fn from_layout_width_resolves_lengths_and_percentages_and_defaults_to_auto() {
        let storage = empty_calc_storage();
        assert_eq!(
            from_layout_width(LayoutWidth::Px(PixelValue::px(0.0)), &storage, 16.0, 16.0),
            Dimension::length(0.0)
        );
        assert_eq!(
            from_layout_width(LayoutWidth::Px(PixelValue::px(-50.0)), &storage, 16.0, 16.0),
            Dimension::length(-50.0),
            "a negative width is nonsense CSS, but the bridge passes it straight through"
        );
        assert_eq!(
            from_layout_width(
                LayoutWidth::Px(PixelValue::percent(100.0)),
                &storage,
                16.0,
                16.0
            ),
            Dimension::percent(1.0)
        );
        assert_eq!(
            from_layout_height(LayoutHeight::Px(PixelValue::em(3.0)), &storage, 16.0, 16.0),
            Dimension::length(48.0),
            "em uses the 16px fallback, NOT the em_size argument"
        );
        // Viewport units cannot be resolved here → auto.
        assert_eq!(
            from_layout_width(
                LayoutWidth::Px(PixelValue::from_metric(SizeMetric::Vw, 100.0)),
                &storage,
                16.0,
                16.0
            ),
            Dimension::auto()
        );
        // NaN is already flattened to 0 by PixelValue itself.
        assert_eq!(
            from_layout_width(LayoutWidth::Px(PixelValue::px(f32::NAN)), &storage, 16.0, 16.0),
            Dimension::length(0.0)
        );
        let huge = from_layout_height(
            LayoutHeight::Px(PixelValue::px(f32::INFINITY)),
            &storage,
            16.0,
            16.0,
        );
        assert!(huge.value().is_finite(), "an infinite height reached taffy");
    }
    #[test]
    fn from_layout_width_ignores_the_font_sizes_for_non_calc_values() {
        // em_size/rem_size are only wired into the calc() context; the Px path uses
        // the hard-coded 16px fallback. Passing NaN font sizes must therefore not
        // corrupt a plain `width: 2em`.
        let storage = empty_calc_storage();
        assert_eq!(
            from_layout_width(
                LayoutWidth::Px(PixelValue::em(2.0)),
                &storage,
                f32::NAN,
                f32::INFINITY
            ),
            Dimension::length(32.0)
        );
    }
    #[test]
    fn from_layout_width_calc_produces_a_calc_dimension_and_pins_the_context() {
        let storage = empty_calc_storage();
        let items = CalcAstItemVec::from_vec(vec![
            CalcAstItem::Value(PixelValue::percent(100.0)),
            CalcAstItem::Sub,
            CalcAstItem::Value(PixelValue::px(20.0)),
        ]);
        let d = from_layout_width(LayoutWidth::Calc(items), &storage, 20.0, 10.0);
        let raw = d.into_raw();
        assert!(raw.is_calc(), "calc() must reach taffy as a calc Dimension");
        assert_eq!(storage.borrow().len(), 1);
        // SAFETY: the Box is still owned by `storage`, exactly as during a layout pass.
        let ctx = unsafe { &*raw.calc_value().cast::<CalcResolveContext>() };
        assert_eq!(ctx.em_size, 20.0);
        assert_eq!(ctx.rem_size, 10.0);
        assert_eq!(ctx.items.as_ref().len(), 3);
    }
    #[test]
    fn store_calc_and_make_dimension_keeps_every_pointer_valid_across_vec_growth() {
        // The whole reason for Box<CalcResolveContext>: the outer Vec reallocates
        // many times while taffy is still holding raw pointers into it.
        let storage = empty_calc_storage();
        let dims: Vec<Dimension> = (0..256usize)
            .map(|i| {
                let items = CalcAstItemVec::from_vec(vec![CalcAstItem::Value(PixelValue::px(
                    i as f32,
                ))]);
                store_calc_and_make_dimension(items, &storage, i as f32, 16.0)
            })
            .collect();
        assert_eq!(storage.borrow().len(), 256);
        for (i, d) in dims.iter().enumerate() {
            let raw = d.into_raw();
            assert!(raw.is_calc(), "#{i} is not a calc dimension");
            // SAFETY: every Box is still alive in `storage`.
            let ctx = unsafe { &*raw.calc_value().cast::<CalcResolveContext>() };
            assert_eq!(
                ctx.em_size, i as f32,
                "context #{i} moved when the Vec reallocated"
            );
            assert_eq!(ctx.rem_size, 16.0);
            assert_eq!(ctx.items.as_ref().len(), 1);
        }
    }
    #[test]
    fn store_calc_and_make_dimension_accepts_an_empty_ast_and_nan_font_sizes() {
        let storage = empty_calc_storage();
        let d = store_calc_and_make_dimension(
            CalcAstItemVec::from_vec(Vec::new()),
            &storage,
            f32::NAN,
            f32::INFINITY,
        );
        let raw = d.into_raw();
        assert!(raw.is_calc());
        assert_eq!(storage.borrow().len(), 1);
        // SAFETY: the Box is still owned by `storage`.
        let ctx = unsafe { &*raw.calc_value().cast::<CalcResolveContext>() };
        assert!(ctx.items.as_ref().is_empty());
        assert!(
            ctx.em_size.is_nan() && ctx.rem_size.is_infinite(),
            "the font sizes are stored verbatim — evaluate_calc has to cope"
        );
    }
    #[test]
    fn store_calc_and_make_dimension_hands_out_a_distinct_pointer_per_call() {
        let storage = empty_calc_storage();
        let a = store_calc_and_make_dimension(
            CalcAstItemVec::from_vec(Vec::new()),
            &storage,
            1.0,
            1.0,
        );
        let b = store_calc_and_make_dimension(
            CalcAstItemVec::from_vec(Vec::new()),
            &storage,
            2.0,
            2.0,
        );
        assert_ne!(
            a.into_raw().calc_value(),
            b.into_raw().calc_value(),
            "two calc() values must not alias the same context"
        );
        assert_ne!(a, b);
    }
    // ==================================================================
    // compute_taffy_scrollbar_info — needs a real StyledDom + LayoutTree
    // ==================================================================
    #[cfg(all(feature = "text_layout", feature = "font_loading"))]
    mod with_layout_context {
        use azul_core::{
            dom::{Dom, DomId},
            selection::TextSelection,
        };
        use azul_css::{props::basic::FontRef, LayoutDebugMessage};
        use super::*;
        use crate::{
            font_traits::FontManager,
            solver3::{cache, layout_tree::generate_layout_tree},
        };
        /// Owns everything a `LayoutContext` borrows.
        struct Env {
            styled_dom: StyledDom,
            font_manager: FontManager<FontRef>,
            text_selections: BTreeMap<DomId, TextSelection>,
            counters: HashMap<(usize, String), i32>,
            image_cache: azul_core::resources::ImageCache,
            debug_messages: Option<Vec<LayoutDebugMessage>>,
        }
        impl Env {
            fn new() -> Self {
                let mut dom = Dom::create_body();
                let (css, _warnings) = azul_css::parser2::new_from_str("");
                Self {
                    styled_dom: StyledDom::create(&mut dom, css),
                    font_manager: FontManager::new(rust_fontconfig::FcFontCache::default())
                        .expect("FontManager over an empty font cache"),
                    text_selections: BTreeMap::new(),
                    counters: HashMap::new(),
                    image_cache: azul_core::resources::ImageCache::default(),
                    debug_messages: None,
                }
            }
            fn ctx(&mut self) -> LayoutContext<'_, FontRef> {
                LayoutContext {
            reflowed_ifcs: std::collections::BTreeSet::new(),
                    style_cache: Default::default(),
                    scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
                    styled_dom: &self.styled_dom,
                    font_manager: &self.font_manager,
                    text_selections: &self.text_selections,
                    debug_messages: &mut self.debug_messages,
                    counters: &mut self.counters,
                    viewport_size: LogicalSize::new(800.0, 600.0),
                    fragmentation_context: None,
                    cursor_is_visible: true,
                    cursor_locations: Vec::new(),
                    preedit_text: None,
                    cache_map: cache::LayoutCacheMap::default(),
                    image_cache: &self.image_cache,
                    content_overlay: None,
                    system_style: None,
                    get_system_time_fn: azul_core::task::GetSystemTimeCallback {
                        cb: azul_core::task::get_system_time_libstd,
                    },
                }
            }
        }
        #[test]
        fn compute_taffy_scrollbar_info_returns_defaults_for_an_out_of_range_node() {
            let mut env = Env::new();
            let mut ctx = env.ctx();
            let tree = generate_layout_tree(&mut ctx).expect("a plain body dom builds");
            for idx in [usize::MAX, tree.nodes.len(), tree.nodes.len() + 1] {
                let (info, w, h) = compute_taffy_scrollbar_info(
                    &ctx,
                    &tree,
                    idx,
                    100.0,
                    100.0,
                    500.0,
                    500.0,
                    ContentSizeOrigin::BorderBox,
                );
                assert!(!info.needs_horizontal, "#{idx}");
                assert!(!info.needs_vertical, "#{idx}");
                assert_eq!(w, 0.0);
                assert_eq!(h, 0.0);
            }
        }
        #[test]
        fn compute_taffy_scrollbar_info_never_reports_a_negative_or_nan_content_size() {
            let mut env = Env::new();
            let mut ctx = env.ctx();
            let tree = generate_layout_tree(&mut ctx).expect("a plain body dom builds");
            let root = tree.root;
            let extremes = [
                0.0f32,
                -1.0,
                f32::NAN,
                f32::INFINITY,
                f32::NEG_INFINITY,
                f32::MAX,
                f32::MIN,
            ];
            for v in extremes {
                for c in extremes {
                    let (_info, w, h) = compute_taffy_scrollbar_info(
                        &ctx,
                        &tree,
                        root,
                        v,
                        v,
                        c,
                        c,
                        ContentSizeOrigin::BorderBox,
                    );
                    assert!(
                        !w.is_nan() && w >= 0.0,
                        "result={v} content={c} → content width {w}"
                    );
                    assert!(
                        !h.is_nan() && h >= 0.0,
                        "result={v} content={c} → content height {h}"
                    );
                }
            }
        }
        #[test]
        fn compute_taffy_scrollbar_info_needs_no_scrollbars_for_an_overflow_visible_body() {
            let mut env = Env::new();
            let mut ctx = env.ctx();
            let tree = generate_layout_tree(&mut ctx).expect("a plain body dom builds");
            let root = tree.root;
            // Content far larger than the box: `overflow: visible` still must not
            // ask for scrollbars (only `auto`/`scroll` do).
            let (info, w, h) = compute_taffy_scrollbar_info(
                &ctx,
                &tree,
                root,
                100.0,
                100.0,
                10_000.0,
                10_000.0,
                ContentSizeOrigin::BorderBox,
            );
            assert!(!info.needs_horizontal);
            assert!(!info.needs_vertical);
            assert_eq!(info.scrollbar_width, 0.0);
            assert_eq!(info.scrollbar_height, 0.0);
            assert!(w > 0.0 && h > 0.0, "the taffy content size is passed back");
        }
    }
}