1use crate::box_model::BoxModel;
7use crate::fragment::{Fragment, FragmentKind};
8use crate::geometry::{Point, Size};
9use crate::layout::{self, LayoutContext};
10use crate::style::*;
11use crate::tree::NodeId;
12use crate::values::LengthPercentageAuto;
13
14pub fn layout_flex(
16 ctx: &LayoutContext,
17 node: NodeId,
18 containing_block_width: f32,
19 containing_block_height: f32,
20) -> Fragment {
21 let style = ctx.tree.style(node);
22 let mut fragment = Fragment::new(node, FragmentKind::Box);
23
24 let border = BoxModel::resolve_border(style);
26 let padding = BoxModel::resolve_padding(style, containing_block_width);
27 let margin = BoxModel::resolve_margin(style, containing_block_width);
28
29 fragment.border = border;
30 fragment.padding = padding;
31 fragment.margin = margin;
32
33 let content_box_width = match style.width.resolve(containing_block_width) {
35 Some(mut w) => {
36 if style.box_sizing == BoxSizing::BorderBox {
37 w = (w - border.horizontal() - padding.horizontal()).max(0.0);
38 }
39 let min_w = style.min_width.resolve(containing_block_width);
40 let max_w = style
41 .max_width
42 .resolve(containing_block_width)
43 .unwrap_or(f32::INFINITY);
44 w.max(min_w).min(max_w)
45 }
46 None => (containing_block_width
47 - border.horizontal()
48 - padding.horizontal()
49 - margin.horizontal())
50 .max(0.0),
51 };
52
53 let is_row = style.flex_direction.is_row();
54 let is_reverse = style.flex_direction.is_reverse();
55 let is_wrap = style.flex_wrap != FlexWrap::Nowrap;
56 let _is_wrap_reverse = style.flex_wrap == FlexWrap::WrapReverse;
57
58 let main_size = if is_row {
59 content_box_width
60 } else {
61 style
62 .height
63 .resolve(containing_block_height)
64 .unwrap_or(containing_block_height)
65 };
66 let cross_size_available = if is_row {
67 style.height.resolve(containing_block_height)
68 } else {
69 Some(content_box_width)
70 };
71
72 let children = ctx.tree.children(node);
74 let mut items: Vec<FlexItem> = Vec::new();
75
76 for &child_id in children {
77 let child_style = ctx.tree.style(child_id);
78 if child_style.display.is_none() {
79 continue;
80 }
81 if child_style.position.is_absolutely_positioned() {
82 let child_frag =
84 layout::layout_node(ctx, child_id, content_box_width, containing_block_height);
85 fragment.children.push(child_frag);
86 continue;
87 }
88
89 let item = collect_flex_item(
90 ctx,
91 child_id,
92 is_row,
93 content_box_width,
94 containing_block_height,
95 );
96 items.push(item);
97 }
98
99 let lines = collect_flex_lines(&items, main_size, is_wrap);
106
107 let mut resolved_lines = Vec::new();
109
110 for line in &lines {
111 let resolved = resolve_flexible_lengths(line, &items, main_size);
112 resolved_lines.push(resolved);
113 }
114
115 let mut line_cross_sizes: Vec<f32> = Vec::new();
117 for (line_idx, line) in lines.iter().enumerate() {
118 let mut max_cross: f32 = 0.0;
119 for &item_idx in &line.item_indices {
120 let item = &items[item_idx];
121 let resolved_main = resolved_lines[line_idx].sizes[&item_idx];
122
123 let cross = compute_item_cross_size(
125 ctx,
126 item,
127 resolved_main,
128 is_row,
129 content_box_width,
130 containing_block_height,
131 );
132 max_cross = max_cross.max(cross);
133 }
134 line_cross_sizes.push(max_cross);
135 }
136
137 let total_cross: f32 = line_cross_sizes.iter().sum();
139 let container_cross = cross_size_available.unwrap_or(total_cross);
140
141 let mut cross_offset: f32 = 0.0;
145
146 let extra_cross = (container_cross - total_cross).max(0.0);
148 let (cross_start, cross_between) =
149 distribute_alignment(style.align_content, extra_cross, resolved_lines.len());
150 cross_offset += cross_start;
151
152 for (line_idx, line) in lines.iter().enumerate() {
153 let line_cross = line_cross_sizes[line_idx];
154 let resolved = &resolved_lines[line_idx];
155
156 let total_main: f32 = line
158 .item_indices
159 .iter()
160 .map(|&i| resolved.sizes[&i] + items[i].main_margin())
161 .sum();
162 let extra_main = (main_size - total_main).max(0.0);
163 let (main_start, main_between) =
164 distribute_justify(style.justify_content, extra_main, line.item_indices.len());
165
166 let mut main_offset = main_start;
167
168 let indices: Vec<usize> = if is_reverse {
169 line.item_indices.iter().rev().copied().collect()
170 } else {
171 line.item_indices.clone()
172 };
173
174 for &item_idx in &indices {
175 let item = &items[item_idx];
176 let item_main = resolved.sizes[&item_idx];
177
178 let (item_width, item_height) = if is_row {
180 (item_main, line_cross)
181 } else {
182 (line_cross, item_main)
183 };
184
185 let mut child_frag = layout_flex_item(
186 ctx,
187 item.node,
188 item_width,
189 item_height,
190 is_row,
191 content_box_width,
192 containing_block_height,
193 );
194
195 let align = effective_align(style.align_items, ctx.tree.style(item.node).align_self);
197 let item_cross = if is_row {
198 child_frag.border_box().height
199 } else {
200 child_frag.border_box().width
201 };
202 let cross_align_offset = match align {
203 AlignItems::FlexStart | AlignItems::Start => 0.0,
204 AlignItems::FlexEnd | AlignItems::End => line_cross - item_cross,
205 AlignItems::Center => (line_cross - item_cross) / 2.0,
206 AlignItems::Stretch => 0.0,
207 AlignItems::Baseline => 0.0, };
209
210 if is_row {
212 child_frag.position = Point::new(
213 main_offset + child_frag.margin.left,
214 cross_offset + cross_align_offset + child_frag.margin.top,
215 );
216 } else {
217 child_frag.position = Point::new(
218 cross_offset + cross_align_offset + child_frag.margin.left,
219 main_offset + child_frag.margin.top,
220 );
221 }
222
223 main_offset += item_main + item.main_margin() + main_between;
224 fragment.children.push(child_frag);
225 }
226
227 cross_offset += line_cross + cross_between;
228 }
229
230 let content_height = if is_row { container_cross } else { main_size };
232 let final_height = style
233 .height
234 .resolve(containing_block_height)
235 .unwrap_or(content_height);
236
237 let min_h = style.min_height.resolve(containing_block_height);
238 let max_h = style
239 .max_height
240 .resolve(containing_block_height)
241 .unwrap_or(f32::INFINITY);
242
243 fragment.size = Size::new(content_box_width, final_height.max(min_h).min(max_h));
244
245 fragment
246}
247
248struct FlexItem {
250 node: NodeId,
251 flex_base_size: f32,
252 hypothetical_main_size: f32,
253 flex_grow: f32,
254 flex_shrink: f32,
255 min_main: f32,
256 max_main: f32,
257 main_margin_start: f32,
258 main_margin_end: f32,
259}
260
261impl FlexItem {
262 fn main_margin(&self) -> f32 {
263 self.main_margin_start + self.main_margin_end
264 }
265}
266
267struct FlexLine {
268 item_indices: Vec<usize>,
269}
270
271struct ResolvedLine {
272 sizes: std::collections::HashMap<usize, f32>,
273}
274
275fn collect_flex_item(
276 ctx: &LayoutContext,
277 node: NodeId,
278 is_row: bool,
279 containing_width: f32,
280 containing_height: f32,
281) -> FlexItem {
282 let style = ctx.tree.style(node);
283 let box_model = BoxModel::resolve(style, containing_width);
284
285 let flex_base_size = match &style.flex_basis {
287 LengthPercentageAuto::Length(v) => *v,
288 LengthPercentageAuto::Percentage(pct) => {
289 let reference = if is_row {
290 containing_width
291 } else {
292 containing_height
293 };
294 reference * pct
295 }
296 LengthPercentageAuto::Auto => {
297 let main_size = if is_row { &style.width } else { &style.height };
299 let reference = if is_row {
300 containing_width
301 } else {
302 containing_height
303 };
304 match main_size.resolve(reference) {
305 Some(v) => v,
306 None => {
307 let child_frag =
309 layout::layout_node(ctx, node, containing_width, containing_height);
310 if is_row {
311 child_frag.size.width
312 } else {
313 child_frag.size.height
314 }
315 }
316 }
317 }
318 };
319
320 let (min_main, max_main) = if is_row {
321 (
322 style.min_width.resolve(containing_width),
323 style
324 .max_width
325 .resolve(containing_width)
326 .unwrap_or(f32::INFINITY),
327 )
328 } else {
329 (
330 style.min_height.resolve(containing_height),
331 style
332 .max_height
333 .resolve(containing_height)
334 .unwrap_or(f32::INFINITY),
335 )
336 };
337
338 let hypothetical = flex_base_size.max(min_main).min(max_main);
339
340 let (main_margin_start, main_margin_end) = if is_row {
341 (box_model.margin.left, box_model.margin.right)
342 } else {
343 (box_model.margin.top, box_model.margin.bottom)
344 };
345
346 FlexItem {
347 node,
348 flex_base_size,
349 hypothetical_main_size: hypothetical,
350 flex_grow: style.flex_grow,
351 flex_shrink: style.flex_shrink,
352 min_main,
353 max_main,
354 main_margin_start,
355 main_margin_end,
356 }
357}
358
359fn collect_flex_lines(items: &[FlexItem], main_size: f32, wrap: bool) -> Vec<FlexLine> {
361 if items.is_empty() {
362 return vec![FlexLine {
363 item_indices: Vec::new(),
364 }];
365 }
366
367 if !wrap {
368 return vec![FlexLine {
369 item_indices: (0..items.len()).collect(),
370 }];
371 }
372
373 let mut lines = Vec::new();
374 let mut current_line = Vec::new();
375 let mut line_main = 0.0f32;
376
377 for (i, item) in items.iter().enumerate() {
378 let item_outer = item.hypothetical_main_size + item.main_margin();
379 if !current_line.is_empty() && line_main + item_outer > main_size {
380 lines.push(FlexLine {
381 item_indices: std::mem::take(&mut current_line),
382 });
383 line_main = 0.0;
384 }
385 current_line.push(i);
386 line_main += item_outer;
387 }
388
389 if !current_line.is_empty() {
390 lines.push(FlexLine {
391 item_indices: current_line,
392 });
393 }
394
395 lines
396}
397
398fn resolve_flexible_lengths(line: &FlexLine, items: &[FlexItem], main_size: f32) -> ResolvedLine {
400 let mut sizes = std::collections::HashMap::new();
401
402 let total_hypothetical: f32 = line
404 .item_indices
405 .iter()
406 .map(|&i| items[i].hypothetical_main_size + items[i].main_margin())
407 .sum();
408 let free_space = main_size - total_hypothetical;
409
410 let growing = free_space > 0.0;
411
412 let mut frozen: Vec<bool> = vec![false; items.len()];
414 let mut target_sizes: Vec<f32> = items.iter().map(|i| i.hypothetical_main_size).collect();
415
416 for &idx in &line.item_indices {
417 let item = &items[idx];
418 if (growing && item.flex_grow == 0.0) || (!growing && item.flex_shrink == 0.0) {
419 frozen[idx] = true;
420 }
421 if growing && item.flex_base_size > item.hypothetical_main_size {
423 frozen[idx] = true;
424 }
425 if !growing && item.flex_base_size < item.hypothetical_main_size {
426 frozen[idx] = true;
427 }
428 }
429
430 for _ in 0..10 {
432 let frozen_space: f32 = line
434 .item_indices
435 .iter()
436 .filter(|&&i| frozen[i])
437 .map(|&i| target_sizes[i] + items[i].main_margin())
438 .sum();
439
440 let unfrozen_base: f32 = line
441 .item_indices
442 .iter()
443 .filter(|&&i| !frozen[i])
444 .map(|&i| items[i].flex_base_size + items[i].main_margin())
445 .sum();
446
447 let remaining = main_size - frozen_space - unfrozen_base;
448
449 if growing {
451 let total_grow: f32 = line
452 .item_indices
453 .iter()
454 .filter(|&&i| !frozen[i])
455 .map(|&i| items[i].flex_grow)
456 .sum();
457
458 if total_grow > 0.0 {
459 for &idx in &line.item_indices {
460 if !frozen[idx] {
461 let ratio = items[idx].flex_grow / total_grow;
462 target_sizes[idx] = items[idx].flex_base_size + remaining * ratio;
463 }
464 }
465 }
466 } else {
467 let total_shrink_scaled: f32 = line
468 .item_indices
469 .iter()
470 .filter(|&&i| !frozen[i])
471 .map(|&i| items[i].flex_shrink * items[i].flex_base_size)
472 .sum();
473
474 if total_shrink_scaled > 0.0 {
475 for &idx in &line.item_indices {
476 if !frozen[idx] {
477 let ratio = (items[idx].flex_shrink * items[idx].flex_base_size)
478 / total_shrink_scaled;
479 target_sizes[idx] = items[idx].flex_base_size + remaining * ratio;
480 }
481 }
482 }
483 }
484
485 let mut any_frozen = false;
487 for &idx in &line.item_indices {
488 if frozen[idx] {
489 continue;
490 }
491 let clamped = target_sizes[idx]
492 .max(items[idx].min_main)
493 .min(items[idx].max_main);
494 if (clamped - target_sizes[idx]).abs() > 0.001 {
495 target_sizes[idx] = clamped;
496 frozen[idx] = true;
497 any_frozen = true;
498 }
499 }
500
501 if !any_frozen {
502 for &idx in &line.item_indices {
504 frozen[idx] = true;
505 }
506 break;
507 }
508 }
509
510 for &idx in &line.item_indices {
512 target_sizes[idx] = target_sizes[idx].max(0.0);
513 sizes.insert(idx, target_sizes[idx]);
514 }
515
516 ResolvedLine { sizes }
517}
518
519fn compute_item_cross_size(
520 ctx: &LayoutContext,
521 item: &FlexItem,
522 main_size: f32,
523 is_row: bool,
524 containing_width: f32,
525 containing_height: f32,
526) -> f32 {
527 let style = ctx.tree.style(item.node);
528 let box_model = BoxModel::resolve(style, containing_width);
529
530 if is_row {
531 match style.height.resolve(containing_height) {
533 Some(h) => h + box_model.border.vertical() + box_model.padding.vertical(),
534 None => {
535 let frag = layout::layout_node(ctx, item.node, main_size, containing_height);
536 frag.border_box().height
537 }
538 }
539 } else {
540 match style.width.resolve(containing_width) {
542 Some(w) => w + box_model.border.horizontal() + box_model.padding.horizontal(),
543 None => {
544 let frag = layout::layout_node(ctx, item.node, containing_width, main_size);
545 frag.border_box().width
546 }
547 }
548 }
549}
550
551fn layout_flex_item(
552 ctx: &LayoutContext,
553 node: NodeId,
554 width: f32,
555 height: f32,
556 _is_row: bool,
557 _containing_width: f32,
558 _containing_height: f32,
559) -> Fragment {
560 layout::layout_node(ctx, node, width, height)
563}
564
565fn effective_align(container: AlignItems, item: AlignSelf) -> AlignItems {
566 match item {
567 AlignSelf::Auto => container,
568 AlignSelf::Stretch => AlignItems::Stretch,
569 AlignSelf::FlexStart => AlignItems::FlexStart,
570 AlignSelf::FlexEnd => AlignItems::FlexEnd,
571 AlignSelf::Center => AlignItems::Center,
572 AlignSelf::Baseline => AlignItems::Baseline,
573 AlignSelf::Start => AlignItems::Start,
574 AlignSelf::End => AlignItems::End,
575 }
576}
577
578fn distribute_alignment(align: AlignContent, extra: f32, line_count: usize) -> (f32, f32) {
580 if line_count == 0 {
581 return (0.0, 0.0);
582 }
583 match align {
584 AlignContent::FlexStart | AlignContent::Start => (0.0, 0.0),
585 AlignContent::FlexEnd | AlignContent::End => (extra, 0.0),
586 AlignContent::Center => (extra / 2.0, 0.0),
587 AlignContent::SpaceBetween => {
588 if line_count <= 1 {
589 (0.0, 0.0)
590 } else {
591 (0.0, extra / (line_count - 1) as f32)
592 }
593 }
594 AlignContent::SpaceAround => {
595 let gap = extra / line_count as f32;
596 (gap / 2.0, gap)
597 }
598 AlignContent::SpaceEvenly => {
599 let gap = extra / (line_count + 1) as f32;
600 (gap, gap)
601 }
602 AlignContent::Stretch => (0.0, 0.0),
603 }
604}
605
606fn distribute_justify(justify: JustifyContent, extra: f32, item_count: usize) -> (f32, f32) {
608 if item_count == 0 {
609 return (0.0, 0.0);
610 }
611 match justify {
612 JustifyContent::FlexStart | JustifyContent::Start => (0.0, 0.0),
613 JustifyContent::FlexEnd | JustifyContent::End => (extra, 0.0),
614 JustifyContent::Center => (extra / 2.0, 0.0),
615 JustifyContent::SpaceBetween => {
616 if item_count <= 1 {
617 (0.0, 0.0)
618 } else {
619 (0.0, extra / (item_count - 1) as f32)
620 }
621 }
622 JustifyContent::SpaceAround => {
623 let gap = extra / item_count as f32;
624 (gap / 2.0, gap)
625 }
626 JustifyContent::SpaceEvenly => {
627 let gap = extra / (item_count + 1) as f32;
628 (gap, gap)
629 }
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::layout::{compute_layout, FixedWidthTextMeasure};
637 use crate::style::ComputedStyle;
638 use crate::tree::BoxTreeBuilder;
639
640 #[test]
641 fn test_flex_row_equal_items() {
642 let mut builder = BoxTreeBuilder::new();
643 let root_style = ComputedStyle {
644 display: Display::FLEX,
645 ..ComputedStyle::block()
646 };
647 let root = builder.root(root_style);
648
649 for _ in 0..3 {
650 let mut item_style = ComputedStyle::block();
651 item_style.flex_grow = 1.0;
652 item_style.height = LengthPercentageAuto::px(50.0);
653 builder.element(root, item_style);
654 }
655
656 let tree = builder.build();
657 let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(900.0, 600.0));
658
659 let children = tree.children(tree.root());
661 for (i, &child) in children.iter().enumerate() {
662 let rect = result.bounding_rect(child).unwrap();
663 assert!(
664 (rect.width - 300.0).abs() < 1.0,
665 "item {} width: {}",
666 i,
667 rect.width
668 );
669 }
670 }
671
672 #[test]
673 fn test_flex_justify_space_between() {
674 let mut builder = BoxTreeBuilder::new();
675 let root_style = ComputedStyle {
676 display: Display::FLEX,
677 justify_content: JustifyContent::SpaceBetween,
678 ..ComputedStyle::block()
679 };
680 let root = builder.root(root_style);
681
682 for _ in 0..3 {
683 let mut item_style = ComputedStyle::block();
684 item_style.width = LengthPercentageAuto::px(100.0);
685 item_style.height = LengthPercentageAuto::px(50.0);
686 builder.element(root, item_style);
687 }
688
689 let tree = builder.build();
690 let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(600.0, 600.0));
691
692 let children = tree.children(tree.root());
693 let r0 = result.bounding_rect(children[0]).unwrap();
694 let r1 = result.bounding_rect(children[1]).unwrap();
695 let r2 = result.bounding_rect(children[2]).unwrap();
696
697 assert!((r0.x - 0.0).abs() < 1.0);
698 assert!((r1.x - 250.0).abs() < 1.0); assert!((r2.x - 500.0).abs() < 1.0);
700 }
701
702 #[test]
703 fn test_distribute_justify_space_evenly() {
704 let (start, between) = distribute_justify(JustifyContent::SpaceEvenly, 400.0, 3);
705 assert_eq!(start, 100.0);
706 assert_eq!(between, 100.0);
707 }
708}