Skip to main content

telar_layout_core/
style.rs

1use taffy::{
2    Dimension, Display, FlexDirection, FlexWrap, GridAutoFlow, 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/// The parts of a style that cannot be turned into physical edges until a [`Direction`] is known. Kept
35/// alongside the resolved `taffy::Style` rather than folded into it, so a direction flip can re-resolve the
36/// original intent instead of trying to un-swap edges it can no longer tell apart from physical ones.
37#[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    /// Set by [`LayoutStyle::flex_row`]: the main axis is the inline axis, so it reverses under RTL. An
46    /// explicit [`LayoutStyle::flex_row_reverse`] leaves this clear — it means "reversed" in either direction.
47    pub(crate) row_follows_direction: bool,
48}
49
50impl LogicalStyle {
51    /// Whether any edge needs re-resolving on a direction flip. A direction-following row alone does not: it
52    /// is a single flag the engine can toggle in place, without the original style to resolve against.
53    pub(crate) fn has_edges(&self) -> bool {
54        self.padding_start.is_some()
55            || self.padding_end.is_some()
56            || self.margin_start.is_some()
57            || self.margin_end.is_some()
58            || self.inset_start.is_some()
59            || self.inset_end.is_some()
60    }
61}
62
63#[derive(Clone)]
64pub struct LayoutStyle {
65    pub(crate) inner: Style,
66    pub(crate) logical: LogicalStyle,
67}
68
69impl LayoutStyle {
70    pub fn new() -> Self {
71        Self {
72            inner: Style {
73                display: Display::Block,
74                ..Style::default()
75            },
76            logical: LogicalStyle::default(),
77        }
78    }
79
80    /// A flex row along the inline axis: items run left-to-right under [`Direction::Ltr`] and right-to-left
81    /// under [`Direction::Rtl`], the way `flex-direction: row` follows `dir` on the web. Use
82    /// [`flex_row_reverse`](Self::flex_row_reverse) for a row that is reversed in both directions.
83    pub fn flex_row(mut self) -> Self {
84        self.inner.display = Display::Flex;
85        self.inner.flex_direction = FlexDirection::Row;
86        self.logical.row_follows_direction = true;
87        self
88    }
89
90    /// A flex row laid out against the writing direction, unconditionally. Unlike
91    /// [`flex_row`](Self::flex_row) this is a physical choice and does not flip with [`Direction`].
92    pub fn flex_row_reverse(mut self) -> Self {
93        self.inner.display = Display::Flex;
94        self.inner.flex_direction = FlexDirection::RowReverse;
95        self.logical.row_follows_direction = false;
96        self
97    }
98
99    pub fn flex_column(mut self) -> Self {
100        self.inner.display = Display::Flex;
101        self.inner.flex_direction = FlexDirection::Column;
102        self.logical.row_follows_direction = false;
103        self
104    }
105
106    pub fn flex_wrap(mut self) -> Self {
107        self.inner.flex_wrap = FlexWrap::Wrap;
108        self
109    }
110
111    /// Takes the node out of normal flow (`position: absolute`) with all four insets pinned to 0, so it
112    /// fills its containing block without affecting sibling layout — used by `overlay` to cover the
113    /// viewport. Combine with `flex_column`/alignment to position the overlay's content within the layer.
114    pub fn absolute_fill(mut self) -> Self {
115        self.inner.position = taffy::Position::Absolute;
116        let zero = LengthPercentageAuto::length(0.0);
117        self.inner.inset = taffy::Rect {
118            left: zero,
119            right: zero,
120            top: zero,
121            bottom: zero,
122        };
123        self
124    }
125
126    /// The node's `width` in pixels if it is a definite length, else `None` (e.g. percent or auto).
127    /// Lets widgets with an intrinsic size (e.g. `<svg>`/`<img>`) inspect a caller-supplied width before registering their layout leaf.
128    pub fn width_px(&self) -> Option<f32> {
129        self.inner.size.width.into_option()
130    }
131
132    /// True when `width` was left at its default, which taffy also treats as `auto`.
133    pub fn is_width_auto(&self) -> bool {
134        self.inner.size.width.is_auto()
135    }
136
137    pub fn width(mut self, dim: impl Into<SizeDimension>) -> Self {
138        self.inner.size.width = dim.into().into();
139        self
140    }
141
142    /// The node's `height` in pixels if it is a definite length, else `None` (e.g. percent or auto).
143    pub fn height_px(&self) -> Option<f32> {
144        self.inner.size.height.into_option()
145    }
146
147    /// True when `height` was left at its default, which taffy also treats as `auto`.
148    pub fn is_height_auto(&self) -> bool {
149        self.inner.size.height.is_auto()
150    }
151
152    pub fn height(mut self, dim: impl Into<SizeDimension>) -> Self {
153        self.inner.size.height = dim.into().into();
154        self
155    }
156
157    pub fn min_width(mut self, dim: impl Into<SizeDimension>) -> Self {
158        self.inner.min_size.width = dim.into().into();
159        self
160    }
161
162    pub fn min_height(mut self, dim: impl Into<SizeDimension>) -> Self {
163        self.inner.min_size.height = dim.into().into();
164        self
165    }
166
167    /// The node's `max-width` in pixels if it is a definite length, else `None`
168    /// (e.g. percent or unset). Used by the layout pass to pin a resolved width.
169    pub fn max_width_px(&self) -> Option<f32> {
170        self.inner.max_size.width.into_option()
171    }
172
173    pub fn max_width(mut self, dim: impl Into<SizeDimension>) -> Self {
174        self.inner.max_size.width = dim.into().into();
175        self
176    }
177
178    pub fn max_height(mut self, dim: impl Into<SizeDimension>) -> Self {
179        self.inner.max_size.height = dim.into().into();
180        self
181    }
182
183    pub fn flex_grow(mut self, grow: f32) -> Self {
184        self.inner.flex_grow = grow;
185        self
186    }
187
188    pub fn flex_shrink(mut self, shrink: f32) -> Self {
189        self.inner.flex_shrink = shrink;
190        self
191    }
192
193    pub fn flex_basis(mut self, dim: impl Into<SizeDimension>) -> Self {
194        self.inner.flex_basis = dim.into().into();
195        self
196    }
197
198    pub fn padding_all(mut self, px: f32) -> Self {
199        let value = LengthPercentage::length(px);
200        self.inner.padding = taffy::geometry::Rect {
201            left: value,
202            right: value,
203            top: value,
204            bottom: value,
205        };
206        self
207    }
208
209    pub fn padding_horizontal(mut self, px: f32) -> Self {
210        self.inner.padding.left = LengthPercentage::length(px);
211        self.inner.padding.right = LengthPercentage::length(px);
212        self
213    }
214
215    pub fn padding_vertical(mut self, px: f32) -> Self {
216        self.inner.padding.top = LengthPercentage::length(px);
217        self.inner.padding.bottom = LengthPercentage::length(px);
218        self
219    }
220
221    pub fn padding_top(mut self, px: f32) -> Self {
222        self.inner.padding.top = LengthPercentage::length(px);
223        self
224    }
225
226    pub fn padding_bottom(mut self, px: f32) -> Self {
227        self.inner.padding.bottom = LengthPercentage::length(px);
228        self
229    }
230
231    pub fn padding_left(mut self, px: f32) -> Self {
232        self.inner.padding.left = LengthPercentage::length(px);
233        self
234    }
235
236    pub fn padding_right(mut self, px: f32) -> Self {
237        self.inner.padding.right = LengthPercentage::length(px);
238        self
239    }
240
241    /// Padding on the edge the text starts from — `left` under [`Direction::Ltr`], `right` under
242    /// [`Direction::Rtl`].
243    pub fn padding_start(mut self, px: f32) -> Self {
244        self.logical.padding_start = Some(px);
245        self
246    }
247
248    /// Padding on the edge the text runs towards — `right` under [`Direction::Ltr`], `left` under
249    /// [`Direction::Rtl`].
250    pub fn padding_end(mut self, px: f32) -> Self {
251        self.logical.padding_end = Some(px);
252        self
253    }
254
255    pub fn margin_all(mut self, px: f32) -> Self {
256        let value = LengthPercentageAuto::length(px);
257        self.inner.margin = taffy::geometry::Rect {
258            left: value,
259            right: value,
260            top: value,
261            bottom: value,
262        };
263        self
264    }
265
266    pub fn margin_horizontal(mut self, px: f32) -> Self {
267        self.inner.margin.left = LengthPercentageAuto::length(px);
268        self.inner.margin.right = LengthPercentageAuto::length(px);
269        self
270    }
271
272    pub fn margin_vertical(mut self, px: f32) -> Self {
273        self.inner.margin.top = LengthPercentageAuto::length(px);
274        self.inner.margin.bottom = LengthPercentageAuto::length(px);
275        self
276    }
277
278    pub fn margin_top(mut self, px: f32) -> Self {
279        self.inner.margin.top = LengthPercentageAuto::length(px);
280        self
281    }
282
283    pub fn margin_bottom(mut self, px: f32) -> Self {
284        self.inner.margin.bottom = LengthPercentageAuto::length(px);
285        self
286    }
287
288    pub fn margin_left(mut self, px: f32) -> Self {
289        self.inner.margin.left = LengthPercentageAuto::length(px);
290        self
291    }
292
293    pub fn margin_right(mut self, px: f32) -> Self {
294        self.inner.margin.right = LengthPercentageAuto::length(px);
295        self
296    }
297
298    /// Margin on the edge the text starts from — `left` under [`Direction::Ltr`], `right` under
299    /// [`Direction::Rtl`].
300    pub fn margin_start(mut self, px: f32) -> Self {
301        self.logical.margin_start = Some(px);
302        self
303    }
304
305    /// Margin on the edge the text runs towards — `right` under [`Direction::Ltr`], `left` under
306    /// [`Direction::Rtl`].
307    pub fn margin_end(mut self, px: f32) -> Self {
308        self.logical.margin_end = Some(px);
309        self
310    }
311
312    /// Inset from the edge the text starts from, for a node already taken out of flow (see
313    /// [`absolute_fill`](Self::absolute_fill)); ignored on an in-flow node, as `inset` is in CSS.
314    pub fn inset_start(mut self, px: f32) -> Self {
315        self.logical.inset_start = Some(px);
316        self
317    }
318
319    /// Inset from the edge the text runs towards, for a node already taken out of flow.
320    pub fn inset_end(mut self, px: f32) -> Self {
321        self.logical.inset_end = Some(px);
322        self
323    }
324
325    pub fn gap(mut self, px: f32) -> Self {
326        self.inner.gap = taffy::geometry::Size {
327            width: LengthPercentage::length(px),
328            height: LengthPercentage::length(px),
329        };
330        self
331    }
332
333    pub fn gap_x(mut self, px: f32) -> Self {
334        self.inner.gap.width = LengthPercentage::length(px);
335        self
336    }
337
338    pub fn gap_y(mut self, px: f32) -> Self {
339        self.inner.gap.height = LengthPercentage::length(px);
340        self
341    }
342
343    pub fn align_items(mut self, value: AlignItems) -> Self {
344        self.inner.align_items = Some(value);
345        self
346    }
347
348    pub fn align_self_stretch(mut self) -> Self {
349        self.inner.align_self = Some(taffy::AlignSelf::STRETCH);
350        self
351    }
352
353    /// Overrides the parent's `align_items` for this child, centering it on the cross axis instead of
354    /// stretching — so a fixed-size child (e.g. a square icon chip) keeps its size and stays centered.
355    pub fn align_self_center(mut self) -> Self {
356        self.inner.align_self = Some(taffy::AlignSelf::CENTER);
357        self
358    }
359
360    /// Aligns this child to the start of the cross axis, overriding the parent's `align_items`.
361    pub fn align_self_start(mut self) -> Self {
362        self.inner.align_self = Some(taffy::AlignSelf::FLEX_START);
363        self
364    }
365
366    /// Aligns this child to the end of the cross axis, overriding the parent's `align_items`.
367    pub fn align_self_end(mut self) -> Self {
368        self.inner.align_self = Some(taffy::AlignSelf::FLEX_END);
369        self
370    }
371
372    pub fn justify_content(mut self, value: JustifyContent) -> Self {
373        self.inner.justify_content = Some(value);
374        self
375    }
376
377    pub fn display_grid(mut self) -> Self {
378        self.inner.display = Display::Grid;
379        self
380    }
381
382    pub fn grid_template_columns(mut self, tracks: Vec<TemplateTrack>) -> Self {
383        self.inner.grid_template_columns = tracks
384            .into_iter()
385            .map(|t| t.into_template_component())
386            .collect();
387        self
388    }
389
390    pub fn grid_template_rows(mut self, tracks: Vec<TemplateTrack>) -> Self {
391        self.inner.grid_template_rows = tracks
392            .into_iter()
393            .map(|t| t.into_template_component())
394            .collect();
395        self
396    }
397
398    pub fn grid_auto_flow_row(mut self) -> Self {
399        self.inner.grid_auto_flow = GridAutoFlow::Row;
400        self
401    }
402
403    pub fn grid_auto_flow_column(mut self) -> Self {
404        self.inner.grid_auto_flow = GridAutoFlow::Column;
405        self
406    }
407
408    pub fn grid_column(mut self, start: i16, end: i16) -> Self {
409        self.inner.grid_column = taffy::geometry::Line {
410            start: taffy::style_helpers::line(start),
411            end: taffy::style_helpers::line(end),
412        };
413        self
414    }
415
416    pub fn grid_row(mut self, start: i16, end: i16) -> Self {
417        self.inner.grid_row = taffy::geometry::Line {
418            start: taffy::style_helpers::line(start),
419            end: taffy::style_helpers::line(end),
420        };
421        self
422    }
423
424    pub fn grid_column_span(mut self, count: u16) -> Self {
425        self.inner.grid_column = taffy::geometry::Line {
426            start: GridPlacement::Span(count),
427            end: GridPlacement::Auto,
428        };
429        self
430    }
431
432    pub fn grid_row_span(mut self, count: u16) -> Self {
433        self.inner.grid_row = taffy::geometry::Line {
434            start: GridPlacement::Span(count),
435            end: GridPlacement::Auto,
436        };
437        self
438    }
439
440    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
441        self.inner.aspect_ratio = Some(ratio);
442        self
443    }
444
445    /// The physical `taffy::Style` this describes under `direction`. Called by the engine at every point a
446    /// style reaches a node, and again for each affected node when the direction flips.
447    pub(crate) fn resolve(&self, direction: Direction) -> Style {
448        let mut style = self.inner.clone();
449        let logical = &self.logical;
450        if logical.row_follows_direction && direction.is_rtl() {
451            style.flex_direction = FlexDirection::RowReverse;
452        }
453        let (start, end) = if direction.is_rtl() {
454            (Edge::Right, Edge::Left)
455        } else {
456            (Edge::Left, Edge::Right)
457        };
458        for (edge, px) in [(start, logical.padding_start), (end, logical.padding_end)] {
459            if let Some(px) = px {
460                *edge.of_mut(&mut style.padding) = LengthPercentage::length(px);
461            }
462        }
463        for (edge, px) in [(start, logical.margin_start), (end, logical.margin_end)] {
464            if let Some(px) = px {
465                *edge.of_mut(&mut style.margin) = LengthPercentageAuto::length(px);
466            }
467        }
468        for (edge, px) in [(start, logical.inset_start), (end, logical.inset_end)] {
469            if let Some(px) = px {
470                *edge.of_mut(&mut style.inset) = LengthPercentageAuto::length(px);
471            }
472        }
473        style
474    }
475}
476
477#[derive(Clone, Copy)]
478enum Edge {
479    Left,
480    Right,
481}
482
483impl Edge {
484    fn of_mut<T>(self, rect: &mut taffy::geometry::Rect<T>) -> &mut T {
485        match self {
486            Edge::Left => &mut rect.left,
487            Edge::Right => &mut rect.right,
488        }
489    }
490}
491
492impl Default for LayoutStyle {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn logical_padding_resolves_to_the_edge_the_direction_starts_from() {
504        let style = LayoutStyle::new().padding_start(8.0).padding_end(2.0);
505        let ltr = style.resolve(Direction::Ltr);
506        assert_eq!(ltr.padding.left, LengthPercentage::length(8.0));
507        assert_eq!(ltr.padding.right, LengthPercentage::length(2.0));
508        let rtl = style.resolve(Direction::Rtl);
509        assert_eq!(rtl.padding.right, LengthPercentage::length(8.0));
510        assert_eq!(rtl.padding.left, LengthPercentage::length(2.0));
511    }
512
513    #[test]
514    fn a_physical_edge_is_left_alone_by_the_direction() {
515        let style = LayoutStyle::new().padding_left(12.0);
516        for direction in [Direction::Ltr, Direction::Rtl] {
517            let resolved = style.resolve(direction);
518            assert_eq!(resolved.padding.left, LengthPercentage::length(12.0));
519            assert_eq!(resolved.padding.right, LengthPercentage::length(0.0));
520        }
521    }
522
523    #[test]
524    fn resolving_twice_does_not_accumulate() {
525        // The engine re-resolves from the same LayoutStyle on every flip, so resolution must be a pure function of the intent.
526        let style = LayoutStyle::new().padding_start(8.0);
527        let _ = style.resolve(Direction::Rtl);
528        let back = style.resolve(Direction::Ltr);
529        assert_eq!(back.padding.left, LengthPercentage::length(8.0));
530        assert_eq!(back.padding.right, LengthPercentage::length(0.0));
531    }
532
533    #[test]
534    fn a_row_reverses_under_rtl_but_an_explicit_reverse_does_not_flip_back() {
535        let row = LayoutStyle::new().flex_row();
536        assert_eq!(
537            row.resolve(Direction::Ltr).flex_direction,
538            FlexDirection::Row
539        );
540        assert_eq!(
541            row.resolve(Direction::Rtl).flex_direction,
542            FlexDirection::RowReverse
543        );
544        let reversed = LayoutStyle::new().flex_row_reverse();
545        for direction in [Direction::Ltr, Direction::Rtl] {
546            assert_eq!(
547                reversed.resolve(direction).flex_direction,
548                FlexDirection::RowReverse,
549                "an explicit reverse is physical"
550            );
551        }
552    }
553
554    #[test]
555    fn a_column_is_unaffected_by_direction() {
556        let col = LayoutStyle::new().flex_column();
557        for direction in [Direction::Ltr, Direction::Rtl] {
558            assert_eq!(col.resolve(direction).flex_direction, FlexDirection::Column);
559        }
560    }
561
562    #[test]
563    fn only_logical_edges_need_the_style_kept_for_a_flip() {
564        assert!(!LayoutStyle::new().flex_row().logical.has_edges());
565        assert!(LayoutStyle::new().margin_start(4.0).logical.has_edges());
566        assert!(LayoutStyle::new().inset_end(4.0).logical.has_edges());
567    }
568
569    #[test]
570    fn style_default_is_block() {
571        let style = LayoutStyle::new();
572        assert_eq!(style.inner.display, Display::Block);
573    }
574
575    #[test]
576    fn style_width_sets_dimension() {
577        let style = LayoutStyle::new().width(120.0);
578        assert_eq!(style.inner.size.width, Dimension::length(120.0));
579    }
580
581    #[test]
582    fn style_width_px_reads_back_length() {
583        let style = LayoutStyle::new().width(120.0);
584        assert_eq!(style.width_px(), Some(120.0));
585        assert!(!style.is_width_auto());
586    }
587
588    #[test]
589    fn style_width_px_none_for_percent_or_default() {
590        assert_eq!(LayoutStyle::new().width_px(), None);
591        assert!(LayoutStyle::new().is_width_auto());
592        let percent = LayoutStyle::new().width(SizeDimension::Percent(0.5));
593        assert_eq!(percent.width_px(), None);
594        assert!(!percent.is_width_auto());
595    }
596
597    #[test]
598    fn style_width_percent_sets_dimension() {
599        let style = LayoutStyle::new().width(SizeDimension::Percent(0.5));
600        assert_eq!(style.inner.size.width, Dimension::percent(0.5));
601    }
602
603    #[test]
604    fn style_height_sets_dimension() {
605        let style = LayoutStyle::new().height(80.0);
606        assert_eq!(style.inner.size.height, Dimension::length(80.0));
607    }
608
609    #[test]
610    fn style_max_width_sets_dimension() {
611        let style = LayoutStyle::new().max_width(200.0);
612        assert_eq!(style.inner.max_size.width, Dimension::length(200.0));
613    }
614
615    #[test]
616    fn style_max_height_sets_dimension() {
617        let style = LayoutStyle::new().max_height(150.0);
618        assert_eq!(style.inner.max_size.height, Dimension::length(150.0));
619    }
620
621    #[test]
622    fn style_flex_basis_percent_sets_dimension() {
623        let style = LayoutStyle::new().flex_basis(SizeDimension::Percent(0.5));
624        assert_eq!(style.inner.flex_basis, Dimension::percent(0.5));
625    }
626
627    #[test]
628    fn style_flex_row_sets_direction() {
629        let style = LayoutStyle::new().flex_row();
630        assert_eq!(style.inner.flex_direction, FlexDirection::Row);
631    }
632
633    #[test]
634    fn style_flex_column_sets_direction() {
635        let style = LayoutStyle::new().flex_column();
636        assert_eq!(style.inner.flex_direction, FlexDirection::Column);
637    }
638
639    #[test]
640    fn style_align_items_center_sets_field() {
641        let style = LayoutStyle::new().align_items(AlignItems::CENTER);
642        assert_eq!(style.inner.align_items, Some(taffy::AlignItems::CENTER));
643    }
644
645    #[test]
646    fn style_justify_center_sets_field() {
647        let style = LayoutStyle::new().justify_content(JustifyContent::CENTER);
648        assert_eq!(
649            style.inner.justify_content,
650            Some(taffy::JustifyContent::CENTER)
651        );
652    }
653
654    #[test]
655    fn style_default_impl_matches_new() {
656        let style = LayoutStyle::default();
657        assert_eq!(style.inner.display, Display::Block);
658    }
659
660    #[test]
661    fn style_padding_horizontal_sets_left_right() {
662        let style = LayoutStyle::new().padding_horizontal(10.0);
663        assert_eq!(style.inner.padding.left, LengthPercentage::length(10.0));
664        assert_eq!(style.inner.padding.right, LengthPercentage::length(10.0));
665    }
666
667    #[test]
668    fn style_padding_vertical_sets_top_bottom() {
669        let style = LayoutStyle::new().padding_vertical(8.0);
670        assert_eq!(style.inner.padding.top, LengthPercentage::length(8.0));
671        assert_eq!(style.inner.padding.bottom, LengthPercentage::length(8.0));
672    }
673
674    #[test]
675    fn style_padding_top_sets_field() {
676        let style = LayoutStyle::new().padding_top(4.0);
677        assert_eq!(style.inner.padding.top, LengthPercentage::length(4.0));
678    }
679
680    #[test]
681    fn style_margin_horizontal_sets_left_right() {
682        let style = LayoutStyle::new().margin_horizontal(12.0);
683        assert_eq!(style.inner.margin.left, LengthPercentageAuto::length(12.0));
684        assert_eq!(style.inner.margin.right, LengthPercentageAuto::length(12.0));
685    }
686
687    #[test]
688    fn style_margin_top_sets_field() {
689        let style = LayoutStyle::new().margin_top(6.0);
690        assert_eq!(style.inner.margin.top, LengthPercentageAuto::length(6.0));
691    }
692}