use ratatui::{
layout::Flex,
prelude::*,
widgets::{Block, Paragraph, Row, Table, TableState},
};
use ratatui_macros::{constraints, horizontal, vertical};
use tui_big_text::{BigTextBuilder, PixelSize};
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(Debug, Default, Clone)]
pub struct Dashboard {
pub title: String,
pub subtitle: String,
pub avatar: String,
pub actions: Vec<Action>,
pub footer: Vec<String>,
}
#[derive(Debug, Default, Clone)]
pub struct Action {
pub description: String,
pub keyboard: String,
}
impl Action {
pub fn new<I: Into<String>>(i: (I, I)) -> Self {
Self {
description: i.0.into(),
keyboard: i.1.into(),
}
}
}
impl<T> From<(T, T)> for Action
where
T: Into<String>,
{
fn from(value: (T, T)) -> Self {
let description = value.0.into();
let keyboard = value.1.into();
Self { description, keyboard }
}
}
#[derive(Default)]
pub struct DashboardBuilder(Dashboard);
impl DashboardBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.0.title = title.into();
self
}
pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
self.0.subtitle = subtitle.into();
self
}
pub fn avatar(mut self, avatar: impl Into<String>) -> Self {
self.0.avatar = avatar.into();
self
}
pub fn actions(mut self, actions: Vec<impl Into<Action>>) -> Self {
self.0.actions = actions.into_iter().map(|i| i.into()).collect();
self
}
pub fn footer(mut self, footer: Vec<String>) -> Self {
self.0.footer = footer;
self
}
pub fn build(self) -> Dashboard {
self.0
}
}
impl Dashboard {
fn render_header(&self, area: Rect, buf: &mut Buffer) {
let [top_1, top_2] = vertical![==10%, ==5%]
.vertical_margin(2)
.split(area)
.to_vec()
.try_into()
.unwrap();
let top_center_1 = horizontal![==(self.title.chars().count() as u16 * 4)]
.flex(Flex::Center)
.split(top_1)[0];
let top_center_2 = horizontal![==(self.subtitle.chars().count() as u16)]
.flex(Flex::Center)
.split(top_2)[0];
let title = Line::raw(&self.title).style(Style::new().light_red());
let title = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![title])
.build()
.unwrap();
let subtitle = Line::raw(&self.subtitle).style(Style::new().blue().bold());
title.render(top_center_1, buf);
subtitle.render(top_center_2, buf);
}
fn render_table<'a, 'b: 'a>(&'b mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
let [_, main] = horizontal![==36%, ==57%]
.horizontal_margin(3)
.split(area)
.to_vec()
.try_into()
.unwrap();
let lines = {
let to_line = |(idx, a): (usize, &'a Action)| {
let style = match state.selected() {
Some(i) if i == idx => Style::default().italic().bold().underlined(),
_ => Style::default(),
};
let description = a.description.as_str().blue().style(style);
let keyboard = a.keyboard.as_str().to_uppercase().light_red().bold();
Line::default().spans(vec![description, keyboard]).style(style)
};
self.actions.iter().enumerate().map(to_line)
};
let mut table_width = 0;
let mut table_height = 0;
for i in lines.clone() {
table_width = table_width.max(i.width() + 2);
table_height += 1;
}
let table = {
let rows = lines.map(Row::new);
let widths = constraints![==95%, ==5%];
let block = Block::bordered().title("Actions").title_alignment(Alignment::Center);
Table::new(rows, widths).highlight_symbol(" >> ").cyan().block(block)
};
let [_, main] = vertical![==20%, ==table_height + 2].split(main).to_vec().try_into().unwrap();
StatefulWidget::render(table, main, buf, state);
}
fn render_avatar(&self, area: Rect, buf: &mut Buffer) {
let [_, left] = horizontal![==8%, ==27%].split(area).to_vec().try_into().unwrap();
let [_, left] = vertical![==20%, ==60%].split(left).to_vec().try_into().unwrap();
let avatar = {
let lines = self.avatar.lines().map(|i| Line::from(i.light_blue())).collect::<Vec<_>>();
let block = Block::bordered().title("Miku").title_alignment(Alignment::Center);
Paragraph::new(lines).block(block)
};
avatar.render(left, buf);
}
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
let [_, bottom] = vertical![==95%, ==5%]
.vertical_margin(1)
.split(area)
.to_vec()
.try_into()
.unwrap();
let lines = self.footer.iter().map(Line::raw).collect::<Vec<_>>();
let footer = Paragraph::new(lines).centered().bold().light_cyan().italic();
footer.render(bottom, buf);
}
}
impl StatefulWidget for Dashboard {
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
self.render_header(area, buf);
self.render_table(area, buf, state);
self.render_avatar(area, buf);
self.render_footer(area, buf);
}
}