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}