use std::cell::Cell;
use crossterm::event::KeyEvent;
use ratada::prelude::*;
use ratada::theme::{
ColorOverrides, GlyphVariant, Glyphs, Palette, Skin, ThemeRegistry,
};
use ratada::{
chrome, gauge, header,
list::{self, ListView},
markdown::MarkdownView,
shortcut_hints::{self, HintGroup, HintStyle},
spinner::Spinner,
statusbar, style,
table::{Column, Row, Table},
tabs,
tree::{TreeItem, TreeView},
};
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::Modifier,
text::Line,
};
const BRAND: &str = "ratada";
const TABS: &[(&str, &str)] =
&[("1", "Overview"), ("2", "Gallery"), ("3", "Settings")];
const GAUGE_RATIO: f64 = 0.62;
const README: &str = "\
# ratada
A reusable **ratatui** widget toolkit.
- modals, forms, pickers
- tables, trees, lists
- a themeable `Skin`
> [!NOTE]
> One frame, many widgets.
";
struct Gallery {
spinner: Spinner,
list_offset: Cell<usize>,
}
impl Gallery {
fn new() -> Self {
Self {
spinner: Spinner::new(),
list_offset: Cell::new(0),
}
}
fn render_dashboard(&self, frame: &mut Frame, area: Rect, skin: &Skin) {
let tab_height = tabs::height(BRAND, TABS, area.width as usize);
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(tab_height), Constraint::Min(0), Constraint::Length(1), Constraint::Length(2), Constraint::Length(1), ])
.split(area);
header::render(frame, rows[0], skin, BRAND, "widget gallery");
tabs::render(frame, rows[1], skin, BRAND, TABS, 1);
self.render_body(frame, rows[2], skin);
gauge::render(
frame,
rows[3],
&skin.palette,
GAUGE_RATIO,
"Indexing 62%",
);
Self::render_hints(frame, rows[4], skin);
self.render_status(frame, rows[5], skin);
}
fn render_body(&self, frame: &mut Frame, area: Rect, skin: &Skin) {
let columns = Layout::default()
.direction(Direction::Horizontal)
.spacing(1)
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(28),
Constraint::Percentage(32),
])
.split(area);
Self::render_table(frame, columns[0], skin);
self.render_middle(frame, columns[1], skin);
Self::render_markdown(frame, columns[2], skin);
}
fn render_table(frame: &mut Frame, area: Rect, skin: &Skin) {
let columns = vec![
Column::text("Id").widths(4, 6),
Column::text("Title").widths(10, 24).wrap(true),
Column::number("Amount").widths(6, 8),
Column::date("Due").widths(10, 10),
];
let rows = vec![
Row::new(vec![
"4acc".into(),
"Write the quarterly report".into(),
"120".into(),
"2026-06-25".into(),
]),
Row::new(vec![
"2fca".into(),
"Pay rent".into(),
"900".into(),
"2026-07-01".into(),
])
.with_style(style::dim()),
Row::new(vec![
"9cf7".into(),
"Read the docs".into(),
"30".into(),
"2026-05-01".into(),
]),
];
Table::new(columns, rows)
.boxed(chrome::BoxDecor::new().caption("Tasks"))
.render(frame, area, skin);
}
fn render_middle(&self, frame: &mut Frame, area: Rect, skin: &Skin) {
let split = Layout::default()
.direction(Direction::Vertical)
.spacing(1)
.constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(area);
TreeView::new(sample_tree())
.boxed(chrome::BoxDecor::new().caption("Files"))
.render(frame, split[0], skin);
list::render_boxed(
frame,
split[1],
skin,
ListView {
rows: fruit_rows(),
selected: 1,
offset: &self.list_offset,
},
&chrome::BoxDecor::new().caption("Fruit"),
true,
);
}
fn render_markdown(frame: &mut Frame, area: Rect, skin: &Skin) {
MarkdownView::new(README)
.boxed(chrome::BoxDecor::new().caption("README"))
.render(frame, area, skin);
}
fn render_hints(frame: &mut Frame, area: Rect, skin: &Skin) {
let navigation = [("j/k", "move"), ("g/G", "top/bottom")];
let actions = [("a", "add"), ("d", "delete"), ("?", "help")];
let groups = [
HintGroup {
label: "Navigation",
hints: &navigation,
},
HintGroup {
label: "Actions",
hints: &actions,
},
];
shortcut_hints::render(
frame,
area,
&groups,
&HintStyle {
key: style::fg(skin.palette.accent)
.add_modifier(Modifier::BOLD),
..HintStyle::default()
},
);
}
fn render_status(&self, frame: &mut Frame, area: Rect, skin: &Skin) {
let left = format!(
" {} 3 tasks \u{b7} 1 done \u{b7} 2 open",
self.spinner.frame(skin.glyphs.variant)
);
statusbar::render(
frame,
area,
skin,
skin.palette.selection,
&left,
"saved ",
);
}
}
impl Screen for Gallery {
type Error = std::io::Error;
fn render(&self, frame: &mut Frame) {
self.render_dashboard(frame, frame.area(), &default_skin());
}
fn handle_key(
&mut self,
_key: KeyEvent,
_tui: &mut Tui,
) -> std::io::Result<Flow> {
Ok(Flow::Quit)
}
}
fn default_skin() -> Skin {
let base = ThemeRegistry::builtin().resolve("default");
Skin::new(
Palette::resolve(base, &ColorOverrides::default()),
Glyphs::new(GlyphVariant::Unicode),
)
}
fn sample_tree() -> Vec<TreeItem> {
vec![
TreeItem::node(
"src",
vec![
TreeItem::node(
"table",
vec![
TreeItem::leaf("model.rs"),
TreeItem::leaf("render.rs"),
],
),
TreeItem::leaf("lib.rs"),
],
),
TreeItem::leaf("Cargo.toml"),
]
}
fn fruit_rows() -> Vec<Line<'static>> {
["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig"]
.iter()
.map(|item| Line::from(format!(" {item}")))
.collect()
}
fn main() -> std::io::Result<()> {
let mut tui = Tui::new()?;
run(&mut tui, &mut Gallery::new())
}