feat(layout): add support for multiple weighted constraints by chunks

pull/519/head
Florian Dehau 3 years ago
parent 3797863e14
commit c64d754f88

@ -6,7 +6,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders}, widgets::{BarChart, Block, Borders},
Terminal, Terminal,
@ -73,7 +73,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(f.size()); .split(f.size());
let barchart = BarChart::default() let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL)) .block(Block::default().title("Data1").borders(Borders::ALL))
@ -85,7 +88,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(chunks[1]); .split(chunks[1]);
let barchart = BarChart::default() let barchart = BarChart::default()

@ -6,7 +6,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::Span, text::Span,
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
@ -42,13 +42,19 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(4) .margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(f.size()); .split(f.size());
// Top two inner blocks // Top two inner blocks
let top_chunks = Layout::default() let top_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(chunks[0]); .split(chunks[0]);
// Top left inner block with green background // Top left inner block with green background
@ -75,7 +81,10 @@ fn main() -> Result<(), Box<dyn Error>> {
// Bottom two inner blocks // Bottom two inner blocks
let bottom_chunks = Layout::default() let bottom_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(chunks[1]); .split(chunks[1]);
// Bottom left block with all default borders // Bottom left block with all default borders

@ -6,7 +6,7 @@ use std::{error::Error, io, time::Duration};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect, Unit},
style::Color, style::Color,
widgets::{ widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle}, canvas::{Canvas, Map, MapResolution, Rectangle},
@ -94,7 +94,10 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.draw(|f| { terminal.draw(|f| {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(f.size()); .split(f.size());
let canvas = Canvas::default() let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World")) .block(Block::default().borders(Borders::ALL).title("World"))

@ -9,7 +9,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
symbols, symbols,
text::Span, text::Span,
@ -83,14 +83,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let size = f.size(); let size = f.size();
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints([
[ Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), ])
]
.as_ref(),
)
.split(size); .split(size);
let x_labels = vec![ let x_labels = vec![
Span::styled( Span::styled(

@ -1,7 +1,8 @@
use crate::demo::App; use crate::demo::App;
use std::iter::FromIterator;
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Constraints, Direction, Layout, Rect, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
symbols, symbols,
text::{Span, Spans}, text::{Span, Spans},
@ -15,7 +16,13 @@ use tui::{
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) { pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default() let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .constraints([
Constraint::eq(3).weight(10).into(),
Constraints::from_iter([
Constraint::eq(Unit::Percentage(100)),
Constraint::gte(10).weight(10),
]),
])
.split(f.size()); .split(f.size());
let titles = app let titles = app
.tabs .tabs
@ -41,14 +48,14 @@ where
B: Backend, B: Backend,
{ {
let chunks = Layout::default() let chunks = Layout::default()
.constraints( .constraints([
[ Constraint::eq(9).weight(10).into(),
Constraint::Length(9), Constraints::from_iter([
Constraint::Min(8), Constraint::eq(Unit::Percentage(100)),
Constraint::Length(7), Constraint::gte(8).weight(10),
] ]),
.as_ref(), Constraint::eq(7).weight(10).into(),
) ])
.split(area); .split(area);
draw_gauges(f, app, chunks[0]); draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]); draw_charts(f, app, chunks[1]);
@ -60,14 +67,7 @@ where
B: Backend, B: Backend,
{ {
let chunks = Layout::default() let chunks = Layout::default()
.constraints( .constraints([Constraint::eq(2), Constraint::eq(3), Constraint::eq(1)])
[
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
)
.margin(1) .margin(1)
.split(area); .split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs"); let block = Block::default().borders(Borders::ALL).title("Graphs");
@ -114,9 +114,12 @@ where
B: Backend, B: Backend,
{ {
let constraints = if app.show_chart { let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)] vec![
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
]
} else { } else {
vec![Constraint::Percentage(100)] vec![Constraint::eq(Unit::Percentage(100))]
}; };
let chunks = Layout::default() let chunks = Layout::default()
.constraints(constraints) .constraints(constraints)
@ -124,11 +127,17 @@ where
.split(area); .split(area);
{ {
let chunks = Layout::default() let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(chunks[0]); .split(chunks[0]);
{ {
let chunks = Layout::default() let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.split(chunks[0]); .split(chunks[0]);
@ -302,7 +311,10 @@ where
B: Backend, B: Backend,
{ {
let chunks = Layout::default() let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(30)),
Constraint::eq(Unit::Percentage(70)),
])
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.split(area); .split(area);
let up_style = Style::default().fg(Color::Green); let up_style = Style::default().fg(Color::Green);
@ -324,11 +336,7 @@ where
.bottom_margin(1), .bottom_margin(1),
) )
.block(Block::default().title("Servers").borders(Borders::ALL)) .block(Block::default().title("Servers").borders(Borders::ALL))
.widths(&[ .widths([Constraint::eq(15), Constraint::eq(15), Constraint::eq(10)]);
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
]);
f.render_widget(table, chunks[0]); f.render_widget(table, chunks[0]);
let map = Canvas::default() let map = Canvas::default()
@ -382,7 +390,10 @@ where
{ {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .constraints([
Constraint::eq(Unit::Ratio(1, 2)),
Constraint::eq(Unit::Ratio(1, 2)),
])
.split(area); .split(area);
let colors = [ let colors = [
Color::Reset, Color::Reset,
@ -416,10 +427,10 @@ where
.collect(); .collect();
let table = Table::new(items) let table = Table::new(items)
.block(Block::default().title("Colors").borders(Borders::ALL)) .block(Block::default().title("Colors").borders(Borders::ALL))
.widths(&[ .widths([
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
]); ]);
f.render_widget(table, chunks[0]); f.render_widget(table, chunks[0]);
} }

@ -6,7 +6,7 @@ use std::{error::Error, io, time::Duration};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::Span, text::Span,
widgets::{Block, Borders, Gauge}, widgets::{Block, Borders, Gauge},
@ -69,15 +69,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(2)
.constraints( .constraints([
[ Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), ])
]
.as_ref(),
)
.split(f.size()); .split(f.size());
let gauge = Gauge::default() let gauge = Gauge::default()

@ -6,7 +6,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
widgets::{Block, Borders}, widgets::{Block, Borders},
Terminal, Terminal,
}; };
@ -25,14 +25,11 @@ fn main() -> Result<(), Box<dyn Error>> {
terminal.draw(|f| { terminal.draw(|f| {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints([
[ Constraint::eq(Unit::Percentage(10)),
Constraint::Percentage(10), Constraint::eq(Unit::Percentage(80)),
Constraint::Percentage(80), Constraint::eq(Unit::Percentage(10)),
Constraint::Percentage(10), ])
]
.as_ref(),
)
.split(f.size()); .split(f.size());
let block = Block::default().title("Block").borders(Borders::ALL); let block = Block::default().title("Block").borders(Borders::ALL);

@ -9,7 +9,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Corner, Direction, Layout}, layout::{Constraint, Corner, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, List, ListItem}, widgets::{Block, Borders, List, ListItem},
@ -114,7 +114,10 @@ fn main() -> Result<(), Box<dyn Error>> {
// Create two chunks with equal horizontal screen space // Create two chunks with equal horizontal screen space
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(f.size()); .split(f.size());
// Iterate through all elements in the `items` app and append some debug text to it. // Iterate through all elements in the `items` app and append some debug text to it.

@ -6,7 +6,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap}, widgets::{Block, Borders, Paragraph, Wrap},
@ -42,12 +42,11 @@ fn main() -> Result<(), Box<dyn Error>> {
.margin(5) .margin(5)
.constraints( .constraints(
[ [
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
] ]
.as_ref(),
) )
.split(size); .split(size);

@ -6,7 +6,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, Clear, Paragraph, Wrap}, widgets::{Block, Borders, Clear, Paragraph, Wrap},
@ -18,26 +18,20 @@ use tui::{
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default() let popup_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(vec![
[ Constraint::eq(Unit::Percentage((100 - percent_y) / 2)),
Constraint::Percentage((100 - percent_y) / 2), Constraint::eq(Unit::Percentage(percent_y)),
Constraint::Percentage(percent_y), Constraint::eq(Unit::Percentage((100 - percent_y) / 2)),
Constraint::Percentage((100 - percent_y) / 2), ])
]
.as_ref(),
)
.split(r); .split(r);
Layout::default() Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints( .constraints(vec![
[ Constraint::eq(Unit::Percentage((100 - percent_x) / 2)),
Constraint::Percentage((100 - percent_x) / 2), Constraint::eq(Unit::Percentage(percent_x)),
Constraint::Percentage(percent_x), Constraint::eq(Unit::Percentage((100 - percent_x) / 2)),
Constraint::Percentage((100 - percent_x) / 2), ])
]
.as_ref(),
)
.split(popup_layout[1])[1] .split(popup_layout[1])[1]
} }
@ -57,7 +51,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::eq(Unit::Percentage(50)), Constraint::eq(Unit::Percentage(50))])
.split(size); .split(size);
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";

@ -9,7 +9,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, Sparkline}, widgets::{Block, Borders, Sparkline},
Terminal, Terminal,
@ -68,15 +68,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(2)
.constraints( .constraints([
[ Constraint::eq(3).weight(10),
Constraint::Length(3), Constraint::eq(3).weight(10),
Constraint::Length(3), Constraint::eq(7).weight(10),
Constraint::Length(7), Constraint::eq(Unit::Percentage(100)),
Constraint::Min(0), ])
]
.as_ref(),
)
.split(f.size()); .split(f.size());
let sparkline = Sparkline::default() let sparkline = Sparkline::default()
.block( .block(

@ -2,11 +2,11 @@
mod util; mod util;
use crate::util::event::{Event, Events}; use crate::util::event::{Event, Events};
use std::{error::Error, io}; use std::{error::Error, io, iter::FromIterator};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Layout}, layout::{Constraint, Constraints, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState}, widgets::{Block, Borders, Cell, Row, Table, TableState},
Terminal, Terminal,
@ -89,7 +89,7 @@ fn main() -> Result<(), Box<dyn Error>> {
loop { loop {
terminal.draw(|f| { terminal.draw(|f| {
let rects = Layout::default() let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref()) .constraints(vec![Constraint::eq(Unit::Percentage(100))])
.margin(5) .margin(5)
.split(f.size()); .split(f.size());
@ -109,18 +109,27 @@ fn main() -> Result<(), Box<dyn Error>> {
.max() .max()
.unwrap_or(0) .unwrap_or(0)
+ 1; + 1;
let cells = item.iter().map(|c| Cell::from(*c)); let cells = item
Row::new(cells).height(height as u16).bottom_margin(1) .iter()
.map(|c| Cell::from(*c).style(Style::default().bg(Color::Yellow)));
Row::new(cells).height(height as u16)
}); });
let t = Table::new(rows) let t = Table::new(rows)
.header(header) .header(header)
.block(Block::default().borders(Borders::ALL).title("Table")) .block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style) .highlight_style(selected_style)
.highlight_symbol(">> ") .highlight_symbol(">> ")
.widths(&[ .widths([
Constraint::Percentage(50), Constraint::gte(20).weight(30).into(),
Constraint::Length(30), Constraints::from_iter([
Constraint::Max(10), Constraint::eq(20).weight(10),
Constraint::gte(10).weight(30),
]),
Constraints::from_iter([
Constraint::eq(Unit::Percentage(100)),
Constraint::gte(30).weight(20),
]),
]); ]);
f.render_stateful_widget(t, rects[0], &mut table.state); f.render_stateful_widget(t, rects[0], &mut table.state);
})?; })?;

@ -9,7 +9,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, Tabs}, widgets::{Block, Borders, Tabs},
@ -42,7 +42,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(5) .margin(5)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .constraints([
Constraint::eq(3).weight(10),
Constraint::eq(Unit::Percentage(100)),
])
.split(size); .split(size);
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black)); let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));

@ -18,7 +18,7 @@ use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans, Text}, text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph}, widgets::{Block, Borders, List, ListItem, Paragraph},
@ -71,14 +71,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(2)
.constraints( .constraints([
[ Constraint::eq(1).weight(10),
Constraint::Length(1), Constraint::eq(3).weight(10),
Constraint::Length(3), Constraint::eq(Unit::Percentage(100)),
Constraint::Min(1), ])
]
.as_ref(),
)
.split(f.size()); .split(f.size());
let (msg, style) = match app.input_mode { let (msg, style) = match app.input_mode {

@ -21,28 +21,128 @@ pub enum Direction {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Constraint { pub enum Unit {
// TODO: enforce range 0 - 100 Length(u16),
Percentage(u16), Percentage(u16),
Ratio(u32, u32), Ratio(u32, u32),
Length(u16),
Max(u16),
Min(u16),
} }
impl Constraint { impl From<u16> for Unit {
pub fn apply(&self, length: u16) -> u16 { fn from(v: u16) -> Unit {
Unit::Length(v)
}
}
impl Unit {
pub(crate) fn apply(&self, length: u16) -> u16 {
match *self { match *self {
Constraint::Percentage(p) => length * p / 100, Unit::Percentage(p) => length * p / 100,
Constraint::Ratio(num, den) => { Unit::Ratio(num, den) => {
let r = num * u32::from(length) / den; let r = num * u32::from(length) / den;
r as u16 r as u16
} }
Constraint::Length(l) => length.min(l), Unit::Length(l) => length.min(l),
Constraint::Max(m) => length.min(m), }
Constraint::Min(m) => length.max(m), }
fn check(&self) {
match *self {
Unit::Percentage(p) => {
assert!(
p <= 100,
"Percentages should be between 0 and 100 inclusively."
);
}
Unit::Ratio(num, den) => {
assert!(
num <= den,
"Ratio numerator should be less than or equalt to denominator."
);
}
_ => {}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Operator {
Equal,
GreaterThanOrEqual,
LessThanOrEqual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Constraint {
operator: Operator,
unit: Unit,
weight: u8,
}
impl Constraint {
pub fn gte<T>(u: T) -> Constraint
where
T: Into<Unit>,
{
let u = u.into();
u.check();
Constraint {
operator: Operator::GreaterThanOrEqual,
unit: u,
weight: 0,
}
}
pub fn lte<T>(u: T) -> Constraint
where
T: Into<Unit>,
{
let u = u.into();
u.check();
Constraint {
operator: Operator::LessThanOrEqual,
unit: u,
weight: 0,
} }
} }
pub fn eq<T>(u: T) -> Constraint
where
T: Into<Unit>,
{
let u = u.into();
u.check();
Constraint {
operator: Operator::Equal,
unit: u,
weight: 0,
}
}
pub fn weight(mut self, weight: u8) -> Constraint {
self.weight = weight;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Constraints(Vec<Constraint>);
impl From<Constraint> for Constraints {
fn from(c: Constraint) -> Constraints {
Constraints(vec![c])
}
}
impl<T> std::iter::FromIterator<T> for Constraints
where
T: Into<Constraint>,
{
fn from_iter<I>(iter: I) -> Constraints
where
I: IntoIterator<Item = T>,
{
Constraints(iter.into_iter().map(|c| c.into()).collect())
}
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -62,10 +162,7 @@ pub enum Alignment {
pub struct Layout { pub struct Layout {
direction: Direction, direction: Direction,
margin: Margin, margin: Margin,
constraints: Vec<Constraint>, constraints: Vec<Constraints>,
/// Whether the last chunk of the computed layout should be expanded to fill the available
/// space.
expand_to_fill: bool,
} }
thread_local! { thread_local! {
@ -81,17 +178,17 @@ impl Default for Layout {
vertical: 0, vertical: 0,
}, },
constraints: Vec::new(), constraints: Vec::new(),
expand_to_fill: true,
} }
} }
} }
impl Layout { impl Layout {
pub fn constraints<C>(mut self, constraints: C) -> Layout pub fn constraints<I>(mut self, constraints: I) -> Layout
where where
C: Into<Vec<Constraint>>, I: IntoIterator,
I::Item: Into<Constraints>,
{ {
self.constraints = constraints.into(); self.constraints = constraints.into_iter().map(|c| c.into()).collect();
self self
} }
@ -118,20 +215,18 @@ impl Layout {
self self
} }
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
self.expand_to_fill = expand_to_fill;
self
}
/// Wrapper function around the cassowary-rs solver to be able to split a given /// Wrapper function around the cassowary-rs solver to be able to split a given
/// area into smaller ones based on the preferred widths or heights and the direction. /// area into smaller ones based on the preferred widths or heights and the direction.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use tui::layout::{Rect, Constraint, Direction, Layout}; /// # use tui::layout::{Rect, Constraint, Direction, Layout, Unit};
/// let chunks = Layout::default() /// let chunks = Layout::default()
/// .direction(Direction::Vertical) /// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref()) /// .constraints([
/// Constraint::eq(5).weight(10),
/// Constraint::eq(Unit::Percentage(100)).weight(5)
/// ])
/// .split(Rect { /// .split(Rect {
/// x: 2, /// x: 2,
/// y: 2, /// y: 2,
@ -158,7 +253,7 @@ impl Layout {
/// ///
/// let chunks = Layout::default() /// let chunks = Layout::default()
/// .direction(Direction::Horizontal) /// .direction(Direction::Horizontal)
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref()) /// .constraints([Constraint::eq(Unit::Ratio(1, 3)), Constraint::eq(Unit::Ratio(2, 3))])
/// .split(Rect { /// .split(Rect {
/// x: 0, /// x: 0,
/// y: 0, /// y: 0,
@ -231,57 +326,25 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()), Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
}); });
} }
if layout.expand_to_fill {
if let Some(last) = elements.last() {
ccs.push(match layout.direction {
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
});
}
}
match layout.direction { match layout.direction {
Direction::Horizontal => { Direction::Horizontal => {
for pair in elements.windows(2) { for pair in elements.windows(2) {
ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x); ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
} }
for (i, size) in layout.constraints.iter().enumerate() { for (i, c) in layout.constraints.iter().enumerate() {
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y)); ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height)); ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
ccs.push(match *size { apply_constraints(&mut ccs, c, elements[i].width, dest_area.width);
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].width
| EQ(WEAK)
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
});
} }
} }
Direction::Vertical => { Direction::Vertical => {
for pair in elements.windows(2) { for pair in elements.windows(2) {
ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y); ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
} }
for (i, size) in layout.constraints.iter().enumerate() { for (i, c) in layout.constraints.iter().enumerate() {
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x)); ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width)); ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
ccs.push(match *size { apply_constraints(&mut ccs, c, elements[i].height, dest_area.height);
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].height
| EQ(WEAK)
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
});
} }
} }
} }
@ -309,21 +372,29 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
_ => {} _ => {}
} }
} }
results
}
if layout.expand_to_fill { fn apply_constraints(
// Fix imprecision by extending the last item a bit if necessary ccs: &mut Vec<CassowaryConstraint>,
if let Some(last) = results.last_mut() { constraints: &Constraints,
match layout.direction { var: Variable,
Direction::Vertical => { total_length: u16,
last.height = dest_area.bottom() - last.y; ) {
} for c in &constraints.0 {
Direction::Horizontal => { let weight = WEAK + f64::from(c.weight);
last.width = dest_area.right() - last.x; let value = match c.unit {
} Unit::Length(v) => f64::from(v),
} Unit::Percentage(v) => f64::from(v * total_length) / 100.0,
} Unit::Ratio(n, d) => f64::from(total_length) * f64::from(n) / f64::from(d),
};
let operator = match c.operator {
Operator::GreaterThanOrEqual => GE(weight),
Operator::Equal => EQ(weight),
Operator::LessThanOrEqual => LE(weight),
};
ccs.push(var | operator | value);
} }
results
} }
/// A container used by the solver inside split /// A container used by the solver inside split
@ -476,6 +547,12 @@ impl Rect {
mod tests { mod tests {
use super::*; use super::*;
#[test]
#[should_panic]
fn constraint_invalid_percentages() {
Constraint::eq(Unit::Percentage(110));
}
#[test] #[test]
fn test_vertical_split_by_height() { fn test_vertical_split_by_height() {
let target = Rect { let target = Rect {
@ -487,14 +564,12 @@ mod tests {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints([
[ Constraint::eq(10),
Constraint::Percentage(10), Constraint::eq(10),
Constraint::Max(5), Constraint::lte(5),
Constraint::Min(1), Constraint::gte(1),
] ])
.as_ref(),
)
.split(target); .split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>()); assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());

@ -111,7 +111,7 @@
//! use tui::Terminal; //! use tui::Terminal;
//! use tui::backend::TermionBackend; //! use tui::backend::TermionBackend;
//! use tui::widgets::{Widget, Block, Borders}; //! use tui::widgets::{Widget, Block, Borders};
//! use tui::layout::{Layout, Constraint, Direction}; //! use tui::layout::{Layout, Constraint, Direction, Unit};
//! //!
//! fn main() -> Result<(), io::Error> { //! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout().into_raw_mode()?; //! let stdout = io::stdout().into_raw_mode()?;
@ -123,10 +123,10 @@
//! .margin(1) //! .margin(1)
//! .constraints( //! .constraints(
//! [ //! [
//! Constraint::Percentage(10), //! Constraint::eq(Unit::Percentage(10)),
//! Constraint::Percentage(80), //! Constraint::eq(Unit::Percentage(80)),
//! Constraint::Percentage(10) //! Constraint::eq(Unit::Percentage(10))
//! ].as_ref() //! ]
//! ) //! )
//! .split(f.size()); //! .split(f.size());
//! let block = Block::default() //! let block = Block::default()
@ -142,10 +142,7 @@
//! } //! }
//! ``` //! ```
//! //!
//! This let you describe responsive terminal UI by nesting layouts. You should note that by //! This let you describe responsive terminal UI by nesting layouts.
//! default the computed layout tries to fill the available space completely. So if for any reason
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
//! corresponding area.
pub mod backend; pub mod backend;
pub mod buffer; pub mod buffer;

@ -1,6 +1,6 @@
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Rect}, layout::{Rect, Unit},
style::{Color, Style}, style::{Color, Style},
symbols, symbols,
text::{Span, Spans}, text::{Span, Spans},
@ -225,7 +225,7 @@ pub struct Chart<'a> {
/// The widget base style /// The widget base style
style: Style, style: Style,
/// Constraints used to determine whether the legend should be shown or not /// Constraints used to determine whether the legend should be shown or not
hidden_legend_constraints: (Constraint, Constraint), hidden_legend_constraints: (Unit, Unit),
} }
impl<'a> Chart<'a> { impl<'a> Chart<'a> {
@ -236,7 +236,7 @@ impl<'a> Chart<'a> {
y_axis: Axis::default(), y_axis: Axis::default(),
style: Default::default(), style: Default::default(),
datasets, datasets,
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)), hidden_legend_constraints: (Unit::Ratio(1, 4), Unit::Ratio(1, 4)),
} }
} }
@ -266,17 +266,17 @@ impl<'a> Chart<'a> {
/// ///
/// ``` /// ```
/// # use tui::widgets::Chart; /// # use tui::widgets::Chart;
/// # use tui::layout::Constraint; /// # use tui::layout::Unit;
/// let constraints = ( /// let constraints = (
/// Constraint::Ratio(1, 3), /// Unit::Ratio(1, 3),
/// Constraint::Ratio(1, 4) /// Unit::Ratio(1, 4)
/// ); /// );
/// // Hide the legend when either its width is greater than 33% of the total widget width /// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height. /// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart = Chart::new(vec![]) /// let _chart: Chart = Chart::new(vec![])
/// .hidden_legend_constraints(constraints); /// .hidden_legend_constraints(constraints);
/// ``` /// ```
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> { pub fn hidden_legend_constraints(mut self, constraints: (Unit, Unit)) -> Chart<'a> {
self.hidden_legend_constraints = constraints; self.hidden_legend_constraints = constraints;
self self
} }
@ -562,7 +562,7 @@ mod tests {
struct LegendTestCase { struct LegendTestCase {
chart_area: Rect, chart_area: Rect,
hidden_legend_constraints: (Constraint, Constraint), hidden_legend_constraints: (Unit, Unit),
legend_area: Option<Rect>, legend_area: Option<Rect>,
} }
@ -572,12 +572,12 @@ mod tests {
let cases = [ let cases = [
LegendTestCase { LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100), chart_area: Rect::new(0, 0, 100, 100),
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)), hidden_legend_constraints: (Unit::Ratio(1, 4), Unit::Ratio(1, 4)),
legend_area: Some(Rect::new(88, 0, 12, 12)), legend_area: Some(Rect::new(88, 0, 12, 12)),
}, },
LegendTestCase { LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100), chart_area: Rect::new(0, 0, 100, 100),
hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)), hidden_legend_constraints: (Unit::Ratio(1, 10), Unit::Ratio(1, 4)),
legend_area: None, legend_area: None,
}, },
]; ];

@ -1,6 +1,6 @@
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Constraints, Direction, Layout, Rect, Unit},
style::Style, style::Style,
text::Text, text::Text,
widgets::{Block, StatefulWidget, Widget}, widgets::{Block, StatefulWidget, Widget},
@ -178,7 +178,7 @@ impl<'a> Row<'a> {
/// // As any other widget, a Table can be wrapped in a Block. /// // As any other widget, a Table can be wrapped in a Block.
/// .block(Block::default().title("Table")) /// .block(Block::default().title("Table"))
/// // Columns widths are constrained in the same way as Layout... /// // Columns widths are constrained in the same way as Layout...
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) /// .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(10)])
/// // ...and they can be separated by a fixed spacing. /// // ...and they can be separated by a fixed spacing.
/// .column_spacing(1) /// .column_spacing(1)
/// // If you wish to highlight a row in any specific way when it is selected... /// // If you wish to highlight a row in any specific way when it is selected...
@ -193,7 +193,7 @@ pub struct Table<'a> {
/// Base style for the widget /// Base style for the widget
style: Style, style: Style,
/// Width constraints for each column /// Width constraints for each column
widths: &'a [Constraint], widths: Vec<Constraints>,
/// Space between each column /// Space between each column
column_spacing: u16, column_spacing: u16,
/// Style used to render the selected row /// Style used to render the selected row
@ -214,7 +214,7 @@ impl<'a> Table<'a> {
Self { Self {
block: None, block: None,
style: Style::default(), style: Style::default(),
widths: &[], widths: Vec::new(),
column_spacing: 1, column_spacing: 1,
highlight_style: Style::default(), highlight_style: Style::default(),
highlight_symbol: None, highlight_symbol: None,
@ -233,16 +233,12 @@ impl<'a> Table<'a> {
self self
} }
pub fn widths(mut self, widths: &'a [Constraint]) -> Self { pub fn widths<I>(mut self, widths: I) -> Self
let between_0_and_100 = |&w| match w { where
Constraint::Percentage(p) => p <= 100, I: IntoIterator,
_ => true, I::Item: Into<Constraints>,
}; {
assert!( self.widths = widths.into_iter().map(|w| w.into()).collect();
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self self
} }
@ -266,16 +262,24 @@ impl<'a> Table<'a> {
self self
} }
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> { fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> (u16, Vec<u16>) {
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1); let mut constraints: Vec<Constraints> = Vec::with_capacity(self.widths.len() * 2 + 1);
if has_selection { if has_selection {
let highlight_symbol_width = let highlight_symbol_width =
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0); self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
constraints.push(Constraint::Length(highlight_symbol_width)); constraints.push(
Constraint::eq(Unit::Length(highlight_symbol_width))
.weight(u8::MAX)
.into(),
);
} }
for constraint in self.widths { for constraint in &self.widths {
constraints.push(*constraint); constraints.push(constraint.clone());
constraints.push(Constraint::Length(self.column_spacing)); constraints.push(
Constraint::eq(Unit::Length(self.column_spacing))
.weight(u8::MAX)
.into(),
);
} }
if !self.widths.is_empty() { if !self.widths.is_empty() {
constraints.pop(); constraints.pop();
@ -283,17 +287,18 @@ impl<'a> Table<'a> {
let mut chunks = Layout::default() let mut chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints(constraints) .constraints(constraints)
.expand_to_fill(false)
.split(Rect { .split(Rect {
x: 0, x: 0,
y: 0, y: 0,
width: max_width, width: max_width,
height: 1, height: 1,
}); });
if has_selection { let first_column = if has_selection {
chunks.remove(0); chunks.remove(0).width
} } else {
chunks.iter().step_by(2).map(|c| c.width).collect() 0
};
(first_column, chunks.iter().map(|c| c.width).collect())
} }
fn get_row_bounds( fn get_row_bounds(
@ -381,7 +386,8 @@ impl<'a> StatefulWidget for Table<'a> {
}; };
let has_selection = state.selected.is_some(); let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection); let (first_column, columns_widths) =
self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or(""); let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width()); let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0; let mut current_height = 0;
@ -401,9 +407,14 @@ impl<'a> StatefulWidget for Table<'a> {
); );
let mut col = table_area.left(); let mut col = table_area.left();
if has_selection { if has_selection {
col += (highlight_symbol.width() as u16).min(table_area.width); col += first_column
} }
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) { for (i, (width, cell)) in columns_widths
.iter()
.step_by(2)
.zip(header.cells.iter())
.enumerate()
{
render_cell( render_cell(
buf, buf,
cell, cell,
@ -414,7 +425,7 @@ impl<'a> StatefulWidget for Table<'a> {
height: max_header_height, height: max_header_height,
}, },
); );
col += *width + self.column_spacing; col += *width + columns_widths.get(i * 2 + 1).unwrap_or(&0);
} }
current_height += max_header_height; current_height += max_header_height;
rows_height = rows_height.saturating_sub(max_header_height); rows_height = rows_height.saturating_sub(max_header_height);
@ -450,13 +461,18 @@ impl<'a> StatefulWidget for Table<'a> {
&blank_symbol &blank_symbol
}; };
let (col, _) = let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style); buf.set_stringn(col, row, symbol, first_column as usize, table_row.style);
col col
} else { } else {
col col
}; };
let mut col = table_row_start_col; let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { for (i, (width, cell)) in columns_widths
.iter()
.step_by(2)
.zip(table_row.cells.iter())
.enumerate()
{
render_cell( render_cell(
buf, buf,
cell, cell,
@ -467,7 +483,7 @@ impl<'a> StatefulWidget for Table<'a> {
height: table_row.height, height: table_row.height,
}, },
); );
col += *width + self.column_spacing; col += *width + columns_widths.get(i * 2 + 1).unwrap_or(&0);
} }
if is_selected { if is_selected {
buf.set_style(table_row_area, self.highlight_style); buf.set_style(table_row_area, self.highlight_style);
@ -492,14 +508,3 @@ impl<'a> Widget for Table<'a> {
StatefulWidget::render(self, area, buf, &mut state); StatefulWidget::render(self, area, buf, &mut state);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn table_invalid_percentages() {
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
}
}

@ -1,7 +1,7 @@
use tui::{ use tui::{
backend::TestBackend, backend::TestBackend,
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
symbols, symbols,
text::Span, text::Span,
@ -18,7 +18,10 @@ fn widgets_gauge_renders() {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(f.size()); .split(f.size());
let gauge = Gauge::default() let gauge = Gauge::default()
@ -87,7 +90,10 @@ fn widgets_gauge_renders_no_unicode() {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
])
.split(f.size()); .split(f.size());
let gauge = Gauge::default() let gauge = Gauge::default()

@ -1,7 +1,7 @@
use tui::{ use tui::{
backend::TestBackend, backend::TestBackend,
buffer::Buffer, buffer::Buffer,
layout::Constraint, layout::{Constraint, Unit},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans}, text::{Span, Spans},
widgets::{Block, Borders, Cell, Row, Table, TableState}, widgets::{Block, Borders, Cell, Row, Table, TableState},
@ -25,11 +25,7 @@ fn widgets_table_column_spacing_can_be_changed() {
]) ])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(&[ .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(5)])
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(column_spacing); .column_spacing(column_spacing);
f.render_widget(table, size); f.render_widget(table, size);
}) })
@ -132,11 +128,7 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
// columns of zero width show nothing // columns of zero width show nothing
test_case( test_case(
&[ vec![Constraint::eq(0), Constraint::eq(0), Constraint::eq(0)],
Constraint::Length(0),
Constraint::Length(0),
Constraint::Length(0),
],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│ │", "│ │",
@ -153,11 +145,7 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
// columns of 1 width trim // columns of 1 width trim
test_case( test_case(
&[ vec![Constraint::eq(1), Constraint::eq(1), Constraint::eq(1)],
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│H H H │", "│H H H │",
@ -174,11 +162,7 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
// columns of large width just before pushing a column off // columns of large width just before pushing a column off
test_case( test_case(
&[ vec![Constraint::eq(8), Constraint::eq(8), Constraint::eq(8)],
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(8),
],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│Head1 Head2 Head3 │", "│Head1 Head2 Head3 │",
@ -221,10 +205,10 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
// columns of zero width show nothing // columns of zero width show nothing
test_case( test_case(
&[ vec![
Constraint::Percentage(0), Constraint::eq(Unit::Percentage(0)),
Constraint::Percentage(0), Constraint::eq(Unit::Percentage(0)),
Constraint::Percentage(0), Constraint::eq(Unit::Percentage(0)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -242,10 +226,10 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
// columns of not enough width trims the data // columns of not enough width trims the data
test_case( test_case(
&[ vec![
Constraint::Percentage(11), Constraint::eq(Unit::Percentage(11)),
Constraint::Percentage(11), Constraint::eq(Unit::Percentage(11)),
Constraint::Percentage(11), Constraint::eq(Unit::Percentage(11)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -263,10 +247,10 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
// columns of large width just before pushing a column off // columns of large width just before pushing a column off
test_case( test_case(
&[ vec![
Constraint::Percentage(33), Constraint::eq(Unit::Percentage(33)),
Constraint::Percentage(33), Constraint::eq(Unit::Percentage(33)),
Constraint::Percentage(33), Constraint::eq(Unit::Percentage(33)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -284,7 +268,10 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
// percentages summing to 100 should give equal widths // percentages summing to 100 should give equal widths
test_case( test_case(
&[Constraint::Percentage(50), Constraint::Percentage(50)], vec![
Constraint::eq(Unit::Percentage(50)),
Constraint::eq(Unit::Percentage(50)),
],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│Head1 Head2 │", "│Head1 Head2 │",
@ -326,10 +313,10 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
// columns of zero width show nothing // columns of zero width show nothing
test_case( test_case(
&[ vec![
Constraint::Percentage(0), Constraint::eq(Unit::Percentage(0)),
Constraint::Length(0), Constraint::eq(0),
Constraint::Percentage(0), Constraint::eq(Unit::Percentage(0)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -347,10 +334,10 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
// columns of not enough width trims the data // columns of not enough width trims the data
test_case( test_case(
&[ vec![
Constraint::Percentage(11), Constraint::eq(Unit::Percentage(11)),
Constraint::Length(20), Constraint::eq(20),
Constraint::Percentage(11), Constraint::eq(Unit::Percentage(11)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -368,10 +355,10 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
// columns of large width just before pushing a column off // columns of large width just before pushing a column off
test_case( test_case(
&[ vec![
Constraint::Percentage(33), Constraint::eq(Unit::Percentage(33)),
Constraint::Length(10), Constraint::eq(10),
Constraint::Percentage(33), Constraint::eq(Unit::Percentage(33)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -389,10 +376,10 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
// columns of large size (>100% total) hide the last column // columns of large size (>100% total) hide the last column
test_case( test_case(
&[ vec![
Constraint::Percentage(60), Constraint::eq(Unit::Percentage(60)),
Constraint::Length(10), Constraint::eq(10),
Constraint::Percentage(60), Constraint::eq(Unit::Percentage(60)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -436,10 +423,10 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
// columns of zero width show nothing // columns of zero width show nothing
test_case( test_case(
&[ vec![
Constraint::Ratio(0, 1), Constraint::eq(Unit::Ratio(0, 1)),
Constraint::Ratio(0, 1), Constraint::eq(Unit::Ratio(0, 1)),
Constraint::Ratio(0, 1), Constraint::eq(Unit::Ratio(0, 1)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -457,10 +444,10 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
// columns of not enough width trims the data // columns of not enough width trims the data
test_case( test_case(
&[ vec![
Constraint::Ratio(1, 9), Constraint::eq(Unit::Ratio(1, 9)),
Constraint::Ratio(1, 9), Constraint::eq(Unit::Ratio(1, 9)),
Constraint::Ratio(1, 9), Constraint::eq(Unit::Ratio(1, 9)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -478,10 +465,10 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
// columns of large width just before pushing a column off // columns of large width just before pushing a column off
test_case( test_case(
&[ vec![
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
Constraint::Ratio(1, 3), Constraint::eq(Unit::Ratio(1, 3)),
], ],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
@ -499,7 +486,10 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
// percentages summing to 100 should give equal widths // percentages summing to 100 should give equal widths
test_case( test_case(
&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], vec![
Constraint::eq(Unit::Ratio(1, 2)),
Constraint::eq(Unit::Ratio(1, 2)),
],
Buffer::with_lines(vec![ Buffer::with_lines(vec![
"┌────────────────────────────┐", "┌────────────────────────────┐",
"│Head1 Head2 │", "│Head1 Head2 │",
@ -532,11 +522,7 @@ fn widgets_table_can_have_rows_with_multi_lines() {
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ") .highlight_symbol(">> ")
.widths(&[ .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(5)])
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1); .column_spacing(1);
f.render_stateful_widget(table, size, state); f.render_stateful_widget(table, size, state);
}) })
@ -635,11 +621,7 @@ fn widgets_table_can_have_elements_styled_individually() {
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT)) .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.highlight_symbol(">> ") .highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_style(Style::default().add_modifier(Modifier::BOLD))
.widths(&[ .widths([Constraint::eq(6), Constraint::eq(6), Constraint::eq(6)])
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1); .column_spacing(1);
f.render_stateful_widget(table, size, &mut state); f.render_stateful_widget(table, size, &mut state);
}) })
@ -696,11 +678,7 @@ fn widgets_table_should_render_even_if_empty() {
let table = Table::new(vec![]) let table = Table::new(vec![])
.header(Row::new(vec!["Head1", "Head2", "Head3"])) .header(Row::new(vec!["Head1", "Head2", "Head3"]))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT)) .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.widths(&[ .widths([Constraint::eq(6), Constraint::eq(6), Constraint::eq(6)])
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1); .column_spacing(1);
f.render_widget(table, size); f.render_widget(table, size);
}) })
@ -736,11 +714,11 @@ fn widgets_table_columns_dont_panic() {
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ") .highlight_symbol(">> ")
.column_spacing(1) .column_spacing(1)
.widths(&[ .widths([
Constraint::Percentage(15), Constraint::eq(Unit::Percentage(15)),
Constraint::Percentage(15), Constraint::eq(Unit::Percentage(15)),
Constraint::Percentage(25), Constraint::eq(Unit::Percentage(25)),
Constraint::Percentage(45), Constraint::eq(Unit::Percentage(45)),
]); ]);
let mut state = TableState::default(); let mut state = TableState::default();
@ -771,11 +749,7 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() {
]) ])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(&[ .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(5)])
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1); .column_spacing(1);
f.render_stateful_widget(table, size, &mut state); f.render_stateful_widget(table, size, &mut state);
}) })
@ -800,11 +774,7 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() {
let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])]) let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(&[ .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(5)])
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1); .column_spacing(1);
f.render_stateful_widget(table, size, &mut state); f.render_stateful_widget(table, size, &mut state);
}) })

Loading…
Cancel
Save