Skip to main content

telar_layout_core/
style.rs

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/// 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    /// Set by `LayoutEngine::make_flex_row` for a node whose own declared style never called `flex_row`.
49    pub(crate) row_forced: bool,
50    /// Set by [`LayoutStyle::display_none`] or `LayoutEngine::set_display`: out of flow regardless of `inner.display`.
51    pub(crate) hidden: bool,
52    /// Set by `LayoutEngine::set_min_height`: overrides `inner.min_size.height`.
53    pub(crate) min_height_override: Option<f32>,
54    /// Set by `LayoutEngine::set_leading_margin`; `(is_row, px)`, placed by the engine since which physical edge is "leading" depends on the parent's axis.
55    pub(crate) leading_margin: Option<(bool, f32)>,
56}
57
58impl LogicalStyle {
59    /// Whether any edge needs re-resolving on a direction flip. A direction-following row alone does not: it
60    /// is a single flag the engine can toggle in place, without the original style to resolve against.
61    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    /// Whether the engine must keep this node's full style around: an edge, or out-of-band mutator state.
71    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/// A box's four margins, named by axis so they follow the writing direction rather than the screen.
81///
82/// The nine builders this replaces mixed two vocabularies — seven physical, two logical — and nothing in the
83/// name of `margin_left` said which of the two it belonged to.
84#[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    /// A **block** box, as in CSS: children stack vertically and the flex properties do nothing.
120    ///
121    /// Worth saying out loud because the ones that do nothing do it silently. [`gap`](Self::gap),
122    /// [`justify_content`](Self::justify_content) and [`align_items`](Self::align_items) all belong to
123    /// flex layout, so on a box that never called [`flex_row`](Self::flex_row) or
124    /// [`flex_column`](Self::flex_column) they are accepted and ignored — a row written without
125    /// `flex_row` comes out as a column, and the reading on screen is not "that row is a column" but
126    /// "why is this panel twice as tall as it should be".
127    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    /// A flex row along the inline axis: items run left-to-right under [`Direction::Ltr`] and right-to-left
138    /// under [`Direction::Rtl`], the way `flex-direction: row` follows `dir` on the web. Use
139    /// [`flex_row_reverse`](Self::flex_row_reverse) for a row that is reversed in both directions.
140    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    /// A flex row laid out against the writing direction, unconditionally. Unlike
148    /// [`flex_row`](Self::flex_row) this is a physical choice and does not flip with [`Direction`].
149    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    /// Declares the node out of layout flow (no space, not laid out) as part of its own style — e.g. a tab panel that should start inactive, as opposed to the out-of-band `LayoutEngine::set_display`.
169    pub fn display_none(mut self) -> Self {
170        self.logical.hidden = true;
171        self
172    }
173
174    /// Takes the node out of normal flow (`position: absolute`) with all four insets pinned to 0, so it
175    /// fills its containing block without affecting sibling layout — used by `overlay` to cover the
176    /// viewport. Combine with `flex_column`/alignment to position the overlay's content within the layer.
177    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    /// Takes the node out of normal flow (`position: absolute`) leaving every inset at `auto`, so the
190    /// edges it is pinned by are exactly the ones the caller names. [`absolute_fill`](Self::absolute_fill)
191    /// is this plus all four insets at 0; a floating panel wants three of them and its own size on the
192    /// fourth axis, which pinning everything would override.
193    pub fn absolute(mut self) -> Self {
194        self.inner.position = taffy::Position::Absolute;
195        self
196    }
197
198    /// Inset from the top edge, for a node already taken out of flow. Physical, not logical: `top` does not
199    /// swap under RTL the way [`inset_start`](Self::inset_start) does.
200    pub fn inset_top(mut self, px: f32) -> Self {
201        self.inner.inset.top = LengthPercentageAuto::length(px);
202        self
203    }
204
205    /// Inset from the bottom edge, for a node already taken out of flow.
206    pub fn inset_bottom(mut self, px: f32) -> Self {
207        self.inner.inset.bottom = LengthPercentageAuto::length(px);
208        self
209    }
210
211    /// The node's `width` in pixels if it is a definite length, else `None` (e.g. percent or auto).
212    /// Lets widgets with an intrinsic size (e.g. `<svg>`/`<img>`) inspect a caller-supplied width before registering their layout leaf.
213    pub fn width_px(&self) -> Option<f32> {
214        self.inner.size.width.into_option()
215    }
216
217    /// True when `width` was left at its default, which taffy also treats as `auto`.
218    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    /// The node's `height` in pixels if it is a definite length, else `None` (e.g. percent or auto).
228    pub fn height_px(&self) -> Option<f32> {
229        self.inner.size.height.into_option()
230    }
231
232    /// True when `height` was left at its default, which taffy also treats as `auto`.
233    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    /// The node's `max-width` in pixels if it is a definite length, else `None`
253    /// (e.g. percent or unset). Used by the layout pass to pin a resolved width.
254    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    /// Padding on the edge the text starts from — `left` under [`Direction::Ltr`], `right` under
327    /// [`Direction::Rtl`].
328    pub fn padding_start(mut self, px: f32) -> Self {
329        self.logical.padding_start = Some(px);
330        self
331    }
332
333    /// Padding on the edge the text runs towards — `right` under [`Direction::Ltr`], `left` under
334    /// [`Direction::Rtl`].
335    pub fn padding_end(mut self, px: f32) -> Self {
336        self.logical.padding_end = Some(px);
337        self
338    }
339
340    /// All four margins at once, named by axis rather than by side so they follow the writing direction.
341    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    /// Margin on the edge the block axis starts from — the top, in every writing mode this engine supports.
349    pub fn margin_block_start(mut self, px: f32) -> Self {
350        self.inner.margin.top = LengthPercentageAuto::length(px);
351        self
352    }
353
354    /// Margin on the edge the block axis ends at — the bottom.
355    pub fn margin_block_end(mut self, px: f32) -> Self {
356        self.inner.margin.bottom = LengthPercentageAuto::length(px);
357        self
358    }
359
360    /// Margin on the edge the text starts from — `left` under [`Direction::Ltr`], `right` under
361    /// [`Direction::Rtl`].
362    pub fn margin_inline_start(mut self, px: f32) -> Self {
363        self.logical.margin_start = Some(px);
364        self
365    }
366
367    /// Margin on the edge the text runs towards — `right` under [`Direction::Ltr`], `left` under
368    /// [`Direction::Rtl`].
369    pub fn margin_inline_end(mut self, px: f32) -> Self {
370        self.logical.margin_end = Some(px);
371        self
372    }
373
374    /// A margin from the viewport's physical left edge, which does **not** follow the writing direction.
375    ///
376    /// The one place that is right: placing an in-flow box at an x already worked out in physical viewport
377    /// coordinates — a dropdown panel under its trigger, a picker under its anchor. Those come from a
378    /// laid-out rect, so mirroring them under RTL would put the panel on the wrong side of the screen. For a
379    /// margin that is part of a box's own spacing, use [`margin_inline_start`](Self::margin_inline_start).
380    pub fn margin_from_left(mut self, px: f32) -> Self {
381        self.inner.margin.left = LengthPercentageAuto::length(px);
382        self
383    }
384
385    /// Inset from the edge the text starts from, for a node already taken out of flow (see
386    /// [`absolute_fill`](Self::absolute_fill)); ignored on an in-flow node, as `inset` is in CSS.
387    pub fn inset_start(mut self, px: f32) -> Self {
388        self.logical.inset_start = Some(px);
389        self
390    }
391
392    /// Inset from the edge the text runs towards, for a node already taken out of flow.
393    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    /// Overrides the parent's `align_items` for this child, centering it on the cross axis instead of
427    /// stretching — so a fixed-size child (e.g. a square icon chip) keeps its size and stays centered.
428    pub fn align_self_center(mut self) -> Self {
429        self.inner.align_self = Some(taffy::AlignSelf::CENTER);
430        self
431    }
432
433    /// Aligns this child to the start of the cross axis, overriding the parent's `align_items`.
434    pub fn align_self_start(mut self) -> Self {
435        self.inner.align_self = Some(taffy::AlignSelf::FLEX_START);
436        self
437    }
438
439    /// Aligns this child to the end of the cross axis, overriding the parent's `align_items`.
440    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    /// The physical `taffy::Style` this describes under `direction`. Called by the engine at every point a
485    /// style reaches a node, and again for each affected node when the direction flips.
486    ///
487    /// Does not place the leading margin ([`LogicalStyle::leading_margin`]) — the engine does that afterwards, since it needs the parent's axis to know which physical edge "leading" means.
488    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        // The engine re-resolves from the same LayoutStyle on every flip, so resolution must be a pure function of the intent.
577        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    // The block pair is physical-by-construction: there is no vertical writing mode here, so block start is the top in every direction. The inline pair is the one a flip moves, and it is the one kept in `logical` for `resolve` to place.
737    #[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    // What `margin_from_left` exists for: an x already in physical viewport coordinates must not mirror.
747    #[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}