mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-30 06:21:31 +00:00
feat: Change priority of constraints and add split_with_spacers
✨ (#788)
Follow up to https://github.com/ratatui-org/ratatui/pull/783 This PR introduces different priorities for each kind of constraint. This PR also adds tests that specifies this behavior. This PR resolves a number of broken tests. Fixes https://github.com/ratatui-org/ratatui/issues/827 With this PR, the layout algorithm will do the following in order: 1. Ensure that all the segments are within the user provided area and ensure that all segments and spacers are aligned next to each other 2. if a user provides a `layout.spacing`, it will enforce it. 3. ensure proportional elements are all proportional to each other 4. if a user provides a `Fixed(v)` constraint, it will enforce it. 5. `Min` / `Max` binding inequality constraints 6. `Length` 7. `Percentage` 8. `Ratio` 9. collapse `Min` or collapse `Max` 10. grow `Proportional` as much as possible 11. grow spacers as much as possible This PR also returns the spacer areas as `Rects` to the user. Users can then draw into the spacers as they see fit (thanks @joshka for the idea). Here's a screenshot with the modified flex example: <img width="569" alt="image" src="https://github.com/ratatui-org/ratatui/assets/1813121/46c8901d-882c-43b0-ba87-b1d455099d8f"> This PR introduces a `strengths` module that has "default" weights that give stable solutions as well as predictable behavior.
This commit is contained in:
parent
c1ed5c3637
commit
be4fdaa0c7
112
examples/flex.rs
112
examples/flex.rs
@ -25,6 +25,7 @@ use ratatui::{
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
symbols::line,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
@ -32,35 +33,56 @@ use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
(
|
||||
"Min(u16) takes any excess space when using `Stretch` or `StretchLast`",
|
||||
&[Fixed(20), Min(20), Max(20)],
|
||||
&[Fixed(10), Min(10), Max(10), Percentage(10), Ratio(1,10)],
|
||||
),
|
||||
(
|
||||
"Proportional(u16) takes any excess space in all `Flex` layouts",
|
||||
"Proportional(u16) takes any excess space always",
|
||||
&[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)],
|
||||
),
|
||||
(
|
||||
"In `StretchLast`, last constraint of lowest priority takes excess space",
|
||||
"Here's all constraints in one line",
|
||||
&[Fixed(10), Min(10), Max(10), Percentage(10), Ratio(1,10), Proportional(1)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Percentage(50), Percentage(25), Ratio(1, 8), Min(10)],
|
||||
),
|
||||
(
|
||||
"In `StretchLast`, the last constraint of lowest priority takes excess space",
|
||||
&[Length(20), Fixed(20), Percentage(20)],
|
||||
),
|
||||
("", &[Fixed(20), Percentage(20), Length(20)]),
|
||||
("", &[Percentage(20), Length(20), Fixed(20)]),
|
||||
("", &[Length(20), Length(15)]),
|
||||
("Spacing has no effect in `SpaceAround` and `SpaceBetween`", &[Proportional(1), Proportional(1)]),
|
||||
("A lowest priority constraint will be broken before a high priority constraint", &[Ratio(1,4), Percentage(20)]),
|
||||
("`Length` is higher priority than `Percentage`", &[Percentage(20), Length(10)]),
|
||||
("`Min/Max` is higher priority than `Length`", &[Length(10), Max(20)]),
|
||||
("", &[Length(100), Min(20)]),
|
||||
("`Fixed` is higher priority than `Min/Max`", &[Max(20), Fixed(10)]),
|
||||
("", &[Min(20), Fixed(90)]),
|
||||
("Proportional is the lowest priority and will fill any excess space", &[Proportional(1), Ratio(1, 4)]),
|
||||
("Proportional can be used to scale proportionally with other Proportional blocks", &[Proportional(1), Percentage(20), Proportional(2)]),
|
||||
("", &[Ratio(1, 3), Percentage(20), Ratio(2, 3)]),
|
||||
("StretchLast will stretch the last lowest priority constraint\nStretch will only stretch equal weighted constraints", &[Length(20), Length(15)]),
|
||||
("", &[Percentage(20), Length(15)]),
|
||||
("`Proportional(u16)` fills up excess space, but is lower priority to spacers.\ni.e. Proportional will only have widths in Flex::Stretch and Flex::StretchLast", &[Proportional(1), Proportional(1)]),
|
||||
("", &[Length(20), Fixed(20)]),
|
||||
(
|
||||
"When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
|
||||
&[Min(20), Max(20)],
|
||||
),
|
||||
(
|
||||
"`SpaceBetween` stretches when there's only one constraint",
|
||||
"",
|
||||
&[Max(20)],
|
||||
),
|
||||
("", &[Min(20), Max(20), Length(20), Fixed(20)]),
|
||||
("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]),
|
||||
("", &[Proportional(0), Proportional(0)]),
|
||||
(
|
||||
"`Proportional(1)` can be to scale with respect to other `Proportional(2)`",
|
||||
&[Proportional(1), Proportional(2)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Proportional(1), Min(10), Max(10), Proportional(2)],
|
||||
),
|
||||
(
|
||||
"`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:",
|
||||
&[
|
||||
@ -116,12 +138,6 @@ fn main() -> Result<()> {
|
||||
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
// Each line in the example is a layout
|
||||
// so 13 examples * 7 = 91 currently
|
||||
// Plus additional layout for tabs ...
|
||||
Layout::init_cache(120);
|
||||
|
||||
App::default().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
@ -233,11 +249,19 @@ impl Widget for App {
|
||||
self.tabs().render(tabs, buf);
|
||||
let scroll_needed = self.render_demo(demo, buf);
|
||||
let axis_width = if scroll_needed {
|
||||
axis.width - 1
|
||||
axis.width.saturating_sub(1)
|
||||
} else {
|
||||
axis.width
|
||||
};
|
||||
self.axis(axis_width, self.spacing).render(axis, buf);
|
||||
let spacing = if matches!(
|
||||
self.selected_tab,
|
||||
SelectedTab::SpaceBetween | SelectedTab::SpaceAround
|
||||
) {
|
||||
0
|
||||
} else {
|
||||
self.spacing
|
||||
};
|
||||
self.axis(axis_width, spacing).render(axis, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,7 +288,7 @@ impl App {
|
||||
} else {
|
||||
format!("{} px", width)
|
||||
};
|
||||
let bar_width = width - 2; // we want to `<` and `>` at the ends
|
||||
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
Paragraph::new(width_bar.dark_gray()).centered()
|
||||
}
|
||||
@ -397,10 +421,11 @@ impl Widget for Example {
|
||||
let title_height = get_description_height(&self.description);
|
||||
let layout = Layout::vertical([Fixed(title_height), Proportional(0)]);
|
||||
let [title, illustrations] = area.split(&layout);
|
||||
let blocks = Layout::horizontal(&self.constraints)
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(self.flex)
|
||||
.spacing(self.spacing)
|
||||
.split(illustrations);
|
||||
.split_with_spacers(illustrations);
|
||||
|
||||
if !self.description.is_empty() {
|
||||
Paragraph::new(
|
||||
@ -417,10 +442,59 @@ impl Widget for Example {
|
||||
self.illustration(*constraint, block.width)
|
||||
.render(*block, buf);
|
||||
}
|
||||
|
||||
for spacer in spacers.iter() {
|
||||
self.render_spacer(*spacer, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn render_spacer(&self, spacer: Rect, buf: &mut Buffer) {
|
||||
if spacer.width > 1 {
|
||||
let corners_only = symbols::border::Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(corners_only)
|
||||
.border_style(Style::reset().dark_gray())
|
||||
.render(spacer, buf);
|
||||
} else {
|
||||
Paragraph::new(Text::from(vec![
|
||||
Line::from(""),
|
||||
Line::from("│"),
|
||||
Line::from("│"),
|
||||
Line::from(""),
|
||||
]))
|
||||
.style(Style::reset().dark_gray())
|
||||
.render(spacer, buf);
|
||||
}
|
||||
let width = spacer.width;
|
||||
let label = if width > 4 {
|
||||
format!("{width} px")
|
||||
} else if width > 2 {
|
||||
format!("{width}")
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let text = Text::from(vec![
|
||||
Line::raw(""),
|
||||
Line::raw(""),
|
||||
Line::styled(label, Style::reset().dark_gray()),
|
||||
]);
|
||||
Paragraph::new(text)
|
||||
.style(Style::reset().dark_gray())
|
||||
.alignment(Alignment::Center)
|
||||
.render(spacer, buf);
|
||||
}
|
||||
|
||||
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
|
||||
let main_color = color_for_constraint(constraint);
|
||||
let fg_color = Color::White;
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::EnumIs;
|
||||
|
||||
/// A constraint that defines the size of a layout element.
|
||||
///
|
||||
@ -14,9 +15,12 @@ use itertools::Itertools;
|
||||
/// Constraints are prioritized in the following order:
|
||||
///
|
||||
/// 1. [`Constraint::Fixed`]
|
||||
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
|
||||
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
|
||||
/// 4. [`Constraint::Proportional`]
|
||||
/// 2. [`Constraint::Min`]
|
||||
/// 3. [`Constraint::Max`]
|
||||
/// 4. [`Constraint::Length`]
|
||||
/// 5. [`Constraint::Percentage`]
|
||||
/// 6. [`Constraint::Ratio`]
|
||||
/// 7. [`Constraint::Proportional`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@ -43,8 +47,97 @@ use itertools::Itertools;
|
||||
/// // Create a layout with proportional sizes for each element
|
||||
/// let constraints = Constraint::from_proportional_lengths([1, 2, 1]);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, EnumIs)]
|
||||
pub enum Constraint {
|
||||
/// Applies a fixed size to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
/// [`Constraint::Fixed`] will take precedence over all other constraints.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Fixed(40), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Fixed(20), Fixed(20), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌──────────────────┐┌────────┐
|
||||
/// │ 20 px ││ 20 px ││ 10 px │
|
||||
/// └──────────────────┘└──────────────────┘└────────┘
|
||||
/// ```
|
||||
Fixed(u16),
|
||||
/// Applies a minimum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at least the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Min(u16),
|
||||
/// Applies a maximum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at most the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Max(u16),
|
||||
/// Applies a length constraint to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Length(20), Fixed(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Length(20), Length(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌────────────────────────────┐
|
||||
/// │ 20 px ││ 30 px │
|
||||
/// └──────────────────┘└────────────────────────────┘
|
||||
/// ```
|
||||
Length(u16),
|
||||
/// Applies a percentage of the available space to the element
|
||||
///
|
||||
/// Converts the given percentage to a floating-point value and multiplies that with area.
|
||||
@ -91,51 +184,6 @@ pub enum Constraint {
|
||||
/// └───────────┘└──────────┘└───────────┘└──────────┘
|
||||
/// ```
|
||||
Ratio(u32, u32),
|
||||
/// Applies a fixed size to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
/// [`Constraint::Fixed`] will take precedence over all other constraints.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Fixed(40), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Fixed(20), Fixed(20), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌──────────────────┐┌────────┐
|
||||
/// │ 20 px ││ 20 px ││ 10 px │
|
||||
/// └──────────────────┘└──────────────────┘└────────┘
|
||||
/// ```
|
||||
Fixed(u16),
|
||||
/// Applies a length constraint to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Length(20), Fixed(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Length(20), Length(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌────────────────────────────┐
|
||||
/// │ 20 px ││ 30 px │
|
||||
/// └──────────────────┘└────────────────────────────┘
|
||||
/// ```
|
||||
Length(u16),
|
||||
/// Applies the scaling factor proportional to all other [`Constraint::Proportional`] elements
|
||||
/// to fill excess space
|
||||
///
|
||||
@ -161,50 +209,6 @@ pub enum Constraint {
|
||||
/// └───────────┘└───────────────────────┘└──────────┘
|
||||
/// ```
|
||||
Proportional(u16),
|
||||
/// Applies a maximum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at most the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Max(u16),
|
||||
/// Applies a minimum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at least the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Min(u16),
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
|
@ -206,7 +206,6 @@ pub enum Flex {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20) │
|
||||
|
1216
src/layout/layout.rs
1216
src/layout/layout.rs
File diff suppressed because it is too large
Load Diff
@ -399,28 +399,26 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
]),
|
||||
);
|
||||
|
||||
// This test is unstable and should not be in the test suite
|
||||
//
|
||||
// // columns of large size (>100% total) hide the last column
|
||||
// test_case(
|
||||
// &[
|
||||
// Constraint::Percentage(60),
|
||||
// Constraint::Length(10),
|
||||
// Constraint::Proportional(60),
|
||||
// ],
|
||||
// Buffer::with_lines(vec![
|
||||
// "┌────────────────────────────┐",
|
||||
// "│Head1 Head2 │",
|
||||
// "│ │",
|
||||
// "│Row11 Row12 │",
|
||||
// "│Row21 Row22 │",
|
||||
// "│Row31 Row32 │",
|
||||
// "│Row41 Row42 │",
|
||||
// "│ │",
|
||||
// "│ │",
|
||||
// "└────────────────────────────┘",
|
||||
// ]),
|
||||
// );
|
||||
// columns of large size (>100% total) hide the last column
|
||||
test_case(
|
||||
&[
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Length(10),
|
||||
Constraint::Percentage(60),
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
Loading…
x
Reference in New Issue
Block a user