Skip to main content

rustmotion_core/css/
taffy_bridge.rs

1use taffy::prelude as tf;
2
3use super::style::{
4    AlignContent, AlignItems, AlignSelf, BoxSizing, CssStyle, Display, Edges, FlexDirection,
5    FlexWrap, Gap, GridAutoFlow, GridLine, GridLineEnd, GridTrack, GridTrackKeyword,
6    JustifyContent, JustifyItems, JustifySelf, Overflow, Position, Size,
7};
8use super::units::{LengthContext, LengthPercentage, ParsedLength};
9
10#[derive(Debug, Clone, Copy, Default)]
11pub struct ConversionContext {
12    pub length: LengthContext,
13}
14
15impl ConversionContext {
16    pub fn for_viewport(viewport_width: f32, viewport_height: f32) -> Self {
17        Self {
18            length: LengthContext {
19                viewport_width,
20                viewport_height,
21                ..LengthContext::default()
22            },
23        }
24    }
25}
26
27pub fn to_taffy_style(css: &CssStyle, ctx: &ConversionContext) -> tf::Style {
28    let mut style = tf::Style::DEFAULT;
29
30    style.display = match css.display {
31        Some(Display::None) => tf::Display::None,
32        Some(Display::Block) => tf::Display::Block,
33        Some(Display::Flex) => tf::Display::Flex,
34        Some(Display::Grid) => tf::Display::Grid,
35        _ => tf::Display::Block,
36    };
37
38    style.position = match css.position {
39        Some(Position::Absolute) => tf::Position::Absolute,
40        _ => tf::Position::Relative,
41    };
42
43    style.inset = tf::Rect {
44        top: lp_to_lp_auto(css.top.as_ref(), ctx),
45        right: lp_to_lp_auto(css.right.as_ref(), ctx),
46        bottom: lp_to_lp_auto(css.bottom.as_ref(), ctx),
47        left: lp_to_lp_auto(css.left.as_ref(), ctx),
48    };
49
50    style.size = tf::Size {
51        width: size_to_dim(css.width.as_ref(), ctx),
52        height: size_to_dim(css.height.as_ref(), ctx),
53    };
54    style.min_size = tf::Size {
55        width: size_to_dim(css.min_width.as_ref(), ctx),
56        height: size_to_dim(css.min_height.as_ref(), ctx),
57    };
58    style.max_size = tf::Size {
59        width: size_to_dim(css.max_width.as_ref(), ctx),
60        height: size_to_dim(css.max_height.as_ref(), ctx),
61    };
62    style.aspect_ratio = css.aspect_ratio;
63
64    if let Some(bs) = css.box_sizing {
65        style.box_sizing = match bs {
66            BoxSizing::ContentBox => tf::BoxSizing::ContentBox,
67            BoxSizing::BorderBox => tf::BoxSizing::BorderBox,
68        };
69    }
70
71    style.margin = edges_to_rect_lpa(css.margin.as_ref(), ctx);
72    style.padding = edges_to_rect_lp(css.padding.as_ref(), ctx);
73    style.border = border_widths(css.border.as_ref(), ctx);
74
75    style.flex_direction = match css.flex_direction {
76        Some(FlexDirection::Row) => tf::FlexDirection::Row,
77        Some(FlexDirection::RowReverse) => tf::FlexDirection::RowReverse,
78        Some(FlexDirection::Column) => tf::FlexDirection::Column,
79        Some(FlexDirection::ColumnReverse) => tf::FlexDirection::ColumnReverse,
80        None => tf::FlexDirection::Column,
81    };
82    if let Some(w) = css.flex_wrap {
83        style.flex_wrap = match w {
84            FlexWrap::Nowrap => tf::FlexWrap::NoWrap,
85            FlexWrap::Wrap => tf::FlexWrap::Wrap,
86            FlexWrap::WrapReverse => tf::FlexWrap::WrapReverse,
87        };
88    }
89    if let Some(j) = css.justify_content {
90        style.justify_content = Some(match j {
91            JustifyContent::FlexStart | JustifyContent::Start => tf::JustifyContent::Start,
92            JustifyContent::FlexEnd | JustifyContent::End => tf::JustifyContent::End,
93            JustifyContent::Center => tf::JustifyContent::Center,
94            JustifyContent::SpaceBetween => tf::JustifyContent::SpaceBetween,
95            JustifyContent::SpaceAround => tf::JustifyContent::SpaceAround,
96            JustifyContent::SpaceEvenly => tf::JustifyContent::SpaceEvenly,
97        });
98    }
99    if let Some(a) = css.align_items {
100        style.align_items = Some(align_items_to_taffy(a));
101    }
102    if let Some(a) = css.align_self {
103        style.align_self = align_self_to_taffy(a);
104    }
105    if let Some(a) = css.align_content {
106        style.align_content = Some(align_content_to_taffy(a));
107    }
108    if let Some(grow) = css.flex_grow {
109        style.flex_grow = grow;
110    }
111    if let Some(shrink) = css.flex_shrink {
112        style.flex_shrink = shrink;
113    }
114    if let Some(basis) = css.flex_basis.as_ref() {
115        style.flex_basis = size_to_dim(Some(basis), ctx);
116    }
117    if css.order.is_some() {
118        static WARNED_ORDER: std::sync::Once = std::sync::Once::new();
119        WARNED_ORDER.call_once(|| {
120            eprintln!(
121                "Warning: `order` is not supported by the layout engine (no flex/grid item \
122                 reordering primitive) — it is ignored. Reorder the component's JSON `children` \
123                 array instead to change paint/layout order."
124            );
125        });
126    }
127
128    if let Some(gap) = css.gap.as_ref() {
129        let (row, col) = match gap {
130            Gap::Uniform(v) => (lp_to_lp(v, ctx), lp_to_lp(v, ctx)),
131            Gap::RowColumn { row, column } => (lp_to_lp(row, ctx), lp_to_lp(column, ctx)),
132        };
133        style.gap = tf::Size {
134            width: col,
135            height: row,
136        };
137    }
138
139    if let Some(tracks) = css.grid_template_columns.as_ref() {
140        style.grid_template_columns = tracks
141            .iter()
142            .map(|t| tf::GridTemplateComponent::Single(grid_track_sizing(t, ctx)))
143            .collect();
144    }
145    if let Some(tracks) = css.grid_template_rows.as_ref() {
146        style.grid_template_rows = tracks
147            .iter()
148            .map(|t| tf::GridTemplateComponent::Single(grid_track_sizing(t, ctx)))
149            .collect();
150    }
151    if let Some(flow) = css.grid_auto_flow {
152        style.grid_auto_flow = match flow {
153            GridAutoFlow::Row => tf::GridAutoFlow::Row,
154            GridAutoFlow::Column => tf::GridAutoFlow::Column,
155            GridAutoFlow::RowDense => tf::GridAutoFlow::RowDense,
156            GridAutoFlow::ColumnDense => tf::GridAutoFlow::ColumnDense,
157        };
158    }
159    if let Some(gc) = css.grid_column.as_ref() {
160        style.grid_column = grid_placement_line(gc);
161    }
162    if let Some(gr) = css.grid_row.as_ref() {
163        style.grid_row = grid_placement_line(gr);
164    }
165    if let Some(ji) = css.justify_items {
166        style.justify_items = Some(justify_items_to_taffy(ji));
167    }
168    if let Some(js) = css.justify_self {
169        style.justify_self = justify_self_to_taffy(js);
170    }
171
172    if let Some(o) = css.overflow {
173        let v = overflow_to_taffy(o);
174        style.overflow = taffy::Point { x: v, y: v };
175    }
176    if let Some(o) = css.overflow_x {
177        style.overflow.x = overflow_to_taffy(o);
178    }
179    if let Some(o) = css.overflow_y {
180        style.overflow.y = overflow_to_taffy(o);
181    }
182
183    style
184}
185
186pub(crate) fn content_box_inset(css: &CssStyle, ctx: &ConversionContext) -> (f32, f32) {
187    let (pt, pr, pb, pl) = css.padding.as_ref().map(Edges::resolve).unwrap_or_default();
188    let padding = (
189        pt.resolve(&ctx.length),
190        pr.resolve(&ctx.length),
191        pb.resolve(&ctx.length),
192        pl.resolve(&ctx.length),
193    );
194    let border = resolve_border_widths_px(css.border.as_ref(), ctx);
195    (
196        padding.1 + padding.3 + border.1 + border.3,
197        padding.0 + padding.2 + border.0 + border.2,
198    )
199}
200
201fn resolve_border_widths_px(
202    b: Option<&super::style::BorderEdges>,
203    ctx: &ConversionContext,
204) -> (f32, f32, f32, f32) {
205    let Some(b) = b else {
206        return (0.0, 0.0, 0.0, 0.0);
207    };
208    let uniform = b.width.as_ref().map(Edges::resolve);
209    let pick_side = |side: Option<&super::style::BorderSide>, idx: usize| -> f32 {
210        if let Some(side) = side {
211            if let Some(w) = side.width.as_ref() {
212                return w.resolve(&ctx.length);
213            }
214        }
215        if let Some((t, r, btm, l)) = uniform.as_ref() {
216            let pick = match idx {
217                0 => t,
218                1 => r,
219                2 => btm,
220                3 => l,
221                _ => t,
222            };
223            return pick.resolve(&ctx.length);
224        }
225        0.0
226    };
227    (
228        pick_side(b.top.as_ref(), 0),
229        pick_side(b.right.as_ref(), 1),
230        pick_side(b.bottom.as_ref(), 2),
231        pick_side(b.left.as_ref(), 3),
232    )
233}
234
235fn align_items_to_taffy(a: AlignItems) -> tf::AlignItems {
236    match a {
237        AlignItems::Stretch => tf::AlignItems::Stretch,
238        AlignItems::FlexStart | AlignItems::Start => tf::AlignItems::Start,
239        AlignItems::FlexEnd | AlignItems::End => tf::AlignItems::End,
240        AlignItems::Center => tf::AlignItems::Center,
241        AlignItems::Baseline => tf::AlignItems::Baseline,
242    }
243}
244
245fn align_self_to_taffy(a: AlignSelf) -> Option<tf::AlignSelf> {
246    Some(match a {
247        AlignSelf::Auto => return None,
248        AlignSelf::Stretch => tf::AlignSelf::Stretch,
249        AlignSelf::FlexStart | AlignSelf::Start => tf::AlignSelf::Start,
250        AlignSelf::FlexEnd | AlignSelf::End => tf::AlignSelf::End,
251        AlignSelf::Center => tf::AlignSelf::Center,
252        AlignSelf::Baseline => tf::AlignSelf::Baseline,
253    })
254}
255
256fn justify_items_to_taffy(j: JustifyItems) -> tf::AlignItems {
257    match j {
258        JustifyItems::Stretch => tf::AlignItems::Stretch,
259        JustifyItems::Start => tf::AlignItems::Start,
260        JustifyItems::End => tf::AlignItems::End,
261        JustifyItems::Center => tf::AlignItems::Center,
262        JustifyItems::Legacy => tf::AlignItems::Start,
263    }
264}
265
266fn justify_self_to_taffy(j: JustifySelf) -> Option<tf::AlignSelf> {
267    Some(match j {
268        JustifySelf::Auto => return None,
269        JustifySelf::Stretch => tf::AlignSelf::Stretch,
270        JustifySelf::Start => tf::AlignSelf::Start,
271        JustifySelf::End => tf::AlignSelf::End,
272        JustifySelf::Center => tf::AlignSelf::Center,
273    })
274}
275
276fn align_content_to_taffy(a: AlignContent) -> tf::AlignContent {
277    match a {
278        AlignContent::Stretch => tf::AlignContent::Stretch,
279        AlignContent::FlexStart | AlignContent::Start => tf::AlignContent::Start,
280        AlignContent::FlexEnd | AlignContent::End => tf::AlignContent::End,
281        AlignContent::Center => tf::AlignContent::Center,
282        AlignContent::SpaceBetween => tf::AlignContent::SpaceBetween,
283        AlignContent::SpaceAround => tf::AlignContent::SpaceAround,
284        AlignContent::SpaceEvenly => tf::AlignContent::SpaceEvenly,
285    }
286}
287
288fn overflow_to_taffy(o: Overflow) -> taffy::Overflow {
289    match o {
290        Overflow::Visible => taffy::Overflow::Visible,
291        Overflow::Hidden | Overflow::Clip => taffy::Overflow::Hidden,
292        Overflow::Auto | Overflow::Scroll => taffy::Overflow::Scroll,
293    }
294}
295
296fn lp_to_lp(v: &LengthPercentage, ctx: &ConversionContext) -> tf::LengthPercentage {
297    match v.parse() {
298        ParsedLength::Px(p) => tf::LengthPercentage::length(p),
299        ParsedLength::Percent(p) => tf::LengthPercentage::percent(p / 100.0),
300        ParsedLength::Em(em) => tf::LengthPercentage::length(em * ctx.length.font_size),
301        ParsedLength::Rem(r) => tf::LengthPercentage::length(r * ctx.length.root_font_size),
302        ParsedLength::Vw(p) => tf::LengthPercentage::length(p / 100.0 * ctx.length.viewport_width),
303        ParsedLength::Vh(p) => tf::LengthPercentage::length(p / 100.0 * ctx.length.viewport_height),
304        ParsedLength::Fr(_) | ParsedLength::Auto => tf::LengthPercentage::length(0.0),
305    }
306}
307
308fn lp_to_lp_auto(
309    v: Option<&LengthPercentage>,
310    ctx: &ConversionContext,
311) -> tf::LengthPercentageAuto {
312    let Some(v) = v else {
313        return tf::LengthPercentageAuto::auto();
314    };
315    match v.parse() {
316        ParsedLength::Auto => tf::LengthPercentageAuto::auto(),
317        ParsedLength::Px(p) => tf::LengthPercentageAuto::length(p),
318        ParsedLength::Percent(p) => tf::LengthPercentageAuto::percent(p / 100.0),
319        ParsedLength::Em(em) => tf::LengthPercentageAuto::length(em * ctx.length.font_size),
320        ParsedLength::Rem(r) => tf::LengthPercentageAuto::length(r * ctx.length.root_font_size),
321        ParsedLength::Vw(p) => {
322            tf::LengthPercentageAuto::length(p / 100.0 * ctx.length.viewport_width)
323        }
324        ParsedLength::Vh(p) => {
325            tf::LengthPercentageAuto::length(p / 100.0 * ctx.length.viewport_height)
326        }
327        ParsedLength::Fr(_) => tf::LengthPercentageAuto::auto(),
328    }
329}
330
331fn size_to_dim(s: Option<&Size>, ctx: &ConversionContext) -> tf::Dimension {
332    let Some(s) = s else {
333        return tf::Dimension::auto();
334    };
335    match s {
336        Size::Auto(_) => tf::Dimension::auto(),
337        Size::Length(lp) => match lp.parse() {
338            ParsedLength::Auto => tf::Dimension::auto(),
339            ParsedLength::Px(p) => tf::Dimension::length(p),
340            ParsedLength::Percent(p) => tf::Dimension::percent(p / 100.0),
341            ParsedLength::Em(em) => tf::Dimension::length(em * ctx.length.font_size),
342            ParsedLength::Rem(r) => tf::Dimension::length(r * ctx.length.root_font_size),
343            ParsedLength::Vw(p) => tf::Dimension::length(p / 100.0 * ctx.length.viewport_width),
344            ParsedLength::Vh(p) => tf::Dimension::length(p / 100.0 * ctx.length.viewport_height),
345            ParsedLength::Fr(_) => tf::Dimension::auto(),
346        },
347        Size::Keyword(_) => tf::Dimension::auto(),
348    }
349}
350
351fn edges_to_rect_lp(e: Option<&Edges>, ctx: &ConversionContext) -> tf::Rect<tf::LengthPercentage> {
352    let Some(e) = e else {
353        return tf::Rect {
354            top: tf::LengthPercentage::length(0.0),
355            right: tf::LengthPercentage::length(0.0),
356            bottom: tf::LengthPercentage::length(0.0),
357            left: tf::LengthPercentage::length(0.0),
358        };
359    };
360    let (top, right, bottom, left) = e.resolve();
361    tf::Rect {
362        top: lp_to_lp(&top, ctx),
363        right: lp_to_lp(&right, ctx),
364        bottom: lp_to_lp(&bottom, ctx),
365        left: lp_to_lp(&left, ctx),
366    }
367}
368
369fn edges_to_rect_lpa(
370    e: Option<&Edges>,
371    ctx: &ConversionContext,
372) -> tf::Rect<tf::LengthPercentageAuto> {
373    let Some(e) = e else {
374        return tf::Rect {
375            top: tf::LengthPercentageAuto::length(0.0),
376            right: tf::LengthPercentageAuto::length(0.0),
377            bottom: tf::LengthPercentageAuto::length(0.0),
378            left: tf::LengthPercentageAuto::length(0.0),
379        };
380    };
381    let (top, right, bottom, left) = e.resolve();
382    tf::Rect {
383        top: lp_to_lp_auto(Some(&top), ctx),
384        right: lp_to_lp_auto(Some(&right), ctx),
385        bottom: lp_to_lp_auto(Some(&bottom), ctx),
386        left: lp_to_lp_auto(Some(&left), ctx),
387    }
388}
389
390fn border_widths(
391    b: Option<&super::style::BorderEdges>,
392    ctx: &ConversionContext,
393) -> tf::Rect<tf::LengthPercentage> {
394    let Some(b) = b else {
395        return tf::Rect {
396            top: tf::LengthPercentage::length(0.0),
397            right: tf::LengthPercentage::length(0.0),
398            bottom: tf::LengthPercentage::length(0.0),
399            left: tf::LengthPercentage::length(0.0),
400        };
401    };
402    let uniform = b.width.as_ref().map(|e| e.resolve());
403    let pick_side = |side: Option<&super::style::BorderSide>, idx: usize| -> tf::LengthPercentage {
404        if let Some(side) = side {
405            if let Some(w) = side.width.as_ref() {
406                let lp = LengthPercentage::Px(w.resolve(&ctx.length));
407                return lp_to_lp(&lp, ctx);
408            }
409        }
410        if let Some((t, r, btm, l)) = uniform.as_ref() {
411            let pick = match idx {
412                0 => t,
413                1 => r,
414                2 => btm,
415                3 => l,
416                _ => t,
417            };
418            return lp_to_lp(pick, ctx);
419        }
420        tf::LengthPercentage::length(0.0)
421    };
422    tf::Rect {
423        top: pick_side(b.top.as_ref(), 0),
424        right: pick_side(b.right.as_ref(), 1),
425        bottom: pick_side(b.bottom.as_ref(), 2),
426        left: pick_side(b.left.as_ref(), 3),
427    }
428}
429
430fn grid_track_sizing(t: &GridTrack, ctx: &ConversionContext) -> tf::TrackSizingFunction {
431    match t {
432        GridTrack::Fr(n) => tf::flex(*n),
433        GridTrack::Keyword(k) => grid_keyword_sizing(*k),
434        GridTrack::Length(lp) => grid_length_sizing(lp, ctx),
435        GridTrack::Minmax { min, max } => {
436            tf::minmax(grid_track_min(min, ctx), grid_track_max(max, ctx))
437        }
438    }
439}
440
441fn grid_keyword_sizing(k: GridTrackKeyword) -> tf::TrackSizingFunction {
442    match k {
443        GridTrackKeyword::Auto => tf::auto(),
444        GridTrackKeyword::MinContent => tf::min_content(),
445        GridTrackKeyword::MaxContent => tf::max_content(),
446    }
447}
448
449fn grid_length_sizing(lp: &LengthPercentage, ctx: &ConversionContext) -> tf::TrackSizingFunction {
450    match lp.parse() {
451        ParsedLength::Fr(n) => tf::flex(n),
452        ParsedLength::Auto => tf::auto(),
453        ParsedLength::Px(p) => tf::length(p),
454        ParsedLength::Percent(p) => tf::percent(p / 100.0),
455        ParsedLength::Em(em) => tf::length(em * ctx.length.font_size),
456        ParsedLength::Rem(r) => tf::length(r * ctx.length.root_font_size),
457        ParsedLength::Vw(p) => tf::length(p / 100.0 * ctx.length.viewport_width),
458        ParsedLength::Vh(p) => tf::length(p / 100.0 * ctx.length.viewport_height),
459    }
460}
461
462fn grid_track_min(t: &GridTrack, ctx: &ConversionContext) -> tf::MinTrackSizingFunction {
463    match t {
464        GridTrack::Fr(_) => tf::auto(),
465        GridTrack::Keyword(GridTrackKeyword::Auto) => tf::auto(),
466        GridTrack::Keyword(GridTrackKeyword::MinContent) => tf::min_content(),
467        GridTrack::Keyword(GridTrackKeyword::MaxContent) => tf::max_content(),
468        GridTrack::Length(lp) => grid_length_min(lp, ctx),
469        GridTrack::Minmax { min, .. } => grid_track_min(min, ctx),
470    }
471}
472
473fn grid_length_min(lp: &LengthPercentage, ctx: &ConversionContext) -> tf::MinTrackSizingFunction {
474    match lp.parse() {
475        ParsedLength::Fr(_) | ParsedLength::Auto => tf::auto(),
476        ParsedLength::Px(p) => tf::length(p),
477        ParsedLength::Percent(p) => tf::percent(p / 100.0),
478        ParsedLength::Em(em) => tf::length(em * ctx.length.font_size),
479        ParsedLength::Rem(r) => tf::length(r * ctx.length.root_font_size),
480        ParsedLength::Vw(p) => tf::length(p / 100.0 * ctx.length.viewport_width),
481        ParsedLength::Vh(p) => tf::length(p / 100.0 * ctx.length.viewport_height),
482    }
483}
484
485fn grid_track_max(t: &GridTrack, ctx: &ConversionContext) -> tf::MaxTrackSizingFunction {
486    match t {
487        GridTrack::Fr(n) => tf::fr(*n),
488        GridTrack::Keyword(GridTrackKeyword::Auto) => tf::auto(),
489        GridTrack::Keyword(GridTrackKeyword::MinContent) => tf::min_content(),
490        GridTrack::Keyword(GridTrackKeyword::MaxContent) => tf::max_content(),
491        GridTrack::Length(lp) => grid_length_max(lp, ctx),
492        GridTrack::Minmax { max, .. } => grid_track_max(max, ctx),
493    }
494}
495
496fn grid_length_max(lp: &LengthPercentage, ctx: &ConversionContext) -> tf::MaxTrackSizingFunction {
497    match lp.parse() {
498        ParsedLength::Fr(n) => tf::fr(n),
499        ParsedLength::Auto => tf::auto(),
500        ParsedLength::Px(p) => tf::length(p),
501        ParsedLength::Percent(p) => tf::percent(p / 100.0),
502        ParsedLength::Em(em) => tf::length(em * ctx.length.font_size),
503        ParsedLength::Rem(r) => tf::length(r * ctx.length.root_font_size),
504        ParsedLength::Vw(p) => tf::length(p / 100.0 * ctx.length.viewport_width),
505        ParsedLength::Vh(p) => tf::length(p / 100.0 * ctx.length.viewport_height),
506    }
507}
508
509fn grid_placement_line(g: &GridLine) -> tf::Line<tf::GridPlacement> {
510    let start = g.start.map(|GridLineEnd::Index(i)| i as i16);
511    let end = g.end.map(|GridLineEnd::Index(i)| i as i16);
512    match (start, end, g.span) {
513        (Some(s), Some(e), _) => tf::Line {
514            start: tf::line(s),
515            end: tf::line(e),
516        },
517        (Some(s), None, Some(n)) => tf::Line {
518            start: tf::line(s),
519            end: tf::span(n),
520        },
521        (Some(s), None, None) => tf::Line {
522            start: tf::line(s),
523            end: tf::auto(),
524        },
525        (None, Some(e), Some(n)) => tf::Line {
526            start: tf::span(n),
527            end: tf::line(e),
528        },
529        (None, Some(e), None) => tf::Line {
530            start: tf::auto(),
531            end: tf::line(e),
532        },
533        (None, None, Some(n)) => tf::Line {
534            start: tf::span(n),
535            end: tf::auto(),
536        },
537        (None, None, None) => tf::Line::auto(),
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544    use crate::css::style::*;
545
546    fn ctx() -> ConversionContext {
547        ConversionContext::default()
548    }
549
550    #[test]
551    fn empty_style_yields_taffy_default() {
552        let css = CssStyle::default();
553        let s = to_taffy_style(&css, &ctx());
554        assert_eq!(s.display, tf::Display::Block);
555        assert_eq!(s.flex_grow, 0.0);
556    }
557
558    #[test]
559    fn flex_column_with_gap() {
560        let css = CssStyle {
561            display: Some(Display::Flex),
562            flex_direction: Some(FlexDirection::Column),
563            gap: Some(Gap::Uniform(LengthPercentage::Px(16.0))),
564            align_items: Some(AlignItems::Center),
565            ..Default::default()
566        };
567        let s = to_taffy_style(&css, &ctx());
568        assert_eq!(s.display, tf::Display::Flex);
569        assert_eq!(s.flex_direction, tf::FlexDirection::Column);
570        assert_eq!(s.align_items, Some(tf::AlignItems::Center));
571        assert_eq!(s.gap.height, tf::LengthPercentage::length(16.0));
572    }
573
574    #[test]
575    fn flex_display_without_explicit_direction_defaults_to_column() {
576        let css = CssStyle {
577            display: Some(Display::Flex),
578            ..Default::default()
579        };
580        let s = to_taffy_style(&css, &ctx());
581        assert_eq!(
582            s.flex_direction,
583            tf::FlexDirection::Column,
584            "a `display: flex` container with no `flex-direction` must default to \
585             Column, matching SKILL.md's documented default and the scene root's \
586             behavior — taffy's own default (Row) must not leak through"
587        );
588    }
589
590    #[test]
591    fn padding_uniform_resolved() {
592        let css = CssStyle {
593            padding: Some(Edges::Uniform(LengthPercentage::Px(24.0))),
594            ..Default::default()
595        };
596        let s = to_taffy_style(&css, &ctx());
597        assert_eq!(s.padding.top, tf::LengthPercentage::length(24.0));
598        assert_eq!(s.padding.left, tf::LengthPercentage::length(24.0));
599    }
600
601    #[test]
602    fn width_percent() {
603        let css = CssStyle {
604            width: Some(Size::Length(LengthPercentage::String("50%".into()))),
605            ..Default::default()
606        };
607        let s = to_taffy_style(&css, &ctx());
608        assert_eq!(s.size.width, tf::Dimension::percent(0.5));
609    }
610
611    #[test]
612    fn position_absolute_inset() {
613        let css = CssStyle {
614            position: Some(Position::Absolute),
615            top: Some(LengthPercentage::Px(10.0)),
616            left: Some(LengthPercentage::Px(20.0)),
617            ..Default::default()
618        };
619        let s = to_taffy_style(&css, &ctx());
620        assert_eq!(s.position, tf::Position::Absolute);
621        assert_eq!(s.inset.top, tf::LengthPercentageAuto::length(10.0));
622        assert_eq!(s.inset.left, tf::LengthPercentageAuto::length(20.0));
623    }
624
625    #[test]
626    fn flex_grow_shrink_basis() {
627        let css = CssStyle {
628            flex_grow: Some(2.0),
629            flex_shrink: Some(0.5),
630            flex_basis: Some(Size::Length(LengthPercentage::Px(100.0))),
631            ..Default::default()
632        };
633        let s = to_taffy_style(&css, &ctx());
634        assert_eq!(s.flex_grow, 2.0);
635        assert_eq!(s.flex_shrink, 0.5);
636        assert_eq!(s.flex_basis, tf::Dimension::length(100.0));
637    }
638
639    #[test]
640    fn overflow_hidden() {
641        let css = CssStyle {
642            overflow: Some(Overflow::Hidden),
643            ..Default::default()
644        };
645        let s = to_taffy_style(&css, &ctx());
646        assert_eq!(s.overflow.x, taffy::Overflow::Hidden);
647        assert_eq!(s.overflow.y, taffy::Overflow::Hidden);
648    }
649
650    #[test]
651    fn grid_display_translated() {
652        let css = CssStyle {
653            display: Some(Display::Grid),
654            ..Default::default()
655        };
656        let s = to_taffy_style(&css, &ctx());
657        assert_eq!(s.display, tf::Display::Grid);
658    }
659
660    #[test]
661    fn grid_template_columns_fr_tracks_translated() {
662        let css = CssStyle {
663            grid_template_columns: Some(vec![GridTrack::Fr(1.0), GridTrack::Fr(2.0)]),
664            ..Default::default()
665        };
666        let s = to_taffy_style(&css, &ctx());
667        assert_eq!(s.grid_template_columns.len(), 2);
668    }
669
670    #[test]
671    fn grid_template_rows_string_fr_tracks_translated() {
672        let css = CssStyle {
673            grid_template_rows: Some(vec![
674                GridTrack::Length(LengthPercentage::String("1fr".into())),
675                GridTrack::Length(LengthPercentage::String("1fr".into())),
676            ]),
677            ..Default::default()
678        };
679        let s = to_taffy_style(&css, &ctx());
680        assert_eq!(s.grid_template_rows.len(), 2);
681    }
682
683    #[test]
684    fn grid_auto_flow_column_translated() {
685        let css = CssStyle {
686            grid_auto_flow: Some(GridAutoFlow::Column),
687            ..Default::default()
688        };
689        let s = to_taffy_style(&css, &ctx());
690        assert_eq!(s.grid_auto_flow, tf::GridAutoFlow::Column);
691    }
692
693    #[test]
694    fn grid_column_start_and_span_translated() {
695        let css = CssStyle {
696            grid_column: Some(GridLine {
697                start: Some(GridLineEnd::Index(2)),
698                span: Some(3),
699                ..Default::default()
700            }),
701            ..Default::default()
702        };
703        let s = to_taffy_style(&css, &ctx());
704        assert!(matches!(s.grid_column.start, tf::GridPlacement::Line(_)));
705        assert!(matches!(s.grid_column.end, tf::GridPlacement::Span(3)));
706    }
707
708    #[test]
709    fn grid_row_start_end_translated() {
710        let css = CssStyle {
711            grid_row: Some(GridLine {
712                start: Some(GridLineEnd::Index(1)),
713                end: Some(GridLineEnd::Index(3)),
714                ..Default::default()
715            }),
716            ..Default::default()
717        };
718        let s = to_taffy_style(&css, &ctx());
719        assert!(matches!(s.grid_row.start, tf::GridPlacement::Line(_)));
720        assert!(matches!(s.grid_row.end, tf::GridPlacement::Line(_)));
721    }
722
723    #[test]
724    fn grid_gap_applies_to_both_axes() {
725        let css = CssStyle {
726            display: Some(Display::Grid),
727            gap: Some(Gap::Uniform(LengthPercentage::Px(24.0))),
728            ..Default::default()
729        };
730        let s = to_taffy_style(&css, &ctx());
731        assert_eq!(s.gap.width, tf::LengthPercentage::length(24.0));
732        assert_eq!(s.gap.height, tf::LengthPercentage::length(24.0));
733    }
734
735    #[test]
736    fn grid_three_fr_columns_produce_distinct_x_offsets() {
737        let root_css = CssStyle {
738            display: Some(Display::Grid),
739            width: Some(Size::Length(LengthPercentage::Px(900.0))),
740            height: Some(Size::Length(LengthPercentage::Px(300.0))),
741            grid_template_columns: Some(vec![
742                GridTrack::Fr(1.0),
743                GridTrack::Fr(1.0),
744                GridTrack::Fr(1.0),
745            ]),
746            ..Default::default()
747        };
748        let root_style = to_taffy_style(&root_css, &ctx());
749        let leaf_style = to_taffy_style(&CssStyle::default(), &ctx());
750
751        let mut tree: tf::TaffyTree = tf::TaffyTree::new();
752        let c1 = tree.new_leaf(leaf_style.clone()).unwrap();
753        let c2 = tree.new_leaf(leaf_style.clone()).unwrap();
754        let c3 = tree.new_leaf(leaf_style).unwrap();
755        let root = tree.new_with_children(root_style, &[c1, c2, c3]).unwrap();
756
757        tree.compute_layout(
758            root,
759            tf::Size {
760                width: tf::AvailableSpace::Definite(900.0),
761                height: tf::AvailableSpace::Definite(300.0),
762            },
763        )
764        .unwrap();
765
766        let x1 = tree.layout(c1).unwrap().location.x;
767        let x2 = tree.layout(c2).unwrap().location.x;
768        let x3 = tree.layout(c3).unwrap().location.x;
769        let w1 = tree.layout(c1).unwrap().size.width;
770
771        assert_ne!(x1, x2, "columns 1 and 2 must not share an x-offset");
772        assert_ne!(x2, x3, "columns 2 and 3 must not share an x-offset");
773        assert!(
774            (x1 - 0.0).abs() < 0.5,
775            "column 1 should start at x=0, got {x1}"
776        );
777        assert!(
778            (x2 - 300.0).abs() < 1.0,
779            "column 2 should start at ~300px, got {x2}"
780        );
781        assert!(
782            (x3 - 600.0).abs() < 1.0,
783            "column 3 should start at ~600px, got {x3}"
784        );
785        assert!(
786            (w1 - 300.0).abs() < 1.0,
787            "each 1fr column should be ~300px wide, got {w1}"
788        );
789    }
790
791    #[test]
792    fn box_sizing_content_box_is_translated() {
793        let css = CssStyle {
794            box_sizing: Some(BoxSizing::ContentBox),
795            ..Default::default()
796        };
797        let s = to_taffy_style(&css, &ctx());
798        assert_eq!(s.box_sizing, tf::BoxSizing::ContentBox);
799    }
800
801    #[test]
802    fn box_sizing_defaults_to_border_box() {
803        let css = CssStyle::default();
804        let s = to_taffy_style(&css, &ctx());
805        assert_eq!(s.box_sizing, tf::BoxSizing::BorderBox);
806    }
807
808    #[test]
809    fn justify_items_is_translated_for_grid_children() {
810        let css = CssStyle {
811            display: Some(Display::Grid),
812            justify_items: Some(JustifyItems::Center),
813            ..Default::default()
814        };
815        let s = to_taffy_style(&css, &ctx());
816        assert_eq!(s.justify_items, Some(tf::AlignItems::Center));
817    }
818
819    #[test]
820    fn justify_self_is_translated() {
821        let css = CssStyle {
822            justify_self: Some(JustifySelf::End),
823            ..Default::default()
824        };
825        let s = to_taffy_style(&css, &ctx());
826        assert_eq!(s.justify_self, Some(tf::AlignSelf::End));
827    }
828
829    #[test]
830    fn justify_self_auto_falls_back_to_parent_justify_items() {
831        let css = CssStyle {
832            justify_self: Some(JustifySelf::Auto),
833            ..Default::default()
834        };
835        let s = to_taffy_style(&css, &ctx());
836        assert_eq!(s.justify_self, None);
837    }
838}