mirror of
https://github.com/ratatui-org/ratatui
synced 2024-09-20 06:31:59 +00:00
082cbcbc50
This change simplifys UI code that uses the Frame type. E.g.: ```rust fn draw<B: Backend>(frame: &mut Frame<B>) { // ... } ``` Frame was generic over Backend because it stored a reference to the terminal in the field. Instead it now directly stores the viewport area and current buffer. These are provided at creation time and are valid for the duration of the frame. BREAKING CHANGE: Frame is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an instance of a Frame. E.g. the above code becomes: ```rust fn draw(frame: &mut Frame) { // ... } ```
252 lines
8.5 KiB
Rust
252 lines
8.5 KiB
Rust
use std::{
|
|
error::Error,
|
|
io::{stdout, Stdout},
|
|
ops::ControlFlow,
|
|
time::Duration,
|
|
};
|
|
|
|
use crossterm::{
|
|
event::{self, Event, KeyCode},
|
|
execute,
|
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
};
|
|
use itertools::Itertools;
|
|
use ratatui::{
|
|
prelude::*,
|
|
widgets::{
|
|
block::{Position, Title},
|
|
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
|
},
|
|
};
|
|
|
|
// These type aliases are used to make the code more readable by reducing repetition of the generic
|
|
// types. They are not necessary for the functionality of the code.
|
|
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
|
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
|
|
|
fn main() -> Result<()> {
|
|
let mut terminal = setup_terminal()?;
|
|
let result = run(&mut terminal);
|
|
restore_terminal(terminal)?;
|
|
|
|
if let Err(err) = result {
|
|
eprintln!("{err:?}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn setup_terminal() -> Result<Terminal> {
|
|
enable_raw_mode()?;
|
|
let mut stdout = stdout();
|
|
execute!(stdout, EnterAlternateScreen)?;
|
|
let backend = CrosstermBackend::new(stdout);
|
|
let terminal = Terminal::new(backend)?;
|
|
Ok(terminal)
|
|
}
|
|
|
|
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
|
|
disable_raw_mode()?;
|
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn run(terminal: &mut Terminal) -> Result<()> {
|
|
loop {
|
|
terminal.draw(ui)?;
|
|
if handle_events()?.is_break() {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_events() -> Result<ControlFlow<()>> {
|
|
if event::poll(Duration::from_millis(100))? {
|
|
if let Event::Key(key) = event::read()? {
|
|
if let KeyCode::Char('q') = key.code {
|
|
return Ok(ControlFlow::Break(()));
|
|
}
|
|
}
|
|
}
|
|
Ok(ControlFlow::Continue(()))
|
|
}
|
|
|
|
fn ui(frame: &mut Frame) {
|
|
let (title_area, layout) = calculate_layout(frame.size());
|
|
|
|
render_title(frame, title_area);
|
|
|
|
let paragraph = placeholder_paragraph();
|
|
|
|
render_borders(¶graph, Borders::ALL, frame, layout[0][0]);
|
|
render_borders(¶graph, Borders::NONE, frame, layout[0][1]);
|
|
render_borders(¶graph, Borders::LEFT, frame, layout[1][0]);
|
|
render_borders(¶graph, Borders::RIGHT, frame, layout[1][1]);
|
|
render_borders(¶graph, Borders::TOP, frame, layout[2][0]);
|
|
render_borders(¶graph, Borders::BOTTOM, frame, layout[2][1]);
|
|
|
|
render_border_type(¶graph, BorderType::Plain, frame, layout[3][0]);
|
|
render_border_type(¶graph, BorderType::Rounded, frame, layout[3][1]);
|
|
render_border_type(¶graph, BorderType::Double, frame, layout[4][0]);
|
|
render_border_type(¶graph, BorderType::Thick, frame, layout[4][1]);
|
|
|
|
render_styled_block(¶graph, frame, layout[5][0]);
|
|
render_styled_borders(¶graph, frame, layout[5][1]);
|
|
render_styled_title(¶graph, frame, layout[6][0]);
|
|
render_styled_title_content(¶graph, frame, layout[6][1]);
|
|
render_multiple_titles(¶graph, frame, layout[7][0]);
|
|
render_multiple_title_positions(¶graph, frame, layout[7][1]);
|
|
render_padding(¶graph, frame, layout[8][0]);
|
|
render_nested_blocks(¶graph, frame, layout[8][1]);
|
|
}
|
|
|
|
/// Calculate the layout of the UI elements.
|
|
///
|
|
/// Returns a tuple of the title area and the main areas.
|
|
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
|
|
let layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
|
|
.split(area);
|
|
let title_area = layout[0];
|
|
let main_areas = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(vec![Constraint::Max(4); 9])
|
|
.split(layout[1])
|
|
.iter()
|
|
.map(|&area| {
|
|
Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
|
|
.split(area)
|
|
.to_vec()
|
|
})
|
|
.collect_vec();
|
|
(title_area, main_areas)
|
|
}
|
|
|
|
fn render_title(frame: &mut Frame, area: Rect) {
|
|
frame.render_widget(
|
|
Paragraph::new("Block example. Press q to quit")
|
|
.dark_gray()
|
|
.alignment(Alignment::Center),
|
|
area,
|
|
);
|
|
}
|
|
|
|
fn placeholder_paragraph() -> Paragraph<'static> {
|
|
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
|
|
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
|
|
}
|
|
|
|
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(border)
|
|
.title(format!("Borders::{border:#?}", border = border));
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_border_type(
|
|
paragraph: &Paragraph,
|
|
border_type: BorderType,
|
|
frame: &mut Frame,
|
|
area: Rect,
|
|
) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.border_type(border_type)
|
|
.title(format!("BorderType::{border_type:#?}"));
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.border_style(Style::new().blue().on_white().bold().italic())
|
|
.title("Styled borders");
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.style(Style::new().blue().on_white().bold().italic())
|
|
.title("Styled block");
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
|
|
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.title("Styled title")
|
|
.title_style(Style::new().blue().on_white().bold().italic());
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let title = Line::from(vec![
|
|
"Styled ".blue().on_white().bold().italic(),
|
|
"title content".red().on_white().bold().italic(),
|
|
]);
|
|
let block = Block::new().borders(Borders::ALL).title(title);
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.title("Multiple".blue().on_white().bold().italic())
|
|
.title("Titles".red().on_white().bold().italic());
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.title(
|
|
Title::from("top left")
|
|
.position(Position::Top)
|
|
.alignment(Alignment::Left),
|
|
)
|
|
.title(
|
|
Title::from("top center")
|
|
.position(Position::Top)
|
|
.alignment(Alignment::Center),
|
|
)
|
|
.title(
|
|
Title::from("top right")
|
|
.position(Position::Top)
|
|
.alignment(Alignment::Right),
|
|
)
|
|
.title(
|
|
Title::from("bottom left")
|
|
.position(Position::Bottom)
|
|
.alignment(Alignment::Left),
|
|
)
|
|
.title(
|
|
Title::from("bottom center")
|
|
.position(Position::Bottom)
|
|
.alignment(Alignment::Center),
|
|
)
|
|
.title(
|
|
Title::from("bottom right")
|
|
.position(Position::Bottom)
|
|
.alignment(Alignment::Right),
|
|
);
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let block = Block::new()
|
|
.borders(Borders::ALL)
|
|
.title("Padding")
|
|
.padding(Padding::new(5, 10, 1, 2));
|
|
frame.render_widget(paragraph.clone().block(block), area);
|
|
}
|
|
|
|
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
|
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
|
|
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
|
|
let inner = outer_block.inner(area);
|
|
frame.render_widget(outer_block, area);
|
|
frame.render_widget(paragraph.clone().block(inner_block), inner);
|
|
}
|