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