From 5131c813ce5de078be0458c9a067bca2d6b38921 Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Mon, 15 Jan 2024 10:39:00 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20layout=20spacing=20=E2=9C=A8=20(#?= =?UTF-8?q?821)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a `spacing` feature for layouts. Spacing can be added between items of a layout. --- src/layout/layout.rs | 204 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 198 insertions(+), 6 deletions(-) diff --git a/src/layout/layout.rs b/src/layout/layout.rs index 022d5c97..bd6ca265 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -91,6 +91,7 @@ pub struct Layout { constraints: Vec, margin: Margin, flex: Flex, + spacing: u16, } /// A container used by the solver inside split @@ -137,9 +138,8 @@ impl Layout { { Layout { direction, - margin: Margin::new(0, 0), constraints: constraints.into_iter().map(Into::into).collect(), - flex: Flex::default(), + ..Layout::default() } } @@ -341,12 +341,66 @@ impl Layout { self } - /// Sets flex options for justify content + /// The `flex` method allows you to specify the flex behavior of the layout. + /// + /// # Arguments + /// + /// * `flex`: A `Flex` enum value that represents the flex behavior of the layout. It can be one + /// of the following: + /// - [`Flex::Stretch`]: The items are stretched equally after satisfying constraints to fill + /// excess space. + /// - [`Flex::StretchLast`]: The last item is stretched to fill the excess space. + /// - [`Flex::Start`]: The items are aligned to the start of the layout. + /// - [`Flex::Center`]: The items are aligned to the center of the layout. + /// - [`Flex::End`]: The items are aligned to the end of the layout. + /// - [`Flex::SpaceAround`]: The items are evenly distributed with equal space around them. + /// - [`Flex::SpaceBetween`]: The items are evenly distributed with equal space between them. + /// + /// # Examples + /// + /// In this example, the items in the layout will be aligned to the start. + /// + /// ```rust + /// # use ratatui::layout::{Flex, Layout, Constraint::*}; + /// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Start); + /// ``` + /// + /// In this example, the items in the layout will be stretched equally to fill the available + /// space. + /// + /// ```rust + /// # use ratatui::layout::{Flex, Layout, Constraint::*}; + /// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Stretch); + /// ``` pub const fn flex(mut self, flex: Flex) -> Layout { self.flex = flex; self } + /// Sets the spacing between items in the layout. + /// + /// The `spacing` method sets the spacing between items in the layout. The spacing is applied + /// evenly between all items. The spacing value represents the number of cells between each + /// item. + /// + /// # Examples + /// + /// In this example, the spacing between each item in the layout is set to 2 cells. + /// + /// ```rust + /// # use ratatui::layout::{Layout, Constraint::*}; + /// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(2); + /// ``` + /// + /// # Notes + /// + /// - If the layout has only one item, the spacing will not be applied. + /// - Spacing will not be applied for `Flex::SpaceAround` and `Flex::SpaceBetween` + pub const fn spacing(mut self, spacing: u16) -> Layout { + self.spacing = spacing; + self + } + /// Set whether chunks should be of equal size. /// /// This determines how the space is distributed when the constraints are satisfied. By default, @@ -434,10 +488,27 @@ impl Layout { }; let area_size = area_end - area_start; + let spacers_added = if layout.spacing == 0 { + false + } else { + matches!( + layout.flex, + Flex::Stretch | Flex::StretchLast | Flex::Start | Flex::Center | Flex::End + ) + }; + let constraints = if spacers_added { + Itertools::intersperse( + layout.constraints.iter().cloned(), + Constraint::Fixed(layout.spacing), + ) + .collect_vec() + } else { + layout.constraints.clone() + }; + // create an element for each constraint that needs to be applied. Each element defines the // variables that will be used to compute the layout. - let elements: Vec = layout - .constraints + let elements: Vec = constraints .iter() .map(|_| Element::constrain(&mut solver, (area_start, area_end))) .try_collect()?; @@ -639,7 +710,7 @@ impl Layout { } // apply the constraints - for (&constraint, &element) in layout.constraints.iter().zip(elements.iter()) { + for (&constraint, &element) in constraints.iter().zip(elements.iter()) { match constraint { Constraint::Fixed(l) => { // when fixed is used, element size matching value provided will be the first @@ -776,6 +847,7 @@ impl Layout { }, } }) + .step_by(if spacers_added { 2 } else { 1 }) .collect::>(); Ok(results) } @@ -861,6 +933,7 @@ mod tests { margin: Margin::new(0, 0), constraints: vec![], flex: Flex::default(), + spacing: 0, } ); } @@ -905,6 +978,19 @@ mod tests { margin: Margin::new(0, 0), constraints: vec![Constraint::Min(0)], flex: Flex::default(), + spacing: 0, + } + ); + assert_eq!( + Layout::vertical([Constraint::Min(0)]) + .spacing(1) + .flex(Flex::Start), + Layout { + direction: Direction::Vertical, + margin: Margin::new(0, 0), + constraints: vec![Constraint::Min(0)], + flex: Flex::Start, + spacing: 1, } ); } @@ -918,6 +1004,19 @@ mod tests { margin: Margin::new(0, 0), constraints: vec![Constraint::Min(0)], flex: Flex::default(), + spacing: 0, + } + ); + assert_eq!( + Layout::horizontal([Constraint::Min(0)]) + .spacing(1) + .flex(Flex::Start), + Layout { + direction: Direction::Horizontal, + margin: Margin::new(0, 0), + constraints: vec![Constraint::Min(0)], + flex: Flex::Start, + spacing: 1, } ); } @@ -2008,6 +2107,99 @@ mod tests { ); } + #[test] + fn flex_spacing() { + // reference no spacing + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Length(20), Length(20), Length(20)]) + .flex(Flex::Start) + .spacing(0), + ); + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(0, 20), (20, 20), (40, 20)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Length(20), Length(20), Length(20)]) + .flex(Flex::Center) + .spacing(2), + ); + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(18, 20), (40, 20), (62, 20)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Length(20), Length(20), Length(20)]) + .flex(Flex::End) + .spacing(2), + ); + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(36, 20), (58, 20), (80, 20)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Length(20), Length(20), Length(20)]) + .flex(Flex::Stretch) + .spacing(2), + ); + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(0, 32), (34, 32), (68, 32)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Length(20), Length(20), Length(20)]) + .flex(Flex::StretchLast) + .spacing(2), + ); + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(0, 20), (22, 20), (44, 56)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Fixed(20), Fixed(20), Fixed(20)]) + .flex(Flex::StretchLast) + .spacing(2), + ); + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(0, 20), (22, 20), (44, 56)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Fixed(20), Fixed(20), Fixed(20)]) + .flex(Flex::Stretch) + .spacing(2), + ); + // TODO: broken test + // this test breaks because the spacers have the same priority as the constraints + // This will _only_ happen when every constraint is `Fixed` AND when + // `Flex::Stretch` is used. + // No easy way to fix it right now. + assert_ne!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(0, 32), (34, 32), (68, 32)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Fixed(20), Fixed(20), Fixed(20)]) + .flex(Flex::SpaceAround) + .spacing(2), + ); + // never add spacers for space around + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(10, 20), (40, 20), (70, 20)] + ); + let [a, b, c] = Rect::new(0, 0, 100, 1).split( + &Layout::horizontal([Fixed(20), Fixed(20), Fixed(20)]) + .flex(Flex::SpaceBetween) + .spacing(2), + ); + // never add spacers for space between + assert_eq!( + [(a.x, a.width), (b.x, b.width), (c.x, c.width)], + [(0, 20), (40, 20), (80, 20)] + ); + } + #[test] fn flex() { // length should be spaced around