⎇ 7e8d5b1d733ed84c0db8e8d359f84df371bdaadc
src/compute/grid/alignment.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
merge `alignment_based_offset` match cases
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) {
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 direction: Direction,
67) -> (Size<f32>, f32, f32) {
68 let grid_area_size = Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top };
69
70 let style = tree.get_grid_child_style(node);
71
72 let overflow = style.overflow();
73 let scrollbar_width = style.scrollbar_width();
74 let aspect_ratio = style.aspect_ratio();
75 let justify_self = style.justify_self();
76 let align_self = style.align_self();
77
78 let position = style.position();
79 let inset_horizontal = style
80 .inset()
81 .horizontal_components()
82 .map(|size| size.resolve_to_option(grid_area_size.width, |val, basis| tree.calc(val, basis)));
83 let inset_vertical = style
84 .inset()
85 .vertical_components()
86 .map(|size| size.resolve_to_option(grid_area_size.height, |val, basis| tree.calc(val, basis)));
87 let padding =
88 style.padding().map(|p| p.resolve_or_zero(Some(grid_area_size.width), |val, basis| tree.calc(val, basis)));
89 let border =
90 style.border().map(|p| p.resolve_or_zero(Some(grid_area_size.width), |val, basis| tree.calc(val, basis)));
91 let padding_border_size = (padding + border).sum_axes();
92
93 let box_sizing_adjustment =
94 if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
95
96 let inherent_size = style
97 .size()
98 .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
99 .maybe_apply_aspect_ratio(aspect_ratio)
100 .maybe_add(box_sizing_adjustment);
101 let min_size = style
102 .min_size()
103 .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
104 .maybe_add(box_sizing_adjustment)
105 .or(padding_border_size.map(Some))
106 .maybe_max(padding_border_size)
107 .maybe_apply_aspect_ratio(aspect_ratio);
108 let max_size = style
109 .max_size()
110 .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis))
111 .maybe_apply_aspect_ratio(aspect_ratio)
112 .maybe_add(box_sizing_adjustment);
113
114 // Resolve default alignment styles if they are set on neither the parent or the node itself
115 // Note: if the child has a preferred aspect ratio but neither width or height are set, then the width is stretched
116 // and the then height is calculated from the width according the aspect ratio
117 // See: https://www.w3.org/TR/css-grid-1/#grid-item-sizing
118 let alignment_styles = InBothAbsAxis {
119 horizontal: justify_self.or(container_alignment_styles.horizontal).unwrap_or_else(|| {
120 if inherent_size.width.is_some() {
121 AlignSelf::Start
122 } else {
123 AlignSelf::Stretch
124 }
125 }),
126 vertical: align_self.or(container_alignment_styles.vertical).unwrap_or_else(|| {
127 if inherent_size.height.is_some() || aspect_ratio.is_some() {
128 AlignSelf::Start
129 } else {
130 AlignSelf::Stretch
131 }
132 }),
133 };
134
135 // Note: This is not a bug. It is part of the CSS spec that both horizontal and vertical margins
136 // resolve against the WIDTH of the grid area.
137 let margin =
138 style.margin().map(|margin| margin.resolve_to_option(grid_area_size.width, |val, basis| tree.calc(val, basis)));
139
140 let grid_area_minus_item_margins_size = Size {
141 width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right),
142 height: grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim,
143 };
144
145 // If node is absolutely positioned and width is not set explicitly, then deduce it
146 // from left, right and container_content_box if both are set.
147 let width = inherent_size.width.or_else(|| {
148 // Apply width derived from both the left and right properties of an absolutely
149 // positioned element being set
150 if position == Position::Absolute {
151 if let (Some(left), Some(right)) = (inset_horizontal.start, inset_horizontal.end) {
152 return Some(f32_max(grid_area_minus_item_margins_size.width - left - right, 0.0));
153 }
154 }
155
156 // Apply width based on stretch alignment if:
157 // - Alignment style is "stretch"
158 // - The node is not absolutely positioned
159 // - The node does not have auto margins in this axis.
160 if margin.left.is_some()
161 && margin.right.is_some()
162 && alignment_styles.horizontal == AlignSelf::Stretch
163 && position != Position::Absolute
164 {
165 return Some(grid_area_minus_item_margins_size.width);
166 }
167
168 None
169 });
170
171 // Reapply aspect ratio after stretch and absolute position width adjustments
172 let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
173
174 let height = height.or_else(|| {
175 if position == Position::Absolute {
176 if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) {
177 return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0));
178 }
179 }
180
181 // Apply height based on stretch alignment if:
182 // - Alignment style is "stretch"
183 // - The node is not absolutely positioned
184 // - The node does not have auto margins in this axis.
185 if margin.top.is_some()
186 && margin.bottom.is_some()
187 && alignment_styles.vertical == AlignSelf::Stretch
188 && position != Position::Absolute
189 {
190 return Some(grid_area_minus_item_margins_size.height);
191 }
192
193 None
194 });
195 // Reapply aspect ratio after stretch and absolute position height adjustments
196 let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
197
198 // Clamp size by min and max width/height
199 let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
200
201 // Layout node
202 let item_direction = style.direction();
203 drop(style);
204
205 let size = if position == Position::Absolute && (width.is_none() || height.is_none()) {
206 tree.measure_child_size_both(
207 node,
208 Size { width, height },
209 grid_area_size.map(Option::Some),
210 grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
211 SizingMode::InherentSize,
212 item_direction,
213 Line::FALSE,
214 )
215 .map(Some)
216 } else {
217 Size { width, height }
218 };
219
220 let layout_output = tree.perform_child_layout(
221 node,
222 size,
223 grid_area_size.map(Option::Some),
224 grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
225 SizingMode::InherentSize,
226 item_direction,
227 Line::FALSE,
228 );
229
230 // Resolve final size
231 let Size { width, height } = size.unwrap_or(layout_output.size).maybe_clamp(min_size, max_size);
232
233 let (x, x_margin) = align_item_within_area(
234 Line { start: grid_area.left, end: grid_area.right },
235 justify_self.unwrap_or(alignment_styles.horizontal),
236 width,
237 position,
238 inset_horizontal,
239 margin.horizontal_components(),
240 0.0,
241 direction,
242 );
243 let (y, y_margin) = align_item_within_area(
244 Line { start: grid_area.top, end: grid_area.bottom },
245 align_self.unwrap_or(alignment_styles.vertical),
246 height,
247 position,
248 inset_vertical,
249 margin.vertical_components(),
250 baseline_shim,
251 Direction::Ltr,
252 );
253
254 let scrollbar_size = Size {
255 width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
256 height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
257 };
258
259 let resolved_margin = Rect { left: x_margin.start, right: x_margin.end, top: y_margin.start, bottom: y_margin.end };
260
261 tree.set_unrounded_layout(
262 node,
263 &Layout {
264 order,
265 location: Point { x, y },
266 size: Size { width, height },
267 #[cfg(feature = "content_size")]
268 content_size: layout_output.content_size,
269 scrollbar_size,
270 padding,
271 border,
272 margin: resolved_margin,
273 },
274 );
275
276 #[cfg(feature = "content_size")]
277 let contribution = compute_content_size_contribution(
278 Point { x: x - grid_area.left, y: y - grid_area.top },
279 Size { width, height },
280 layout_output.content_size,
281 overflow,
282 );
283 #[cfg(not(feature = "content_size"))]
284 let contribution = Size::ZERO;
285
286 (contribution, y, height)
287}
288
289/// Align and size a grid item along a single axis
290#[allow(clippy::too_many_arguments)]
291pub(super) fn align_item_within_area(
292 grid_area: Line<f32>,
293 alignment_style: AlignSelf,
294 resolved_size: f32,
295 position: Position,
296 inset: Line<Option<f32>>,
297 margin: Line<Option<f32>>,
298 baseline_shim: f32,
299 direction: Direction,
300) -> (f32, Line<f32>) {
301 // Calculate grid area dimension in the axis
302 let non_auto_margin = Line { start: margin.start.unwrap_or(0.0) + baseline_shim, end: margin.end.unwrap_or(0.0) };
303 let grid_area_size = f32_max(grid_area.end - grid_area.start, 0.0);
304 let free_space = f32_max(grid_area_size - resolved_size - non_auto_margin.sum(), 0.0);
305
306 // Expand auto margins to fill available space
307 let auto_margin_count = margin.start.is_none() as u8 + margin.end.is_none() as u8;
308 let auto_margin_size = if auto_margin_count > 0 { free_space / auto_margin_count as f32 } else { 0.0 };
309 let resolved_margin = Line {
310 start: margin.start.unwrap_or(auto_margin_size) + baseline_shim,
311 end: margin.end.unwrap_or(auto_margin_size),
312 };
313
314 // Compute offset in the axis
315 let alignment_based_offset = match alignment_style {
316 // TODO: Add support for baseline alignment. For now we treat it as "start".
317 AlignSelf::Start | AlignSelf::FlexStart | AlignSelf::Baseline | AlignSelf::Stretch => {
318 if direction.is_rtl() {
319 free_space + resolved_margin.start
320 } else {
321 resolved_margin.start
322 }
323 }
324 AlignSelf::End | AlignSelf::FlexEnd => {
325 if direction.is_rtl() {
326 resolved_margin.start
327 } else {
328 grid_area_size - resolved_size - resolved_margin.end
329 }
330 }
331 AlignSelf::Center => (grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0,
332 };
333
334 let offset_within_area = if position == Position::Absolute {
335 if let Some(start) = inset.start {
336 start + non_auto_margin.start
337 } else if let Some(end) = inset.end {
338 grid_area_size - end - resolved_size - non_auto_margin.end
339 } else {
340 alignment_based_offset
341 }
342 } else {
343 alignment_based_offset
344 };
345
346 let mut start = grid_area.start + offset_within_area;
347 if position == Position::Relative {
348 start += inset.start.or(inset.end.map(|pos| -pos)).unwrap_or(0.0);
349 }
350
351 (start, resolved_margin)
352}