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