taffy/compute/grid/
alignment.rs

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