⎇ 66bcb10bff823bfde7828eeef133cb2fa7de7dba
src/compute/grid/alignment.rs
History
Who touched what
raw
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;
5use crate::geometry::{Line, Point, Rect, Size};
6use crate::style::{AlignContent, AlignItems, AlignSelf, AvailableSpace, Position};
7use crate::tree::{Layout, SizingMode};
8use crate::tree::{LayoutTree, NodeId};
9use crate::util::sys::f32_max;
10use crate::util::MaybeMath;
11use crate::util::{MaybeResolve, ResolveOrZero};
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 LayoutTree,
58 node: NodeId,
59 order: u32,
60 grid_area: Rect<f32>,
61 container_alignment_styles: InBothAbsAxis<Option<AlignItems>>,
62 baseline_shim: f32,
63) {
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.style(node);
67 let aspect_ratio = style.aspect_ratio;
68 let justify_self = style.justify_self;
69 let align_self = style.align_self;
70
71 let position = style.position;
72 let inset_horizontal = style.inset.horizontal_components().map(|size| size.resolve_to_option(grid_area_size.width));
73 let inset_vertical = style.inset.vertical_components().map(|size| size.resolve_to_option(grid_area_size.height));
74 let padding = style.padding.map(|p| p.resolve_or_zero(Some(grid_area_size.width)));
75 let border = style.border.map(|p| p.resolve_or_zero(Some(grid_area_size.width)));
76 let padding_border_size = (padding + border).sum_axes();
77 let inherent_size = style.size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
78 let min_size = style
79 .min_size
80 .maybe_resolve(grid_area_size)
81 .or(padding_border_size.map(Some))
82 .maybe_max(padding_border_size)
83 .maybe_apply_aspect_ratio(aspect_ratio);
84 let max_size = style.max_size.maybe_resolve(grid_area_size).maybe_apply_aspect_ratio(aspect_ratio);
85
86 // Resolve default alignment styles if they are set on neither the parent or the node itself
87 // Note: if the child has a preferred aspect ratio but neither width or height are set, then the width is stretched
88 // and the then height is calculated from the width according the aspect ratio
89 // See: https://www.w3.org/TR/css-grid-1/#grid-item-sizing
90 let alignment_styles = InBothAbsAxis {
91 horizontal: justify_self.or(container_alignment_styles.horizontal).unwrap_or_else(|| {
92 if inherent_size.width.is_some() {
93 AlignSelf::Start
94 } else {
95 AlignSelf::Stretch
96 }
97 }),
98 vertical: align_self.or(container_alignment_styles.vertical).unwrap_or_else(|| {
99 if inherent_size.height.is_some() || aspect_ratio.is_some() {
100 AlignSelf::Start
101 } else {
102 AlignSelf::Stretch
103 }
104 }),
105 };
106
107 // Note: This is not a bug. It is part of the CSS spec that both horizontal and vertical margins
108 // resolve against the WIDTH of the grid area.
109 let margin = style.margin.map(|margin| margin.resolve_to_option(grid_area_size.width));
110
111 let grid_area_minus_item_margins_size = Size {
112 width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right),
113 height: grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim,
114 };
115
116 // If node is absolutely positioned and width is not set explicitly, then deduce it
117 // from left, right and container_content_box if both are set.
118 let width = inherent_size.width.or_else(|| {
119 // Apply width derived from both the left and right properties of an absolutely
120 // positioned element being set
121 if position == Position::Absolute {
122 if let (Some(left), Some(right)) = (inset_horizontal.start, inset_horizontal.end) {
123 return Some(f32_max(grid_area_minus_item_margins_size.width - left - right, 0.0));
124 }
125 }
126
127 // Apply width based on stretch alignment if:
128 // - Alignment style is "stretch"
129 // - The node is not absolutely positioned
130 // - The node does not have auto margins in this axis.
131 if margin.left.is_some()
132 && margin.right.is_some()
133 && alignment_styles.horizontal == AlignSelf::Stretch
134 && position != Position::Absolute
135 {
136 return Some(grid_area_minus_item_margins_size.width);
137 }
138
139 None
140 });
141
142 // Reapply aspect ratio after stretch and absolute position width adjustments
143 let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
144
145 let height = height.or_else(|| {
146 if position == Position::Absolute {
147 if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) {
148 return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0));
149 }
150 }
151
152 // Apply height based on stretch alignment if:
153 // - Alignment style is "stretch"
154 // - The node is not absolutely positioned
155 // - The node does not have auto margins in this axis.
156 if margin.top.is_some()
157 && margin.bottom.is_some()
158 && alignment_styles.vertical == AlignSelf::Stretch
159 && position != Position::Absolute
160 {
161 return Some(grid_area_minus_item_margins_size.height);
162 }
163
164 None
165 });
166 // Reapply aspect ratio after stretch and absolute position height adjustments
167 let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
168
169 // Clamp size by min and max width/height
170 let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
171
172 // Layout node
173 let measured_size_and_baselines = tree.perform_child_layout(
174 node,
175 Size { width, height },
176 grid_area_size.map(Option::Some),
177 grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
178 SizingMode::InherentSize,
179 Line::FALSE,
180 );
181
182 // Resolve final size
183 let Size { width, height } =
184 Size { width, height }.unwrap_or(measured_size_and_baselines.size).maybe_clamp(min_size, max_size);
185
186 let x = align_item_within_area(
187 Line { start: grid_area.left, end: grid_area.right },
188 justify_self.unwrap_or(alignment_styles.horizontal),
189 width,
190 position,
191 inset_horizontal,
192 margin.horizontal_components(),
193 0.0,
194 );
195 let y = align_item_within_area(
196 Line { start: grid_area.top, end: grid_area.bottom },
197 align_self.unwrap_or(alignment_styles.vertical),
198 height,
199 position,
200 inset_vertical,
201 margin.vertical_components(),
202 baseline_shim,
203 );
204
205 *tree.layout_mut(node) = Layout { order, size: Size { width, height }, location: Point { x, y } };
206}
207
208/// Align and size a grid item along a single axis
209pub(super) fn align_item_within_area(
210 grid_area: Line<f32>,
211 alignment_style: AlignSelf,
212 resolved_size: f32,
213 position: Position,
214 inset: Line<Option<f32>>,
215 margin: Line<Option<f32>>,
216 baseline_shim: f32,
217) -> f32 {
218 // Calculate grid area dimension in the axis
219 let non_auto_margin = Line { start: margin.start.unwrap_or(0.0) + baseline_shim, end: margin.end.unwrap_or(0.0) };
220 let grid_area_size = f32_max(grid_area.end - grid_area.start, 0.0);
221 let free_space = f32_max(grid_area_size - resolved_size - non_auto_margin.sum(), 0.0);
222
223 // Expand auto margins to fill available space
224 let auto_margin_count = margin.start.is_none() as u8 + margin.end.is_none() as u8;
225 let auto_margin_size = if auto_margin_count > 0 { free_space / auto_margin_count as f32 } else { 0.0 };
226 let resolved_margin = Line {
227 start: margin.start.unwrap_or(auto_margin_size) + baseline_shim,
228 end: margin.end.unwrap_or(auto_margin_size),
229 };
230
231 // Compute offset in the axis
232 let alignment_based_offset = match alignment_style {
233 AlignSelf::Start | AlignSelf::FlexStart => resolved_margin.start,
234 AlignSelf::End | AlignSelf::FlexEnd => grid_area_size - resolved_size - resolved_margin.end,
235 AlignSelf::Center => (grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0,
236 // TODO: Add support for baseline alignment. For now we treat it as "start".
237 AlignSelf::Baseline => resolved_margin.start,
238 AlignSelf::Stretch => resolved_margin.start,
239 };
240
241 let offset_within_area = if position == Position::Absolute {
242 if let Some(start) = inset.start {
243 start + non_auto_margin.start
244 } else if let Some(end) = inset.end {
245 grid_area_size - end - resolved_size - non_auto_margin.end
246 } else {
247 alignment_based_offset
248 }
249 } else {
250 alignment_based_offset
251 };
252
253 let mut start = grid_area.start + offset_within_area;
254 if position == Position::Relative {
255 start += inset.start.or(inset.end.map(|pos| -pos)).unwrap_or(0.0);
256 }
257
258 start
259}