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