ribir_widgets/layout/
flex.rs

1use ribir_core::prelude::{log::warn, *};
2
3use super::{Direction, Expanded};
4
5/// How the children should be placed along the main axis in a flex layout.
6#[derive(Debug, Copy, Clone, PartialEq, Default)]
7pub enum JustifyContent {
8  /// Place the children as close to the start of the main axis as possible.
9  #[default]
10  Start,
11  ///Place the children as close to the middle of the main axis as possible.
12  Center,
13  /// Place the children as close to the end of the main axis as possible.
14  End,
15  /// The children are evenly distributed within the alignment container along
16  /// the main axis. The spacing between each pair of adjacent items is the
17  /// same. The first item is flush with the main-start edge, and the last
18  /// item is flush with the main-end edge.
19  SpaceBetween,
20  /// The children are evenly distributed within the alignment container
21  /// along the main axis. The spacing between each pair of adjacent items is
22  /// the same. The empty space before the first and after the last item
23  /// equals half of the space between each pair of adjacent items.
24  SpaceAround,
25  /// The children are evenly distributed within the alignment container along
26  /// the main axis. The spacing between each pair of adjacent items, the
27  /// main-start edge and the first item, and the main-end edge and the last
28  /// item, are all exactly the same.
29  SpaceEvenly,
30}
31
32#[derive(Default, MultiChild, Declare, Query, Clone, PartialEq)]
33pub struct Flex {
34  /// Reverse the main axis.
35  #[declare(default)]
36  pub reverse: bool,
37  /// Whether flex items are forced onto one line or can wrap onto multiple
38  /// lines
39  #[declare(default)]
40  pub wrap: bool,
41  /// Sets how flex items are placed in the flex container defining the main
42  /// axis and the direction
43  #[declare(default)]
44  pub direction: Direction,
45  /// How the children should be placed along the cross axis in a flex layout.
46  #[declare(default)]
47  pub align_items: Align,
48  /// How the children should be placed along the main axis in a flex layout.
49  #[declare(default)]
50  pub justify_content: JustifyContent,
51  /// Define item between gap in main axis
52  #[declare(default)]
53  pub item_gap: f32,
54  /// Define item between gap in cross axis
55  #[declare(default)]
56  pub line_gap: f32,
57}
58
59/// A type help to declare flex widget as horizontal.
60pub struct Row;
61
62/// A type help to declare flex widget as Vertical.
63pub 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    // cross direction need calculate cross_axis_gap but last line don't need.
157    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    // All children perform layout.
181    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      // flex-item need use empty space to resize after all fixed widget performed
204      // layout.
205      let line = &mut self.current_line;
206      if let Some(flex) = flex {
207        // expanded child size is zero, it don't need calculate
208        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    // cross gap don't use calc offset
276    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}