1use super::types::GridTrack;
3use crate::compute::common::alignment::{
4 apply_alignment_fallback, child_first_baseline, compute_alignment_offset, resolve_self_alignment_safety,
5};
6use crate::geometry::{InBothAbsAxis, Line, Point, Rect, Size};
7use crate::style::{
8 AlignContent, AlignItems, AlignItemsKeyword, AlignSelf, AvailableSpace, CoreStyle, GridItemStyle, Overflow,
9 Position,
10};
11use crate::tree::{Layout, LayoutPartialTreeExt, NodeId, OofCandidates, SizingMode};
12use crate::util::sys::f32_max;
13use crate::util::{MaybeMath, MaybeResolve, ResolveOrZero};
14
15#[cfg(feature = "content_size")]
16use crate::compute::common::scrollable_overflow::compute_scrollable_overflow_contribution;
17use crate::compute::common::sizing_keyword::{resolve_sizing_keyword, SizingKeywordResolution};
18use crate::{AbsoluteAxis, BoxSizing, Direction, LayoutGridContainer};
19
20pub(super) fn align_tracks(
24 grid_container_content_box_size: f32,
25 padding: Line<f32>,
26 border: Line<f32>,
27 tracks: &mut [GridTrack],
28 track_alignment_style: AlignContent,
29 axis_is_reversed: bool,
30) {
31 let used_size: f32 = tracks.iter().map(|track| track.base_size).sum();
32 let free_space = grid_container_content_box_size - used_size;
33 let origin = padding.start + border.start;
34
35 let num_tracks = tracks.iter().skip(1).step_by(2).filter(|track| !track.is_collapsed).count();
37
38 let gap = 0.0;
41 let layout_is_reversed = false;
42 let track_alignment = apply_alignment_fallback(free_space, num_tracks, track_alignment_style);
43 let track_alignment = if axis_is_reversed { track_alignment.reversed() } else { track_alignment };
44
45 let empty_grid_offset = if num_tracks == 0 {
49 compute_alignment_offset(free_space, num_tracks, gap, track_alignment, layout_is_reversed, true)
50 } else {
51 0.0
52 };
53
54 let mut total_offset = origin + empty_grid_offset;
57 let mut seen_non_collapsed_track = false;
58 let mut position_track = |i: usize, track: &mut GridTrack| {
59 let is_gutter = i % 2 == 0;
61 let is_non_collapsed_track = !is_gutter && !track.is_collapsed;
62
63 let is_first = is_non_collapsed_track && !seen_non_collapsed_track;
65
66 let offset = if is_non_collapsed_track {
67 compute_alignment_offset(free_space, num_tracks, gap, track_alignment, layout_is_reversed, is_first)
68 } else {
69 0.0
70 };
71
72 track.offset = total_offset + offset;
73 total_offset = total_offset + offset + track.base_size;
74 if is_non_collapsed_track {
75 seen_non_collapsed_track = true;
76 }
77 };
78 if axis_is_reversed {
79 tracks.iter_mut().rev().enumerate().for_each(|(i, track)| position_track(i, track));
80 } else {
81 tracks.iter_mut().enumerate().for_each(|(i, track)| position_track(i, track));
82 }
83}
84
85#[allow(clippy::too_many_arguments)]
87pub(super) fn align_and_position_item(
88 tree: &mut impl LayoutGridContainer,
89 node: NodeId,
90 order: u32,
91 grid_area: Rect<f32>,
92 containing_block_size: Size<f32>,
95 container_alignment_styles: InBothAbsAxis<AlignItems>,
96 baseline_shim: f32,
97 direction: Direction,
98 container_border_box_width: f32,
99 container_border: Rect<f32>,
100 #[cfg(feature = "content_size")] container_is_scroll_container: bool,
101 bubbled_candidates: &mut OofCandidates,
102) -> (Rect<f32>, f32, f32, Option<f32>) {
103 let grid_area_size = Size { width: grid_area.right - grid_area.left, height: grid_area.bottom - grid_area.top };
104
105 let style = tree.get_grid_child_style(node);
106
107 let overflow = style.overflow();
108 #[cfg(feature = "content_size")]
109 let contain = style.contain();
110 let scrollbar_width = style.scrollbar_width();
111 let aspect_ratio = style.aspect_ratio();
112 let is_replaced = style.is_replaced();
113 let item_direction = style.direction();
118 let justify_self = style.justify_self().unwrap_or(container_alignment_styles.horizontal).resolve_self_relative(
119 item_direction,
120 direction,
121 true,
122 );
123 let align_self = style.align_self().unwrap_or(container_alignment_styles.vertical).resolve_self_relative(
124 item_direction,
125 direction,
126 false,
127 );
128
129 let position = style.position();
130 let inset_horizontal = style
131 .inset()
132 .horizontal_components()
133 .map(|size| size.resolve_to_option(containing_block_size.width, |val, basis| tree.calc(val, basis)));
134 let inset_vertical = style
135 .inset()
136 .vertical_components()
137 .map(|size| size.resolve_to_option(containing_block_size.height, |val, basis| tree.calc(val, basis)));
138 let padding = style
139 .padding()
140 .map(|p| p.resolve_or_zero(Some(containing_block_size.width), |val, basis| tree.calc(val, basis)));
141 let border = style
142 .border()
143 .map(|p| p.resolve_or_zero(Some(containing_block_size.width), |val, basis| tree.calc(val, basis)));
144 let padding_border_size = (padding + border).sum_axes();
145
146 let box_sizing_adjustment =
147 if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
148
149 let size_style = style.size();
150 let inherent_size = size_style
151 .maybe_resolve(containing_block_size, |val, basis| tree.calc(val, basis))
152 .maybe_apply_aspect_ratio(aspect_ratio)
153 .maybe_add(box_sizing_adjustment);
154 let min_size = style
155 .min_size()
156 .maybe_resolve(containing_block_size, |val, basis| tree.calc(val, basis))
157 .maybe_add(box_sizing_adjustment)
158 .or(padding_border_size.map(Some))
159 .maybe_max(padding_border_size)
160 .maybe_apply_aspect_ratio(aspect_ratio);
161 let max_size = style
162 .max_size()
163 .maybe_resolve(containing_block_size, |val, basis| tree.calc(val, basis))
164 .maybe_apply_aspect_ratio(aspect_ratio)
165 .maybe_add(box_sizing_adjustment);
166
167 let alignment_styles = InBothAbsAxis {
173 horizontal: match justify_self.keyword {
174 AlignItemsKeyword::Normal => {
175 if inherent_size.width.is_some() || size_style.width.is_sizing_keyword() || is_replaced {
176 AlignSelf::START
177 } else {
178 AlignSelf::STRETCH
179 }
180 }
181 _ => justify_self,
182 },
183 vertical: match align_self.keyword {
184 AlignItemsKeyword::Normal => {
185 if inherent_size.height.is_some()
186 || size_style.height.is_sizing_keyword()
187 || aspect_ratio.is_some()
188 || is_replaced
189 {
190 AlignSelf::START
191 } else {
192 AlignSelf::STRETCH
193 }
194 }
195 _ => align_self,
196 },
197 };
198
199 let margin = style
202 .margin()
203 .map(|margin| margin.resolve_to_option(containing_block_size.width, |val, basis| tree.calc(val, basis)));
204
205 drop(style);
206
207 let grid_area_minus_item_margins_size = Size {
208 width: grid_area_size.width.maybe_sub(margin.left).maybe_sub(margin.right).max(0.0),
209 height: (grid_area_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - baseline_shim).max(0.0),
210 };
211
212 let keyword_width = inherent_size.width.is_none().then(|| {
216 resolve_sizing_keyword(
217 size_style.width,
218 Some(grid_area_minus_item_margins_size.width),
219 Some(containing_block_size.width),
220 |val, basis| tree.calc(val, basis),
221 )
222 });
223 let keyword_height = inherent_size.height.is_none().then(|| {
224 resolve_sizing_keyword(
225 size_style.height,
226 Some(grid_area_minus_item_margins_size.height),
227 Some(containing_block_size.height),
228 |val, basis| tree.calc(val, basis),
229 )
230 });
231
232 let keyword_measured_size: Size<Option<f32>> = match (&keyword_width, &keyword_height) {
234 (
235 Some(Some(SizingKeywordResolution::Measure(available_width))),
236 Some(Some(SizingKeywordResolution::Measure(available_height))),
237 ) if !position.is_out_of_flow() => tree
238 .measure_child_size_both(
239 node,
240 Size::NONE,
241 containing_block_size.map(Option::Some),
242 Size { width: *available_width, height: *available_height },
243 SizingMode::InherentSize,
244 Line::FALSE,
245 )
246 .map(Option::Some),
247 _ => Size::NONE,
248 };
249
250 let width = inherent_size.width.or_else(|| {
253 if position.is_out_of_flow() {
256 if let (Some(left), Some(right)) = (inset_horizontal.start, inset_horizontal.end) {
257 return Some(f32_max(grid_area_minus_item_margins_size.width - left - right, 0.0));
258 }
259 }
260
261 if let Some(Some(resolution)) = keyword_width {
262 return Some(match resolution {
263 SizingKeywordResolution::Exact(width) => width,
264 SizingKeywordResolution::Measure(available_width) => keyword_measured_size.width.unwrap_or_else(|| {
265 tree.measure_child_size(
266 node,
267 Size::NONE,
268 containing_block_size.map(Option::Some),
269 Size {
270 width: available_width,
271 height: AvailableSpace::Definite(grid_area_minus_item_margins_size.height),
272 },
273 SizingMode::InherentSize,
274 AbsoluteAxis::Horizontal,
275 Line::FALSE,
276 )
277 }),
278 });
279 }
280
281 if margin.left.is_some()
286 && margin.right.is_some()
287 && alignment_styles.horizontal == AlignSelf::STRETCH
288 && !position.is_out_of_flow()
289 {
290 return Some(grid_area_minus_item_margins_size.width);
291 }
292
293 None
294 });
295
296 let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio);
298
299 let height = height.or_else(|| {
300 if position.is_out_of_flow() {
301 if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) {
302 return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0));
303 }
304 }
305
306 if let Some(Some(resolution)) = keyword_height {
307 return Some(match resolution {
308 SizingKeywordResolution::Exact(height) => height,
309 SizingKeywordResolution::Measure(available_height) => {
310 keyword_measured_size.height.unwrap_or_else(|| {
311 tree.measure_child_size(
312 node,
313 Size { width, height: None },
314 containing_block_size.map(Option::Some),
315 Size {
316 width: width
317 .map(AvailableSpace::Definite)
318 .unwrap_or(AvailableSpace::Definite(grid_area_minus_item_margins_size.width)),
319 height: available_height,
320 },
321 SizingMode::InherentSize,
322 AbsoluteAxis::Vertical,
323 Line::FALSE,
324 )
325 })
326 }
327 });
328 }
329
330 if margin.top.is_some()
335 && margin.bottom.is_some()
336 && alignment_styles.vertical == AlignSelf::STRETCH
337 && !position.is_out_of_flow()
338 {
339 return Some(grid_area_minus_item_margins_size.height);
340 }
341
342 None
343 });
344 let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio);
346
347 let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size);
349
350 let size = if position.is_out_of_flow() && (width.is_none() || height.is_none()) {
352 tree.measure_child_size_both(
353 node,
354 Size { width, height },
355 containing_block_size.map(Option::Some),
356 grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
357 SizingMode::InherentSize,
358 Line::FALSE,
359 )
360 .map(Some)
361 } else {
362 Size { width, height }
363 };
364
365 let mut layout_output = tree.perform_child_layout(
366 node,
367 size,
368 containing_block_size.map(Option::Some),
369 grid_area_minus_item_margins_size.map(AvailableSpace::Definite),
370 SizingMode::InherentSize,
371 Line::FALSE,
372 );
373
374 let Size { width, height } = size.unwrap_or(layout_output.size).maybe_clamp(min_size, max_size);
376
377 let (x, x_margin) = align_item_within_area(
378 Line { start: grid_area.left, end: grid_area.right },
379 alignment_styles.horizontal,
380 width,
381 position,
382 inset_horizontal,
383 margin.horizontal_components(),
384 0.0,
385 direction,
386 );
387 let (y_without_relative_offset, y_margin) = align_item_within_area(
388 Line { start: grid_area.top, end: grid_area.bottom },
389 alignment_styles.vertical,
390 height,
391 position,
392 inset_vertical,
393 margin.vertical_components(),
394 baseline_shim,
395 Direction::Ltr,
396 );
397 let x = x + relative_offset(position, inset_horizontal, direction);
400 let y = y_without_relative_offset + relative_offset(position, inset_vertical, Direction::Ltr);
401
402 let scrollbar_size = Size {
403 width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
404 height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
405 };
406
407 let resolved_margin = Rect { left: x_margin.start, right: x_margin.end, top: y_margin.start, bottom: y_margin.end };
408
409 let mut item_candidates = layout_output.oof_candidates.take();
412 if !item_candidates.is_empty() {
413 item_candidates.translate(Point { x, y });
414 bubbled_candidates.append(&mut item_candidates);
415 }
416
417 tree.set_unrounded_layout(
418 node,
419 &Layout {
420 order,
421 location: Point { x, y },
422 size: Size { width, height },
423 #[cfg(feature = "content_size")]
424 scrollable_overflow_rect: layout_output.scrollable_overflow_rect,
425 scrollbar_size,
426 padding,
427 border,
428 margin: resolved_margin,
429 },
430 );
431
432 #[cfg(feature = "content_size")]
433 let contribution = {
434 let contribution_location = if direction.is_rtl() {
437 Point { x: container_border_box_width - (x + width) - container_border.right, y: y - container_border.top }
438 } else {
439 Point { x: x - container_border.left, y: y - container_border.top }
440 };
441 compute_scrollable_overflow_contribution(
442 contribution_location,
443 Size { width, height },
444 layout_output.scrollable_overflow_rect,
445 overflow,
446 contain,
447 container_is_scroll_container,
448 )
449 };
450 #[cfg(not(feature = "content_size"))]
451 let contribution = Rect::ZERO;
452
453 let baseline = child_first_baseline(layout_output.baselines.first, height, overflow.y);
454 (contribution, y_without_relative_offset, height, baseline)
455}
456
457#[allow(clippy::too_many_arguments)]
459pub(super) fn align_item_within_area(
460 grid_area: Line<f32>,
461 alignment_style: AlignSelf,
462 resolved_size: f32,
463 position: Position,
464 inset: Line<Option<f32>>,
465 margin: Line<Option<f32>>,
466 baseline_shim: f32,
467 direction: Direction,
468) -> (f32, Line<f32>) {
469 let non_auto_margin = Line { start: margin.start.unwrap_or(0.0) + baseline_shim, end: margin.end.unwrap_or(0.0) };
471 let grid_area_size = f32_max(grid_area.end - grid_area.start, 0.0);
472 let free_space = f32_max(grid_area_size - resolved_size - non_auto_margin.sum(), 0.0);
473
474 let auto_margin_count = margin.start.is_none() as u8 + margin.end.is_none() as u8;
476 let auto_margin_size = if auto_margin_count > 0 { free_space / auto_margin_count as f32 } else { 0.0 };
477 let resolved_margin = Line {
478 start: margin.start.unwrap_or(auto_margin_size) + baseline_shim,
479 end: margin.end.unwrap_or(auto_margin_size),
480 };
481
482 let overflows = resolved_size + non_auto_margin.sum() > grid_area_size;
483 let alignment_keyword = resolve_self_alignment_safety(alignment_style, overflows);
484
485 let alignment_based_offset = match alignment_keyword {
487 AlignItemsKeyword::Start
489 | AlignItemsKeyword::FlexStart
490 | AlignItemsKeyword::Baseline
491 | AlignItemsKeyword::Stretch => {
492 if direction.is_rtl() {
493 grid_area_size - resolved_size - resolved_margin.end
494 } else {
495 resolved_margin.start
496 }
497 }
498 AlignItemsKeyword::End | AlignItemsKeyword::FlexEnd => {
499 if direction.is_rtl() {
500 resolved_margin.start
501 } else {
502 grid_area_size - resolved_size - resolved_margin.end
503 }
504 }
505 AlignItemsKeyword::Center => {
506 (grid_area_size - resolved_size + resolved_margin.start - resolved_margin.end) / 2.0
507 }
508 AlignItemsKeyword::Normal | AlignItemsKeyword::SelfStart | AlignItemsKeyword::SelfEnd => unreachable!(),
511 };
512
513 let offset_within_area = if position.is_out_of_flow() {
514 match (inset.start, inset.end) {
515 (Some(start), Some(end)) => {
516 if direction.is_rtl() {
517 grid_area_size - end - resolved_size - non_auto_margin.end
518 } else {
519 start + non_auto_margin.start
520 }
521 }
522 (Some(start), None) => start + non_auto_margin.start,
523 (None, Some(end)) => grid_area_size - end - resolved_size - non_auto_margin.end,
524 (None, None) => alignment_based_offset,
525 }
526 } else {
527 alignment_based_offset
528 };
529
530 (grid_area.start + offset_within_area, resolved_margin)
531}
532
533fn relative_offset(position: Position, inset: Line<Option<f32>>, direction: Direction) -> f32 {
535 if position != Position::Relative {
536 return 0.0;
537 }
538 let relative_inset = if direction.is_rtl() {
539 inset.end.map(|pos| -pos).or(inset.start)
540 } else {
541 inset.start.or(inset.end.map(|pos| -pos))
542 };
543 relative_inset.unwrap_or(0.0)
544}