1use ribir_core::prelude::{log::warn, *};
2
3use super::{Direction, Expanded};
4
5#[derive(Debug, Copy, Clone, PartialEq, Default)]
7pub enum JustifyContent {
8 #[default]
10 Start,
11 Center,
13 End,
15 SpaceBetween,
20 SpaceAround,
25 SpaceEvenly,
30}
31
32#[derive(Default, MultiChild, Declare, Query, Clone, PartialEq)]
33pub struct Flex {
34 #[declare(default)]
36 pub reverse: bool,
37 #[declare(default)]
40 pub wrap: bool,
41 #[declare(default)]
44 pub direction: Direction,
45 #[declare(default)]
47 pub align_items: Align,
48 #[declare(default)]
50 pub justify_content: JustifyContent,
51 #[declare(default)]
53 pub item_gap: f32,
54 #[declare(default)]
56 pub line_gap: f32,
57}
58
59pub struct Row;
61
62pub struct Column;
64
65impl Declare for Row {
66 type Builder = FlexDeclarer;
67 fn declarer() -> Self::Builder { Flex::declarer().direction(Direction::Horizontal) }
68}
69
70impl Declare for Column {
71 type Builder = FlexDeclarer;
72 fn declarer() -> Self::Builder { Flex::declarer().direction(Direction::Vertical) }
73}
74
75impl Render for Flex {
76 fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size {
77 if Align::Stretch == self.align_items && self.wrap {
78 warn!("stretch align and wrap property is conflict");
79 }
80 let direction = self.direction;
81 let max_size = FlexSize::from_size(clamp.max, direction);
82 let mut min_size = FlexSize::from_size(clamp.min, direction);
83 if Align::Stretch == self.align_items {
84 min_size.cross = max_size.cross;
85 }
86 let mut layouter = FlexLayouter {
87 max: max_size,
88 min: min_size,
89 reverse: self.reverse,
90 dir: direction,
91 align_items: self.align_items,
92 justify_content: self.justify_content,
93 wrap: self.wrap,
94 main_axis_gap: self.item_gap,
95 cross_axis_gap: self.line_gap,
96 current_line: <_>::default(),
97 lines: vec![],
98 };
99 layouter.layout(ctx)
100 }
101
102 #[inline]
103 fn paint(&self, _: &mut PaintingCtx) {}
104}
105
106#[derive(Debug, Clone, Copy, Default)]
107struct FlexSize {
108 main: f32,
109 cross: f32,
110}
111
112impl FlexSize {
113 fn to_size(self, dir: Direction) -> Size {
114 match dir {
115 Direction::Horizontal => Size::new(self.main, self.cross),
116 Direction::Vertical => Size::new(self.cross, self.main),
117 }
118 }
119
120 fn from_size(size: Size, dir: Direction) -> Self {
121 match dir {
122 Direction::Horizontal => Self { main: size.width, cross: size.height },
123 Direction::Vertical => Self { cross: size.width, main: size.height },
124 }
125 }
126
127 fn zero() -> Self { Self { main: 0., cross: 0. } }
128}
129
130impl std::ops::Sub for FlexSize {
131 type Output = Self;
132 fn sub(self, rhs: Self) -> Self::Output {
133 FlexSize { main: self.main - rhs.main, cross: self.cross - rhs.cross }
134 }
135}
136
137struct FlexLayouter {
138 max: FlexSize,
139 min: FlexSize,
140 reverse: bool,
141 dir: Direction,
142 align_items: Align,
143 justify_content: JustifyContent,
144 wrap: bool,
145 current_line: MainLineInfo,
146 lines: Vec<MainLineInfo>,
147 main_axis_gap: f32,
148 cross_axis_gap: f32,
149}
150
151impl FlexLayouter {
152 fn layout(&mut self, ctx: &mut LayoutCtx) -> Size {
153 self.perform_children_layout(ctx);
154 self.flex_children_layout(ctx);
155
156 let cross = self
158 .lines
159 .iter()
160 .fold(-self.cross_axis_gap, |sum, l| sum + l.cross_line_height + self.cross_axis_gap);
161 let main = match self.justify_content {
162 JustifyContent::Start | JustifyContent::Center | JustifyContent::End => self
163 .lines
164 .iter()
165 .fold(0f32, |max, l| max.max(l.main_width)),
166 JustifyContent::SpaceBetween | JustifyContent::SpaceAround | JustifyContent::SpaceEvenly => {
167 self.max.main
168 }
169 };
170 let size = FlexSize { cross, main };
171 let &mut Self { max, min, dir, .. } = self;
172 let size = size
173 .to_size(dir)
174 .clamp(min.to_size(dir), max.to_size(dir));
175 self.update_children_position(FlexSize::from_size(size, dir), ctx);
176 size
177 }
178
179 fn perform_children_layout(&mut self, ctx: &mut LayoutCtx) {
180 let mut layouter = ctx.first_child_layouter();
182 let &mut Self { max, min, wrap, dir, .. } = self;
183 let min = if self.align_items == Align::Stretch {
184 FlexSize { main: 0., cross: min.cross }
185 } else {
186 FlexSize::zero()
187 };
188 let mut gap = 0.;
189 while let Some(mut l) = layouter {
190 let mut max = max;
191 if !wrap {
192 max.main -= self.current_line.main_width;
193 }
194
195 let clamp = BoxClamp { max: max.to_size(dir), min: min.to_size(dir) };
196
197 let size = l.perform_widget_layout(clamp);
198 let size = FlexSize::from_size(size, dir);
199
200 let mut flex = None;
201 l.query_type(|expanded: &Expanded| flex = Some(expanded.flex));
202
203 let line = &mut self.current_line;
206 if let Some(flex) = flex {
207 if size.main > 0. {
209 line.flex_sum += flex;
210 }
211 line.main_width += gap;
212 } else {
213 if wrap && !line.is_empty() && line.main_width + size.main > max.main {
214 self.place_line();
215 } else {
216 line.main_width += gap;
217 }
218
219 let line = &mut self.current_line;
220 line.main_width += size.main;
221 line.cross_line_height = line.cross_line_height.max(size.cross);
222 }
223
224 self
225 .current_line
226 .items_info
227 .push(FlexLayoutInfo { size, flex, pos: <_>::default() });
228
229 layouter = l.into_next_sibling();
230 if layouter.is_some() && !FlexLayouter::is_space_layout(self.justify_content) {
231 gap = self.main_axis_gap;
232 } else {
233 gap = 0.;
234 }
235 }
236 self.place_line();
237 }
238
239 fn is_space_layout(justify_content: JustifyContent) -> bool {
240 matches!(
241 justify_content,
242 JustifyContent::SpaceAround | JustifyContent::SpaceBetween | JustifyContent::SpaceEvenly
243 )
244 }
245
246 fn flex_children_layout(&mut self, ctx: &mut LayoutCtx) {
247 let mut layouter = ctx.first_child_layouter();
248 self.lines.iter_mut().for_each(|line| {
249 let flex_unit = (self.max.main - line.main_width) / line.flex_sum;
250 line.items_info.iter_mut().for_each(|info| {
251 let mut l = layouter.take().unwrap();
252 if info.size.main > 0. {
253 if let Some(flex) = info.flex {
254 let &mut Self { mut max, mut min, dir, .. } = self;
255 let main = flex_unit * flex;
256 max.main = main;
257 min.main = main;
258 let clamp = BoxClamp { max: max.to_size(dir), min: min.to_size(dir) };
259 let size = l.perform_widget_layout(clamp);
260 info.size = FlexSize::from_size(size, dir);
261 line.main_width += info.size.main;
262 line.cross_line_height = line.cross_line_height.max(info.size.cross);
263 }
264 }
265
266 layouter = l.into_next_sibling();
267 });
268 });
269 }
270
271 fn update_children_position(&mut self, bound: FlexSize, ctx: &mut LayoutCtx) {
272 let Self { reverse, dir, align_items, justify_content, lines, .. } = self;
273
274 let cross_size = lines.iter().map(|l| l.cross_line_height).sum();
275 let cross_gap_count =
277 if !lines.is_empty() { (lines.len() - 1) as f32 * self.cross_axis_gap } else { 0. };
278 let cross_offset = align_items.align_value(cross_size, bound.cross - cross_gap_count);
279
280 macro_rules! update_position {
281 ($($rev: ident)?) => {
282 let mut cross = cross_offset - self.cross_axis_gap;
283 lines.iter_mut()$(.$rev())?.for_each(|line| {
284 let (mut main, step) = line.place_args(bound.main, *justify_content, self.main_axis_gap);
285 line.items_info.iter_mut()$(.$rev())?.for_each(|item| {
286 let item_cross_offset =
287 align_items.align_value(item.size.cross, line.cross_line_height);
288
289 item.pos.cross = cross + item_cross_offset + self.cross_axis_gap;
290 item.pos.main = main;
291 main = main + item.size.main + step;
292 });
293 cross += line.cross_line_height + self.cross_axis_gap;
294 });
295 };
296 }
297 if *reverse {
298 update_position!(rev);
299 } else {
300 update_position!();
301 }
302
303 let mut layouter = ctx.first_child_layouter();
304 self.lines.iter_mut().for_each(|line| {
305 line.items_info.iter_mut().for_each(|info| {
306 let mut l = layouter.take().unwrap();
307 l.update_position(info.pos.to_size(*dir).to_vector().to_point());
308 layouter = l.into_next_sibling();
309 })
310 });
311 }
312
313 fn place_line(&mut self) {
314 if !self.current_line.is_empty() {
315 self
316 .lines
317 .push(std::mem::take(&mut self.current_line));
318 }
319 }
320}
321
322#[derive(Default)]
323struct MainLineInfo {
324 main_width: f32,
325 items_info: Vec<FlexLayoutInfo>,
326 flex_sum: f32,
327 cross_line_height: f32,
328}
329
330struct FlexLayoutInfo {
331 pos: FlexSize,
332 size: FlexSize,
333 flex: Option<f32>,
334}
335
336impl MainLineInfo {
337 fn is_empty(&self) -> bool { self.items_info.is_empty() }
338
339 fn place_args(&self, main_max: f32, justify_content: JustifyContent, gap: f32) -> (f32, f32) {
340 if self.items_info.is_empty() {
341 return (0., 0.);
342 }
343
344 let item_cnt = self.items_info.len() as f32;
345 match justify_content {
346 JustifyContent::Start => (0., gap),
347 JustifyContent::Center => ((main_max - self.main_width) / 2., gap),
348 JustifyContent::End => (main_max - self.main_width, gap),
349 JustifyContent::SpaceAround => {
350 let step = (main_max - self.main_width) / item_cnt;
351 (step / 2., step)
352 }
353 JustifyContent::SpaceBetween => {
354 let step = (main_max - self.main_width) / (item_cnt - 1.);
355 (0., step)
356 }
357 JustifyContent::SpaceEvenly => {
358 let step = (main_max - self.main_width) / (item_cnt + 1.);
359 (step, step)
360 }
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use ribir_core::test_helper::*;
368 use ribir_dev_helper::*;
369
370 use super::*;
371 use crate::prelude::*;
372
373 fn horizontal_line() -> impl WidgetBuilder {
374 fn_widget! {
375 @Flex {
376 @{
377 (0..10).map(|_| SizedBox { size: Size::new(10., 20.) })
378 }
379 }
380 }
381 }
382 widget_layout_test!(horizontal_line, width == 100., height == 20.,);
383
384 fn vertical_line() -> impl WidgetBuilder {
385 fn_widget! {
386 @Flex {
387 direction: Direction::Vertical,
388 @{ (0..10).map(|_| SizedBox { size: Size::new(10., 20.) })}
389 }
390 }
391 }
392 widget_layout_test!(vertical_line, width == 10., height == 200.,);
393
394 fn row_wrap() -> impl WidgetBuilder {
395 let size = Size::new(200., 20.);
396 fn_widget! {
397 @Flex {
398 wrap: true,
399 @{ (0..3).map(|_| SizedBox { size }) }
400 }
401 }
402 }
403 widget_layout_test!(
404 row_wrap,
405 wnd_size = Size::new(500., 500.),
406 {path = [0], width == 400., height == 40.,}
407 {path = [0, 0], width == 200., height == 20.,}
408 {path = [0, 1], x == 200., width == 200., height == 20.,}
409 {path = [0, 2], rect == ribir_geom::rect(0., 20., 200., 20.),}
410 );
411
412 fn reverse_row_wrap() -> impl WidgetBuilder {
413 let size = Size::new(200., 20.);
414 fn_widget! {
415 @Flex {
416 wrap: true,
417 reverse: true,
418 @{ (0..3).map(|_| SizedBox { size }) }
419 }
420 }
421 }
422 widget_layout_test!(
423 reverse_row_wrap,
424 wnd_size = Size::new(500., 500.),
425 { path = [0], size == Size::new(400., 40.),}
426 { path = [0,0], rect == ribir_geom::rect(200., 20., 200., 20.),}
427 { path = [0, 1], rect == ribir_geom::rect(0., 20., 200., 20.),}
428 { path = [0, 2], rect == ribir_geom::rect(0., 0., 200., 20.),}
429 );
430
431 fn main_axis_gap() -> impl WidgetBuilder {
432 fn_widget! {
433 @Row {
434 item_gap: 15.,
435 @SizedBox { size: Size::new(120., 20.) }
436 @SizedBox { size: Size::new(80., 20.) }
437 @SizedBox { size: Size::new(30., 20.) }
438 }
439 }
440 }
441 widget_layout_test!(
442 main_axis_gap,
443 wnd_size = Size::new(500., 40.),
444 { path = [0, 0], rect == ribir_geom::rect(0., 0., 120., 20.),}
445 { path = [0, 1], rect == ribir_geom::rect(135., 0., 80., 20.),}
446 { path = [0, 2], rect == ribir_geom::rect(230., 0., 30., 20.),}
447 );
448
449 fn main_axis_reverse_gap() -> impl WidgetBuilder {
450 fn_widget! {
451 @Row {
452 item_gap: 15.,
453 reverse: true,
454 @SizedBox { size: Size::new(120., 20.) }
455 @SizedBox { size: Size::new(80., 20.) }
456 @SizedBox { size: Size::new(30., 20.) }
457 }
458 }
459 }
460 widget_layout_test!(
461 main_axis_reverse_gap,
462 wnd_size = Size::new(500., 40.),
463 { path = [0, 0], rect == ribir_geom::rect(140., 0., 120., 20.),}
464 { path = [0, 1], rect == ribir_geom::rect(45., 0., 80., 20.),}
465 { path = [0, 2], rect == ribir_geom::rect(0., 0., 30., 20.),}
466 );
467
468 fn main_axis_expand() -> impl WidgetBuilder {
469 fn_widget! {
470 @Row {
471 item_gap: 15.,
472 @SizedBox { size: Size::new(120., 20.) }
473 @Expanded {
474 flex: 1.,
475 @SizedBox { size: Size::new(10., 20.) }
476 }
477 @SizedBox { size: Size::new(80., 20.) }
478 @Expanded {
479 flex: 2.,
480 @SizedBox { size: Size::new(10., 20.) }
481 }
482 @SizedBox { size: Size::new(30., 20.) }
483 }
484 }
485 }
486 widget_layout_test!(
487 main_axis_expand,
488 wnd_size = Size::new(500., 40.),
489 { path = [0, 0], rect == ribir_geom::rect(0., 0., 120., 20.),}
490 { path = [0, 1], rect == ribir_geom::rect(135., 0., 70., 20.),}
491 { path = [0, 2], rect == ribir_geom::rect(220., 0., 80., 20.),}
492 { path = [0, 3], rect == ribir_geom::rect(315., 0., 140., 20.),}
493 { path = [0, 4], rect == ribir_geom::rect(470., 0., 30., 20.),}
494 );
495
496 fn cross_axis_gap() -> impl WidgetBuilder {
497 let size = Size::new(200., 20.);
498 fn_widget! {
499 @Flex {
500 wrap: true,
501 line_gap: 10.,
502 align_items: Align::Center,
503 @{ (0..3).map(|_| SizedBox { size }) }
504 }
505 }
506 }
507 widget_layout_test!(
508 cross_axis_gap,
509 wnd_size = Size::new(500., 500.),
510 { path = [0], rect == ribir_geom::rect(0., 0., 400., 50.),}
511 { path = [0, 0], rect == ribir_geom::rect(0., 0., 200., 20.),}
512 { path = [0, 1], rect == ribir_geom::rect(200., 0., 200., 20.),}
513 { path = [0, 2], rect == ribir_geom::rect(0., 30., 200., 20.),}
514 );
515
516 fn cross_align(align: Align) -> impl WidgetBuilder {
517 fn_widget! {
518 @Row {
519 align_items: align,
520 @SizedBox { size: Size::new(100., 20.) }
521 @SizedBox { size: Size::new(100., 30.) }
522 @SizedBox { size: Size::new(100., 40.) }
523 }
524 }
525 }
526
527 fn start_cross_align() -> impl WidgetBuilder { cross_align(Align::Start) }
528 widget_layout_test!(
529 start_cross_align,
530 { path =[0], width == 300., height == 40., }
531 { path =[0, 0], rect == ribir_geom::rect(0., 0., 100., 20.),}
532 { path =[0, 1], rect == ribir_geom::rect(100., 0., 100., 30.),}
533 { path =[0, 2], rect == ribir_geom::rect(200., 0., 100., 40.),}
534 );
535
536 fn center_cross_align() -> impl WidgetBuilder { cross_align(Align::Center) }
537 widget_layout_test!(
538 center_cross_align,
539 { path =[0], width == 300., height == 40., }
540 { path =[0, 0], rect == ribir_geom::rect(0., 10., 100., 20.),}
541 { path =[0, 1], rect == ribir_geom::rect(100., 5., 100., 30.),}
542 { path =[0, 2], rect == ribir_geom::rect(200., 0., 100., 40.),}
543 );
544
545 fn end_cross_align() -> impl WidgetBuilder { cross_align(Align::End) }
546 widget_layout_test!(
547 end_cross_align,
548 { path =[0], width == 300., height == 40., }
549 { path =[0, 0], rect == ribir_geom::rect(0., 20., 100., 20.),}
550 { path =[0, 1], rect == ribir_geom::rect(100., 10., 100., 30.),}
551 { path =[0, 2], rect == ribir_geom::rect(200., 0., 100., 40.),}
552 );
553
554 fn stretch_cross_align() -> impl WidgetBuilder { cross_align(Align::Stretch) }
555 widget_layout_test!(
556 stretch_cross_align,
557 wnd_size = Size::new(500., 40.),
558 { path =[0], width == 300., height == 40., }
559 { path =[0, 0], rect == ribir_geom::rect(0., 0., 100., 40.),}
560 { path =[0, 1], rect == ribir_geom::rect(100., 0., 100., 40.),}
561 { path =[0, 2], rect == ribir_geom::rect(200., 0., 100., 40.),}
562 );
563
564 fn main_align(justify_content: JustifyContent) -> impl WidgetBuilder {
565 let item_size = Size::new(100., 20.);
566 fn_widget! {
567 @SizedBox {
568 size: Size::new(500., 500.),
569 @Row {
570 justify_content,
571 align_items: Align::Start,
572 @SizedBox { size: item_size }
573 @SizedBox { size: item_size }
574 @SizedBox { size: item_size }
575 }
576 }
577 }
578 }
579
580 fn start_main_align() -> impl WidgetBuilder { main_align(JustifyContent::Start) }
581 widget_layout_test!(
582 start_main_align,
583 wnd_size = Size::new(500., 500.),
584 { path =[0, 0], width == 500., height == 500.,}
585 { path =[0, 0, 0], x == 0.,}
586 { path =[0, 0, 1], x == 100.,}
587 { path =[0, 0, 2], x == 200.,}
588 );
589
590 fn center_main_align() -> impl WidgetBuilder { main_align(JustifyContent::Center) }
591 widget_layout_test!(
592 center_main_align,
593 wnd_size = Size::new(500., 500.),
594 { path =[0, 0], width == 500., height == 500.,}
595 { path =[0, 0, 0], x == 100.,}
596 { path =[0, 0, 1], x == 200.,}
597 { path =[0, 0, 2], x == 300.,}
598 );
599
600 fn end_main_align() -> impl WidgetBuilder { main_align(JustifyContent::End) }
601 widget_layout_test!(
602 end_main_align,
603 wnd_size = Size::new(500., 500.),
604 { path =[0, 0], width == 500., height == 500.,}
605 { path =[0, 0, 0], x == 200.,}
606 { path =[0, 0, 1], x == 300.,}
607 { path =[0, 0, 2], x == 400.,}
608 );
609
610 fn space_between_align() -> impl WidgetBuilder { main_align(JustifyContent::SpaceBetween) }
611 widget_layout_test!(
612 space_between_align,
613 wnd_size = Size::new(500., 500.),
614 { path =[0, 0], width == 500., height == 500.,}
615 { path =[0, 0, 0], x == 0.,}
616 { path =[0, 0, 1], x == 200.,}
617 { path =[0, 0, 2], x == 400.,}
618 );
619
620 fn space_around_align() -> impl WidgetBuilder { main_align(JustifyContent::SpaceAround) }
621 const AROUND_SPACE: f32 = 200.0 / 3.0;
622 widget_layout_test!(
623 space_around_align,
624 wnd_size = Size::new(500., 500.),
625 { path =[0, 0], width == 500., height == 500.,}
626 { path =[0, 0, 0], x == 0.5 * AROUND_SPACE,}
627 { path =[0, 0, 1], x == 100. + AROUND_SPACE * 1.5,}
628 { path =[0, 0, 2], x == 2.5 * AROUND_SPACE+ 200.,}
629 );
630
631 fn space_evenly_align() -> impl WidgetBuilder { main_align(JustifyContent::SpaceEvenly) }
632 widget_layout_test!(
633 space_evenly_align,
634 wnd_size = Size::new(500., 500.),
635 { path =[0, 0], width == 500., height == 500.,}
636 { path =[0, 0, 0], x == 50.,}
637 { path =[0, 0, 1], x == 200.,}
638 { path =[0, 0, 2], x == 350.,}
639 );
640
641 fn flex_expand() -> impl WidgetBuilder {
642 fn_widget! {
643 @SizedBox {
644 size: Size::new(500., 25.),
645 @Flex {
646 direction: Direction::Horizontal,
647 @Expanded {
648 flex: 1.,
649 @SizedBox { size: INFINITY_SIZE,}
650 }
651 @SizedBox { size: Size::new(100., 20.) }
652 @Expanded {
653 flex: 3.,
654 @SizedBox { size: INFINITY_SIZE, }
655 }
656 }
657 }
658 }
659 }
660 widget_layout_test!(
661 flex_expand,
662 wnd_size = Size::new(500., 500.),
663 { path = [0, 0], rect == ribir_geom::rect(0., 0., 500., 25.),}
664 { path = [0, 0, 0], rect == ribir_geom::rect(0., 0., 100., 25.),}
665 { path = [0, 0, 2], rect == ribir_geom::rect(200., 0., 300., 25.),}
666 );
667}