feat: add ergonomic methods for layouting Rects (#1909)

This commit introduces new methods for the `Rect` struct that simplify
the process of splitting a `Rect` into sub-rects according to a given
`Layout`. By putting these methods on the `Rect` struct, we make it a
bit more natural that a layout is applied to the `Rect` itself, rather
than passing a `Rect` to the `Layout` struct to be split.

Adds:
- `Rect::layout` and `Rect::try_layout` methods that allow splitting a
  `Rect` into an array of sub-rects according to a given `Layout`.
- `Rect::layout_vec` method that returns a `Vec` of sub-rects.
- `Layout::try_areas` method that returns an array of sub-rects, with
  compile-time checks for the number of constraints. This is added
  mainly for consistency with the new `Rect` methods.

```rust
use ratatui_core::layout::{Layout, Constraint, Rect};
let area = Rect::new(0, 0, 10, 10);
let layout = Layout::vertical([Constraint::Fill(1); 2]);

// Rect::layout() infers the number of constraints at compile time:
let [top, main] = area.layout(&layout);

// Rect::try_layout() and Layout::try_areas() do the same, but return a
// Result:
let [top, main] = area.try_layout(&layout)?;
let [top, main] = layout.try_areas(area)?;

// Rect::layout_vec() returns a Vec of sub-rects:
let areas_vec = area.layout_vec(&layout);

// you can also explicitly specify the number of constraints:
let areas = area.layout::<2>(&layout);
let areas = area.try_layout::<2>(&layout)?;
let areas = layout.try_areas::<2>(area)?;
```
This commit is contained in:
Josh McKinney 2025-06-28 01:23:34 -07:00 committed by GitHub
parent d99984f1e9
commit 6dcd53bc6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 343 additions and 177 deletions

View File

@ -68,7 +68,7 @@ impl App {
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let constraints = Constraint::from_lengths([1, 1, 2, 1]);
let [greeting, timer, squares, position] = Layout::vertical(constraints).areas(area);
let [greeting, timer, squares, position] = area.layout(&Layout::vertical(constraints));
// render an ephemeral greeting widget
Greeting::new("Ratatui!").render(greeting, buf);
@ -174,9 +174,9 @@ struct BlueSquare;
impl Widget for &BoxedSquares {
fn render(self, area: Rect, buf: &mut Buffer) {
let constraints = vec![Constraint::Length(4); self.squares.len()];
let areas = Layout::horizontal(constraints).split(area);
for (widget, area) in self.squares.iter().zip(areas.iter()) {
widget.render_ref(*area, buf);
let areas = area.layout_vec(&Layout::horizontal(constraints));
for (widget, area) in self.squares.iter().zip(areas) {
widget.render_ref(area, buf);
}
}
}

View File

@ -78,8 +78,8 @@ impl App {
}
fn render(&self, frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
let [title_area, body_area] = vertical.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
let [title_area, body_area] = frame.area().layout(&layout);
let title = Line::from("Ratatui async example").centered().bold();
frame.render_widget(title, title_area);
frame.render_widget(&self.pull_requests, body_area);

View File

@ -81,11 +81,10 @@ fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date
)),
]);
let vertical = Layout::vertical([
let [text_area, area] = frame.area().layout(&Layout::vertical([
Constraint::Length(header.height() as u16),
Constraint::Fill(1),
]);
let [text_area, area] = vertical.areas(frame.area());
]));
frame.render_widget(header.centered(), text_area);
calendar_style
.render_year(frame, area, selected_date)
@ -133,16 +132,13 @@ impl StyledCalendar {
fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> {
let events = events(date)?;
let area = area.inner(Margin {
vertical: 1,
horizontal: 1,
});
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
let areas = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row)
.to_vec()
});
let vertical = Layout::vertical([Constraint::Ratio(1, 3); 3]);
let horizontal = &Layout::horizontal([Constraint::Ratio(1, 4); 4]);
let areas = area
.inner(Margin::new(1, 1))
.layout_vec(&vertical)
.into_iter()
.flat_map(|row| row.layout_vec(horizontal));
for (i, area) in areas.enumerate() {
let month = date
.replace_day(1)

View File

@ -153,16 +153,15 @@ impl App {
let vertical = Layout::vertical([
Constraint::Length(header.height() as u16),
Constraint::Percentage(50),
Constraint::Percentage(50),
Constraint::Fill(1),
Constraint::Fill(1),
]);
let [text_area, up, down] = vertical.areas(frame.area());
let [text_area, up, down] = frame.area().layout(&vertical);
frame.render_widget(header.centered(), text_area);
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [draw, pong] = horizontal.areas(up);
let [map, boxes] = horizontal.areas(down);
let horizontal = Layout::horizontal([Constraint::Fill(1); 2]);
let [draw, pong] = up.layout(&horizontal);
let [map, boxes] = down.layout(&horizontal);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.draw_canvas(draw), draw);

View File

@ -108,10 +108,11 @@ impl App {
}
fn render(&self, frame: &mut Frame) {
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
let [animated_chart, bar_chart] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
let vertical = Layout::vertical([Constraint::Fill(1); 2]);
let [top, bottom] = frame.area().layout(&vertical);
let horizontal = Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]);
let [animated_chart, bar_chart] = top.layout(&horizontal);
let [line_chart, scatter] = bottom.layout(&Layout::horizontal([Constraint::Fill(1); 2]));
self.render_animated_chart(frame, animated_chart);
render_barchart(frame, bar_chart);

View File

@ -82,12 +82,12 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
let areas = vertical.iter().flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
});
let vertical = Layout::vertical([Constraint::Length(1); 2]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 8); 8]);
let areas = inner
.layout_vec(&vertical)
.into_iter()
.flat_map(|area| area.layout_vec(&horizontal));
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) {
let color_name = fg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
@ -100,12 +100,12 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
let areas = vertical.iter().flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
});
let vertical = Layout::vertical([Constraint::Length(1); 2]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 8); 8]);
let areas = inner
.layout_vec(&vertical)
.into_iter()
.flat_map(|area| area.layout_vec(&horizontal));
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) {
let color_name = bg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);

View File

@ -124,8 +124,8 @@ impl App {
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
let [top, colors] = area.layout(&Layout::vertical([Length(1), Min(0)]));
let [title, fps] = top.layout(&Layout::horizontal([Min(0), Length(8)]));
Text::from("colors_rgb example. Press q to quit")
.centered()
.render(title, buf);

View File

@ -242,14 +242,13 @@ impl Widget for &App {
swap_legend_area,
_,
blocks_area,
] = Layout::vertical([
] = area.layout(&Layout::vertical([
Length(2), // header
Length(2), // instructions
Length(1), // swap key legend
Length(1), // gap
Fill(1), // blocks
])
.areas(area);
]));
App::header().render(header_area, buf);
App::instructions().render(instructions_area, buf);
@ -319,9 +318,8 @@ impl App {
}
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
.spacing(1)
.areas(area);
let main_layout = Layout::vertical([Length(3), Fill(1)]).spacing(1);
let [user_constraints, area] = area.layout(&main_layout);
self.render_user_constraints_legend(user_constraints, buf);
@ -332,7 +330,7 @@ impl App {
space_between,
space_around,
space_evenly,
] = Layout::vertical([Length(7); 6]).areas(area);
] = area.layout(&Layout::vertical([Length(7); 6]));
self.render_layout_block(Flex::Start, start, buf);
self.render_layout_block(Flex::Center, center, buf);
@ -353,8 +351,8 @@ impl App {
}
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
let [label_area, axis_area, blocks_area] =
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
let layout = Layout::vertical([Length(1), Max(1), Length(4)]);
let [label_area, axis_area, blocks_area] = area.layout(&layout);
if label_area.height > 0 {
format!("Flex::{flex:?}").bold().render(label_area, buf);

View File

@ -140,7 +140,7 @@ impl App {
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
let [tabs, axis, demo] = area.layout(&Layout::vertical([Length(3), Length(3), Fill(0)]));
self.render_tabs(tabs, buf);
Self::render_axis(axis, buf);
@ -281,8 +281,8 @@ impl Widget for SelectedTab {
impl SelectedTab {
fn render_length_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 4]);
let [example1, example2, example3, _] = area.layout(&layout);
Example::new(&[Length(20), Length(20)]).render(example1, buf);
Example::new(&[Length(20), Min(20)]).render(example2, buf);
@ -290,8 +290,8 @@ impl SelectedTab {
}
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
@ -301,8 +301,8 @@ impl SelectedTab {
}
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 5]);
let [example1, example2, example3, example4, _] = area.layout(&layout);
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
@ -311,15 +311,15 @@ impl SelectedTab {
}
fn render_fill_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
let [example1, example2, _] = area.layout(&Layout::vertical([Length(EXAMPLE_HEIGHT); 3]));
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
}
fn render_min_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
@ -329,8 +329,8 @@ impl SelectedTab {
}
fn render_max_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
@ -354,9 +354,10 @@ impl Example {
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let [area, _] =
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
let blocks = Layout::horizontal(&self.constraints).split(area);
let vertical = Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]);
let horizontal = Layout::horizontal(&self.constraints);
let [area, _] = area.layout(&vertical);
let blocks = area.layout_vec(&horizontal);
for (block, constraint) in blocks.iter().zip(&self.constraints) {
Self::illustration(*constraint, block.width).render(*block, buf);

View File

@ -15,7 +15,7 @@ use crossterm::event::{
};
use crossterm::execute;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::Line;
use ratatui::widgets::{Paragraph, Widget};
@ -167,13 +167,13 @@ fn run(mut terminal: DefaultTerminal) -> Result<()> {
}
fn render(frame: &mut Frame, states: [State; 3]) {
let vertical = Layout::vertical([
let layout = Layout::vertical([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
]);
let [title, buttons, help, _] = vertical.areas(frame.area());
let [title, buttons, help, _] = frame.area().layout(&layout);
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),
@ -184,13 +184,8 @@ fn render(frame: &mut Frame, states: [State; 3]) {
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
let horizontal = Layout::horizontal([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
]);
let [red, green, blue, _] = horizontal.areas(area);
let layout = Layout::horizontal([Constraint::Length(15); 3]).flex(Flex::Start);
let [red, green, blue] = area.layout(&layout);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);

View File

@ -129,12 +129,12 @@ impl App {
/// matter, but for larger apps this can be a significant performance improvement.
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([
let layout = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = vertical.areas(area);
let [title_bar, tab, bottom_bar] = area.layout(&layout);
Block::new().style(THEME.root).render(area, buf);
self.render_title_bar(title_bar, buf);
@ -146,7 +146,7 @@ impl Widget for &App {
impl App {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
let [title, tabs] = layout.areas(area);
let [title, tabs] = area.layout(&layout);
Span::styled("Ratatui", THEME.app_title).render(title, buf);
let titles = Tab::iter().map(Tab::title);

View File

@ -134,7 +134,7 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
let vertical = Layout::vertical([height]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
let [area] = area.layout(&vertical);
let [area] = area.layout(&horizontal);
area
}

View File

@ -24,8 +24,8 @@ impl AboutTab {
impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [logo_area, description] = horizontal.areas(area);
let layout = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [logo_area, description] = area.layout(&layout);
render_crate_description(description, buf);
let eye_state = if self.row_index % 2 == 0 {
MascotEyeColor::Default

View File

@ -71,15 +71,15 @@ impl Widget for EmailTab {
horizontal: 2,
});
Clear.render(area, buf);
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = vertical.areas(area);
let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = area.layout(&layout);
render_inbox(self.row_index, inbox, buf);
render_email(self.row_index, email, buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = vertical.areas(area);
let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = area.layout(&layout);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
@ -130,8 +130,8 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
let inner = block.inner(area);
block.render(area, buf);
if let Some(email) = email {
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = vertical.areas(inner);
let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = inner.layout(&layout);
let headers = vec![
Line::from(vec![
"From: ".set_style(theme.header),

View File

@ -135,8 +135,8 @@ impl Widget for RecipeTab {
horizontal: 2,
vertical: 1,
});
let [recipe, ingredients] =
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
let layout = Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]);
let [recipe, ingredients] = area.layout(&layout);
render_recipe(recipe, buf);
render_ingredients(self.row_index, ingredients, buf);

View File

@ -39,8 +39,8 @@ impl Widget for TracerouteTab {
Block::new().style(THEME.content).render(area, buf);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
let [left, map] = horizontal.areas(area);
let [hops, pings] = vertical.areas(left);
let [left, map] = area.layout(&horizontal);
let [hops, pings] = left.layout(&vertical);
render_hops(self.row_index, hops, buf);
render_ping(self.row_index, pings, buf);

View File

@ -41,16 +41,16 @@ impl Widget for WeatherTab {
horizontal: 2,
vertical: 1,
});
let [main, _, gauges] = Layout::vertical([
let tab_layout = Layout::vertical([
Constraint::Min(0),
Constraint::Length(1),
Constraint::Length(1),
])
.areas(area);
let [calendar, charts] =
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
let [simple, horizontal] =
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
]);
let [main, _, gauges] = area.layout(&tab_layout);
let main_layout = Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]);
let [calendar, charts] = main.layout(&main_layout);
let charts_layout = Layout::vertical([Constraint::Length(29), Constraint::Min(0)]);
let [simple, horizontal] = charts.layout(&charts_layout);
render_calendar(calendar, buf);
render_simple_barchart(simple, buf);

View File

@ -256,7 +256,7 @@ fn example_height() -> u16 {
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]);
let [tabs, axis, demo] = layout.areas(area);
let [tabs, axis, demo] = area.layout(&layout);
self.tabs().render(tabs, buf);
let scroll_needed = self.render_demo(demo, buf);
let axis_width = if scroll_needed {
@ -421,7 +421,7 @@ impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let title_height = get_description_height(&self.description);
let layout = Layout::vertical([Length(title_height), Fill(0)]);
let [title, illustrations] = layout.areas(area);
let [title, illustrations] = area.layout(&layout);
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.flex(self.flex)

View File

@ -103,10 +103,10 @@ impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min, Ratio};
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, gauge_area, footer_area] = layout.areas(area);
let [header_area, gauge_area, footer_area] = area.layout(&layout);
let layout = Layout::vertical([Ratio(1, 4); 4]);
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = gauge_area.layout(&layout);
render_header(header_area, buf);
render_footer(footer_area, buf);

View File

@ -230,8 +230,8 @@ fn render(frame: &mut Frame, downloads: &Downloads) {
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = vertical.areas(area);
let [list_area, gauge_area] = horizontal.areas(main);
let [progress_area, main] = area.layout(&vertical);
let [list_area, gauge_area] = main.layout(&horizontal);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();

View File

@ -115,8 +115,8 @@ impl InputForm {
///
/// The cursor is placed at the end of the focused field.
fn render(&self, frame: &mut Frame) {
let [first_name_area, last_name_area, age_area] =
Layout::vertical(Constraint::from_lengths([1, 1, 1])).areas(frame.area());
let layout = Layout::vertical(Constraint::from_lengths([1, 1, 1]));
let [first_name_area, last_name_area, age_area] = frame.area().layout(&layout);
frame.render_widget(&self.first_name, first_name_area);
frame.render_widget(&self.last_name, last_name_area);
@ -185,11 +185,11 @@ impl StringField {
impl Widget for &StringField {
fn render(self, area: Rect, buf: &mut Buffer) {
let constraints = [
let layout = Layout::horizontal([
Constraint::Length(self.label.len() as u16 + 2),
Constraint::Fill(1),
];
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
]);
let [label_area, value_area] = area.layout(&layout);
let label = Line::from_iter([self.label, ": "]).bold();
label.render(label_area, buf);
self.value.clone().render(value_area, buf);
@ -250,11 +250,11 @@ impl AgeField {
impl Widget for &AgeField {
fn render(self, area: Rect, buf: &mut Buffer) {
let constraints = [
let layout = Layout::horizontal([
Constraint::Length(self.label.len() as u16 + 2),
Constraint::Fill(1),
];
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
]);
let [label_area, value_area] = area.layout(&layout);
let label = Line::from_iter([self.label, ": "]).bold();
let value = self.value.to_string();
label.render(label_area, buf);

View File

@ -33,8 +33,8 @@ fn main() -> Result<()> {
}
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [text_area, main_area] = vertical.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [text_area, main_area] = frame.area().layout(&layout);
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),

View File

@ -41,8 +41,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame, show_popup: bool) {
let area = frame.area();
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
let [instructions, content] = vertical.areas(area);
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
let [instructions, content] = area.layout(&layout);
frame.render_widget(
Line::from("Press 'p' to toggle popup, 'q' to quit").centered(),
@ -64,7 +64,7 @@ fn render(frame: &mut Frame, show_popup: bool) {
fn centered_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
let [area] = area.layout(&vertical);
let [area] = area.layout(&horizontal);
area
}

View File

@ -186,8 +186,8 @@ impl App {
}
fn render(&mut self, frame: &mut Frame) {
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
let rects = vertical.split(frame.area());
let layout = Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
let rects = frame.area().layout_vec(&layout);
self.set_colors();

View File

@ -178,15 +178,15 @@ impl App {
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [header_area, main_area, footer_area] = Layout::vertical([
let main_layout = Layout::vertical([
Constraint::Length(2),
Constraint::Fill(1),
Constraint::Length(1),
])
.areas(area);
]);
let [header_area, content_area, footer_area] = area.layout(&main_layout);
let [list_area, item_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
let content_layout = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]);
let [list_area, item_area] = content_area.layout(&content_layout);
App::render_header(header_area, buf);
App::render_footer(footer_area, buf);

View File

@ -155,12 +155,12 @@ impl App {
}
fn render(&self, frame: &mut Frame) {
let vertical = Layout::vertical([
let layout = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]);
let [help_area, input_area, messages_area] = vertical.areas(frame.area());
let [help_area, input_area, messages_area] = frame.area().layout(&layout);
let (msg, style) = match self.input_mode {
InputMode::Normal => (

View File

@ -32,9 +32,8 @@ fn main() -> Result<()> {
}
fn render(frame: &mut Frame, temperatures: &[u8]) {
let [title, main] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)])
.spacing(1)
.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [title, main] = frame.area().layout(&layout);
frame.render_widget("Weather demo".bold().into_centered_line(), title);
frame.render_widget(vertical_barchart(temperatures), main);

View File

@ -1,5 +1,6 @@
use alloc::rc::Rc;
use alloc::vec::Vec;
use core::array::TryFromSliceError;
use core::iter;
#[cfg(feature = "layout-cache")]
use core::num::NonZeroUsize;
@ -550,8 +551,43 @@ impl Layout {
/// let areas = layout.areas::<2>(area);
/// ```
pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
let (areas, _) = self.split_with_spacers(area);
areas.as_ref().try_into().expect("invalid number of rects")
let areas = self.split(area);
areas.as_ref().try_into().unwrap_or_else(|_| {
panic!(
"invalid number of rects: expected {N}, found {}",
areas.len()
)
})
}
/// Split the rect into a number of sub-rects according to the given [`Layout`].
///
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// This method requires the number of constraints to be known at compile time. If you don't
/// know the number of constraints at compile time, use [`Layout::split`] instead.
///
/// # Errors
///
/// Returns an error if the number of constraints is not equal to the length of the returned
/// array.
///
/// # Examples
///
/// ```rust
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let area = Rect::new(0, 0, 10, 10);
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.try_areas(area)?;
///
/// // or explicitly specify the number of constraints:
/// let areas = layout.try_areas::<2>(area)?;
/// # Ok::<(), core::array::TryFromSliceError>(())
/// ```
pub fn try_areas<const N: usize>(&self, area: Rect) -> Result<[Rect; N], TryFromSliceError> {
self.split(area).as_ref().try_into()
}
/// Split the rect into a number of sub-rects according to the given [`Layout`] and return just

View File

@ -1,4 +1,5 @@
#![warn(missing_docs)]
use core::array::TryFromSliceError;
use core::cmp::{max, min};
use core::fmt;
@ -482,9 +483,7 @@ impl Rect {
/// ```
#[must_use]
pub fn centered_horizontally(self, constraint: Constraint) -> Self {
let [area] = Layout::horizontal([constraint])
.flex(Flex::Center)
.areas(self);
let [area] = self.layout(&Layout::horizontal([constraint]).flex(Flex::Center));
area
}
@ -502,9 +501,7 @@ impl Rect {
/// ```
#[must_use]
pub fn centered_vertically(self, constraint: Constraint) -> Self {
let [area] = Layout::vertical([constraint])
.flex(Flex::Center)
.areas(self);
let [area] = self.layout(&Layout::vertical([constraint]).flex(Flex::Center));
area
}
@ -532,6 +529,99 @@ impl Rect {
.centered_vertically(vertical_constraint)
}
/// Split the rect into a number of sub-rects according to the given [`Layout`].
///
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// This method requires the number of constraints to be known at compile time. If you don't
/// know the number of constraints at compile time, use [`Layout::split`] instead.
///
/// # Panics
///
/// Panics if the number of constraints is not equal to the length of the returned array.
///
/// # Examples
///
/// ```
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let area = Rect::new(0, 0, 10, 10);
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = area.layout(&layout);
/// assert_eq!(top, Rect::new(0, 0, 10, 1));
/// assert_eq!(main, Rect::new(0, 1, 10, 9));
///
/// // or explicitly specify the number of constraints:
/// let areas = area.layout::<2>(&layout);
/// assert_eq!(areas, [Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
/// ```
#[must_use]
pub fn layout<const N: usize>(self, layout: &Layout) -> [Self; N] {
let areas = layout.split(self);
areas.as_ref().try_into().unwrap_or_else(|_| {
panic!(
"invalid number of rects: expected {N}, found {}",
areas.len()
)
})
}
/// Split the rect into a number of sub-rects according to the given [`Layout`].
///
/// An ergonomic wrapper around [`Layout::split`] that returns a [`Vec`] of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// # Examples
///
/// ```
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let area = Rect::new(0, 0, 10, 10);
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let areas = area.layout_vec(&layout);
/// assert_eq!(areas, vec![Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
/// ```
///
/// [`Vec`]: alloc::vec::Vec
#[must_use]
pub fn layout_vec(self, layout: &Layout) -> alloc::vec::Vec<Self> {
layout.split(self).as_ref().to_vec()
}
/// Try to split the rect into a number of sub-rects according to the given [`Layout`].
///
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// # Errors
///
/// Returns an error if the number of constraints is not equal to the length of the returned
/// array.
///
/// # Examples
///
/// ```
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let area = Rect::new(0, 0, 10, 10);
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = area.try_layout(&layout)?;
/// assert_eq!(top, Rect::new(0, 0, 10, 1));
/// assert_eq!(main, Rect::new(0, 1, 10, 9));
///
/// // or explicitly specify the number of constraints:
/// let areas = area.try_layout::<2>(&layout)?;
/// assert_eq!(areas, [Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
/// # Ok::<(), core::array::TryFromSliceError>(())
/// ``````
pub fn try_layout<const N: usize>(
self,
layout: &Layout,
) -> Result<[Self; N], TryFromSliceError> {
layout.split(self).as_ref().try_into()
}
/// indents the x value of the `Rect` by a given `offset`
///
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
@ -892,4 +982,54 @@ mod tests {
Rect::new(1, 2, 3, 1)
);
}
#[test]
fn layout() {
let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
let [a, b] = Rect::new(0, 0, 10, 10).layout(&layout);
assert_eq!(a, Rect::new(0, 0, 3, 10));
assert_eq!(b, Rect::new(3, 0, 7, 10));
let areas = Rect::new(0, 0, 10, 10).layout::<2>(&layout);
assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
}
#[test]
#[should_panic(expected = "invalid number of rects: expected 3, found 1")]
fn layout_invalid_number_of_rects() {
let layout = Layout::horizontal([Constraint::Length(1)]);
let [_, _, _] = Rect::new(0, 0, 10, 10).layout(&layout);
}
#[test]
fn layout_vec() {
let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
let areas = Rect::new(0, 0, 10, 10).layout_vec(&layout);
assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
}
#[test]
fn try_layout() {
let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
let [a, b] = Rect::new(0, 0, 10, 10).try_layout(&layout).unwrap();
assert_eq!(a, Rect::new(0, 0, 3, 10));
assert_eq!(b, Rect::new(3, 0, 7, 10));
let areas = Rect::new(0, 0, 10, 10).try_layout::<2>(&layout).unwrap();
assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
}
#[test]
fn try_layout_invalid_number_of_rects() {
let layout = Layout::horizontal([Constraint::Length(1)]);
Rect::new(0, 0, 10, 10)
.try_layout::<3>(&layout)
.unwrap_err();
}
}

View File

@ -40,8 +40,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Fill(1); 2]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [left, right] = horizontal.areas(main);
let [top, main] = frame.area().layout(&vertical);
let [left, right] = main.layout(&horizontal);
let title = Line::from_iter([
Span::from("BarChart Widget (Grouped)").bold(),

View File

@ -38,8 +38,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Length(28), Constraint::Fill(1)]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [left, right] = horizontal.areas(main);
let [top, main] = frame.area().layout(&vertical);
let [left, right] = main.layout(&horizontal);
let title = Line::from_iter([
Span::from("BarChart Widget").bold(),

View File

@ -38,8 +38,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Percentage(33); 3]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [left, middle, right] = horizontal.areas(main);
let [top, main] = frame.area().layout(&vertical);
let [left, middle, right] = main.layout(&horizontal);
let title = Line::from_iter([
Span::from("Block Widget").bold(),

View File

@ -40,8 +40,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Percentage(50); 2]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [left, right] = horizontal.areas(main);
let [top, main] = frame.area().layout(&vertical);
let [left, right] = main.layout(&horizontal);
let title = Line::from_iter([
Span::from("Calendar Widget").bold(),

View File

@ -40,8 +40,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Percentage(100)]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [area] = horizontal.areas(main);
let [top, main] = frame.area().layout(&vertical);
let [area] = main.layout(&horizontal);
let title = TextLine::from_iter([
Span::from("Canvas Widget").bold(),

View File

@ -37,8 +37,8 @@ fn main() -> Result<()> {
/// Render the UI with a chart.
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("Chart Widget").bold(),

View File

@ -36,13 +36,13 @@ fn main() -> Result<()> {
/// Render the UI with various progress bars.
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([
let constraints = [
Constraint::Length(1),
Constraint::Max(2),
Constraint::Fill(1),
])
.spacing(1);
let [top, first, second] = vertical.areas(frame.area());
];
let layout = Layout::vertical(constraints).spacing(1);
let [top, first, second] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("Gauge Widget").bold(),

View File

@ -98,10 +98,10 @@ impl App {
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Length(3), Min(0)]);
let [header_area, main_area] = layout.areas(area);
let [header_area, main_area] = area.layout(&layout);
let [gauge1_area, gauge4_area, gauge6_area] =
Layout::vertical([Length(2); 3]).areas(main_area);
let gauges_layout = Layout::vertical([Length(2); 3]);
let [gauge1_area, gauge4_area, gauge6_area] = main_area.layout(&gauges_layout);
header(header_area, buf);
@ -112,7 +112,7 @@ impl Widget for &App {
}
fn header(area: Rect, buf: &mut Buffer) {
let [p1_area, p2_area] = Layout::vertical([Length(1), Min(1)]).areas(area);
let [p1_area, p2_area] = area.layout(&Layout::vertical([Length(1), Min(1)]));
Paragraph::new("LineGauge Example")
.bold()
.centered()

View File

@ -42,13 +42,13 @@ fn main() -> color_eyre::Result<()> {
/// Render the UI with various lists.
fn render(frame: &mut Frame, list_state: &mut ListState) {
let vertical = Layout::vertical([
let constraints = [
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Fill(1),
])
.spacing(1);
let [top, first, second] = vertical.areas(frame.area());
];
let layout = Layout::vertical(constraints).spacing(1);
let [top, first, second] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("List Widget").bold(),

View File

@ -50,8 +50,8 @@ fn run(mut terminal: DefaultTerminal, size: RatatuiLogoSize) -> Result<()> {
/// Render the UI with a logo.
fn render(frame: &mut Frame, size: RatatuiLogoSize) {
let [top, bottom] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
let [top, bottom] = frame.area().layout(&layout);
frame.render_widget("Powered by", top);
frame.render_widget(RatatuiLogo::new(size), bottom);

View File

@ -38,8 +38,8 @@ fn main() -> Result<()> {
fn render(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Percentage(50); 2]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [first, second] = horizontal.areas(main);
let [top, main] = frame.area().layout(&vertical);
let [first, second] = main.layout(&horizontal);
let title = Line::from_iter([
Span::from("Paragraph Widget").bold(),

View File

@ -47,8 +47,8 @@ fn main() -> Result<()> {
/// Render the UI with vertical/horizontal scrollbars.
fn render(frame: &mut Frame, vertical: &mut ScrollbarState, horizontal: &mut ScrollbarState) {
let vertical_layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = vertical_layout.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("Scrollbar Widget").bold(),

View File

@ -45,7 +45,8 @@ fn render(frame: &mut Frame) {
Constraint::Fill(1),
Constraint::Fill(1),
];
let [top, first, second, _] = Layout::vertical(constraints).spacing(1).areas(frame.area());
let layout = Layout::vertical(constraints).spacing(1);
let [top, first, second, _] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("Sparkline Widget").bold(),

View File

@ -49,8 +49,8 @@ fn main() -> Result<()> {
/// Render the UI with a table.
fn render(frame: &mut Frame, table_state: &mut TableState) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("Table Widget").bold(),

View File

@ -43,8 +43,8 @@ fn main() -> Result<()> {
/// Render the UI with tabs.
fn render(frame: &mut Frame, selected_tab: usize) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let [top, main] = frame.area().layout(&layout);
let title = Line::from_iter([
Span::from("Tabs Widget").bold(),