tui_dashboard/
lib.rs

1use ratatui::{
2    layout::Flex,
3    prelude::*,
4    widgets::{Block, Paragraph, Row, Table, TableState},
5};
6use ratatui_macros::{constraints, horizontal, vertical};
7use tui_big_text::{BigTextBuilder, PixelSize};
8
9/// Application result type.
10pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
11
12/// Application.
13#[derive(Debug, Default, Clone)]
14pub struct Dashboard<'a> {
15    general_title: String,
16    subtitle: String,
17    avatar: String,
18    avatar_block: Option<Block<'a>>,
19    table: Vec<TableItem>,
20    table_block: Option<Block<'a>>,
21    footer: Vec<String>,
22}
23
24#[derive(Debug, Default, Clone)]
25pub struct TableItem {
26    left: String,
27    right: String,
28}
29
30impl TableItem {
31    pub fn new<I: Into<String>>(i: (I, I)) -> Self {
32        Self {
33            left: i.0.into(),
34            right: i.1.into(),
35        }
36    }
37}
38
39impl<T> From<(T, T)> for TableItem
40where
41    T: Into<String>,
42{
43    fn from(value: (T, T)) -> Self {
44        Self::new(value)
45    }
46}
47
48#[derive(Default)]
49pub struct DashboardBuilder<'a>(Dashboard<'a>);
50
51impl<'a> DashboardBuilder<'a> {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    pub fn general_title(mut self, general_title: impl Into<String>) -> Self {
57        self.0.general_title = general_title.into();
58        self
59    }
60
61    pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
62        self.0.subtitle = subtitle.into();
63        self
64    }
65
66    pub fn avatar(mut self, avatar: impl Into<String>) -> Self {
67        self.0.avatar = avatar.into();
68        self
69    }
70
71    pub fn avatar_block(mut self, block: impl Into<Option<Block<'a>>>) -> Self {
72        self.0.avatar_block = block.into();
73        self
74    }
75
76    pub fn table(mut self, table: Vec<impl Into<TableItem>>) -> Self {
77        self.0.table = table.into_iter().map(|i| i.into()).collect();
78        self
79    }
80
81    pub fn table_block(mut self, block: impl Into<Option<Block<'a>>>) -> Self {
82        self.0.table_block = block.into();
83        self
84    }
85
86    pub fn footer(mut self, footer: Vec<impl Into<String>>) -> Self {
87        self.0.footer = footer.into_iter().map(|i| i.into()).collect();
88        self
89    }
90
91    pub fn build(self) -> Dashboard<'a> {
92        // let header_area = vertical![==20%, ==70%, ==5%].split()
93        self.0
94    }
95}
96
97impl Dashboard<'_> {
98    fn render_header(&self, area: Rect, buf: &mut Buffer) {
99        let [top_1, top_2] = vertical![==10%, ==5%]
100            .vertical_margin(1)
101            .split(area)
102            .to_vec()
103            .try_into()
104            .unwrap();
105
106        let top_center_1 = horizontal![==(self.general_title.chars().count() as u16 * 4)]
107            .flex(Flex::Center)
108            .split(top_1)[0];
109
110        let top_center_2 = horizontal![==(self.subtitle.chars().count() as u16)]
111            .flex(Flex::Center)
112            .split(top_2)[0];
113
114        let title = Line::raw(&self.general_title).style(Style::new().light_red());
115        let title = BigTextBuilder::default()
116            .pixel_size(PixelSize::Quadrant)
117            .lines(vec![title])
118            .build()
119            .unwrap();
120
121        let subtitle = Line::raw(&self.subtitle).style(Style::new().blue().bold());
122        title.render(top_center_1, buf);
123        subtitle.render(top_center_2, buf);
124    }
125
126    fn render_table<'a, 'b: 'a>(&'b mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
127        let [_, main] = horizontal![==36%, ==57%]
128            .horizontal_margin(3)
129            .split(area)
130            .to_vec()
131            .try_into()
132            .unwrap();
133
134        let lines = {
135            let to_line = |(idx, a): (usize, &'a TableItem)| {
136                let style = match state.selected() {
137                    Some(i) if i == idx => Style::default().italic().bold().underlined(),
138                    _ => Style::default(),
139                };
140                let description = a.left.as_str().blue().style(style);
141                let keyboard = a.right.as_str().to_uppercase().light_red().bold();
142                Line::default().spans(vec![description, keyboard]).style(style)
143            };
144            self.table.iter().enumerate().map(to_line)
145        };
146
147        let mut table_width = 0;
148        let mut table_height = 0;
149        for i in lines.clone() {
150            table_width = table_width.max(i.width() + 2);
151            table_height += 1;
152        }
153
154        let table = {
155            let rows = lines.map(Row::new);
156            let widths = constraints![==95%, ==5%];
157            let block = self.table_block.clone().unwrap_or(Block::bordered());
158            Table::new(rows, widths).highlight_symbol(" >> ").cyan().block(block)
159        };
160
161        let [_, main] = vertical![==20%, ==table_height + 2].split(main).to_vec().try_into().unwrap();
162        StatefulWidget::render(table, main, buf, state);
163    }
164
165    fn render_avatar(&self, area: Rect, buf: &mut Buffer) {
166        let lines = self.avatar.lines().map(|i| Line::from(i.light_blue())).collect::<Vec<_>>();
167        let mut avatar_width = 0;
168        let mut avatar_height = 0;
169        for line in lines.iter() {
170            avatar_width = avatar_width.max(line.width());
171            avatar_height += 1;
172        }
173        let [_, left] = horizontal![==8%, ==avatar_width as u16]
174            .split(area)
175            .to_vec()
176            .try_into()
177            .unwrap();
178        let [_, left] = vertical![==20%, ==avatar_height as u16]
179            .split(left)
180            .to_vec()
181            .try_into()
182            .unwrap();
183        let block = self.avatar_block.clone().unwrap_or(Block::bordered());
184        let avatar = Paragraph::new(lines).block(block);
185        avatar.render(left, buf);
186    }
187    fn render_footer(&self, area: Rect, buf: &mut Buffer) {
188        let [_, bottom] = vertical![==95%, ==5%]
189            .vertical_margin(1)
190            .split(area)
191            .to_vec()
192            .try_into()
193            .unwrap();
194        let lines = self.footer.iter().map(Line::raw).collect::<Vec<_>>();
195        let footer = Paragraph::new(lines).centered().bold().light_cyan().italic();
196        footer.render(bottom, buf);
197    }
198}
199
200impl StatefulWidget for Dashboard<'_> {
201    type State = TableState;
202
203    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
204        self.render_header(area, buf);
205        self.render_table(area, buf, state);
206        self.render_avatar(area, buf);
207        self.render_footer(area, buf);
208    }
209}