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

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

            
42
/// Convert PixelValue to pixels, only for absolute units (no %, and em/rem use fallback)
43
/// Used where proper resolution context is not available (grid tracks, etc.)
44
9170
fn pixel_value_to_pixels_fallback(pv: &PixelValue) -> Option<f32> {
45
9170
    match pv.metric {
46
9065
        SizeMetric::Px => Some(pv.number.get()),
47
        SizeMetric::Pt => Some(pv.number.get() * PT_TO_PX),
48
        SizeMetric::In => Some(pv.number.get() * CSS_PX_PER_INCH),
49
        SizeMetric::Cm => Some(pv.number.get() * CSS_PX_PER_INCH / 2.54),
50
        SizeMetric::Mm => Some(pv.number.get() * CSS_PX_PER_INCH / 25.4),
51
        // For em/rem, use DEFAULT_FONT_SIZE as fallback (not ideal but needed without context)
52
        SizeMetric::Em | SizeMetric::Rem => Some(pv.number.get() * DEFAULT_FONT_SIZE),
53
105
        SizeMetric::Percent => None, // Cannot resolve without containing block
54
        // Viewport units: Cannot resolve without viewport context
55
        SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => None,
56
    }
57
9170
}
58

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

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

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

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

            
106
fn translate_track(track: &GridTrackSizing) -> taffy::TrackSizingFunction {
107
    // Helper to resolve PixelValue to absolute pixels (handles em, rem, but not %)
108
    // Grid track sizing in Taffy doesn't support % - only absolute values
109
    let px_to_float = |pv: PixelValue| -> f32 {
110
        pixel_value_to_pixels_fallback(&pv).unwrap_or(0.0)
111
    };
112

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

            
159
fn minmax(min: MinTrackSizingFunction, max: MaxTrackSizingFunction) -> taffy::TrackSizingFunction {
160
    TrackSizingFunction { min, max }
161
}
162

            
163
665
fn layout_display_to_taffy(val: LayoutDisplayValue) -> taffy::Display {
164
665
    match val.get_property_or_default().unwrap_or_default() {
165
        LayoutDisplay::None => taffy::Display::None,
166
315
        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => taffy::Display::Flex,
167
        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => taffy::Display::Grid,
168
350
        _ => taffy::Display::Block,
169
    }
170
665
}
171

            
172
// to determine their CB; Taffy's Position::Absolute handles this for both flex and grid
173
fn layout_position_to_taffy(val: LayoutPositionValue) -> taffy::Position {
174
    match val.get_property_or_default().unwrap_or_default() {
175
        LayoutPosition::Absolute => taffy::Position::Absolute,
176
        LayoutPosition::Fixed => taffy::Position::Absolute, // Taffy has no Fixed variant
177
        LayoutPosition::Relative => taffy::Position::Relative,
178
        LayoutPosition::Static => taffy::Position::Relative,
179
        LayoutPosition::Sticky => taffy::Position::Relative, // Sticky treated as Relative
180
    }
181
}
182

            
183
fn decode_compact_grid_line(v: i16) -> taffy::style::GridPlacement<String> {
184
    if v == azul_css::compact_cache::I16_AUTO || v == azul_css::compact_cache::I16_SENTINEL {
185
        taffy::style::GridPlacement::Auto
186
    } else if v < 0 {
187
        taffy::style::GridPlacement::<String>::from_span((-v) as u16)
188
    } else {
189
        taffy::style::GridPlacement::<String>::from_line_index(v)
190
    }
191
}
192

            
193
fn grid_auto_flow_to_taffy(val: LayoutGridAutoFlowValue) -> taffy::GridAutoFlow {
194
    match val.get_property_or_default().unwrap_or_default() {
195
        LayoutGridAutoFlow::Row => taffy::GridAutoFlow::Row,
196
        LayoutGridAutoFlow::Column => taffy::GridAutoFlow::Column,
197
        LayoutGridAutoFlow::RowDense => taffy::GridAutoFlow::RowDense,
198
        LayoutGridAutoFlow::ColumnDense => taffy::GridAutoFlow::ColumnDense,
199
    }
200
}
201

            
202
/// Convert an azul `GridLine` (single start or end) to a Taffy `GridPlacement`.
203
fn grid_line_to_taffy(
204
    line: &azul_css::props::layout::grid::GridLine,
205
) -> taffy::style::GridPlacement<String> {
206
    use azul_css::props::layout::grid::GridLine as AzGridLine;
207
    use taffy::style_helpers::{TaffyGridLine, TaffyGridSpan};
208
    match line {
209
        AzGridLine::Auto => taffy::style::GridPlacement::Auto,
210
        AzGridLine::Line(n) => {
211
            taffy::style::GridPlacement::<String>::from_line_index(*n as i16)
212
        }
213
        AzGridLine::Span(n) => taffy::style::GridPlacement::<String>::from_span(*n as u16),
214
        AzGridLine::Named(named) => {
215
            // Named lines: use the name with optional span
216
            let name = named.grid_line_name.as_str().to_string();
217
            if named.span_count > 0 {
218
                taffy::style::GridPlacement::NamedSpan(name, named.span_count as u16)
219
            } else {
220
                taffy::style::GridPlacement::NamedLine(name, 0)
221
            }
222
        }
223
    }
224
}
225

            
226
/// Convert an azul `GridPlacement` (grid-column / grid-row) to a Taffy `Line<GridPlacement>`.
227
fn grid_placement_to_taffy(
228
    placement: &azul_css::props::layout::grid::GridPlacement,
229
) -> taffy::Line<taffy::style::GridPlacement<String>> {
230
    taffy::Line {
231
        start: grid_line_to_taffy(&placement.grid_start),
232
        end: grid_line_to_taffy(&placement.grid_end),
233
    }
234
}
235

            
236
665
fn layout_flex_direction_to_taffy(val: LayoutFlexDirectionValue) -> taffy::FlexDirection {
237
665
    match val.get_property_or_default().unwrap_or_default() {
238
490
        LayoutFlexDirection::Row => taffy::FlexDirection::Row,
239
        LayoutFlexDirection::RowReverse => taffy::FlexDirection::RowReverse,
240
175
        LayoutFlexDirection::Column => taffy::FlexDirection::Column,
241
        LayoutFlexDirection::ColumnReverse => taffy::FlexDirection::ColumnReverse,
242
    }
243
665
}
244

            
245
665
fn layout_flex_wrap_to_taffy(val: LayoutFlexWrapValue) -> taffy::FlexWrap {
246
665
    match val.get_property_or_default().unwrap_or_default() {
247
665
        LayoutFlexWrap::NoWrap => taffy::FlexWrap::NoWrap,
248
        LayoutFlexWrap::Wrap => taffy::FlexWrap::Wrap,
249
        LayoutFlexWrap::WrapReverse => taffy::FlexWrap::WrapReverse,
250
    }
251
665
}
252

            
253
665
fn layout_align_items_to_taffy(val: LayoutAlignItemsValue) -> taffy::AlignItems {
254
665
    match val.get_property_or_default().unwrap_or_default() {
255
630
        LayoutAlignItems::Stretch => taffy::AlignItems::Stretch,
256
35
        LayoutAlignItems::Center => taffy::AlignItems::Center,
257
        LayoutAlignItems::Start => taffy::AlignItems::FlexStart,
258
        LayoutAlignItems::End => taffy::AlignItems::FlexEnd,
259
        LayoutAlignItems::Baseline => taffy::AlignItems::Baseline,
260
    }
261
665
}
262

            
263
fn layout_align_self_to_taffy(val: LayoutAlignSelfValue) -> Option<taffy::AlignSelf> {
264
    match val.get_property_or_default().unwrap_or_default() {
265
        LayoutAlignSelf::Auto => None, // Auto means inherit from parent's align-items (for non-abspos; abspos auto computes to itself per spec)
266
        LayoutAlignSelf::Start => Some(taffy::AlignSelf::FlexStart),
267
        LayoutAlignSelf::End => Some(taffy::AlignSelf::FlexEnd),
268
        LayoutAlignSelf::Center => Some(taffy::AlignSelf::Center),
269
        LayoutAlignSelf::Baseline => Some(taffy::AlignSelf::Baseline),
270
        LayoutAlignSelf::Stretch => Some(taffy::AlignSelf::Stretch),
271
    }
272
}
273

            
274
665
fn layout_align_content_to_taffy(val: LayoutAlignContentValue) -> taffy::AlignContent {
275
665
    match val.get_property_or_default().unwrap_or_default() {
276
        LayoutAlignContent::Start => taffy::AlignContent::FlexStart,
277
        LayoutAlignContent::End => taffy::AlignContent::FlexEnd,
278
        LayoutAlignContent::Center => taffy::AlignContent::Center,
279
665
        LayoutAlignContent::Stretch => taffy::AlignContent::Stretch,
280
        LayoutAlignContent::SpaceBetween => taffy::AlignContent::SpaceBetween,
281
        LayoutAlignContent::SpaceAround => taffy::AlignContent::SpaceAround,
282
    }
283
665
}
284

            
285
fn layout_justify_content_to_taffy(val: LayoutJustifyContentValue) -> taffy::JustifyContent {
286
    match val.get_property_or_default().unwrap_or_default() {
287
        LayoutJustifyContent::FlexStart => taffy::JustifyContent::FlexStart,
288
        LayoutJustifyContent::FlexEnd => taffy::JustifyContent::FlexEnd,
289
        LayoutJustifyContent::Start => taffy::JustifyContent::Start,
290
        LayoutJustifyContent::End => taffy::JustifyContent::End,
291
        LayoutJustifyContent::Center => taffy::JustifyContent::Center,
292
        LayoutJustifyContent::SpaceBetween => taffy::JustifyContent::SpaceBetween,
293
        LayoutJustifyContent::SpaceAround => taffy::JustifyContent::SpaceAround,
294
        LayoutJustifyContent::SpaceEvenly => taffy::JustifyContent::SpaceEvenly,
295
    }
296
}
297

            
298
fn layout_justify_items_to_taffy(
299
    val: azul_css::props::property::LayoutJustifyItemsValue,
300
) -> taffy::AlignItems {
301
    use azul_css::props::layout::grid::LayoutJustifyItems;
302
    match val.get_property_or_default().unwrap_or_default() {
303
        LayoutJustifyItems::Start => taffy::AlignItems::Start,
304
        LayoutJustifyItems::End => taffy::AlignItems::End,
305
        LayoutJustifyItems::Center => taffy::AlignItems::Center,
306
        LayoutJustifyItems::Stretch => taffy::AlignItems::Stretch,
307
    }
308
}
309

            
310
// TODO: visibility, z_index still missing
311
// --- CSS <-> Taffy conversion functions ---
312

            
313
use std::{collections::{BTreeMap, HashMap}, sync::Arc};
314

            
315
use azul_core::{dom::NodeId, geom::LogicalSize, styled_dom::StyledDom};
316
use azul_css::props::{
317
    layout::{LayoutHeight, LayoutWidth},
318
    property::{CssProperty, CssPropertyType},
319
};
320
use taffy::{
321
    compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_leaf_layout,
322
    prelude::*, CacheTree, LayoutFlexboxContainer, LayoutGridContainer, LayoutInput, LayoutOutput,
323
    RunMode,
324
};
325

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

            
348
/// Shared scrollbar detection for Taffy-managed flex/grid nodes.
349
///
350
/// When Taffy lays out a flex/grid container, it may expand the container
351
/// beyond the CSS-specified size (Taffy doesn't know about `overflow`).
352
/// This function resolves the CSS-constrained container size, computes
353
/// content vs. container overflow, and returns the resulting ScrollbarRequirements
354
/// plus the effective content size (for `overflow_content_size`).
355
///
356
/// Returns `(scrollbar_info, effective_content_width, effective_content_height)`.
357
717
fn compute_taffy_scrollbar_info<T: ParsedFontTrait>(
358
717
    ctx: &LayoutContext<'_, T>,
359
717
    tree: &LayoutTree,
360
717
    node_idx: usize,
361
717
    result_width: f32,
362
717
    result_height: f32,
363
717
    taffy_content_width: f32,
364
717
    taffy_content_height: f32,
365
717
) -> (crate::solver3::scrollbar::ScrollbarRequirements, f32, f32) {
366
    use crate::solver3::scrollbar::ScrollbarRequirements;
367

            
368
717
    let node = tree.get(node_idx);
369
717
    let dom_id = node.and_then(|n| n.dom_node_id);
370

            
371
717
    let Some(dom_id) = dom_id else {
372
        return (ScrollbarRequirements::default(), 0.0, 0.0);
373
    };
374

            
375
717
    let styled_node_state = ctx
376
717
        .styled_dom
377
717
        .styled_nodes
378
717
        .as_container()
379
717
        .get(dom_id)
380
717
        .map(|s| s.styled_node_state.clone())
381
717
        .unwrap_or_default();
382

            
383
    // Compute padding + border from the node's box_props
384
717
    let (padding_width, padding_height, border_width, border_height, border_left, border_top) = tree
385
717
        .get(node_idx)
386
717
        .map(|node| {
387
717
            let bp = node.box_props.unpack();
388
717
            (
389
717
                bp.padding.left + bp.padding.right,
390
717
                bp.padding.top + bp.padding.bottom,
391
717
                bp.border.left + bp.border.right,
392
717
                bp.border.top + bp.border.bottom,
393
717
                bp.border.left,
394
717
                bp.border.top,
395
717
            )
396
717
        })
397
717
        .unwrap_or((0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
398

            
399
    // Use CSS-specified dimensions as the container constraint.
400
    // Taffy may have expanded the box beyond these, but the CSS spec says
401
    // the container clips at the specified size.
402
717
    let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
403
717
    let css_width = get_css_width(ctx.styled_dom, dom_id, &styled_node_state);
404

            
405
717
    let result_content_w = result_width - padding_width - border_width;
406
717
    let result_content_h = result_height - padding_height - border_height;
407

            
408
717
    let css_container_w = css_width
409
717
        .exact()
410
717
        .and_then(|w| css_width_to_px(w))
411
717
        .unwrap_or(result_content_w)
412
717
        .max(0.0);
413

            
414
717
    let css_container_h = css_height
415
717
        .exact()
416
717
        .and_then(|h| css_height_to_px(h))
417
717
        .unwrap_or(result_content_h)
418
717
        .max(0.0);
419

            
420
    // Content size: use Taffy's content_size if non-zero,
421
    // else result size minus padding/border (Taffy expanded to fit).
422
    //
423
    // IMPORTANT: Taffy's content_size is measured from (0,0) of the border-box,
424
    // so it includes border.left/border.top as a leading offset. The container_size
425
    // is in content-box coordinates (result_width - padding - border). We must
426
    // subtract border.left/top from content_size to align coordinate spaces,
427
    // otherwise we get spurious horizontal scrollbars from the border offset.
428
717
    let content_w = if taffy_content_width > 0.0 {
429
9
        (taffy_content_width - border_left).max(0.0)
430
    } else {
431
708
        result_content_w.max(0.0)
432
    };
433
717
    let content_h = if taffy_content_height > 0.0 {
434
215
        (taffy_content_height - border_top).max(0.0)
435
    } else {
436
502
        result_content_h.max(0.0)
437
    };
438

            
439
717
    let content_size = LogicalSize::new(content_w, content_h);
440
717
    let container_size = LogicalSize::new(css_container_w, css_container_h);
441

            
442
717
    let scrollbar_info =
443
717
        crate::solver3::cache::compute_scrollbar_info_core(ctx, dom_id, &styled_node_state, content_size, container_size);
444

            
445
717
    (scrollbar_info, content_w, content_h)
446
717
}
447

            
448
/// Convert `LayoutWidth::Px(…)` to `f32`, returning None for non-px units.
449
280
fn css_width_to_px(w: azul_css::props::layout::LayoutWidth) -> Option<f32> {
450
280
    match w {
451
280
        azul_css::props::layout::LayoutWidth::Px(px) => pixel_value_to_pixels_fallback(&px),
452
        _ => None,
453
    }
454
280
}
455

            
456
/// Convert `LayoutHeight::Px(…)` to `f32`, returning None for non-px units.
457
315
fn css_height_to_px(h: azul_css::props::layout::LayoutHeight) -> Option<f32> {
458
315
    match h {
459
315
        azul_css::props::layout::LayoutHeight::Px(px) => pixel_value_to_pixels_fallback(&px),
460
        _ => None,
461
    }
462
315
}
463

            
464
// Helper function to convert MultiValue<PixelValue> to LengthPercentageAuto
465
2660
fn multi_value_to_lpa(mv: MultiValue<PixelValue>) -> taffy::LengthPercentageAuto {
466
2660
    match mv {
467
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
468
2660
            taffy::LengthPercentageAuto::auto()
469
        }
470
        MultiValue::Exact(pv) => pixel_value_to_pixels_fallback(&pv)
471
            .map(taffy::LengthPercentageAuto::length)
472
            .or_else(|| {
473
                pv.to_percent()
474
                    .map(|p| taffy::LengthPercentageAuto::percent(p.get()))
475
            })
476
            .unwrap_or_else(taffy::LengthPercentageAuto::auto),
477
    }
478
2660
}
479

            
480
// Helper function to convert MultiValue<PixelValue> to LengthPercentageAuto for margins
481
// CSS spec: margin initial value is 0, but `auto` has special centering meaning in flexbox
482
2660
fn multi_value_to_lpa_margin(mv: MultiValue<PixelValue>) -> taffy::LengthPercentageAuto {
483
2660
    match mv {
484
        MultiValue::Auto => {
485
            taffy::LengthPercentageAuto::auto() // Preserve auto for flexbox centering
486
        }
487
        MultiValue::Initial | MultiValue::Inherit => {
488
            taffy::LengthPercentageAuto::length(0.0) // Margins' initial value is 0
489
        }
490
2660
        MultiValue::Exact(pv) => {
491
2660
            pixel_value_to_pixels_fallback(&pv)
492
2660
                .map(taffy::LengthPercentageAuto::length)
493
2660
                .or_else(|| {
494
                    pv.to_percent()
495
                        .map(|p| taffy::LengthPercentageAuto::percent(p.get()))
496
                })
497
2660
                .unwrap_or_else(|| taffy::LengthPercentageAuto::length(0.0)) // Fallback to 0 for
498
                                                                             // margins
499
        }
500
    }
501
2660
}
502

            
503
// Helper function to convert MultiValue<PixelValue> to LengthPercentage
504
5320
fn multi_value_to_lp(mv: MultiValue<PixelValue>) -> taffy::LengthPercentage {
505
5320
    match mv {
506
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
507
            taffy::LengthPercentage::ZERO
508
        }
509
5320
        MultiValue::Exact(pv) => pixel_value_to_pixels_fallback(&pv)
510
5320
            .map(taffy::LengthPercentage::length)
511
5320
            .or_else(|| {
512
                pv.to_percent()
513
                    .map(|p| taffy::LengthPercentage::percent(p.get()))
514
            })
515
5320
            .unwrap_or_else(|| taffy::LengthPercentage::ZERO),
516
    }
517
5320
}
518

            
519
// Helper function to convert plain PixelValue to LengthPercentage
520
/// Converts Azul's CSS overflow value to Taffy's Overflow enum.
521
///
522
/// Taffy only has Visible, Clip, Hidden, Scroll (no Auto).
523
/// CSS `auto` behaves like `scroll` from a layout perspective —
524
/// it constrains the container and enables scrolling.
525
1330
fn azul_overflow_to_taffy(ov: MultiValue<azul_css::props::layout::LayoutOverflow>) -> taffy::Overflow {
526
    use azul_css::props::layout::LayoutOverflow;
527
1330
    match ov {
528
1330
        MultiValue::Exact(LayoutOverflow::Visible) => taffy::Overflow::Visible,
529
        MultiValue::Exact(LayoutOverflow::Hidden) => taffy::Overflow::Hidden,
530
        MultiValue::Exact(LayoutOverflow::Scroll) => taffy::Overflow::Scroll,
531
        MultiValue::Exact(LayoutOverflow::Auto) => taffy::Overflow::Scroll, // Auto acts like scroll for layout
532
        MultiValue::Exact(LayoutOverflow::Clip) => taffy::Overflow::Clip,
533
        _ => taffy::Overflow::Visible, // default
534
    }
535
1330
}
536

            
537
fn pixel_to_lp(pv: PixelValue) -> taffy::LengthPercentage {
538
    pixel_value_to_pixels_fallback(&pv)
539
        .map(taffy::LengthPercentage::length)
540
        .or_else(|| {
541
            pv.to_percent()
542
                .map(|p| taffy::LengthPercentage::percent(p.get()))
543
        })
544
        .unwrap_or_else(|| taffy::LengthPercentage::ZERO)
545
}
546

            
547
/// Slow path for flex-basis: full property cache lookup + decode.
548
/// Extracted to avoid duplicating the logic in the compact fast-path fallback.
549
fn flex_basis_slow_path(
550
    cache: &azul_core::prop_cache::CssPropertyCache,
551
    node_data: &azul_core::dom::NodeData,
552
    id: &NodeId,
553
    node_state: &azul_core::styled_dom::StyledNodeState,
554
    taffy_style: &mut Style,
555
) -> taffy::Dimension {
556
    cache
557
        .get_property(node_data, id, node_state, &CssPropertyType::FlexBasis)
558
        .and_then(|p| {
559
            if let CssProperty::FlexBasis(v) = p {
560
                let basis = match v.get_property_or_default().unwrap_or_default() {
561
                    LayoutFlexBasis::Auto => taffy::Dimension::auto(),
562
                    LayoutFlexBasis::Exact(pv) => pixel_value_to_pixels_fallback(&pv)
563
                        .map(taffy::Dimension::length)
564
                        .or_else(|| pv.to_percent().map(|p| taffy::Dimension::percent(p.get())))
565
                        .unwrap_or_else(taffy::Dimension::auto),
566
                };
567
                // WORKAROUND: If flex-basis is set and not auto, clear width to let flex-basis
568
                // take precedence. Workaround for Taffy not properly prioritizing flex-basis over width
569
                if !matches!(basis, _auto if _auto == taffy::Dimension::auto()) {
570
                    taffy_style.size.width = taffy::Dimension::auto();
571
                }
572
                Some(basis)
573
            } else {
574
                None
575
            }
576
        })
577
        .unwrap_or_else(taffy::Dimension::auto)
578
}
579

            
580
/// The bridge struct that implements Taffy's traits.
581
/// It holds mutable references to the solver's data structures, allowing Taffy
582
/// to read styles and write layout results back into our `LayoutTree`.
583
struct TaffyBridge<'a, 'b, T: ParsedFontTrait> {
584
    ctx: &'a mut LayoutContext<'b, T>,
585
    tree: &'a mut LayoutTree,
586
    /// Raw pointer to text cache - needed because we can't have multiple &mut references
587
    /// SAFETY: This pointer is only valid for the lifetime of the TaffyBridge
588
    /// and must only be used within compute_child_layout callbacks
589
    text_cache: *mut crate::font_traits::TextLayoutCache,
590
    /// Heap-pinned `CalcResolveContext`s whose addresses are passed into taffy
591
    /// `Dimension::calc(ptr)`. Kept alive for the duration of the layout pass.
592
    /// Uses `RefCell` because `get_core_container_style` takes `&self`.
593
    calc_storage: std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
594
    /// Memoised `translate_style_to_taffy` results, keyed by DOM node id
595
    /// (`usize` = `NodeId::index`). Taffy calls
596
    /// `get_core_container_style` and `should_suppress_cross_intrinsic`
597
    /// many times per node during a single layout pass; each call
598
    /// triggers ~13 `cache.get_property` cascade walks for grid/flex
599
    /// props. Caching the built `Style` cuts this to one build per node.
600
    style_memo: std::cell::RefCell<std::collections::HashMap<usize, Style>>,
601
}
602

            
603
impl<'a, 'b, T: ParsedFontTrait> TaffyBridge<'a, 'b, T> {
604
212
    fn new(
605
212
        ctx: &'a mut LayoutContext<'b, T>,
606
212
        tree: &'a mut LayoutTree,
607
212
        text_cache: *mut crate::font_traits::TextLayoutCache,
608
212
    ) -> Self {
609
212
        Self {
610
212
            ctx,
611
212
            tree,
612
212
            text_cache,
613
212
            calc_storage: std::cell::RefCell::new(Vec::new()),
614
212
            style_memo: std::cell::RefCell::new(std::collections::HashMap::new()),
615
212
        }
616
212
    }
617

            
618
    /// Cache-backed wrapper for `translate_style_to_taffy`. Returns a
619
    /// clone of the memoised `Style` on cache hit, builds + inserts on
620
    /// miss. Keyed by DOM node index (not tree index) because the
621
    /// result depends only on the styled DOM, not on the transient
622
    /// layout tree.
623
3824
    fn translate_style_to_taffy_cached(&self, dom_id: Option<NodeId>) -> Style {
624
3824
        let Some(id) = dom_id else {
625
            return Style::default();
626
        };
627
3824
        let key = id.index();
628
3824
        if let Some(style) = self.style_memo.borrow().get(&key) {
629
3363
            return style.clone();
630
461
        }
631
461
        let style = self.translate_style_to_taffy(dom_id);
632
461
        self.style_memo.borrow_mut().insert(key, style.clone());
633
461
        style
634
3824
    }
635

            
636
    /// Translates CSS properties from the `StyledDom` into a `taffy::Style` struct.
637
    /// This is the core of the integration, mapping one style system to another.
638
461
    fn translate_style_to_taffy(&self, dom_id: Option<NodeId>) -> Style {
639
461
        let Some(id) = dom_id else {
640
            return Style::default();
641
        };
642
461
        let styled_dom = &self.ctx.styled_dom;
643
461
        let node_data = &styled_dom.node_data.as_ref()[id.index()];
644
461
        let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
645
461
        let cache = &styled_dom.css_property_cache.ptr;
646
461
        let mut taffy_style = Style::default();
647

            
648
        // Box Sizing — CSS default is content-box, but Taffy defaults to border-box
649
461
        taffy_style.box_sizing = match get_css_box_sizing(styled_dom, id, node_state).unwrap_or_default() {
650
            azul_css::props::layout::LayoutBoxSizing::BorderBox => taffy::BoxSizing::BorderBox,
651
461
            azul_css::props::layout::LayoutBoxSizing::ContentBox => taffy::BoxSizing::ContentBox,
652
        };
653

            
654
        // Display Mode
655
461
        taffy_style.display =
656
461
            layout_display_to_taffy(CssPropertyValue::Exact(get_display_type(styled_dom, id)));
657

            
658
        // Position
659
461
        taffy_style.position =
660
461
            from_layout_position(get_position(styled_dom, id, node_state).unwrap_or_default());
661

            
662
        // Inset (top, left, bottom, right)
663
461
        taffy_style.inset = taffy::Rect {
664
461
            left: multi_value_to_lpa(get_css_left(styled_dom, id, node_state)),
665
461
            right: multi_value_to_lpa(get_css_right(styled_dom, id, node_state)),
666
461
            top: multi_value_to_lpa(get_css_top(styled_dom, id, node_state)),
667
461
            bottom: multi_value_to_lpa(get_css_bottom(styled_dom, id, node_state)),
668
461
        };
669

            
670
        // Size
671
461
        let width = get_css_width(self.ctx.styled_dom, id, node_state);
672
461
        let height = get_css_height(self.ctx.styled_dom, id, node_state);
673

            
674
        // Resolve node-local font sizes for calc() em/rem resolution
675
461
        let em_size = crate::solver3::getters::get_element_font_size(styled_dom, id, node_state);
676
461
        let rem_size = {
677
461
            let root_id = NodeId::new(0);
678
461
            let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
679
461
            crate::solver3::getters::get_element_font_size(styled_dom, root_id, root_state)
680
        };
681

            
682
461
        let taffy_width = from_layout_width(width.unwrap_or_default(), &self.calc_storage, em_size, rem_size);
683
461
        let taffy_height = from_layout_height(height.unwrap_or_default(), &self.calc_storage, em_size, rem_size);
684

            
685
461
        taffy_style.size = taffy::Size {
686
461
            width: taffy_width,
687
461
            height: taffy_height,
688
461
        };
689

            
690
        // Overflow — CRITICAL for scroll containers.
691
        // Without this, Taffy's flexbox algorithm uses content size as automatic
692
        // minimum size, causing flex containers with overflow:auto/scroll to
693
        // expand to fit all content instead of clipping at the explicit size.
694
        // With overflow: Hidden/Scroll, Taffy sets automatic min size to 0 and
695
        // constrains the container.
696
461
        let overflow_x = get_overflow_x(styled_dom, id, node_state);
697
461
        let overflow_y = get_overflow_y(styled_dom, id, node_state);
698
461
        taffy_style.overflow = taffy::Point {
699
461
            x: azul_overflow_to_taffy(overflow_x),
700
461
            y: azul_overflow_to_taffy(overflow_y),
701
461
        };
702

            
703
        // Min/Max Size
704
        // min-size:auto enables Taffy's auto minimum size algorithm which computes the
705
        // content size suggestion (min-content in main axis) and transferred size suggestion
706
        // (cross size converted through aspect ratio, if any). NOTE: aspect_ratio is not yet
707
        // forwarded to Taffy, so the transferred size suggestion path is incomplete.
708
        // NOTE: In CSS, the default min-width/min-height for flex items is `auto`
709
        // (which resolves to `min-content`), preventing them from shrinking below
710
        // their content size. We must map Auto to Dimension::Auto, NOT to 0px.
711
461
        let min_width_css = get_css_min_width(styled_dom, id, node_state);
712
461
        let min_height_css = get_css_min_height(styled_dom, id, node_state);
713

            
714
        taffy_style.min_size = taffy::Size {
715
461
            width: match min_width_css {
716
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
717
461
                    taffy::Dimension::auto()
718
                }
719
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
720
            },
721
461
            height: match min_height_css {
722
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
723
461
                    taffy::Dimension::auto()
724
                }
725
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
726
            },
727
        };
728

            
729
        // For max-size, we need to handle Auto specially - it should translate to Taffy's auto, not
730
        // a concrete value This is CRITICAL for flexbox stretch to work: items with
731
        // max-height: auto CAN be stretched
732
461
        let max_width_css = get_css_max_width(styled_dom, id, node_state);
733
461
        let max_height_css = get_css_max_height(styled_dom, id, node_state);
734

            
735
        taffy_style.max_size = taffy::Size {
736
461
            width: match max_width_css {
737
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
738
461
                    taffy::Dimension::auto()
739
                }
740
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
741
            },
742
461
            height: match max_height_css {
743
                MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
744
461
                    taffy::Dimension::auto()
745
                }
746
                MultiValue::Exact(v) => pixel_to_lp(v.inner).into(),
747
            },
748
        };
749

            
750
        // Box Model (margin, padding, border)
751
461
        let margin_left_css = get_css_margin_left(styled_dom, id, node_state);
752
461
        let margin_right_css = get_css_margin_right(styled_dom, id, node_state);
753
461
        let margin_top_css = get_css_margin_top(styled_dom, id, node_state);
754
461
        let margin_bottom_css = get_css_margin_bottom(styled_dom, id, node_state);
755

            
756
461
        taffy_style.margin = taffy::Rect {
757
461
            left: multi_value_to_lpa_margin(margin_left_css),
758
461
            right: multi_value_to_lpa_margin(margin_right_css),
759
461
            top: multi_value_to_lpa_margin(margin_top_css),
760
461
            bottom: multi_value_to_lpa_margin(margin_bottom_css),
761
461
        };
762

            
763
461
        taffy_style.padding = taffy::Rect {
764
461
            left: multi_value_to_lp(get_css_padding_left(styled_dom, id, node_state)),
765
461
            right: multi_value_to_lp(get_css_padding_right(styled_dom, id, node_state)),
766
461
            top: multi_value_to_lp(get_css_padding_top(styled_dom, id, node_state)),
767
461
            bottom: multi_value_to_lp(get_css_padding_bottom(styled_dom, id, node_state)),
768
461
        };
769

            
770
461
        taffy_style.border = taffy::Rect {
771
461
            left: multi_value_to_lp(get_css_border_left_width(styled_dom, id, node_state)),
772
461
            right: multi_value_to_lp(get_css_border_right_width(styled_dom, id, node_state)),
773
461
            top: multi_value_to_lp(get_css_border_top_width(styled_dom, id, node_state)),
774
461
            bottom: multi_value_to_lp(get_css_border_bottom_width(styled_dom, id, node_state)),
775
461
        };
776

            
777
        // Grid & gap properties — COMPACT FAST PATH: row_gap/column_gap are
778
        // i16 px × 10 in tier2_dims. The slow-path lookup would walk the
779
        // cascade for every node even though the answer is already encoded.
780
461
        taffy_style.gap = if let Some(ref cc) = cache.compact_cache {
781
461
            let row = cc.tier2_dims[id.index()].row_gap;
782
461
            let col = cc.tier2_dims[id.index()].column_gap;
783
922
            let decode = |raw: i16| -> taffy::LengthPercentage {
784
922
                if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
785
                    taffy::LengthPercentage::length(0.0)
786
                } else {
787
922
                    taffy::LengthPercentage::length(raw as f32 / 10.0)
788
                }
789
922
            };
790
461
            Size {
791
461
                width: decode(col),
792
461
                height: decode(row),
793
461
            }
794
        } else {
795
            cache
796
                .get_property(node_data, &id, node_state, &CssPropertyType::Gap)
797
                .and_then(|p| if let CssProperty::Gap(v) = p { Some(v) } else { None })
798
                .map(|v| {
799
                    let val = v.get_property_or_default().unwrap_or_default().inner;
800
                    let gap_lp = pixel_to_lp(val);
801
                    Size { width: gap_lp, height: gap_lp }
802
                })
803
                .unwrap_or_else(Size::zero)
804
        };
805

            
806
        // Skip grid properties when not in a grid context.
807
        // Grid container props: only if this node has display:grid.
808
        // Grid item props: only if parent has display:grid.
809
461
        let (self_is_grid, parent_is_grid) = cache.compact_cache.as_ref().map_or((false, false), |cc| {
810
            use azul_css::compact_cache::*;
811
461
            let self_t1 = cc.tier1_enums[id.index()];
812
461
            let self_display = ((self_t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8;
813
461
            let grid_val = layout_display_to_u8(azul_css::props::layout::display::LayoutDisplay::Grid);
814
461
            let self_grid = self_display == grid_val;
815

            
816
461
            let parent_idx = styled_dom.node_hierarchy.as_ref()[id.index()].parent_id()
817
461
                .map(|p| p.index()).unwrap_or(0);
818
461
            let parent_t1 = cc.tier1_enums[parent_idx];
819
461
            let parent_display = ((parent_t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8;
820
461
            let parent_grid = parent_display == grid_val;
821
461
            (self_grid, parent_grid)
822
461
        });
823

            
824
461
        if self_is_grid {
825
        taffy_style.grid_template_rows = cache
826
            .get_property(
827
                node_data,
828
                &id,
829
                node_state,
830
                &CssPropertyType::GridTemplateRows,
831
            )
832
            .and_then(|p| {
833
                if let CssProperty::GridTemplateRows(v) = p {
834
                    Some(v.clone())
835
                } else {
836
                    None
837
                }
838
            })
839
            .map(|v| grid_template_rows_to_taffy(v).into())
840
            .unwrap_or_default();
841

            
842
        // Grid template columns - convert GridTemplate to Vec<GridTemplateComponent>
843
        taffy_style.grid_template_columns = cache
844
            .get_property(
845
                node_data,
846
                &id,
847
                node_state,
848
                &CssPropertyType::GridTemplateColumns,
849
            )
850
            .and_then(|p| {
851
                if let CssProperty::GridTemplateColumns(v) = p {
852
                    Some(v.clone())
853
                } else {
854
                    None
855
                }
856
            })
857
            .map(|v| grid_template_columns_to_taffy(v).into())
858
            .unwrap_or_default();
859

            
860
        // Grid template areas - convert GridTemplateAreas to Vec<taffy::GridTemplateArea<String>>
861
        taffy_style.grid_template_areas = cache
862
            .get_property(
863
                node_data,
864
                &id,
865
                node_state,
866
                &CssPropertyType::GridTemplateAreas,
867
            )
868
            .and_then(|p| {
869
                if let CssProperty::GridTemplateAreas(v) = p {
870
                    v.get_property().cloned()
871
                } else {
872
                    None
873
                }
874
            })
875
            .map(|areas| {
876
                areas
877
                    .areas
878
                    .as_ref()
879
                    .iter()
880
                    .map(|a| taffy::GridTemplateArea {
881
                        name: a.name.as_str().to_string(),
882
                        row_start: a.row_start,
883
                        row_end: a.row_end,
884
                        column_start: a.column_start,
885
                        column_end: a.column_end,
886
                    })
887
                    .collect::<Vec<_>>()
888
                    .into()
889
            })
890
            .unwrap_or_default();
891

            
892
        taffy_style.grid_auto_rows = cache
893
            .get_property(node_data, &id, node_state, &CssPropertyType::GridAutoRows)
894
            .and_then(|p| {
895
                if let CssProperty::GridAutoRows(v) = p {
896
                    Some(v.clone())
897
                } else {
898
                    None
899
                }
900
            })
901
            .map(|v| grid_auto_rows_to_taffy(v))
902
            .unwrap_or_default();
903

            
904
        taffy_style.grid_auto_columns = cache
905
            .get_property(
906
                node_data,
907
                &id,
908
                node_state,
909
                &CssPropertyType::GridAutoColumns,
910
            )
911
            .and_then(|p| {
912
                if let CssProperty::GridAutoColumns(v) = p {
913
                    Some(v.clone())
914
                } else {
915
                    None
916
                }
917
            })
918
            .map(|v| grid_auto_columns_to_taffy(v))
919
            .unwrap_or_default();
920

            
921
        taffy_style.grid_auto_flow = if let Some(cc) = cache.compact_cache.as_ref() {
922
            use azul_css::compact_cache::*;
923
            let bits = ((cc.tier1_enums[id.index()] >> GRID_AUTO_FLOW_SHIFT) & GRID_AUTO_FLOW_MASK) as u8;
924
            let val = layout_grid_auto_flow_from_u8(bits);
925
            grid_auto_flow_to_taffy(CssPropertyValue::Exact(val))
926
        } else {
927
            cache
928
                .get_property(node_data, &id, node_state, &CssPropertyType::GridAutoFlow)
929
                .and_then(|p| if let CssProperty::GridAutoFlow(v) = p { Some(*v) } else { None })
930
                .map(|v| grid_auto_flow_to_taffy(v))
931
                .unwrap_or_default()
932
        };
933

            
934
461
        } // end if self_is_grid
935

            
936
461
        if parent_is_grid {
937
        // Grid item placement — read from compact cold cache (Auto/Line/Span)
938
        if let Some(cc) = cache.compact_cache.as_ref() {
939
            let cs = cc.tier2_cold[id.index()].grid_col_start;
940
            let ce = cc.tier2_cold[id.index()].grid_col_end;
941
            if cs != azul_css::compact_cache::I16_AUTO || ce != azul_css::compact_cache::I16_AUTO {
942
                taffy_style.grid_column = taffy::Line { start: decode_compact_grid_line(cs), end: decode_compact_grid_line(ce) };
943
            }
944
            let rs = cc.tier2_cold[id.index()].grid_row_start;
945
            let re = cc.tier2_cold[id.index()].grid_row_end;
946
            if rs != azul_css::compact_cache::I16_AUTO || re != azul_css::compact_cache::I16_AUTO {
947
                taffy_style.grid_row = taffy::Line { start: decode_compact_grid_line(rs), end: decode_compact_grid_line(re) };
948
            }
949
        } else {
950
            if let Some(grid_col) = cache
951
                .get_property(node_data, &id, node_state, &CssPropertyType::GridColumn)
952
                .and_then(|p| if let CssProperty::GridColumn(v) = p { v.get_property().cloned() } else { None })
953
            { taffy_style.grid_column = grid_placement_to_taffy(&grid_col); }
954
            if let Some(grid_row) = cache
955
                .get_property(node_data, &id, node_state, &CssPropertyType::GridRow)
956
                .and_then(|p| if let CssProperty::GridRow(v) = p { v.get_property().cloned() } else { None })
957
            { taffy_style.grid_row = grid_placement_to_taffy(&grid_row); }
958
        }
959
461
        } // end if parent_is_grid
960

            
961
        // Flexbox
962
461
        taffy_style.flex_direction = match get_flex_direction(styled_dom, id, node_state) {
963
461
            MultiValue::Exact(v) => layout_flex_direction_to_taffy(CssPropertyValue::Exact(v)),
964
            _ => taffy::FlexDirection::Row,
965
        };
966
        // COMPACT FAST PATH: flex_wrap is Tier 1 enum
967
461
        taffy_style.flex_wrap = if node_state.is_normal() {
968
461
            if let Some(ref cc) = cache.compact_cache {
969
461
                layout_flex_wrap_to_taffy(CssPropertyValue::Exact(cc.get_flex_wrap(id.index())))
970
            } else {
971
                cache
972
                    .get_property(node_data, &id, node_state, &CssPropertyType::FlexWrap)
973
                    .and_then(|p| if let CssProperty::FlexWrap(v) = p { Some(*v) } else { None })
974
                    .map(layout_flex_wrap_to_taffy)
975
                    .unwrap_or(taffy::FlexWrap::NoWrap)
976
            }
977
        } else {
978
            cache
979
                .get_property(node_data, &id, node_state, &CssPropertyType::FlexWrap)
980
                .and_then(|p| if let CssProperty::FlexWrap(v) = p { Some(*v) } else { None })
981
                .map(layout_flex_wrap_to_taffy)
982
                .unwrap_or(taffy::FlexWrap::NoWrap)
983
        };
984
461
        taffy_style.align_items = match get_align_items(styled_dom, id, node_state) {
985
461
            MultiValue::Exact(v) => Some(layout_align_items_to_taffy(CssPropertyValue::Exact(v))),
986
            _ => None,
987
        };
988
                // CSS spec: default align-items is "normal" which acts like "stretch"
989
                // for non-replaced grid/flex items. Taffy handles this internally when
990
                // align_items is None, so we should NOT force a default here.
991
461
        taffy_style.justify_items = if let Some(cc) = cache.compact_cache.as_ref() {
992
            use azul_css::compact_cache::*;
993
            use azul_css::props::layout::grid::LayoutJustifyItems;
994
461
            let bits = ((cc.tier1_enums[id.index()] >> JUSTIFY_ITEMS_SHIFT) & JUSTIFY_ITEMS_MASK) as u8;
995
461
            let val = layout_justify_items_from_u8(bits);
996
461
            Some(match val {
997
                LayoutJustifyItems::Start => taffy::AlignItems::Start,
998
                LayoutJustifyItems::End => taffy::AlignItems::End,
999
                LayoutJustifyItems::Center => taffy::AlignItems::Center,
461
                LayoutJustifyItems::Stretch => taffy::AlignItems::Stretch,
            })
        } else {
            cache
                .get_property(node_data, &id, node_state, &CssPropertyType::JustifyItems)
                .and_then(|p| if let CssProperty::JustifyItems(v) = p { Some(*v) } else { None })
                .map(|v| layout_justify_items_to_taffy(v))
        };
        // COMPACT FAST PATH: justify-content is in tier1 bits 21-23.
461
        taffy_style.justify_content = if let Some(ref cc) = cache.compact_cache {
            use azul_css::compact_cache::*;
            use azul_css::props::layout::LayoutJustifyContent;
461
            let bits = ((cc.tier1_enums[id.index()] >> JUSTIFY_CONTENT_SHIFT) & JUSTIFY_MASK) as u8;
461
            Some(match layout_justify_content_from_u8(bits) {
426
                LayoutJustifyContent::FlexStart => taffy::JustifyContent::FlexStart,
                LayoutJustifyContent::FlexEnd => taffy::JustifyContent::FlexEnd,
                LayoutJustifyContent::Start => taffy::JustifyContent::Start,
                LayoutJustifyContent::End => taffy::JustifyContent::End,
35
                LayoutJustifyContent::Center => taffy::JustifyContent::Center,
                LayoutJustifyContent::SpaceBetween => taffy::JustifyContent::SpaceBetween,
                LayoutJustifyContent::SpaceAround => taffy::JustifyContent::SpaceAround,
                LayoutJustifyContent::SpaceEvenly => taffy::JustifyContent::SpaceEvenly,
            })
        } else {
            cache
                .get_property(node_data, &id, node_state, &CssPropertyType::JustifyContent)
                .and_then(|p| if let CssProperty::JustifyContent(v) = p { Some(v) } else { None })
                .map(|v| layout_justify_content_to_taffy(v.clone()))
        };
                // 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
461
        taffy_style.flex_grow = if node_state.is_normal() {
461
            if let Some(ref cc) = cache.compact_cache {
461
                if let Some(v) = cc.get_flex_grow(id.index()) {
461
                    v
                } else {
                    // Sentinel: fall through to slow path
                    cache
                        .get_property(node_data, &id, node_state, &CssPropertyType::FlexGrow)
                        .and_then(|p| if let CssProperty::FlexGrow(v) = p {
                            Some(v.get_property_or_default().unwrap_or_default().inner.get())
                        } else { None })
                        .unwrap_or(0.0)
                }
            } else {
                cache
                    .get_property(node_data, &id, node_state, &CssPropertyType::FlexGrow)
                    .and_then(|p| if let CssProperty::FlexGrow(v) = p {
                        Some(v.get_property_or_default().unwrap_or_default().inner.get())
                    } else { None })
                    .unwrap_or(0.0)
            }
        } else {
            cache
                .get_property(node_data, &id, node_state, &CssPropertyType::FlexGrow)
                .and_then(|p| if let CssProperty::FlexGrow(v) = p {
                    Some(v.get_property_or_default().unwrap_or_default().inner.get())
                } else { None })
                .unwrap_or(0.0)
        };
        // COMPACT FAST PATH: flex_shrink stored as u16 × 100
461
        taffy_style.flex_shrink = if node_state.is_normal() {
461
            if let Some(ref cc) = cache.compact_cache {
461
                if let Some(v) = cc.get_flex_shrink(id.index()) {
461
                    v
                } else {
                    // Sentinel: fall through to slow path
                    cache
                        .get_property(node_data, &id, node_state, &CssPropertyType::FlexShrink)
                        .and_then(|p| if let CssProperty::FlexShrink(v) = p {
                            Some(v.get_property_or_default().unwrap_or_default().inner.get())
                        } else { None })
                        .unwrap_or(1.0)
                }
            } else {
                cache
                    .get_property(node_data, &id, node_state, &CssPropertyType::FlexShrink)
                    .and_then(|p| if let CssProperty::FlexShrink(v) = p {
                        Some(v.get_property_or_default().unwrap_or_default().inner.get())
                    } else { None })
                    .unwrap_or(1.0)
            }
        } else {
            cache
                .get_property(node_data, &id, node_state, &CssPropertyType::FlexShrink)
                .and_then(|p| if let CssProperty::FlexShrink(v) = p {
                    Some(v.get_property_or_default().unwrap_or_default().inner.get())
                } else { None })
                .unwrap_or(1.0)
        };
        // COMPACT FAST PATH: flex_basis stored as u32 with PixelValue encoding
461
        taffy_style.flex_basis = if node_state.is_normal() {
461
            if let Some(ref cc) = cache.compact_cache {
461
                let raw = cc.get_flex_basis_raw(id.index());
461
                match raw {
                    azul_css::compact_cache::U32_AUTO
                    | azul_css::compact_cache::U32_NONE
461
                    | azul_css::compact_cache::U32_INITIAL => taffy::Dimension::auto(),
                    azul_css::compact_cache::U32_SENTINEL
                    | azul_css::compact_cache::U32_INHERIT => {
                        // Sentinel/inherit: fall through to slow path
                        flex_basis_slow_path(cache, node_data, &id, node_state, &mut taffy_style)
                    }
                    _ => {
                        // Try to decode the PixelValue from compact u32
                        if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
                            let basis = pixel_value_to_pixels_fallback(&pv)
                                .map(taffy::Dimension::length)
                                .or_else(|| pv.to_percent().map(|p| taffy::Dimension::percent(p.get())))
                                .unwrap_or_else(taffy::Dimension::auto);
                            if !matches!(basis, _auto if _auto == taffy::Dimension::auto()) {
                                taffy_style.size.width = taffy::Dimension::auto();
                            }
                            basis
                        } else {
                            taffy::Dimension::auto()
                        }
                    }
                }
            } else {
                flex_basis_slow_path(cache, node_data, &id, node_state, &mut taffy_style)
            }
        } else {
            flex_basis_slow_path(cache, node_data, &id, node_state, &mut taffy_style)
        };
461
        taffy_style.align_self = if let Some(cc) = cache.compact_cache.as_ref() {
            use azul_css::compact_cache::*;
461
            let bits = ((cc.tier1_enums[id.index()] >> ALIGN_SELF_SHIFT) & ALIGN_SELF_MASK) as u8;
461
            let val = layout_align_self_from_u8(bits);
            use azul_css::props::layout::flex::LayoutAlignSelf;
461
            match val {
460
                LayoutAlignSelf::Auto => None,
                LayoutAlignSelf::Start => Some(taffy::AlignSelf::FlexStart),
                LayoutAlignSelf::End => Some(taffy::AlignSelf::FlexEnd),
                LayoutAlignSelf::Center => Some(taffy::AlignSelf::Center),
                LayoutAlignSelf::Baseline => Some(taffy::AlignSelf::Baseline),
1
                LayoutAlignSelf::Stretch => Some(taffy::AlignSelf::Stretch),
            }
        } else {
            cache
                .get_property(node_data, &id, node_state, &CssPropertyType::AlignSelf)
                .and_then(|p| if let CssProperty::AlignSelf(v) = p { layout_align_self_to_taffy(*v) } else { None })
        };
461
        taffy_style.justify_self = if let Some(cc) = cache.compact_cache.as_ref() {
            use azul_css::compact_cache::*;
            use azul_css::props::layout::grid::LayoutJustifySelf;
461
            let bits = ((cc.tier1_enums[id.index()] >> JUSTIFY_SELF_SHIFT) & JUSTIFY_SELF_MASK) as u8;
461
            let val = layout_justify_self_from_u8(bits);
461
            match val {
461
                LayoutJustifySelf::Auto => None,
                LayoutJustifySelf::Start => Some(taffy::AlignSelf::Start),
                LayoutJustifySelf::End => Some(taffy::AlignSelf::End),
                LayoutJustifySelf::Center => Some(taffy::AlignSelf::Center),
                LayoutJustifySelf::Stretch => Some(taffy::AlignSelf::Stretch),
            }
        } 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(taffy::AlignSelf::Start),
                        LayoutJustifySelf::End => Some(taffy::AlignSelf::End),
                        LayoutJustifySelf::Center => Some(taffy::AlignSelf::Center),
                        LayoutJustifySelf::Stretch => Some(taffy::AlignSelf::Stretch),
                    }
                } else { None })
        };
461
        taffy_style.align_content = match get_align_content(styled_dom, id, node_state) {
461
            MultiValue::Exact(v) => Some(layout_align_content_to_taffy(CssPropertyValue::Exact(v))),
            _ => None,
        };
                // CSS spec: default align-content is "normal". Taffy handles
                // this internally when align_content is None.
461
        taffy_style
461
    }
    /// Gets or computes the Taffy style for a given node index.
2125
    fn get_taffy_style(&self, node_idx: usize) -> Style {
2125
        let dom_id = self.tree.get(node_idx).and_then(|n| n.dom_node_id);
2125
        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.
2125
        let is_root = self.tree.get(node_idx).map(|n| n.parent.is_none()).unwrap_or(false);
2125
        if is_root {
420
            style.margin = taffy::Rect::zero();
1705
        }
        // 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.
2125
        let (suppress_width, suppress_height) = self.should_suppress_cross_intrinsic(node_idx, &style);
2125
        if suppress_width {
969
            // Force width to Auto and set min-width to 0 to allow stretching.
969
            // Taffy treats Auto size + Stretch alignment as a signal to fill the container.
969
            style.size.width = taffy::Dimension::auto(); 
969
            style.min_size.width = taffy::Dimension::length(0.0);
1156
        }
2125
        if suppress_height {
30
            style.size.height = taffy::Dimension::auto();
30
            style.min_size.height = taffy::Dimension::length(0.0);
2095
        }
2125
        style
2125
    }
    /// 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.
2125
    fn should_suppress_cross_intrinsic(&self, node_idx: usize, style: &Style) -> (bool, bool) {
2125
        let Some(node) = self.tree.get(node_idx) else {
            return (false, false);
        };
        // Check if parent is a flex or grid container
2125
        let parent_fc = match self.tree.warm(node_idx).and_then(|w| w.parent_formatting_context.clone()) {
1705
            Some(fc) => fc,
420
            None => return (false, false),
        };
1705
        match parent_fc {
            FormattingContext::Flex => {
                // Get parent node to check its flex-direction and align-items
1699
                let Some(parent_idx) = node.parent else {
                    return (false, false);
                };
1699
                let parent_dom_id = self.tree.get(parent_idx).and_then(|n| n.dom_node_id);
1699
                let parent_style = self.translate_style_to_taffy_cached(parent_dom_id);
                // Determine if flex container is row or column
1699
                let is_row = matches!(
1699
                    parent_style.flex_direction,
                    taffy::FlexDirection::Row | taffy::FlexDirection::RowReverse
                );
                // Get effective align value for this item
                // align-self overrides parent's align-items
1699
                let align = style
1699
                    .align_self
1699
                    .or(parent_style.align_items)
1699
                    .unwrap_or(taffy::AlignSelf::Stretch);
1699
                let should_stretch = matches!(align, taffy::AlignSelf::Stretch);
1699
                if !should_stretch {
175
                    return (false, false);
1524
                }
                // Check if cross-axis size is auto
                // For row flex: cross-axis is height
                // For column flex: cross-axis is width
1524
                let cross_size_is_auto = if is_row {
555
                    style.size.height == taffy::Dimension::auto()
                } else {
969
                    style.size.width == taffy::Dimension::auto()
                };
1524
                if !cross_size_is_auto {
525
                    return (false, false);
999
                }
                // All conditions met: suppress intrinsic cross-size
999
                if is_row {
30
                    (false, true) // Suppress height for row flex
                } else {
969
                    (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
                (false, false)
            }
6
            _ => (false, false),
        }
2125
    }
    /// Helper to get children that participate in layout (i.e., not `display: none`).
1139
    fn get_layout_children(&self, node_idx: usize) -> Vec<usize> {
1139
        let Some(node) = self.tree.get(node_idx) else {
            return Vec::new();
        };
1139
        self.tree.children(node_idx)
1139
            .iter()
1671
            .filter(|&&child_idx| {
1671
                let Some(child_node) = self.tree.get(child_idx) else {
                    return false;
                };
1671
                let Some(child_dom_id) = child_node.dom_node_id else {
                    return true;
                };
                // Check if child has display: none
                use crate::solver3::getters::{get_display_property, MultiValue};
1671
                let display = get_display_property(self.ctx.styled_dom, Some(child_dom_id));
1671
                let is_display_none = matches!(display, MultiValue::Exact(LayoutDisplay::None));
1671
                !is_display_none
1671
            })
1139
            .copied()
1139
            .collect()
1139
    }
}
/// 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.
212
pub fn layout_taffy_subtree<T: ParsedFontTrait>(
212
    ctx: &mut LayoutContext<'_, T>,
212
    tree: &mut LayoutTree,
212
    text_cache: &mut crate::font_traits::TextLayoutCache,
212
    node_idx: usize,
212
    inputs: LayoutInput,
212
) -> LayoutOutput {
212
    let children: Vec<usize> = tree.children(node_idx).to_vec();
    // DEBUG: Log Taffy inputs
212
    if ctx.debug_messages.is_some() {
142
        ctx.debug_info_inner(format!(
142
            "[TAFFY INPUT] node_idx={} known_dims=({:?}, {:?}) available=({:?}, {:?}) \
142
             parent_size=({:?}, {:?}) children={:?}",
142
            node_idx,
142
            inputs.known_dimensions.width,
142
            inputs.known_dimensions.height,
142
            inputs.available_space.width,
142
            inputs.available_space.height,
142
            inputs.parent_size.width,
142
            inputs.parent_size.height,
142
            children
142
        ));
142
    }
    // Clear cache to force re-measure
460
    for &child_idx in &children {
248
        if let Some(child) = tree.warm_mut(child_idx) {
248
            child.taffy_cache.clear();
248
        }
    }
    // 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.
212
    let text_cache_ptr = text_cache as *mut crate::font_traits::TextLayoutCache;
212
    let mut bridge = TaffyBridge::new(ctx, tree, text_cache_ptr);
212
    let node = bridge.tree.get(node_idx).unwrap();
212
    let output = match node.formatting_context {
212
        FormattingContext::Flex => compute_flexbox_layout(&mut bridge, node_idx.into(), inputs),
        FormattingContext::Grid => compute_grid_layout(&mut bridge, node_idx.into(), inputs),
        _ => LayoutOutput::HIDDEN,
    };
    // DEBUG: Log Taffy output
212
    if bridge.ctx.debug_messages.is_some() {
142
        bridge.ctx.debug_info_inner(format!(
142
            "[TAFFY OUTPUT] node_idx={} output_size=({:?}, {:?})",
            node_idx, output.size.width, output.size.height
        ));
        // Log child layout results
250
        for &child_idx in &children {
108
            if let Some(child) = bridge.tree.get(child_idx) {
108
                bridge.ctx.debug_info_inner(format!(
108
                    "[TAFFY CHILD RESULT] child_idx={} used_size={:?} relative_pos={:?}",
108
                    child_idx, child.used_size, bridge.tree.warm(child_idx).and_then(|w| w.relative_position)
                ));
            }
        }
70
    }
212
    output
212
}
// --- Trait Implementations for the Bridge ---
impl<'a, 'b, T: ParsedFontTrait> TraversePartialTree for TaffyBridge<'a, 'b, T> {
    type ChildIter<'c>
        = std::vec::IntoIter<taffy::NodeId>
    where
        Self: 'c;
215
    fn child_ids(&self, node_id: taffy::NodeId) -> Self::ChildIter<'_> {
215
        let node_idx: usize = node_id.into();
215
        let children = self.get_layout_children(node_idx);
215
        children
215
            .into_iter()
251
            .map(|id| id.into())
215
            .collect::<Vec<taffy::NodeId>>()
215
            .into_iter()
215
    }
426
    fn child_count(&self, node_id: taffy::NodeId) -> usize {
426
        let node_idx: usize = node_id.into();
426
        let count = self.get_layout_children(node_idx).len();
426
        count
426
    }
498
    fn get_child_id(&self, node_id: taffy::NodeId, index: usize) -> taffy::NodeId {
498
        self.get_layout_children(node_id.into())[index].into()
498
    }
}
impl<'a, 'b, T: ParsedFontTrait> LayoutPartialTree for TaffyBridge<'a, 'b, T> {
    type CoreContainerStyle<'c>
        = Style
    where
        Self: 'c;
    type CustomIdent = String;
1684
    fn get_core_container_style(&self, node_id: taffy::NodeId) -> Self::CoreContainerStyle<'_> {
1684
        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
1684
        self.get_taffy_style(node_idx)
1684
    }
249
    fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &Layout) {
249
        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.
249
        let (parent_border_left, parent_border_top, parent_padding_left, parent_padding_top) = {
249
            if let Some(child) = self.tree.get(node_idx) {
249
                if let Some(parent_idx) = child.parent {
249
                    if let Some(parent) = self.tree.get(parent_idx) {
249
                        let pbp = parent.box_props.unpack();
249
                        (
249
                            pbp.border.left,
249
                            pbp.border.top,
249
                            pbp.padding.left,
249
                            pbp.padding.top,
249
                        )
                    } else {
                        (0.0, 0.0, 0.0, 0.0)
                    }
                } else {
                    (0.0, 0.0, 0.0, 0.0)
                }
            } else {
                (0.0, 0.0, 0.0, 0.0)
            }
        };
249
        if let Some(node) = self.tree.get_mut(node_idx) {
249
            let size = translate_taffy_size_back(layout.size);
249
            let mut pos = translate_taffy_point_back(layout.location);
            // DEBUG: Log Taffy's raw layout result before adjustment
249
            if self.ctx.debug_messages.is_some() {
109
                self.ctx.debug_info_inner(format!(
109
                    "[TAFFY set_unrounded_layout] node_idx={} taffy_size=({:.2}, {:.2}) \
109
                     taffy_pos=({:.2}, {:.2}) parent_border=({:.2}, {:.2}) parent_padding=({:.2}, \
109
                     {:.2})",
109
                    node_idx,
109
                    layout.size.width,
109
                    layout.size.height,
109
                    layout.location.x,
109
                    layout.location.y,
109
                    parent_border_left,
109
                    parent_border_top,
109
                    parent_padding_left,
109
                    parent_padding_top
109
                ));
143
            }
            // Subtract parent's border and padding offset to convert
            // from border-box-relative to content-box-relative position
249
            pos.x -= parent_border_left + parent_padding_left;
249
            pos.y -= parent_border_top + parent_padding_top;
249
            node.used_size = Some(size);
        }
249
        if let Some(warm) = self.tree.warm_mut(node_idx) {
249
            let mut pos = translate_taffy_point_back(layout.location);
249
            pos.x -= parent_border_left + parent_padding_left;
249
            pos.y -= parent_border_top + parent_padding_top;
249
            warm.relative_position = Some(pos);
249
        }
249
    }
    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 as *const CalcResolveContext) };
        crate::solver3::calc::evaluate_calc(ctx, basis)
    }
721
    fn compute_child_layout(
721
        &mut self,
721
        node_id: taffy::NodeId,
721
        inputs: LayoutInput,
721
    ) -> LayoutOutput {
721
        let node_idx: usize = node_id.into();
        // DEBUG: Log the style being used for this child
721
        if self.ctx.debug_messages.is_some() {
441
            let style = self.get_taffy_style(node_idx);
441
            self.ctx.debug_info_inner(format!(
441
                "[TAFFY compute_child_layout] node_idx={} flex_grow={} flex_shrink={} \
441
                 flex_basis={:?} size=({:?}, {:?}) inputs.known_dims=({:?}, {:?})",
441
                node_idx,
441
                style.flex_grow,
441
                style.flex_shrink,
441
                style.flex_basis,
441
                style.size.width,
441
                style.size.height,
441
                inputs.known_dimensions.width,
441
                inputs.known_dimensions.height
441
            ));
441
        }
        // Get formatting context
721
        let fc = self
721
            .tree
721
            .get(node_idx)
721
            .map(|s| s.formatting_context.clone())
721
            .unwrap_or_default();
721
        let mut result = compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
719
            let node_idx: usize = node_id.into();
719
            let fc = tree
719
                .tree
719
                .get(node_idx)
719
                .map(|s| s.formatting_context.clone())
719
                .unwrap_or_default();
719
            match fc {
3
                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
716
                _ => tree.compute_non_flex_layout(node_idx, inputs),
            }
719
        });
        // Store layout for container nodes - Taffy only calls set_unrounded_layout for leaf nodes
721
        if let Some(node) = self.tree.get_mut(node_idx) {
721
            let size = translate_taffy_size_back(result.size);
721
            node.used_size = Some(size);
721
        }
        // 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.
721
        if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
3
            let taffy_content_width = result.content_size.width;
3
            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.
3
            if taffy_content_width <= 0.0 && taffy_content_height <= 0.0 {
2
                return result;
1
            }
1
            let (scrollbar_info, eff_content_w, eff_content_h) =
1
                compute_taffy_scrollbar_info(
1
                    self.ctx,
1
                    self.tree,
1
                    node_idx,
1
                    result.size.width,
1
                    result.size.height,
1
                    taffy_content_width,
1
                    taffy_content_height,
1
                );
1
            if let Some(warm) = self.tree.warm_mut(node_idx) {
1
                warm.scrollbar_info = Some(scrollbar_info);
1
                // eff_content_w/h are already in content-box coordinates
1
                // (border.left/top subtracted in compute_taffy_scrollbar_info),
1
                // so store directly without further subtraction.
1
                warm.overflow_content_size = Some(LogicalSize::new(
1
                    eff_content_w,
1
                    eff_content_h,
1
                ));
1
            }
718
        }
719
        result
721
    }
}
impl<'a, 'b, T: ParsedFontTrait> TaffyBridge<'a, 'b, T> {
    /// Compute layout for non-flex/grid nodes by delegating to layout_formatting_context.
    /// This handles Block, Inline, Table, InlineBlock formatting contexts recursively.
716
    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.
716
        let (node_padding_width, node_padding_height, node_border_width, node_border_height) = self
716
            .tree
716
            .get(node_idx)
716
            .map(|node| {
716
                let bp = node.box_props.unpack();
716
                (
716
                    bp.padding.left + bp.padding.right,
716
                    bp.padding.top + bp.padding.bottom,
716
                    bp.border.left + bp.border.right,
716
                    bp.border.top + bp.border.bottom,
716
                )
716
            })
716
            .unwrap_or((0.0, 0.0, 0.0, 0.0));
        // 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.
716
        let available_width = inputs
716
            .known_dimensions
716
            .width
716
            .map(|kw| (kw - node_padding_width - node_border_width).max(0.0))
716
            .or_else(|| match inputs.available_space.width {
106
                AvailableSpace::Definite(w) => Some(w),
143
                AvailableSpace::MinContent => None, // Use infinity, return intrinsic min-content
3
                AvailableSpace::MaxContent => None, // Use infinity for max-content
252
            })
716
            .unwrap_or(f32::INFINITY);
716
        let available_height = inputs
716
            .known_dimensions
716
            .height
716
            .map(|kh| (kh - node_padding_height - node_border_height).max(0.0))
716
            .or_else(|| match inputs.available_space.height {
3
                AvailableSpace::Definite(h) => Some(h),
107
                AvailableSpace::MinContent => None, // Use infinity, return intrinsic min-content
106
                AvailableSpace::MaxContent => None,
216
            })
716
            .unwrap_or(f32::INFINITY);
716
        let mut available_size = LogicalSize {
716
            width: available_width,
716
            height: available_height,
716
        };
        // 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).
716
        let available_width_type = if inputs.known_dimensions.width.is_some() {
464
            crate::text3::cache::AvailableSpace::Definite(available_width)
        } else {
252
            match inputs.available_space.width {
106
                AvailableSpace::Definite(w) => crate::text3::cache::AvailableSpace::Definite(w),
143
                AvailableSpace::MinContent => crate::text3::cache::AvailableSpace::MinContent,
3
                AvailableSpace::MaxContent => crate::text3::cache::AvailableSpace::MaxContent,
            }
        };
        // Get text-align from CSS for this node (important for centering content in flex items)
716
        let text_align = self
716
            .tree
716
            .get(node_idx)
716
            .and_then(|node| node.dom_node_id)
716
            .map(|dom_id| {
716
                let node_state =
716
                    &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
716
                crate::solver3::getters::get_text_align(self.ctx.styled_dom, dom_id, node_state)
716
                    .unwrap_or_default()
716
            })
716
            .unwrap_or_default();
        // Convert CSS text-align to our internal TextAlign enum
716
        let fc_text_align = match text_align {
716
            azul_css::props::style::StyleTextAlign::Left => FcTextAlign::Start,
            azul_css::props::style::StyleTextAlign::Right => FcTextAlign::End,
            azul_css::props::style::StyleTextAlign::Center => FcTextAlign::Center,
            azul_css::props::style::StyleTextAlign::Justify => FcTextAlign::Justify,
            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.
716
        let text_cache = unsafe { &mut *self.text_cache };
716
        let constraints = LayoutConstraints {
716
            available_size,
716
            writing_mode: LayoutWritingMode::HorizontalTb,
716
            writing_mode_ctx: super::geometry::WritingModeContext::default(),
716
            bfc_state: None,
716
            text_align: fc_text_align,
716
            containing_block_size: available_size,
716
            available_width_type,
716
        };
        // Use a temporary float cache for this subtree
716
        let mut float_cache = HashMap::new();
        // Call layout_formatting_context - this handles ALL formatting context types
        // including nested flex/grid, tables, BFC, and IFC
716
        let fc_result = crate::solver3::fc::layout_formatting_context(
716
            self.ctx,
716
            self.tree,
716
            text_cache,
716
            node_idx,
716
            &constraints,
716
            &mut float_cache,
        );
716
        match fc_result {
716
            Ok(bfc_result) => {
716
                let output = bfc_result.output;
716
                let content_width = output.overflow_size.width;
716
                let content_height = output.overflow_size.height;
                // Padding/border already computed at start of function
716
                let padding_width = node_padding_width;
716
                let padding_height = node_padding_height;
716
                let border_width = node_border_width;
716
                let border_height = node_border_height;
                // Get intrinsic sizes for min/max-content queries
716
                let intrinsic = self
716
                    .tree
716
                    .warm(node_idx)
716
                    .and_then(|w| w.intrinsic_sizes)
716
                    .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".
716
                let fc = self
716
                    .tree
716
                    .get(node_idx)
716
                    .map(|s| s.formatting_context.clone())
716
                    .unwrap_or_default();
716
                let is_shrink_to_fit = matches!(fc, FormattingContext::InlineBlock)
                    && inputs.known_dimensions.width.is_none();
716
                let effective_content_width = match inputs.available_space.width {
                    AvailableSpace::MinContent => {
143
                        if intrinsic.min_content_width > 0.0 {
3
                            intrinsic.min_content_width
                        } else {
140
                            content_width
                        }
                    }
                    AvailableSpace::MaxContent => {
3
                        if intrinsic.max_content_width > 0.0 {
3
                            intrinsic.max_content_width
                        } else {
                            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)
570
                        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 {
570
                            content_width
                        }
                    }
                };
                // Convert content-box size to border-box size (for when we compute our own size)
716
                let border_box_width = effective_content_width + padding_width + border_width;
716
                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.
716
                let final_width = match inputs.known_dimensions.width {
464
                    Some(border_box_w) => border_box_w,
252
                    None => border_box_width,
                };
                // 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.
716
                let final_height = match inputs.known_dimensions.height {
500
                    Some(border_box_h) => border_box_h,
                    None => {
                        // Check if parent is a grid container and available_space is definite
216
                        let parent_is_grid = self
216
                            .tree
216
                            .get(node_idx)
216
                            .and_then(|n| n.parent)
216
                            .and_then(|p| self.tree.get(p))
216
                            .map(|p| matches!(p.formatting_context, FormattingContext::Grid))
216
                            .unwrap_or(false);
216
                        if parent_is_grid {
                            // For grid items, use available space if content is smaller
                            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)
                                }
                                _ => border_box_height,
                            }
                        } else {
216
                            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.
716
                for (child_idx, child_pos) in output.positions.iter() {
4
                    if let Some(child_warm) = self.tree.warm_mut(*child_idx) {
4
                        child_warm.relative_position = Some(*child_pos);
4
                    }
                }
                // 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.
716
                let (scrollbar_info, _, _) = compute_taffy_scrollbar_info(
716
                    self.ctx,
716
                    self.tree,
716
                    node_idx,
716
                    final_width,
716
                    final_height,
716
                    content_width,
716
                    content_height,
716
                );
                // Store the border-box size and scrollbar_info on the node for display list generation
716
                if let Some(node) = self.tree.get_mut(node_idx) {
716
                    node.used_size = Some(LogicalSize {
716
                        width: final_width,
716
                        height: final_height,
716
                    });
716
                }
716
                if let Some(warm) = self.tree.warm_mut(node_idx) {
716
                    warm.scrollbar_info = Some(scrollbar_info);
716
                    // Store the actual content size for scroll calculations
716
                    warm.overflow_content_size = Some(LogicalSize {
716
                        width: content_width,
716
                        height: content_height,
716
                    });
716
                }
                // Return the same size to Taffy for correct positioning
716
                LayoutOutput {
716
                    size: Size {
716
                        width: final_width,
716
                        height: final_height,
716
                    },
716
                    content_size: Size {
716
                        width: content_width,
716
                        height: content_height,
716
                    },
716
                    first_baselines: taffy::Point {
716
                        x: None,
716
                        y: output.baseline,
716
                    },
716
                    top_margin: taffy::CollapsibleMarginSet::ZERO,
716
                    bottom_margin: taffy::CollapsibleMarginSet::ZERO,
716
                    margins_can_collapse_through: false,
716
                }
            }
            Err(_e) => {
                // Fallback to intrinsic sizes if layout fails
                let intrinsic = self.tree.warm(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,
                }
            }
        }
716
    }
}
impl<'a, 'b, T: ParsedFontTrait> CacheTree for TaffyBridge<'a, 'b, T> {
721
    fn cache_get(
721
        &self,
721
        node_id: taffy::NodeId,
721
        input: &LayoutInput,
721
    ) -> Option<LayoutOutput> {
721
        let node_idx: usize = node_id.into();
721
        self.tree
721
            .warm(node_idx)?
            .taffy_cache
721
            .get(input)
721
    }
719
    fn cache_store(
719
        &mut self,
719
        node_id: taffy::NodeId,
719
        input: &LayoutInput,
719
        layout_output: LayoutOutput,
719
    ) {
719
        let node_idx: usize = node_id.into();
719
        if let Some(warm) = self.tree.warm_mut(node_idx) {
719
            warm.taffy_cache
719
                .store(input, layout_output);
719
        }
719
    }
    fn cache_clear(&mut self, node_id: taffy::NodeId) {
        let node_idx: usize = node_id.into();
        if let Some(warm) = self.tree.warm_mut(node_idx) {
            warm.taffy_cache.clear();
        }
    }
}
impl<'a, 'b, T: ParsedFontTrait> LayoutFlexboxContainer for TaffyBridge<'a, 'b, T> {
    type FlexboxContainerStyle<'c>
        = Style
    where
        Self: 'c;
    type FlexboxItemStyle<'c>
        = Style
    where
        Self: 'c;
433
    fn get_flexbox_container_style(
433
        &self,
433
        node_id: taffy::NodeId,
433
    ) -> Self::FlexboxContainerStyle<'_> {
433
        self.get_core_container_style(node_id)
433
    }
1251
    fn get_flexbox_child_style(&self, child_node_id: taffy::NodeId) -> Self::FlexboxItemStyle<'_> {
1251
        self.get_core_container_style(child_node_id)
1251
    }
}
impl<'a, 'b, T: ParsedFontTrait> LayoutGridContainer for TaffyBridge<'a, 'b, T> {
    type GridContainerStyle<'c>
        = Style
    where
        Self: 'c;
    type GridItemStyle<'c>
        = Style
    where
        Self: 'c;
    fn get_grid_container_style(&self, node_id: taffy::NodeId) -> Self::GridContainerStyle<'_> {
        self.get_core_container_style(node_id)
    }
    fn get_grid_child_style(&self, child_node_id: taffy::NodeId) -> Self::GridItemStyle<'_> {
        self.get_core_container_style(child_node_id)
    }
}
// --- Conversion Functions ---
665
fn from_layout_width(
665
    val: LayoutWidth,
665
    calc_storage: &std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
665
    em_size: f32,
665
    rem_size: f32,
665
) -> Dimension {
665
    match val {
455
        LayoutWidth::Auto => Dimension::auto(),
210
        LayoutWidth::Px(px) => {
210
            match pixel_value_to_pixels_fallback(&px) {
210
                Some(pixels) => Dimension::length(pixels),
                None => match px.to_percent() {
                    Some(p) => Dimension::percent(p.get()),
                    None => Dimension::auto(),
                },
            }
        }
        LayoutWidth::MinContent | LayoutWidth::MaxContent | LayoutWidth::FitContent(_) => Dimension::auto(),
        LayoutWidth::Calc(items) => store_calc_and_make_dimension(items, calc_storage, em_size, rem_size),
    }
665
}
665
fn from_layout_height(
665
    val: LayoutHeight,
665
    calc_storage: &std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
665
    em_size: f32,
665
    rem_size: f32,
665
) -> Dimension {
665
    match val {
280
        LayoutHeight::Auto => Dimension::auto(),
385
        LayoutHeight::Px(px) => {
385
            match pixel_value_to_pixels_fallback(&px) {
280
                Some(pixels) => Dimension::length(pixels),
105
                None => match px.to_percent() {
105
                    Some(p) => Dimension::percent(p.get()),
                    None => Dimension::auto(),
                },
            }
        }
        LayoutHeight::MinContent | LayoutHeight::MaxContent | LayoutHeight::FitContent(_) => Dimension::auto(),
        LayoutHeight::Calc(items) => store_calc_and_make_dimension(items, calc_storage, em_size, rem_size),
    }
665
}
/// 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.
fn store_calc_and_make_dimension(
    items: CalcAstItemVec,
    storage: &std::cell::RefCell<Vec<Box<CalcResolveContext>>>,
    em_size: f32,
    rem_size: f32,
) -> Dimension {
    let boxed = Box::new(CalcResolveContext { items, em_size, rem_size });
    let ptr: *const CalcResolveContext = &*boxed;
    storage.borrow_mut().push(boxed);
    // SAFETY: Box gives ≥8-byte-aligned heap pointer; taffy masks low 3 bits.
    Dimension::calc(ptr as *const ())
}
665
fn from_layout_position(val: LayoutPosition) -> Position {
665
    match val {
665
        LayoutPosition::Static => Position::Relative, // Taffy treats Static as Relative
        LayoutPosition::Relative => Position::Relative,
        LayoutPosition::Absolute => Position::Absolute,
        LayoutPosition::Fixed => Position::Absolute, // Taffy doesn't distinguish Fixed
        LayoutPosition::Sticky => Position::Relative, // Sticky = Relative for Taffy
    }
665
}