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