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