mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 15:14:27 +00:00
feat: Add layout spacing ✨ (#821)
This adds a `spacing` feature for layouts. Spacing can be added between items of a layout.
This commit is contained in:
parent
11e4f6a0ba
commit
5131c813ce
1 changed files with 198 additions and 6 deletions
|
@ -91,6 +91,7 @@ pub struct Layout {
|
||||||
constraints: Vec<Constraint>,
|
constraints: Vec<Constraint>,
|
||||||
margin: Margin,
|
margin: Margin,
|
||||||
flex: Flex,
|
flex: Flex,
|
||||||
|
spacing: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A container used by the solver inside split
|
/// A container used by the solver inside split
|
||||||
|
@ -137,9 +138,8 @@ impl Layout {
|
||||||
{
|
{
|
||||||
Layout {
|
Layout {
|
||||||
direction,
|
direction,
|
||||||
margin: Margin::new(0, 0),
|
|
||||||
constraints: constraints.into_iter().map(Into::into).collect(),
|
constraints: constraints.into_iter().map(Into::into).collect(),
|
||||||
flex: Flex::default(),
|
..Layout::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,12 +341,66 @@ impl Layout {
|
||||||
self
|
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 {
|
pub const fn flex(mut self, flex: Flex) -> Layout {
|
||||||
self.flex = flex;
|
self.flex = flex;
|
||||||
self
|
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.
|
/// Set whether chunks should be of equal size.
|
||||||
///
|
///
|
||||||
/// This determines how the space is distributed when the constraints are satisfied. By default,
|
/// 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 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
|
// create an element for each constraint that needs to be applied. Each element defines the
|
||||||
// variables that will be used to compute the layout.
|
// variables that will be used to compute the layout.
|
||||||
let elements: Vec<Element> = layout
|
let elements: Vec<Element> = constraints
|
||||||
.constraints
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|_| Element::constrain(&mut solver, (area_start, area_end)))
|
.map(|_| Element::constrain(&mut solver, (area_start, area_end)))
|
||||||
.try_collect()?;
|
.try_collect()?;
|
||||||
|
@ -639,7 +710,7 @@ impl Layout {
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply the constraints
|
// apply the constraints
|
||||||
for (&constraint, &element) in layout.constraints.iter().zip(elements.iter()) {
|
for (&constraint, &element) in constraints.iter().zip(elements.iter()) {
|
||||||
match constraint {
|
match constraint {
|
||||||
Constraint::Fixed(l) => {
|
Constraint::Fixed(l) => {
|
||||||
// when fixed is used, element size matching value provided will be the first
|
// 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::<Rc<[Rect]>>();
|
.collect::<Rc<[Rect]>>();
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
@ -861,6 +933,7 @@ mod tests {
|
||||||
margin: Margin::new(0, 0),
|
margin: Margin::new(0, 0),
|
||||||
constraints: vec![],
|
constraints: vec![],
|
||||||
flex: Flex::default(),
|
flex: Flex::default(),
|
||||||
|
spacing: 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -905,6 +978,19 @@ mod tests {
|
||||||
margin: Margin::new(0, 0),
|
margin: Margin::new(0, 0),
|
||||||
constraints: vec![Constraint::Min(0)],
|
constraints: vec![Constraint::Min(0)],
|
||||||
flex: Flex::default(),
|
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),
|
margin: Margin::new(0, 0),
|
||||||
constraints: vec![Constraint::Min(0)],
|
constraints: vec![Constraint::Min(0)],
|
||||||
flex: Flex::default(),
|
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]
|
#[test]
|
||||||
fn flex() {
|
fn flex() {
|
||||||
// length should be spaced around
|
// length should be spaced around
|
||||||
|
|
Loading…
Reference in a new issue