Skip to main content

taffy/compute/grid/
alignment.rs

1//! Alignment of tracks and final positioning of items
2use super::types::GridTrack;
3use crate::compute::common::alignment::{apply_alignment_fallback, compute_alignment_offset};
4use crate::geometry::{InBothAbsAxis, Line, Point, Rect, Size};
5use crate::style::{
6    AlignContent, AlignItems, AlignItemsKeyword, AlignSelf, AvailableSpace, CoreStyle, GridItemStyle, Overflow,
7    Position,
8};
9use crate::tree::{Layout, LayoutPartialTreeExt, NodeId, SizingMode};
10use crate::util::sys::f32_max;
11use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
12
13#[cfg(feature = "content_size")]
14use crate::compute::common::content_size::compute_content_size_contribution;
15use crate::{BoxSizing, Direction, LayoutGridContainer};
16
17/// Align the grid tracks within the grid according to the align-content (rows) or
18/// justify-content (columns) property. This only does anything if the size of the
19/// grid is not equal to the size of the grid container in the axis being aligned.
20pub(super) fn align_tracks(
21    grid_container_content_box_size: f32,
22    padding: Line<f32>,
23    border: Line<f32>,
24    tracks: &mut [GridTrack],
25    track_alignment_style: AlignContent,
26    axis_is_reversed: bool,
27) {
28    let used_size: f32 = tracks.iter().map(|track| track.base_size).sum();
29    let free_space = grid_container_content_box_size - used_size;
30    let origin = padding.start + border.start;
31
32    // Count the number of non-collapsed tracks (not counting gutters)
33    let num_tracks = tracks.iter().skip(1).step_by(2).filter(|track| !track.is_collapsed).count();
34
35    // Grid layout treats gaps as full tracks rather than applying them at alignment so we
36    // simply pass zero here. Grid layout is never reversed.
37    let gap = 0.0;
38    let layout_is_reversed = false;
39    let track_alignment = apply_alignment_fallback(free_space, num_tracks, track_alignment_style);
40    let track_alignment = if axis_is_reversed { track_alignment.reversed() } else { track_alignment };
41
42    // Compute offsets
43    let mut total_offset = origin;
44    let mut seen_non_collapsed_track = false;
45    tracks.iter_mut().enumerate().for_each(|(i, track)| {
46        // Odd tracks are gutters (but slices are zero-indexed, so odd tracks have even indices)
47        let is_gutter = i % 2 == 0;
48        let is_non_collapsed_track = !is_gutter && !track.is_collapsed;
49
50        // Alignment offsets should be applied only to non-collapsed tracks.
51        let is_first = is_non_collapsed_track && !seen_non_collapsed_track;
52
53        let offset = if is_non_collapsed_track {
54            compute_alignment_offset(free_space, num_tracks, gap, track_alignment, layout_is_reversed, is_first)
55        } else {
56            0.0
57        };
58
59        track.offset = total_offset + offset;
60        total_offset = total_offset + offset + track.base_size;
61        if is_non_collapsed_track {
62            seen_non_collapsed_track = true;
63        }
64    });
65}
66
67/// Align and size a grid item into it's final position
68pub(super) fn align_and_position_item(
69    tree: &mut impl LayoutGridContainer,
70    node: NodeId,
71    order: u32,
72    grid_area: Rect<f32>,
73    container_alignment_styles: InBothAbsAxis<Option<AlignItems>>,
74    baseline_shim: f32,
75    direction: Direction,
76) -> (Size<f32>, f32, f32) {
77    let grid_area_size = Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top };
78
79    let style = tree.get_grid_child_style(node);
80
81    let overflow = style.overflow();
82    let scrollbar_width = style.scrollbar_width();
83    let aspect_ratio = style.aspect_ratio();
84    let justify_self = style.justify_self();
85    let align_self = style.align_self();
86
87    let position = style.position();
88    let inset_horizontal = style
89        .inset()
90        .horizontal_components()
91        .map(|size| size.resolve_to_option(grid_area_size.width, |val, basis| tree.calc(val, basis)));
92    let inset_vertical = style
93        .inset()
94        .vertical_components()
95        .map(|size| size.resolve_to_option(grid_area_size.height, |val, basis| tree.calc(val, basis)));
96    let padding =
97        style.padding().map(|p| p.resolve_or_zero(Some(grid_area_size.width), |val, basis| tree.calc(val, basis)));
98    let border =
99        style.border().map(|p| p.resolve_or_zero(Some(grid_area_size.width), |val, basis| tree.calc(val, basis)));
100    let padding_border_size = (padding + border).sum_axes();
101
102    let box_sizing_adjustment =
103        if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
104
105    let inherent_size = style
106        .size()
107        .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
108        .maybe_apply_aspect_ratio(aspect_ratio)
109        .maybe_add(box_sizing_adjustment);
110    let min_size = style
111        .min_size()
112        .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
113        .maybe_add(box_sizing_adjustment)
114        .or(padding_border_size.map(Some))
115        .maybe_max(padding_border_size)
116        .maybe_apply_aspect_ratio(aspect_ratio);
117    let max_size = style
118        .max_size()
119        .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
120        .maybe_apply_aspect_ratio(aspect_ratio)
121        .maybe_add(box_sizing_adjustment);
122
123    // Resolve default alignment styles if they are set on neither the parent or the node itself
124    // Note: if the child has a preferred aspect ratio but neither width or height are set, then the width is stretched
125    // and the then height is calculated from the width according the aspect ratio
126    // See: https://www.w3.org/TR/css-grid-1/#grid-item-sizing
127    let alignment_styles = InBothAbsAxis {
128        horizontal: justify_self.or(container_alignment_styles.horizontal).unwrap_or_else(|| {
129            if inherent_size.width.is_some() {
130                AlignSelf::START
131            } else {
132                AlignSelf::STRETCH
133            }
134        }),
135        vertical: align_self.or(container_alignment_styles.vertical).unwrap_or_else(|| {
136            if inherent_size.height.is_some() || aspect_ratio.is_some() {
137                AlignSelf::START
138            } else {
139                AlignSelf::STRETCH
140            }
141        }),
142    };
143
144    // Note: This is not a bug. It is part of the CSS spec that both horizontal and vertical margins
145    // resolve against the WIDTH of the grid area.
146    let margin =
147        style.margin().map(|margin| margin.resolve_to_option(grid_area_size.width, |val, basis| tree.calc(val, basis)));
148
149    let grid_area_minus_item_margins_size = Size {
150        width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right),
151        height: grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim,
152    };
153
154    // If node is absolutely positioned and width is not set explicitly, then deduce it
155    // from left, right and container_content_box if both are set.
156    let width = inherent_size.width.or_else(|| {
157        // Apply width derived from both the left and right properties of an absolutely
158        // positioned element being set
159        if position == Position::Absolute {
160            if let (Some(left), Some(right)) = (inset_horizontal.start, inset_horizontal.end) {
161                return Some(f32_max(grid_area_minus_item_margins_size.width - left - right, 0.0));
162            }
163        }
164
165        // Apply width based on stretch alignment if:
166        //  - Alignment style is "stretch"
167        //  - The node is not absolutely positioned
168        //  - The node does not have auto margins in this axis.
169        if margin.left.is_some()
170            && margin.right.is_some()
171            && alignment_styles.horizontal == AlignSelf::STRETCH
172            && position != Position::Absolute
173        {
174            return Some(grid_area_minus_item_margins_size.width);
175        }
176
177        None
178    });
179
180    // Reapply aspect ratio after stretch and absolute position width adjustments
181    let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
182
183    let height = height.or_else(|| {
184        if position == Position::Absolute {
185            if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) {
186                return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0));
187            }
188        }
189
190        // Apply height based on stretch alignment if:
191        //  - Alignment style is "stretch"
192        //  - The node is not absolutely positioned
193        //  - The node does not have auto margins in this axis.
194        if margin.top.is_some()
195            && margin.bottom.is_some()
196            && alignment_styles.vertical == AlignSelf::STRETCH
197            && position != Position::Absolute
198        {
199            return Some(grid_area_minus_item_margins_size.height);
200        }
201
202        None
203    });
204    // Reapply aspect ratio after stretch and absolute position height adjustments
205    let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
206
207    // Clamp size by min and max width/height
208    let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
209
210    // Layout node
211    drop(style);
212
213    let size = if position == Position::Absolute && (width.is_none() || height.is_none()) {
214        tree.measure_child_size_both(
215            node,
216            Size { width, height },
217            grid_area_size.map(Option::Some),
218            grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
219            SizingMode::InherentSize,
220            Line::FALSE,
221        )
222        .map(Some)
223    } else {
224        Size { width, height }
225    };
226
227    let layout_output = tree.perform_child_layout(
228        node,
229        size,
230        grid_area_size.map(Option::Some),
231        grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
232        SizingMode::InherentSize,
233        Line::FALSE,
234    );
235
236    // Resolve final size
237    let Size { width, height } = size.unwrap_or(layout_output.size).maybe_clamp(min_size, max_size);
238
239    let (x, x_margin) = align_item_within_area(
240        Line { start: grid_area.left, end: grid_area.right },
241        justify_self.unwrap_or(alignment_styles.horizontal),
242        width,
243        position,
244        inset_horizontal,
245        margin.horizontal_components(),
246        0.0,
247        direction,
248    );
249    let (y, y_margin) = align_item_within_area(
250        Line { start: grid_area.top, end: grid_area.bottom },
251        align_self.unwrap_or(alignment_styles.vertical),
252        height,
253        position,
254        inset_vertical,
255        margin.vertical_components(),
256        baseline_shim,
257        Direction::Ltr,
258    );
259
260    let scrollbar_size = Size {
261        width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
262        height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
263    };
264
265    let resolved_margin = Rect { left: x_margin.start, right: x_margin.end, top: y_margin.start, bottom: y_margin.end };
266
267    tree.set_unrounded_layout(
268        node,
269        &Layout {
270            order,
271            location: Point { x, y },
272            size: Size { width, height },
273            #[cfg(feature = "content_size")]
274            content_size: layout_output.content_size,
275            scrollbar_size,
276            padding,
277            border,
278            margin: resolved_margin,
279        },
280    );
281
282    #[cfg(feature = "content_size")]
283    let contribution = compute_content_size_contribution(
284        Point { x: x - grid_area.left, y: y - grid_area.top },
285        Size { width, height },
286        layout_output.content_size,
287        overflow,
288    );
289    #[cfg(not(feature = "content_size"))]
290    let contribution = Size::ZERO;
291
292    (contribution, y, height)
293}
294
295/// Align and size a grid item along a single axis
296#[allow(clippy::too_many_arguments)]
297pub(super) fn align_item_within_area(
298    grid_area: Line<f32>,
299    alignment_style: AlignSelf,
300    resolved_size: f32,
301    position: Position,
302    inset: Line<Option<f32>>,
303    margin: Line<Option<f32>>,
304    baseline_shim: f32,
305    direction: Direction,
306) -> (f32, Line<f32>) {
307    // Calculate grid area dimension in the axis
308    let non_auto_margin = Line { start: margin.start.unwrap_or(0.0) + baseline_shim, end: margin.end.unwrap_or(0.0) };
309    let grid_area_size = f32_max(grid_area.end - grid_area.start, 0.0);
310    let free_space = f32_max(grid_area_size - resolved_size - non_auto_margin.sum(), 0.0);
311
312    // Expand auto margins to fill available space
313    let auto_margin_count = margin.start.is_none() as u8 + margin.end.is_none() as u8;
314    let auto_margin_size = if auto_margin_count > 0 { free_space / auto_margin_count as f32 } else { 0.0 };
315    let resolved_margin = Line {
316        start: margin.start.unwrap_or(auto_margin_size) + baseline_shim,
317        end: margin.end.unwrap_or(auto_margin_size),
318    };
319
320    // If the alignment uses a "safe" overflow-position keyword and the item would overflow
321    // its grid area, fall back to logical Start to avoid data loss. See CSS Box Alignment 3
322    // ยง4.3 <https://www.w3.org/TR/css-align-3/#overflow-values>. Otherwise, drop the safety
323    // field so the match below operates on a bare keyword and stays exhaustive.
324    let overflows = resolved_size + non_auto_margin.sum() > grid_area_size;
325    let alignment_keyword =
326        if alignment_style.is_safe() && overflows { AlignItemsKeyword::Start } else { alignment_style.keyword() };
327
328    // Compute offset in the axis
329    let alignment_based_offset = match alignment_keyword {
330        // TODO: Add support for baseline alignment. For now we treat it as "start".
331        AlignItemsKeyword::Start
332        | AlignItemsKeyword::FlexStart
333        | AlignItemsKeyword::Baseline
334        | AlignItemsKeyword::Stretch => {
335            if direction.is_rtl() {
336                grid_area_size - resolved_size - resolved_margin.end
337            } else {
338                resolved_margin.start
339            }
340        }
341        AlignItemsKeyword::End | AlignItemsKeyword::FlexEnd => {
342            if direction.is_rtl() {
343                resolved_margin.start
344            } else {
345                grid_area_size - resolved_size - resolved_margin.end
346            }
347        }
348        AlignItemsKeyword::Center => {
349            (grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0
350        }
351    };
352
353    let offset_within_area = if position == Position::Absolute {
354        match (inset.start, inset.end) {
355            (Some(start), Some(end)) => {
356                if direction.is_rtl() {
357                    grid_area_size - end - resolved_size - non_auto_margin.end
358                } else {
359                    start + non_auto_margin.start
360                }
361            }
362            (Some(start), None) => start + non_auto_margin.start,
363            (None, Some(end)) => grid_area_size - end - resolved_size - non_auto_margin.end,
364            (None, None) => alignment_based_offset,
365        }
366    } else {
367        alignment_based_offset
368    };
369
370    let mut start = grid_area.start + offset_within_area;
371    if position == Position::Relative {
372        let relative_inset = if direction.is_rtl() {
373            inset.end.map(|pos| -pos).or(inset.start)
374        } else {
375            inset.start.or(inset.end.map(|pos| -pos))
376        };
377        start += relative_inset.unwrap_or(0.0);
378    }
379
380    (start, resolved_margin)
381}