1use taffy::{
2 Dimension, Display, FlexDirection, FlexWrap, GridPlacement, LengthPercentage,
3 LengthPercentageAuto, Style,
4};
5
6pub use taffy::{AlignItems, AvailableSpace, JustifyContent};
7
8use crate::direction::Direction;
9use crate::track::TemplateTrack;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum SizeDimension {
13 Px(f32),
14 Percent(f32),
15 Auto,
16}
17
18impl From<f32> for SizeDimension {
19 fn from(px: f32) -> Self {
20 SizeDimension::Px(px)
21 }
22}
23
24impl From<SizeDimension> for Dimension {
25 fn from(d: SizeDimension) -> Self {
26 match d {
27 SizeDimension::Px(v) => Dimension::length(v),
28 SizeDimension::Percent(v) => Dimension::percent(v),
29 SizeDimension::Auto => Dimension::auto(),
30 }
31 }
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
38pub(crate) struct LogicalStyle {
39 pub(crate) padding_start: Option<f32>,
40 pub(crate) padding_end: Option<f32>,
41 pub(crate) margin_start: Option<f32>,
42 pub(crate) margin_end: Option<f32>,
43 pub(crate) inset_start: Option<f32>,
44 pub(crate) inset_end: Option<f32>,
45 pub(crate) row_follows_direction: bool,
48 pub(crate) row_forced: bool,
50 pub(crate) hidden: bool,
52 pub(crate) min_height_override: Option<f32>,
54 pub(crate) leading_margin: Option<(bool, f32)>,
56}
57
58impl LogicalStyle {
59 pub(crate) fn has_edges(&self) -> bool {
62 self.padding_start.is_some()
63 || self.padding_end.is_some()
64 || self.margin_start.is_some()
65 || self.margin_end.is_some()
66 || self.inset_start.is_some()
67 || self.inset_end.is_some()
68 }
69
70 pub(crate) fn needs_tracking(&self) -> bool {
72 self.has_edges()
73 || self.row_forced
74 || self.hidden
75 || self.min_height_override.is_some()
76 || self.leading_margin.is_some()
77 }
78}
79
80#[derive(Debug, Clone, Copy, Default, PartialEq)]
85pub struct Margin {
86 pub block_start: f32,
87 pub block_end: f32,
88 pub inline_start: f32,
89 pub inline_end: f32,
90}
91
92impl Margin {
93 pub fn all(px: f32) -> Self {
94 Self {
95 block_start: px,
96 block_end: px,
97 inline_start: px,
98 inline_end: px,
99 }
100 }
101
102 pub fn symmetric(block: f32, inline: f32) -> Self {
103 Self {
104 block_start: block,
105 block_end: block,
106 inline_start: inline,
107 inline_end: inline,
108 }
109 }
110}
111
112#[derive(Clone)]
113pub struct LayoutStyle {
114 pub(crate) inner: Style,
115 pub(crate) logical: LogicalStyle,
116}
117
118impl LayoutStyle {
119 pub fn new() -> Self {
128 Self {
129 inner: Style {
130 display: Display::Block,
131 ..Style::default()
132 },
133 logical: LogicalStyle::default(),
134 }
135 }
136
137 pub fn flex_row(mut self) -> Self {
141 self.inner.display = Display::Flex;
142 self.inner.flex_direction = FlexDirection::Row;
143 self.logical.row_follows_direction = true;
144 self
145 }
146
147 pub fn flex_row_reverse(mut self) -> Self {
150 self.inner.display = Display::Flex;
151 self.inner.flex_direction = FlexDirection::RowReverse;
152 self.logical.row_follows_direction = false;
153 self
154 }
155
156 pub fn flex_column(mut self) -> Self {
157 self.inner.display = Display::Flex;
158 self.inner.flex_direction = FlexDirection::Column;
159 self.logical.row_follows_direction = false;
160 self
161 }
162
163 pub fn flex_wrap(mut self) -> Self {
164 self.inner.flex_wrap = FlexWrap::Wrap;
165 self
166 }
167
168 pub fn display_none(mut self) -> Self {
170 self.logical.hidden = true;
171 self
172 }
173
174 pub fn absolute_fill(mut self) -> Self {
178 self.inner.position = taffy::Position::Absolute;
179 let zero = LengthPercentageAuto::length(0.0);
180 self.inner.inset = taffy::Rect {
181 left: zero,
182 right: zero,
183 top: zero,
184 bottom: zero,
185 };
186 self
187 }
188
189 pub fn absolute(mut self) -> Self {
194 self.inner.position = taffy::Position::Absolute;
195 self
196 }
197
198 pub fn inset_top(mut self, px: f32) -> Self {
201 self.inner.inset.top = LengthPercentageAuto::length(px);
202 self
203 }
204
205 pub fn inset_bottom(mut self, px: f32) -> Self {
207 self.inner.inset.bottom = LengthPercentageAuto::length(px);
208 self
209 }
210
211 pub fn width_px(&self) -> Option<f32> {
214 self.inner.size.width.into_option()
215 }
216
217 pub fn is_width_auto(&self) -> bool {
219 self.inner.size.width.is_auto()
220 }
221
222 pub fn width(mut self, dim: impl Into<SizeDimension>) -> Self {
223 self.inner.size.width = dim.into().into();
224 self
225 }
226
227 pub fn height_px(&self) -> Option<f32> {
229 self.inner.size.height.into_option()
230 }
231
232 pub fn is_height_auto(&self) -> bool {
234 self.inner.size.height.is_auto()
235 }
236
237 pub fn height(mut self, dim: impl Into<SizeDimension>) -> Self {
238 self.inner.size.height = dim.into().into();
239 self
240 }
241
242 pub fn min_width(mut self, dim: impl Into<SizeDimension>) -> Self {
243 self.inner.min_size.width = dim.into().into();
244 self
245 }
246
247 pub fn min_height(mut self, dim: impl Into<SizeDimension>) -> Self {
248 self.inner.min_size.height = dim.into().into();
249 self
250 }
251
252 pub fn max_width_px(&self) -> Option<f32> {
255 self.inner.max_size.width.into_option()
256 }
257
258 pub fn max_width(mut self, dim: impl Into<SizeDimension>) -> Self {
259 self.inner.max_size.width = dim.into().into();
260 self
261 }
262
263 pub fn max_height(mut self, dim: impl Into<SizeDimension>) -> Self {
264 self.inner.max_size.height = dim.into().into();
265 self
266 }
267
268 pub fn flex_grow(mut self, grow: f32) -> Self {
269 self.inner.flex_grow = grow;
270 self
271 }
272
273 pub fn flex_shrink(mut self, shrink: f32) -> Self {
274 self.inner.flex_shrink = shrink;
275 self
276 }
277
278 pub fn flex_basis(mut self, dim: impl Into<SizeDimension>) -> Self {
279 self.inner.flex_basis = dim.into().into();
280 self
281 }
282
283 pub fn padding_all(mut self, px: f32) -> Self {
284 let value = LengthPercentage::length(px);
285 self.inner.padding = taffy::geometry::Rect {
286 left: value,
287 right: value,
288 top: value,
289 bottom: value,
290 };
291 self
292 }
293
294 pub fn padding_horizontal(mut self, px: f32) -> Self {
295 self.inner.padding.left = LengthPercentage::length(px);
296 self.inner.padding.right = LengthPercentage::length(px);
297 self
298 }
299
300 pub fn padding_vertical(mut self, px: f32) -> Self {
301 self.inner.padding.top = LengthPercentage::length(px);
302 self.inner.padding.bottom = LengthPercentage::length(px);
303 self
304 }
305
306 pub fn padding_top(mut self, px: f32) -> Self {
307 self.inner.padding.top = LengthPercentage::length(px);
308 self
309 }
310
311 pub fn padding_bottom(mut self, px: f32) -> Self {
312 self.inner.padding.bottom = LengthPercentage::length(px);
313 self
314 }
315
316 pub fn padding_left(mut self, px: f32) -> Self {
317 self.inner.padding.left = LengthPercentage::length(px);
318 self
319 }
320
321 pub fn padding_right(mut self, px: f32) -> Self {
322 self.inner.padding.right = LengthPercentage::length(px);
323 self
324 }
325
326 pub fn padding_start(mut self, px: f32) -> Self {
329 self.logical.padding_start = Some(px);
330 self
331 }
332
333 pub fn padding_end(mut self, px: f32) -> Self {
336 self.logical.padding_end = Some(px);
337 self
338 }
339
340 pub fn margin(self, m: Margin) -> Self {
342 self.margin_block_start(m.block_start)
343 .margin_block_end(m.block_end)
344 .margin_inline_start(m.inline_start)
345 .margin_inline_end(m.inline_end)
346 }
347
348 pub fn margin_block_start(mut self, px: f32) -> Self {
350 self.inner.margin.top = LengthPercentageAuto::length(px);
351 self
352 }
353
354 pub fn margin_block_end(mut self, px: f32) -> Self {
356 self.inner.margin.bottom = LengthPercentageAuto::length(px);
357 self
358 }
359
360 pub fn margin_inline_start(mut self, px: f32) -> Self {
363 self.logical.margin_start = Some(px);
364 self
365 }
366
367 pub fn margin_inline_end(mut self, px: f32) -> Self {
370 self.logical.margin_end = Some(px);
371 self
372 }
373
374 pub fn margin_from_left(mut self, px: f32) -> Self {
381 self.inner.margin.left = LengthPercentageAuto::length(px);
382 self
383 }
384
385 pub fn inset_start(mut self, px: f32) -> Self {
388 self.logical.inset_start = Some(px);
389 self
390 }
391
392 pub fn inset_end(mut self, px: f32) -> Self {
394 self.logical.inset_end = Some(px);
395 self
396 }
397
398 pub fn gap(mut self, px: f32) -> Self {
399 self.inner.gap = taffy::geometry::Size {
400 width: LengthPercentage::length(px),
401 height: LengthPercentage::length(px),
402 };
403 self
404 }
405
406 pub fn gap_x(mut self, px: f32) -> Self {
407 self.inner.gap.width = LengthPercentage::length(px);
408 self
409 }
410
411 pub fn gap_y(mut self, px: f32) -> Self {
412 self.inner.gap.height = LengthPercentage::length(px);
413 self
414 }
415
416 pub fn align_items(mut self, value: AlignItems) -> Self {
417 self.inner.align_items = Some(value);
418 self
419 }
420
421 pub fn align_self_stretch(mut self) -> Self {
422 self.inner.align_self = Some(taffy::AlignSelf::STRETCH);
423 self
424 }
425
426 pub fn align_self_center(mut self) -> Self {
429 self.inner.align_self = Some(taffy::AlignSelf::CENTER);
430 self
431 }
432
433 pub fn align_self_start(mut self) -> Self {
435 self.inner.align_self = Some(taffy::AlignSelf::FLEX_START);
436 self
437 }
438
439 pub fn align_self_end(mut self) -> Self {
441 self.inner.align_self = Some(taffy::AlignSelf::FLEX_END);
442 self
443 }
444
445 pub fn justify_content(mut self, value: JustifyContent) -> Self {
446 self.inner.justify_content = Some(value);
447 self
448 }
449
450 pub fn display_grid(mut self) -> Self {
451 self.inner.display = Display::Grid;
452 self
453 }
454
455 pub fn grid_template_columns(mut self, tracks: Vec<TemplateTrack>) -> Self {
456 self.inner.grid_template_columns = tracks
457 .into_iter()
458 .map(|t| t.into_template_component())
459 .collect();
460 self
461 }
462
463 pub fn grid_column_span(mut self, count: u16) -> Self {
464 self.inner.grid_column = taffy::geometry::Line {
465 start: GridPlacement::Span(count),
466 end: GridPlacement::Auto,
467 };
468 self
469 }
470
471 pub fn grid_row_span(mut self, count: u16) -> Self {
472 self.inner.grid_row = taffy::geometry::Line {
473 start: GridPlacement::Span(count),
474 end: GridPlacement::Auto,
475 };
476 self
477 }
478
479 pub fn aspect_ratio(mut self, ratio: f32) -> Self {
480 self.inner.aspect_ratio = Some(ratio);
481 self
482 }
483
484 pub(crate) fn resolve(&self, direction: Direction) -> Style {
489 let mut style = self.inner.clone();
490 let logical = &self.logical;
491 if logical.row_follows_direction || logical.row_forced {
492 style.flex_direction = if direction.is_rtl() {
493 FlexDirection::RowReverse
494 } else {
495 FlexDirection::Row
496 };
497 }
498 if logical.hidden {
499 style.display = Display::None;
500 }
501 if let Some(min_height) = logical.min_height_override {
502 style.min_size.height = Dimension::length(min_height);
503 }
504 let (start, end) = if direction.is_rtl() {
505 (Edge::Right, Edge::Left)
506 } else {
507 (Edge::Left, Edge::Right)
508 };
509 for (edge, px) in [(start, logical.padding_start), (end, logical.padding_end)] {
510 if let Some(px) = px {
511 *edge.of_mut(&mut style.padding) = LengthPercentage::length(px);
512 }
513 }
514 for (edge, px) in [(start, logical.margin_start), (end, logical.margin_end)] {
515 if let Some(px) = px {
516 *edge.of_mut(&mut style.margin) = LengthPercentageAuto::length(px);
517 }
518 }
519 for (edge, px) in [(start, logical.inset_start), (end, logical.inset_end)] {
520 if let Some(px) = px {
521 *edge.of_mut(&mut style.inset) = LengthPercentageAuto::length(px);
522 }
523 }
524 style
525 }
526}
527
528#[derive(Clone, Copy)]
529enum Edge {
530 Left,
531 Right,
532}
533
534impl Edge {
535 fn of_mut<T>(self, rect: &mut taffy::geometry::Rect<T>) -> &mut T {
536 match self {
537 Edge::Left => &mut rect.left,
538 Edge::Right => &mut rect.right,
539 }
540 }
541}
542
543impl Default for LayoutStyle {
544 fn default() -> Self {
545 Self::new()
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn logical_padding_resolves_to_the_edge_the_direction_starts_from() {
555 let style = LayoutStyle::new().padding_start(8.0).padding_end(2.0);
556 let ltr = style.resolve(Direction::Ltr);
557 assert_eq!(ltr.padding.left, LengthPercentage::length(8.0));
558 assert_eq!(ltr.padding.right, LengthPercentage::length(2.0));
559 let rtl = style.resolve(Direction::Rtl);
560 assert_eq!(rtl.padding.right, LengthPercentage::length(8.0));
561 assert_eq!(rtl.padding.left, LengthPercentage::length(2.0));
562 }
563
564 #[test]
565 fn a_physical_edge_is_left_alone_by_the_direction() {
566 let style = LayoutStyle::new().padding_left(12.0);
567 for direction in [Direction::Ltr, Direction::Rtl] {
568 let resolved = style.resolve(direction);
569 assert_eq!(resolved.padding.left, LengthPercentage::length(12.0));
570 assert_eq!(resolved.padding.right, LengthPercentage::length(0.0));
571 }
572 }
573
574 #[test]
575 fn resolving_twice_does_not_accumulate() {
576 let style = LayoutStyle::new().padding_start(8.0);
578 let _ = style.resolve(Direction::Rtl);
579 let back = style.resolve(Direction::Ltr);
580 assert_eq!(back.padding.left, LengthPercentage::length(8.0));
581 assert_eq!(back.padding.right, LengthPercentage::length(0.0));
582 }
583
584 #[test]
585 fn a_row_reverses_under_rtl_but_an_explicit_reverse_does_not_flip_back() {
586 let row = LayoutStyle::new().flex_row();
587 assert_eq!(
588 row.resolve(Direction::Ltr).flex_direction,
589 FlexDirection::Row
590 );
591 assert_eq!(
592 row.resolve(Direction::Rtl).flex_direction,
593 FlexDirection::RowReverse
594 );
595 let reversed = LayoutStyle::new().flex_row_reverse();
596 for direction in [Direction::Ltr, Direction::Rtl] {
597 assert_eq!(
598 reversed.resolve(direction).flex_direction,
599 FlexDirection::RowReverse,
600 "an explicit reverse is physical"
601 );
602 }
603 }
604
605 #[test]
606 fn a_column_is_unaffected_by_direction() {
607 let col = LayoutStyle::new().flex_column();
608 for direction in [Direction::Ltr, Direction::Rtl] {
609 assert_eq!(col.resolve(direction).flex_direction, FlexDirection::Column);
610 }
611 }
612
613 #[test]
614 fn only_logical_edges_need_the_style_kept_for_a_flip() {
615 assert!(!LayoutStyle::new().flex_row().logical.has_edges());
616 assert!(
617 LayoutStyle::new()
618 .margin_inline_start(4.0)
619 .logical
620 .has_edges()
621 );
622 assert!(LayoutStyle::new().inset_end(4.0).logical.has_edges());
623 }
624
625 #[test]
626 fn style_default_is_block() {
627 let style = LayoutStyle::new();
628 assert_eq!(style.inner.display, Display::Block);
629 }
630
631 #[test]
632 fn style_width_sets_dimension() {
633 let style = LayoutStyle::new().width(120.0);
634 assert_eq!(style.inner.size.width, Dimension::length(120.0));
635 }
636
637 #[test]
638 fn style_width_px_reads_back_length() {
639 let style = LayoutStyle::new().width(120.0);
640 assert_eq!(style.width_px(), Some(120.0));
641 assert!(!style.is_width_auto());
642 }
643
644 #[test]
645 fn style_width_px_none_for_percent_or_default() {
646 assert_eq!(LayoutStyle::new().width_px(), None);
647 assert!(LayoutStyle::new().is_width_auto());
648 let percent = LayoutStyle::new().width(SizeDimension::Percent(0.5));
649 assert_eq!(percent.width_px(), None);
650 assert!(!percent.is_width_auto());
651 }
652
653 #[test]
654 fn style_width_percent_sets_dimension() {
655 let style = LayoutStyle::new().width(SizeDimension::Percent(0.5));
656 assert_eq!(style.inner.size.width, Dimension::percent(0.5));
657 }
658
659 #[test]
660 fn style_height_sets_dimension() {
661 let style = LayoutStyle::new().height(80.0);
662 assert_eq!(style.inner.size.height, Dimension::length(80.0));
663 }
664
665 #[test]
666 fn style_max_width_sets_dimension() {
667 let style = LayoutStyle::new().max_width(200.0);
668 assert_eq!(style.inner.max_size.width, Dimension::length(200.0));
669 }
670
671 #[test]
672 fn style_max_height_sets_dimension() {
673 let style = LayoutStyle::new().max_height(150.0);
674 assert_eq!(style.inner.max_size.height, Dimension::length(150.0));
675 }
676
677 #[test]
678 fn style_flex_basis_percent_sets_dimension() {
679 let style = LayoutStyle::new().flex_basis(SizeDimension::Percent(0.5));
680 assert_eq!(style.inner.flex_basis, Dimension::percent(0.5));
681 }
682
683 #[test]
684 fn style_flex_row_sets_direction() {
685 let style = LayoutStyle::new().flex_row();
686 assert_eq!(style.inner.flex_direction, FlexDirection::Row);
687 }
688
689 #[test]
690 fn style_flex_column_sets_direction() {
691 let style = LayoutStyle::new().flex_column();
692 assert_eq!(style.inner.flex_direction, FlexDirection::Column);
693 }
694
695 #[test]
696 fn style_align_items_center_sets_field() {
697 let style = LayoutStyle::new().align_items(AlignItems::CENTER);
698 assert_eq!(style.inner.align_items, Some(taffy::AlignItems::CENTER));
699 }
700
701 #[test]
702 fn style_justify_center_sets_field() {
703 let style = LayoutStyle::new().justify_content(JustifyContent::CENTER);
704 assert_eq!(
705 style.inner.justify_content,
706 Some(taffy::JustifyContent::CENTER)
707 );
708 }
709
710 #[test]
711 fn style_default_impl_matches_new() {
712 let style = LayoutStyle::default();
713 assert_eq!(style.inner.display, Display::Block);
714 }
715
716 #[test]
717 fn style_padding_horizontal_sets_left_right() {
718 let style = LayoutStyle::new().padding_horizontal(10.0);
719 assert_eq!(style.inner.padding.left, LengthPercentage::length(10.0));
720 assert_eq!(style.inner.padding.right, LengthPercentage::length(10.0));
721 }
722
723 #[test]
724 fn style_padding_vertical_sets_top_bottom() {
725 let style = LayoutStyle::new().padding_vertical(8.0);
726 assert_eq!(style.inner.padding.top, LengthPercentage::length(8.0));
727 assert_eq!(style.inner.padding.bottom, LengthPercentage::length(8.0));
728 }
729
730 #[test]
731 fn style_padding_top_sets_field() {
732 let style = LayoutStyle::new().padding_top(4.0);
733 assert_eq!(style.inner.padding.top, LengthPercentage::length(4.0));
734 }
735
736 #[test]
738 fn margin_writes_the_block_pair_directly_and_defers_the_inline_pair() {
739 let style = LayoutStyle::new().margin(Margin::symmetric(6.0, 12.0));
740 assert_eq!(style.inner.margin.top, LengthPercentageAuto::length(6.0));
741 assert_eq!(style.inner.margin.bottom, LengthPercentageAuto::length(6.0));
742 assert_eq!(style.logical.margin_start, Some(12.0));
743 assert_eq!(style.logical.margin_end, Some(12.0));
744 }
745
746 #[test]
748 fn a_margin_from_the_left_stays_left_under_rtl() {
749 let style = LayoutStyle::new().margin_from_left(20.0);
750 let ltr = style.resolve(Direction::Ltr);
751 let rtl = style.resolve(Direction::Rtl);
752 assert_eq!(ltr.margin.left, LengthPercentageAuto::length(20.0));
753 assert_eq!(rtl.margin.left, LengthPercentageAuto::length(20.0));
754 }
755}