use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
fn split_two(area: Rect, direction: Direction, constraints: [Constraint; 2]) -> (Rect, Rect) {
let chunks = Layout::default()
.direction(direction)
.constraints(constraints)
.split(area);
(chunks[0], chunks[1])
}
pub fn standard_tui_layout(area: Rect) -> (Rect, Rect) {
split_two(area, Direction::Vertical, [Constraint::Min(10), Constraint::Length(3)])
}
pub fn main_and_sidebar_layout(area: Rect) -> (Rect, Rect) {
split_two(
area,
Direction::Horizontal,
[Constraint::Percentage(60), Constraint::Percentage(40)],
)
}
pub fn two_panel_layout(area: Rect, left_percent: u16) -> (Rect, Rect) {
split_two(
area,
Direction::Horizontal,
[
Constraint::Percentage(left_percent),
Constraint::Percentage(100 - left_percent),
],
)
}
pub fn draw_dashboard_footer(f: &mut Frame, area: Rect, dashboard_url: &str, quit_label: &str) {
render_footer_hints(
f,
area,
&[
("Dashboard", dashboard_url, Color::Cyan),
("Docs", "https://mecha10.dev", Color::Yellow),
(quit_label, "Exit", Color::Red),
],
);
}
pub trait SidebarTui {
fn draw_main(&mut self, f: &mut Frame, area: Rect);
fn draw_sidebar(&self, f: &mut Frame, area: Rect);
fn draw_footer(&self, f: &mut Frame, area: Rect);
fn draw(&mut self, f: &mut Frame) {
let (content_area, footer_area) = standard_tui_layout(f.area());
let (main_area, sidebar_area) = main_and_sidebar_layout(content_area);
self.draw_main(f, main_area);
self.draw_sidebar(f, sidebar_area);
self.draw_footer(f, footer_area);
}
}
pub trait TwoPanelTui {
fn left_panel_percent(&self) -> u16;
fn draw_left(&mut self, f: &mut Frame, area: Rect);
fn draw_right(&mut self, f: &mut Frame, area: Rect);
fn draw_footer(&self, f: &mut Frame, area: Rect);
fn draw(&mut self, f: &mut Frame) {
let (content_area, footer_area) = standard_tui_layout(f.area());
let (left_area, right_area) = two_panel_layout(content_area, self.left_panel_percent());
self.draw_left(f, left_area);
self.draw_right(f, right_area);
self.draw_footer(f, footer_area);
}
}
pub fn render_footer_hints(f: &mut Frame, area: Rect, hints: &[(&str, &str, Color)]) {
let mut spans = vec![Span::raw(" ")];
for (i, (key, label, color)) in hints.iter().enumerate() {
spans.push(Span::styled(
format!("{}:", key),
Style::default().fg(*color).add_modifier(Modifier::BOLD),
));
let trailing = if i + 1 == hints.len() { "" } else { " " };
spans.push(Span::raw(format!(" {}{}", label, trailing)));
}
let footer = Paragraph::new(vec![Line::from(spans)]).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
);
f.render_widget(footer, area);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_tui_layout_splits_content_and_footer() {
let area = Rect::new(0, 0, 100, 40);
let (content, footer) = standard_tui_layout(area);
assert_eq!(footer.height, 3);
assert_eq!(content.height, 37);
assert_eq!(content.y, 0);
assert_eq!(footer.y, 37);
}
#[test]
fn two_panel_layout_splits_by_given_percentage() {
let area = Rect::new(0, 0, 100, 40);
let (left, right) = two_panel_layout(area, 30);
assert_eq!(left.width, 30);
assert_eq!(right.width, 70);
assert_eq!(left.x, 0);
assert_eq!(right.x, 30);
}
#[test]
fn main_and_sidebar_layout_splits_60_40() {
let area = Rect::new(0, 0, 100, 40);
let (main, sidebar) = main_and_sidebar_layout(area);
assert_eq!(main.width, 60);
assert_eq!(sidebar.width, 40);
assert_eq!(main.x, 0);
assert_eq!(sidebar.x, 60);
}
#[test]
fn two_panel_layout_matches_main_and_sidebar_at_60_40() {
let area = Rect::new(0, 0, 100, 40);
let (left, right) = two_panel_layout(area, 60);
let (expected_main, expected_sidebar) = main_and_sidebar_layout(area);
assert_eq!((left.width, left.x), (expected_main.width, expected_main.x));
assert_eq!((right.width, right.x), (expected_sidebar.width, expected_sidebar.x));
}
}