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