mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
feat: Add flex to layout ✨
This PR adds a new way to space elements in a `Layout`. Loosely based on [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), this PR adds a `Flex` enum with the following variants: - Start - Center - End - SpaceAround - SpaceBetween <img width="380" alt="image" src="https://github.com/ratatui-org/ratatui/assets/1813121/b744518c-eae7-4e35-bbc4-fe3c95193cde"> It also adds two more variants, to make this backward compatible and to make it replace `SegmentSize`: - StretchLast (default in the `Flex` enum, also behavior matches old default `SegmentSize::LastTakesRemainder`) - Stretch (behavior matches `SegmentSize::EvenDistribution`) The `Start` variant from above matches `SegmentSize::None`. This allows `Flex` to be a complete replacement for `SegmentSize`, hence this PR also deprecates the `segment_size` constructor on `Layout`. `SegmentSize` is still used in `Table` but under the hood `segment_size` maps to `Flex` with all tests passing unchanged. I also put together a simple example for `Flex` layouts so that I could test it visually, shared below: https://github.com/ratatui-org/ratatui/assets/1813121/c8716c59-493f-4631-add5-feecf4bd4e06
This commit is contained in:
parent
9a3815b66d
commit
de97a1f1da
7 changed files with 768 additions and 61 deletions
|
@ -214,6 +214,11 @@ name = "constraints"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
doc-scrape-examples = false
|
doc-scrape-examples = false
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "flex"
|
||||||
|
required-features = ["crossterm"]
|
||||||
|
doc-scrape-examples = false
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "list"
|
name = "list"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
|
|
252
examples/flex.rs
Normal file
252
examples/flex.rs
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
use std::{error::Error, io};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, Event, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint::*, Flex},
|
||||||
|
prelude::*,
|
||||||
|
widgets::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
// setup terminal
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
// create app and run it
|
||||||
|
let res = run_app(&mut terminal);
|
||||||
|
|
||||||
|
// restore terminal
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
println!("{err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||||
|
let mut selection = ExampleSelection::Stretch;
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| f.render_widget(selection, f.size()))?;
|
||||||
|
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
use KeyCode::*;
|
||||||
|
match key.code {
|
||||||
|
Char('q') => break Ok(()),
|
||||||
|
Char('j') | Char('l') | Down | Right => {
|
||||||
|
selection = selection.next();
|
||||||
|
}
|
||||||
|
Char('k') | Char('h') | Up | Left => {
|
||||||
|
selection = selection.previous();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
enum ExampleSelection {
|
||||||
|
Stretch,
|
||||||
|
StretchLast,
|
||||||
|
Start,
|
||||||
|
Center,
|
||||||
|
End,
|
||||||
|
SpaceAround,
|
||||||
|
SpaceBetween,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExampleSelection {
|
||||||
|
fn previous(&self) -> Self {
|
||||||
|
use ExampleSelection::*;
|
||||||
|
match *self {
|
||||||
|
Stretch => Stretch,
|
||||||
|
StretchLast => Stretch,
|
||||||
|
Start => StretchLast,
|
||||||
|
Center => Start,
|
||||||
|
End => Center,
|
||||||
|
SpaceAround => End,
|
||||||
|
SpaceBetween => SpaceAround,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Self {
|
||||||
|
use ExampleSelection::*;
|
||||||
|
match *self {
|
||||||
|
Stretch => StretchLast,
|
||||||
|
StretchLast => Start,
|
||||||
|
Start => Center,
|
||||||
|
Center => End,
|
||||||
|
End => SpaceAround,
|
||||||
|
SpaceAround => SpaceBetween,
|
||||||
|
SpaceBetween => SpaceBetween,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected(&self) -> usize {
|
||||||
|
use ExampleSelection::*;
|
||||||
|
match self {
|
||||||
|
Stretch => 0,
|
||||||
|
StretchLast => 1,
|
||||||
|
Start => 2,
|
||||||
|
Center => 3,
|
||||||
|
End => 4,
|
||||||
|
SpaceAround => 5,
|
||||||
|
SpaceBetween => 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for ExampleSelection {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [tabs, area] = area.split(&Layout::vertical([Fixed(3), Proportional(0)]));
|
||||||
|
|
||||||
|
self.render_tabs(tabs, buf);
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ExampleSelection::Stretch => self.render_example(area, buf, Flex::Stretch),
|
||||||
|
ExampleSelection::StretchLast => self.render_example(area, buf, Flex::StretchLast),
|
||||||
|
ExampleSelection::Start => self.render_example(area, buf, Flex::Start),
|
||||||
|
ExampleSelection::Center => self.render_example(area, buf, Flex::Center),
|
||||||
|
ExampleSelection::End => self.render_example(area, buf, Flex::End),
|
||||||
|
ExampleSelection::SpaceAround => self.render_example(area, buf, Flex::SpaceAround),
|
||||||
|
ExampleSelection::SpaceBetween => self.render_example(area, buf, Flex::SpaceBetween),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExampleSelection {
|
||||||
|
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Tabs::new(
|
||||||
|
[
|
||||||
|
ExampleSelection::Stretch,
|
||||||
|
ExampleSelection::StretchLast,
|
||||||
|
ExampleSelection::Start,
|
||||||
|
ExampleSelection::Center,
|
||||||
|
ExampleSelection::End,
|
||||||
|
ExampleSelection::SpaceAround,
|
||||||
|
ExampleSelection::SpaceBetween,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|e| format!("{:?}", e)),
|
||||||
|
)
|
||||||
|
.block(Block::bordered().title("Flex Layouts"))
|
||||||
|
.highlight_style(Style::default().yellow())
|
||||||
|
.select(self.selected())
|
||||||
|
.padding(" ", " ")
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_example(&self, area: Rect, buf: &mut Buffer, flex: Flex) {
|
||||||
|
let [example1, example2, example3, example4, example5, example6, _] =
|
||||||
|
area.split(&Layout::vertical([Fixed(8); 7]));
|
||||||
|
|
||||||
|
Example::new([Length(20), Length(10)])
|
||||||
|
.flex(flex)
|
||||||
|
.render(example1, buf);
|
||||||
|
Example::new([Length(20), Fixed(10)])
|
||||||
|
.flex(flex)
|
||||||
|
.render(example2, buf);
|
||||||
|
Example::new([Proportional(1), Proportional(1), Length(40), Fixed(20)])
|
||||||
|
.flex(flex)
|
||||||
|
.render(example3, buf);
|
||||||
|
Example::new([Min(20), Length(40), Fixed(20)])
|
||||||
|
.flex(flex)
|
||||||
|
.render(example4, buf);
|
||||||
|
Example::new([Min(20), Proportional(0), Length(40), Fixed(20)])
|
||||||
|
.flex(flex)
|
||||||
|
.render(example5, buf);
|
||||||
|
Example::new([
|
||||||
|
Min(20),
|
||||||
|
Proportional(0),
|
||||||
|
Percentage(10),
|
||||||
|
Length(40),
|
||||||
|
Fixed(20),
|
||||||
|
])
|
||||||
|
.flex(flex)
|
||||||
|
.render(example6, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Example {
|
||||||
|
constraints: Vec<Constraint>,
|
||||||
|
flex: Flex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Example {
|
||||||
|
fn new<C>(constraints: C) -> Self
|
||||||
|
where
|
||||||
|
C: Into<Vec<Constraint>>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
constraints: constraints.into(),
|
||||||
|
flex: Flex::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flex(mut self, flex: Flex) -> Self {
|
||||||
|
self.flex = flex;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Example {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [title, legend, area] = area.split(&Layout::vertical([Ratio(1, 3); 3]));
|
||||||
|
let blocks = Layout::horizontal(&self.constraints)
|
||||||
|
.flex(self.flex)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
self.heading().render(title, buf);
|
||||||
|
|
||||||
|
self.legend(legend.width as usize).render(legend, buf);
|
||||||
|
|
||||||
|
for (i, (block, _constraint)) in blocks.iter().zip(&self.constraints).enumerate() {
|
||||||
|
let text = format!("{} px", block.width);
|
||||||
|
let fg = Color::Indexed(i as u8 + 1);
|
||||||
|
self.illustration(text, fg).render(*block, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Example {
|
||||||
|
fn heading(&self) -> Paragraph {
|
||||||
|
// Renders the following
|
||||||
|
//
|
||||||
|
// Fixed(40), Proportional(0)
|
||||||
|
let spans = self.constraints.iter().enumerate().map(|(i, c)| {
|
||||||
|
let color = Color::Indexed(i as u8 + 1);
|
||||||
|
Span::styled(format!("{:?}", c), color)
|
||||||
|
});
|
||||||
|
let heading =
|
||||||
|
Line::from(Itertools::intersperse(spans, Span::raw(", ")).collect::<Vec<Span>>());
|
||||||
|
Paragraph::new(heading).block(Block::default().padding(Padding::vertical(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legend(&self, width: usize) -> Paragraph {
|
||||||
|
// a bar like `<----- 80 px ----->`
|
||||||
|
let width_label = format!("{} px", width);
|
||||||
|
let width_bar = format!(
|
||||||
|
"<{width_label:-^width$}>",
|
||||||
|
width = width - width_label.len() / 2
|
||||||
|
);
|
||||||
|
Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn illustration(&self, text: String, fg: Color) -> Paragraph {
|
||||||
|
Paragraph::new(text)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(Block::bordered().style(Style::default().fg(fg)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ mod alignment;
|
||||||
mod constraint;
|
mod constraint;
|
||||||
mod corner;
|
mod corner;
|
||||||
mod direction;
|
mod direction;
|
||||||
|
mod flex;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
mod layout;
|
mod layout;
|
||||||
mod margin;
|
mod margin;
|
||||||
|
@ -13,6 +14,7 @@ pub use alignment::Alignment;
|
||||||
pub use constraint::Constraint;
|
pub use constraint::Constraint;
|
||||||
pub use corner::Corner;
|
pub use corner::Corner;
|
||||||
pub use direction::Direction;
|
pub use direction::Direction;
|
||||||
|
pub use flex::Flex;
|
||||||
pub use layout::Layout;
|
pub use layout::Layout;
|
||||||
pub use margin::Margin;
|
pub use margin::Margin;
|
||||||
pub use rect::*;
|
pub use rect::*;
|
||||||
|
|
190
src/layout/flex.rs
Normal file
190
src/layout/flex.rs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
|
/// Defines the options for layout flex justify content in a container.
|
||||||
|
///
|
||||||
|
/// This enumeration controls the distribution of space when layout constraints are met.
|
||||||
|
///
|
||||||
|
/// - `StretchLast`: Fills the available space within the container, putting excess space into the
|
||||||
|
/// last element.
|
||||||
|
/// - `Stretch`: Always fills the available space within the container.
|
||||||
|
/// - `Start`: Aligns items to the start of the container.
|
||||||
|
/// - `End`: Aligns items to the end of the container.
|
||||||
|
/// - `Center`: Centers items within the container.
|
||||||
|
/// - `SpaceBetween`: Adds excess space between each element.
|
||||||
|
/// - `SpaceAround`: Adds excess space around each element.
|
||||||
|
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum Flex {
|
||||||
|
/// Fills the available space within the container, putting excess space into the last element.
|
||||||
|
/// This matches the default behavior of ratatui and tui applications without [`Flex`]
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌──────────────────────────────────────────────────────────┐
|
||||||
|
/// │ 20 px ││ 60 px │
|
||||||
|
/// └──────────────────┘└──────────────────────────────────────────────────────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌────────────────────────────────────────────────────────────────────┐┌────────┐
|
||||||
|
/// │ 70 px ││ 10 px │
|
||||||
|
/// └────────────────────────────────────────────────────────────────────┘└────────┘
|
||||||
|
/// ```
|
||||||
|
#[default]
|
||||||
|
StretchLast,
|
||||||
|
|
||||||
|
/// Always fills the available space within the container.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Length(40), Length(20)
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────────────────────────┐┌──────────────────────────────────────┐
|
||||||
|
/// │ 40 px ││ 40 px │
|
||||||
|
/// └──────────────────────────────────────┘└──────────────────────────────────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌────────────────────────────────────────────────────────────────────┐┌────────┐
|
||||||
|
/// │ 70 px ││ 10 px │
|
||||||
|
/// └────────────────────────────────────────────────────────────────────┘└────────┘
|
||||||
|
/// ```
|
||||||
|
Stretch,
|
||||||
|
|
||||||
|
/// Aligns items to the start of the container.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌────────┐
|
||||||
|
/// │ 20 px ││ 10 px │
|
||||||
|
/// └──────────────────┘└────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌────────┐
|
||||||
|
/// │ 20 px ││ 10 px │
|
||||||
|
/// └──────────────────┘└────────┘
|
||||||
|
/// ```
|
||||||
|
Start,
|
||||||
|
|
||||||
|
/// Aligns items to the end of the container.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌────────┐
|
||||||
|
/// │ 20 px ││ 10 px │
|
||||||
|
/// └──────────────────┘└────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌────────┐
|
||||||
|
/// │ 20 px ││ 10 px │
|
||||||
|
/// └──────────────────┘└────────┘
|
||||||
|
/// ```
|
||||||
|
End,
|
||||||
|
|
||||||
|
/// Centers items within the container.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌────────┐
|
||||||
|
/// │ 20 px ││ 10 px │
|
||||||
|
/// └──────────────────┘└────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐┌────────┐
|
||||||
|
/// │ 20 px ││ 10 px │
|
||||||
|
/// └──────────────────┘└────────┘
|
||||||
|
/// ```
|
||||||
|
Center,
|
||||||
|
|
||||||
|
/// Adds excess space between each element.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐ ┌────────┐
|
||||||
|
/// │ 20 px │ │ 10 px │
|
||||||
|
/// └──────────────────┘ └────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐ ┌────────┐
|
||||||
|
/// │ 20 px │ │ 10 px │
|
||||||
|
/// └──────────────────┘ └────────┘
|
||||||
|
/// ```
|
||||||
|
SpaceBetween,
|
||||||
|
|
||||||
|
/// Adds excess space around each element.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
///
|
||||||
|
/// Length(20), Length(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐ ┌────────┐
|
||||||
|
/// │ 20 px │ │ 10 px │
|
||||||
|
/// └──────────────────┘ └────────┘
|
||||||
|
///
|
||||||
|
/// Length(20), Fixed(10)
|
||||||
|
///
|
||||||
|
/// <------------------------------------80 px------------------------------------->
|
||||||
|
///
|
||||||
|
/// ┌──────────────────┐ ┌────────┐
|
||||||
|
/// │ 20 px │ │ 10 px │
|
||||||
|
/// └──────────────────┘ └────────┘
|
||||||
|
/// ```
|
||||||
|
SpaceAround,
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
|
@ -8,7 +8,7 @@ use cassowary::{
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
|
|
||||||
use super::SegmentSize;
|
use super::{Flex, SegmentSize};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
type Cache = LruCache<(Rect, Layout), Rc<[Rect]>>;
|
type Cache = LruCache<(Rect, Layout), Rc<[Rect]>>;
|
||||||
|
@ -60,8 +60,7 @@ thread_local! {
|
||||||
/// - [`Layout::margin`]: set the margin of the layout
|
/// - [`Layout::margin`]: set the margin of the layout
|
||||||
/// - [`Layout::horizontal_margin`]: set the horizontal margin of the layout
|
/// - [`Layout::horizontal_margin`]: set the horizontal margin of the layout
|
||||||
/// - [`Layout::vertical_margin`]: set the vertical margin of the layout
|
/// - [`Layout::vertical_margin`]: set the vertical margin of the layout
|
||||||
/// - [`Layout::segment_size`]: set the way the space is distributed when the constraints are
|
/// - [`Layout::flex`]: set the way the space is distributed when the constraints are satisfied
|
||||||
/// satisfied
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
|
@ -91,8 +90,7 @@ pub struct Layout {
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
constraints: Vec<Constraint>,
|
constraints: Vec<Constraint>,
|
||||||
margin: Margin,
|
margin: Margin,
|
||||||
/// option for segment size preferences
|
flex: Flex,
|
||||||
segment_size: SegmentSize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A container used by the solver inside split
|
/// A container used by the solver inside split
|
||||||
|
@ -114,7 +112,7 @@ impl Layout {
|
||||||
/// Default values for the other fields are:
|
/// Default values for the other fields are:
|
||||||
///
|
///
|
||||||
/// - `margin`: 0, 0
|
/// - `margin`: 0, 0
|
||||||
/// - `segment_size`: SegmentSize::LastTakesRemainder
|
/// - `flex`: Flex::Fill
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
@ -141,7 +139,7 @@ impl Layout {
|
||||||
direction,
|
direction,
|
||||||
margin: Margin::new(0, 0),
|
margin: Margin::new(0, 0),
|
||||||
constraints: constraints.into_iter().map(Into::into).collect(),
|
constraints: constraints.into_iter().map(Into::into).collect(),
|
||||||
segment_size: SegmentSize::LastTakesRemainder,
|
flex: Flex::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,6 +341,12 @@ impl Layout {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets flex options for justify content
|
||||||
|
pub const fn flex(mut self, flex: Flex) -> Layout {
|
||||||
|
self.flex = flex;
|
||||||
|
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,
|
||||||
|
@ -350,19 +354,25 @@ impl Layout {
|
||||||
/// equal chunks or to not distribute extra space at all (which is the default used for laying
|
/// equal chunks or to not distribute extra space at all (which is the default used for laying
|
||||||
/// out the columns for [`Table`] widgets).
|
/// out the columns for [`Table`] widgets).
|
||||||
///
|
///
|
||||||
/// Note: If you're using this feature please help us come up with a good name. See [Issue
|
/// This function exists for backwards compatibility reasons. Use [`Layout::flex`] instead.
|
||||||
/// #536](https://github.com/ratatui-org/ratatui/issues/536) for more information.
|
|
||||||
///
|
///
|
||||||
/// [`Table`]: crate::widgets::Table
|
/// - `Flex::StretchLast` does now what `SegmentSize::LastTakesRemainder` did (default).
|
||||||
|
/// - `Flex::Stretch` does now what `SegmentSize::EvenDistribution` did.
|
||||||
|
/// - `Flex::Start` does now what `SegmentSize::None` did.
|
||||||
#[stability::unstable(
|
#[stability::unstable(
|
||||||
feature = "segment-size",
|
feature = "segment-size",
|
||||||
reason = "The name for this feature is not final and may change in the future",
|
reason = "The name for this feature is not final and may change in the future",
|
||||||
issue = "https://github.com/ratatui-org/ratatui/issues/536"
|
issue = "https://github.com/ratatui-org/ratatui/issues/536"
|
||||||
)]
|
)]
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub const fn segment_size(mut self, segment_size: SegmentSize) -> Layout {
|
#[deprecated(since = "0.26.0", note = "You should use `Layout::flex` instead.")]
|
||||||
self.segment_size = segment_size;
|
pub const fn segment_size(self, segment_size: SegmentSize) -> Layout {
|
||||||
self
|
let flex = match segment_size {
|
||||||
|
SegmentSize::None => Flex::Start,
|
||||||
|
SegmentSize::LastTakesRemainder => Flex::StretchLast,
|
||||||
|
SegmentSize::EvenDistribution => Flex::Stretch,
|
||||||
|
};
|
||||||
|
self.flex(flex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper function around the cassowary-rs solver to be able to split a given area into
|
/// Wrapper function around the cassowary-rs solver to be able to split a given area into
|
||||||
|
@ -426,34 +436,168 @@ impl Layout {
|
||||||
|
|
||||||
// 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 = layout
|
let elements: Vec<Element> = layout
|
||||||
.constraints
|
.constraints
|
||||||
.iter()
|
.iter()
|
||||||
.map(|_| Element::new())
|
.map(|_| Element::constrain(&mut solver, (area_start, area_end)))
|
||||||
.collect::<Vec<Element>>();
|
.try_collect()?;
|
||||||
|
|
||||||
// ensure that all the elements are inside the area
|
match layout.flex {
|
||||||
for element in &elements {
|
Flex::SpaceBetween => {
|
||||||
solver.add_constraints(&[
|
let spacers: Vec<Element> = std::iter::repeat_with(|| {
|
||||||
element.start | GE(REQUIRED) | area_start,
|
Element::constrain(&mut solver, (area_start, area_end))
|
||||||
element.end | LE(REQUIRED) | area_end,
|
})
|
||||||
element.start | LE(REQUIRED) | element.end,
|
.take(elements.len().saturating_sub(1)) // one less than the number of elements
|
||||||
])?;
|
.try_collect()?;
|
||||||
}
|
// spacers growing should be the lowest priority
|
||||||
// ensure there are no gaps between the elements
|
for spacer in spacers.iter() {
|
||||||
for pair in elements.windows(2) {
|
solver.add_constraint(spacer.size() | EQ(WEAK) | area_size)?;
|
||||||
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
}
|
||||||
}
|
// Spacers should all be similar in size
|
||||||
// ensure the first element touches the left/top edge of the area
|
// these constraints should not be stronger than existing constraints
|
||||||
if let Some(first) = elements.first() {
|
// but if they are weaker `Min` and `Max` won't be pushed to their desired values
|
||||||
solver.add_constraint(first.start | EQ(REQUIRED) | area_start)?;
|
// I found using `STRONG` gives the most desirable behavior
|
||||||
}
|
for (left, right) in spacers.iter().tuple_combinations() {
|
||||||
if layout.segment_size != SegmentSize::None {
|
solver.add_constraint(left.size() | EQ(STRONG) | right.size())?;
|
||||||
// ensure the last element touches the right/bottom edge of the area
|
}
|
||||||
if let Some(last) = elements.last() {
|
// interleave elements and spacers
|
||||||
solver.add_constraint(last.end | EQ(REQUIRED) | area_end)?;
|
// for `SpaceBetween` we want the following
|
||||||
|
// `[element, spacer, element, spacer, ..., element]`
|
||||||
|
// this is why we use one less spacer than elements
|
||||||
|
for pair in Itertools::interleave(elements.iter(), spacers.iter())
|
||||||
|
.collect::<Vec<&Element>>()
|
||||||
|
.windows(2)
|
||||||
|
{
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flex::SpaceAround => {
|
||||||
|
let spacers: Vec<Element> = std::iter::repeat_with(|| {
|
||||||
|
Element::constrain(&mut solver, (area_start, area_end))
|
||||||
|
})
|
||||||
|
.take(elements.len().saturating_add(1)) // one more than number of elements
|
||||||
|
.try_collect()?;
|
||||||
|
// spacers growing should be the lowest priority
|
||||||
|
for spacer in spacers.iter() {
|
||||||
|
solver.add_constraint(spacer.size() | EQ(WEAK) | area_size)?;
|
||||||
|
}
|
||||||
|
// Spacers should all be similar in size
|
||||||
|
// these constraints should not be stronger than existing constraints
|
||||||
|
// but if they are weaker `Min` and `Max` won't be pushed to their desired values
|
||||||
|
// I found using `STRONG` gives the most desirable behavior
|
||||||
|
for (left, right) in spacers.iter().tuple_combinations() {
|
||||||
|
solver.add_constraint(left.size() | EQ(STRONG) | right.size())?;
|
||||||
|
}
|
||||||
|
// interleave spacers and elements
|
||||||
|
// for `SpaceAround` we want the following
|
||||||
|
// `[spacer, element, spacer, element, ..., element, spacer]`
|
||||||
|
// this is why we use one spacer than elements
|
||||||
|
for pair in Itertools::interleave(spacers.iter(), elements.iter())
|
||||||
|
.collect::<Vec<&Element>>()
|
||||||
|
.windows(2)
|
||||||
|
{
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flex::StretchLast => {
|
||||||
|
// this is the default behavior
|
||||||
|
// within reason, cassowary tends to put excess into the last constraint
|
||||||
|
if let Some(first) = elements.first() {
|
||||||
|
solver.add_constraint(first.start | EQ(REQUIRED) | area_start)?;
|
||||||
|
}
|
||||||
|
if let Some(last) = elements.last() {
|
||||||
|
solver.add_constraint(last.end | EQ(REQUIRED) | area_end)?;
|
||||||
|
}
|
||||||
|
// ensure there are no gaps between the elements
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flex::Stretch => {
|
||||||
|
if let Some(first) = elements.first() {
|
||||||
|
solver.add_constraint(first.start | EQ(REQUIRED) | area_start)?;
|
||||||
|
}
|
||||||
|
if let Some(last) = elements.last() {
|
||||||
|
solver.add_constraint(last.end | EQ(REQUIRED) | area_end)?;
|
||||||
|
}
|
||||||
|
// prefer equal elements if other constraints are all satisfied
|
||||||
|
for (left, right) in elements.iter().tuple_combinations() {
|
||||||
|
solver.add_constraint(left.size() | EQ(WEAK) | right.size())?;
|
||||||
|
}
|
||||||
|
// ensure there are no gaps between the elements
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flex::Center => {
|
||||||
|
// for center, we add two flex elements, one at the beginning and one at the end.
|
||||||
|
// this frees up inner constraints to be their true size
|
||||||
|
let flex_start_element = Element::constrain(&mut solver, (area_start, area_end))?;
|
||||||
|
let flex_end_element = Element::constrain(&mut solver, (area_start, area_end))?;
|
||||||
|
// the start flex element must be before the users constraint
|
||||||
|
if let Some(first) = elements.first() {
|
||||||
|
solver.add_constraints(&[
|
||||||
|
flex_start_element.start | EQ(REQUIRED) | area_start,
|
||||||
|
first.start | EQ(REQUIRED) | flex_start_element.end,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
// the end flex element must be after the users constraint
|
||||||
|
if let Some(last) = elements.last() {
|
||||||
|
solver.add_constraints(&[
|
||||||
|
last.end | EQ(REQUIRED) | flex_end_element.start,
|
||||||
|
flex_end_element.end | EQ(REQUIRED) | area_end,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
// finally we ask for a strong preference to make the starting flex and ending flex
|
||||||
|
// the same size, and this results in the remaining constraints being centered
|
||||||
|
solver.add_constraint(
|
||||||
|
flex_start_element.size() | EQ(STRONG) | flex_end_element.size(),
|
||||||
|
)?;
|
||||||
|
// ensure there are no gaps between the elements
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flex::Start => {
|
||||||
|
// for start, we add one flex element one at the end.
|
||||||
|
// this frees up the end constraints and allows inner constraints to be aligned to
|
||||||
|
// the start
|
||||||
|
let flex_end_element = Element::constrain(&mut solver, (area_start, area_end))?;
|
||||||
|
if let Some(first) = elements.first() {
|
||||||
|
solver.add_constraint(first.start | EQ(REQUIRED) | area_start)?;
|
||||||
|
}
|
||||||
|
if let Some(last) = elements.last() {
|
||||||
|
solver.add_constraints(&[
|
||||||
|
last.end | EQ(REQUIRED) | flex_end_element.start,
|
||||||
|
flex_end_element.end | EQ(REQUIRED) | area_end,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
// ensure there are no gaps between the elements
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flex::End => {
|
||||||
|
// for end, we add one flex element one at the start.
|
||||||
|
// this frees up the start constraints and allows inner constraints to be aligned to
|
||||||
|
// the end
|
||||||
|
let flex_start_element = Element::constrain(&mut solver, (area_start, area_end))?;
|
||||||
|
if let Some(first) = elements.first() {
|
||||||
|
solver.add_constraints(&[
|
||||||
|
flex_start_element.start | EQ(REQUIRED) | area_start,
|
||||||
|
first.start | EQ(REQUIRED) | flex_start_element.end,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
if let Some(last) = elements.last() {
|
||||||
|
solver.add_constraint(last.end | EQ(REQUIRED) | area_end)?;
|
||||||
|
}
|
||||||
|
// ensure there are no gaps between the elements
|
||||||
|
for pair in elements.windows(2) {
|
||||||
|
solver.add_constraint(pair[0].end | EQ(REQUIRED) | pair[1].start)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply the constraints
|
// apply the constraints
|
||||||
for (&constraint, &element) in layout.constraints.iter().zip(elements.iter()) {
|
for (&constraint, &element) in layout.constraints.iter().zip(elements.iter()) {
|
||||||
match constraint {
|
match constraint {
|
||||||
|
@ -490,12 +634,14 @@ impl Layout {
|
||||||
Constraint::Proportional(_) => {
|
Constraint::Proportional(_) => {
|
||||||
// given no other constraints, this segment will grow as much as possible.
|
// given no other constraints, this segment will grow as much as possible.
|
||||||
//
|
//
|
||||||
// in the current implementation, this constraint will not have any effect
|
// We want proportional constraints to behave the same as they do without
|
||||||
// since in every combination of constraints, other constraints governing
|
// spacers but we also want them to be fill excess space
|
||||||
// element size will take a higher priority.
|
// before a spacer fills excess space. This means we want
|
||||||
//
|
// Proportional to be stronger than a spacer constraint but weaker than all the
|
||||||
// this constraint is placed here only for future proofing.
|
// other constraints.
|
||||||
solver.add_constraint(element.size() | EQ(WEAK) | area_size)?;
|
// In my tests, I found choosing an order of magnitude weaker than a `MEDIUM`
|
||||||
|
// constraint did the trick.
|
||||||
|
solver.add_constraint(element.size() | EQ(MEDIUM / 10.0) | area_size)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -554,12 +700,6 @@ impl Layout {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// prefer equal chunks if other constraints are all satisfied
|
|
||||||
if layout.segment_size == SegmentSize::EvenDistribution {
|
|
||||||
for (left, right) in elements.iter().tuple_combinations() {
|
|
||||||
solver.add_constraint(left.size() | EQ(WEAK) | right.size())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let changes: HashMap<Variable, f64> = solver.fetch_changes().iter().copied().collect();
|
let changes: HashMap<Variable, f64> = solver.fetch_changes().iter().copied().collect();
|
||||||
|
|
||||||
|
@ -602,13 +742,30 @@ impl Layout {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element {
|
impl Element {
|
||||||
fn new() -> Element {
|
#[allow(dead_code)]
|
||||||
Element {
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
start: Variable::new(),
|
start: Variable::new(),
|
||||||
end: Variable::new(),
|
end: Variable::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn constrain(
|
||||||
|
solver: &mut Solver,
|
||||||
|
(area_start, area_end): (f64, f64),
|
||||||
|
) -> Result<Self, AddConstraintError> {
|
||||||
|
let e = Element {
|
||||||
|
start: Variable::new(),
|
||||||
|
end: Variable::new(),
|
||||||
|
};
|
||||||
|
solver.add_constraints(&[
|
||||||
|
e.start | GE(REQUIRED) | area_start,
|
||||||
|
e.end | LE(REQUIRED) | area_end,
|
||||||
|
e.start | LE(REQUIRED) | e.end,
|
||||||
|
])?;
|
||||||
|
Ok(e)
|
||||||
|
}
|
||||||
|
|
||||||
fn size(&self) -> Expression {
|
fn size(&self) -> Expression {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
|
@ -663,7 +820,7 @@ mod tests {
|
||||||
direction: Direction::Vertical,
|
direction: Direction::Vertical,
|
||||||
margin: Margin::new(0, 0),
|
margin: Margin::new(0, 0),
|
||||||
constraints: vec![],
|
constraints: vec![],
|
||||||
segment_size: SegmentSize::LastTakesRemainder,
|
flex: Flex::default(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -707,7 +864,7 @@ mod tests {
|
||||||
direction: Direction::Vertical,
|
direction: Direction::Vertical,
|
||||||
margin: Margin::new(0, 0),
|
margin: Margin::new(0, 0),
|
||||||
constraints: vec![Constraint::Min(0)],
|
constraints: vec![Constraint::Min(0)],
|
||||||
segment_size: SegmentSize::LastTakesRemainder,
|
flex: Flex::default(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -720,7 +877,7 @@ mod tests {
|
||||||
direction: Direction::Horizontal,
|
direction: Direction::Horizontal,
|
||||||
margin: Margin::new(0, 0),
|
margin: Margin::new(0, 0),
|
||||||
constraints: vec![Constraint::Min(0)],
|
constraints: vec![Constraint::Min(0)],
|
||||||
segment_size: SegmentSize::LastTakesRemainder,
|
flex: Flex::default(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -820,24 +977,28 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
fn flex_default() {
|
||||||
|
assert_eq!(Layout::default().flex, Flex::StretchLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(deprecated)]
|
||||||
fn segment_size() {
|
fn segment_size() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Layout::default()
|
Layout::default()
|
||||||
.segment_size(SegmentSize::EvenDistribution)
|
.segment_size(SegmentSize::EvenDistribution)
|
||||||
.segment_size,
|
.flex,
|
||||||
SegmentSize::EvenDistribution
|
Flex::Stretch
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Layout::default()
|
Layout::default()
|
||||||
.segment_size(SegmentSize::LastTakesRemainder)
|
.segment_size(SegmentSize::LastTakesRemainder)
|
||||||
.segment_size,
|
.flex,
|
||||||
SegmentSize::LastTakesRemainder
|
Flex::StretchLast
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Layout::default()
|
Layout::default().segment_size(SegmentSize::None).flex,
|
||||||
.segment_size(SegmentSize::None)
|
Flex::Start
|
||||||
.segment_size,
|
|
||||||
SegmentSize::None
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -860,9 +1021,11 @@ mod tests {
|
||||||
/// - overflow: constraint is for more than the full space
|
/// - overflow: constraint is for more than the full space
|
||||||
mod split {
|
mod split {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
assert_buffer_eq,
|
assert_buffer_eq,
|
||||||
|
layout::flex::Flex,
|
||||||
prelude::{Constraint::*, *},
|
prelude::{Constraint::*, *},
|
||||||
widgets::{Paragraph, Widget},
|
widgets::{Paragraph, Widget},
|
||||||
};
|
};
|
||||||
|
@ -1719,5 +1882,98 @@ mod tests {
|
||||||
]));
|
]));
|
||||||
assert_eq!([a.width, b.width, c.width, d.width], [0, 0, 0, 100]);
|
assert_eq!([a.width, b.width, c.width, d.width], [0, 0, 0, 100]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::length_stretches_to_end(Constraint::Length(50), Flex::StretchLast, (0, 100))]
|
||||||
|
#[case::length_stretches(Constraint::Length(50), Flex::Stretch, (0, 100))]
|
||||||
|
#[case::length_left_justified(Constraint::Length(50), Flex::Start, (0, 50))]
|
||||||
|
#[case::length_right_justified(Length(50), Flex::End, (50, 50))]
|
||||||
|
#[case::length_center_justified(Length(50), Flex::Center, (25, 50))]
|
||||||
|
#[case::fixed_stretches_to_end(Fixed(50), Flex::StretchLast, (0, 100))]
|
||||||
|
#[case::fixed_left_justified(Fixed(50), Flex::Start, (0, 50))]
|
||||||
|
#[case::fixed_right_justified(Fixed(50), Flex::End, (50, 50))]
|
||||||
|
#[case::fixed_center_justified(Fixed(50), Flex::Center, (25, 50))]
|
||||||
|
#[case::ratio_stretches_to_end(Ratio(1, 2), Flex::StretchLast, (0, 100))]
|
||||||
|
#[case::ratio_left_justified(Ratio(1, 2), Flex::Start, (0, 50))]
|
||||||
|
#[case::ratio_right_justified(Ratio(1, 2), Flex::End, (50, 50))]
|
||||||
|
#[case::ratio_center_justified(Ratio(1, 2), Flex::Center, (25, 50))]
|
||||||
|
#[case::percent_stretches_to_end(Percentage(50), Flex::StretchLast, (0, 100))]
|
||||||
|
#[case::percent_left_justified(Percentage(50), Flex::Start, (0, 50))]
|
||||||
|
#[case::percent_right_justified(Percentage(50), Flex::End, (50, 50))]
|
||||||
|
#[case::percent_center_justified(Percentage(50), Flex::Center, (25, 50))]
|
||||||
|
#[case::min_stretches_to_end(Min(50), Flex::StretchLast, (0, 100))]
|
||||||
|
#[case::min_left_justified(Min(50), Flex::Start, (0, 50))]
|
||||||
|
#[case::min_right_justified(Min(50), Flex::End, (50, 50))]
|
||||||
|
#[case::min_center_justified(Min(50), Flex::Center, (25, 50))]
|
||||||
|
#[case::max_stretches_to_end(Max(50), Flex::StretchLast, (0, 100))]
|
||||||
|
#[case::max_left_justified(Max(50), Flex::Start, (0, 50))]
|
||||||
|
#[case::max_right_justified(Max(50), Flex::End, (50, 50))]
|
||||||
|
#[case::max_center_justified(Max(50), Flex::Center, (25, 50))]
|
||||||
|
fn flex_one_constraint(
|
||||||
|
#[case] constraint: Constraint,
|
||||||
|
#[case] flex: Flex,
|
||||||
|
#[case] expected_widths: (u16, u16),
|
||||||
|
) {
|
||||||
|
let [a] = Rect::new(0, 0, 100, 1).split(&Layout::horizontal([constraint]).flex(flex));
|
||||||
|
assert_eq!((a.x, a.width), expected_widths);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::length_stretches_to_end([Length(25), Length(25)], Flex::StretchLast, [(0, 25), (25, 75)])]
|
||||||
|
#[case::splits_equally_to_end([Length(25), Length(25)], Flex::Stretch, [(0, 50), (50, 50)])]
|
||||||
|
#[case::lengths_justify_to_start([Length(25), Length(25)], Flex::Start, [(0, 25), (25, 25)])]
|
||||||
|
#[case::length_justifies_to_center([Length(25), Length(25)], Flex::Center, [(25, 25), (50, 25)])]
|
||||||
|
#[case::length_justifies_to_end([Length(25), Length(25)], Flex::End, [(50, 25), (75, 25)])]
|
||||||
|
#[case::fixed_stretches_to_end_last([Fixed(25), Fixed(25)], Flex::StretchLast, [(0, 25), (25, 75)])]
|
||||||
|
#[case::fixed_stretches_to_end([Fixed(25), Fixed(25)], Flex::Stretch, [(0, 50), (50, 50)])]
|
||||||
|
#[case::fixed_justifies_to_start([Fixed(25), Fixed(25)], Flex::Start, [(0, 25), (25, 25)])]
|
||||||
|
#[case::fixed_justifies_to_center([Fixed(25), Fixed(25)], Flex::Center, [(25, 25), (50, 25)])]
|
||||||
|
#[case::fixed_justifies_to_end([Fixed(25), Fixed(25)], Flex::End, [(50, 25), (75, 25)])]
|
||||||
|
#[case::percentage_stretches_to_end_last([Percentage(25), Percentage(25)], Flex::StretchLast, [(0, 25), (25, 75)])]
|
||||||
|
#[case::percentage_stretches_to_end([Percentage(25), Percentage(25)], Flex::Stretch, [(0, 50), (50, 50)])]
|
||||||
|
#[case::percentage_justifies_to_start([Percentage(25), Percentage(25)], Flex::Start, [(0, 25), (25, 25)])]
|
||||||
|
#[case::percentage_justifies_to_center([Percentage(25), Percentage(25)], Flex::Center, [(25, 25), (50, 25)])]
|
||||||
|
#[case::percentage_justifies_to_end([Percentage(25), Percentage(25)], Flex::End, [(50, 25), (75, 25)])]
|
||||||
|
#[case::min_stretches_to_end([Min(25), Min(25)], Flex::StretchLast, [(0, 25), (25, 75)])]
|
||||||
|
#[case::min_stretches_to_end([Min(25), Min(25)], Flex::Stretch, [(0, 50), (50, 50)])]
|
||||||
|
#[case::min_justifies_to_start([Min(25), Min(25)], Flex::Start, [(0, 25), (25, 25)])]
|
||||||
|
#[case::min_justifies_to_center([Min(25), Min(25)], Flex::Center, [(25, 25), (50, 25)])]
|
||||||
|
#[case::min_justifies_to_end([Min(25), Min(25)], Flex::End, [(50, 25), (75, 25)])]
|
||||||
|
#[case::length_spaced_between([Length(25), Length(25)], Flex::SpaceBetween, [(0, 25), (75, 25)])]
|
||||||
|
#[case::length_spaced_around([Length(25), Length(25)], Flex::SpaceAround, [(17, 25), (58, 25)])]
|
||||||
|
fn flex_two_constraints(
|
||||||
|
#[case] constraints: [Constraint; 2],
|
||||||
|
#[case] flex: Flex,
|
||||||
|
#[case] expected_widths: [(u16, u16); 2],
|
||||||
|
) {
|
||||||
|
let [a, b] = Rect::new(0, 0, 100, 1).split(&Layout::horizontal(constraints).flex(flex));
|
||||||
|
assert_eq!([(a.x, a.width), (b.x, b.width)], expected_widths);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::length_spaced_around([Length(25), Length(25), Length(25)], Flex::SpaceBetween, [(0, 25), (38, 25), (75, 25)])]
|
||||||
|
fn flex_three_constraints(
|
||||||
|
#[case] constraints: [Constraint; 3],
|
||||||
|
#[case] flex: Flex,
|
||||||
|
#[case] expected_widths: [(u16, u16); 3],
|
||||||
|
) {
|
||||||
|
let [a, b, c] =
|
||||||
|
Rect::new(0, 0, 100, 1).split(&Layout::horizontal(constraints).flex(flex));
|
||||||
|
assert_eq!(
|
||||||
|
[(a.x, a.width), (b.x, b.width), (c.x, c.width)],
|
||||||
|
expected_widths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flex() {
|
||||||
|
// length should be spaced around
|
||||||
|
let [a, b, c] = Rect::new(0, 0, 100, 1).split(
|
||||||
|
&Layout::horizontal([Length(25), Length(25), Length(25)]).flex(Flex::SpaceAround),
|
||||||
|
);
|
||||||
|
assert!(b.x == 37 || b.x == 38);
|
||||||
|
assert!(b.width == 26 || b.width == 25);
|
||||||
|
assert_eq!([[a.x, a.width], [c.x, c.width]], [[6, 25], [69, 25]]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ mod tests {
|
||||||
constraints: Vec<Constraint>,
|
constraints: Vec<Constraint>,
|
||||||
target: Rect,
|
target: Rect,
|
||||||
) -> Vec<(u16, u16)> {
|
) -> Vec<(u16, u16)> {
|
||||||
|
#[allow(deprecated)]
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(constraints)
|
.constraints(constraints)
|
||||||
|
|
|
@ -741,6 +741,7 @@ impl Table<'_> {
|
||||||
Constraint::Length(self.column_spacing),
|
Constraint::Length(self.column_spacing),
|
||||||
))
|
))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
#[allow(deprecated)]
|
||||||
let layout = Layout::horizontal(constraints)
|
let layout = Layout::horizontal(constraints)
|
||||||
.segment_size(self.segment_size)
|
.segment_size(self.segment_size)
|
||||||
.split(Rect::new(0, 0, max_width, 1));
|
.split(Rect::new(0, 0, max_width, 1));
|
||||||
|
|
Loading…
Reference in a new issue