demo2/tabs/
email.rs

1use itertools::Itertools;
2use ratatui::{
3    buffer::Buffer,
4    layout::{Constraint, Layout, Margin, Rect},
5    style::{Styled, Stylize},
6    text::Line,
7    widgets::{
8        Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
9        Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
10    },
11};
12use unicode_width::UnicodeWidthStr;
13
14use crate::{RgbSwatch, THEME};
15
16#[derive(Debug, Default)]
17pub struct Email {
18    from: &'static str,
19    subject: &'static str,
20    body: &'static str,
21}
22
23const EMAILS: &[Email] = &[
24    Email {
25        from: "Alice <alice@example.com>",
26        subject: "Hello",
27        body: "Hi Bob,\nHow are you?\n\nAlice",
28    },
29    Email {
30        from: "Bob <bob@example.com>",
31        subject: "Re: Hello",
32        body: "Hi Alice,\nI'm fine, thanks!\n\nBob",
33    },
34    Email {
35        from: "Charlie <charlie@example.com>",
36        subject: "Re: Hello",
37        body: "Hi Alice,\nI'm fine, thanks!\n\nCharlie",
38    },
39    Email {
40        from: "Dave <dave@example.com>",
41        subject: "Re: Hello (STOP REPLYING TO ALL)",
42        body: "Hi Everyone,\nPlease stop replying to all.\n\nDave",
43    },
44    Email {
45        from: "Eve <eve@example.com>",
46        subject: "Re: Hello (STOP REPLYING TO ALL)",
47        body: "Hi Everyone,\nI'm reading all your emails.\n\nEve",
48    },
49];
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub struct EmailTab {
53    row_index: usize,
54}
55
56impl EmailTab {
57    /// Select the previous email (with wrap around).
58    pub fn prev(&mut self) {
59        self.row_index = self.row_index.saturating_add(EMAILS.len() - 1) % EMAILS.len();
60    }
61
62    /// Select the next email (with wrap around).
63    pub fn next(&mut self) {
64        self.row_index = self.row_index.saturating_add(1) % EMAILS.len();
65    }
66}
67
68impl Widget for EmailTab {
69    fn render(self, area: Rect, buf: &mut Buffer) {
70        RgbSwatch.render(area, buf);
71        let area = area.inner(Margin {
72            vertical: 1,
73            horizontal: 2,
74        });
75        Clear.render(area, buf);
76        let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
77        let [inbox, email] = vertical.areas(area);
78        render_inbox(self.row_index, inbox, buf);
79        render_email(self.row_index, email, buf);
80    }
81}
82fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
83    let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
84    let [tabs, inbox] = vertical.areas(area);
85    let theme = THEME.email;
86    Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
87        .style(theme.tabs)
88        .highlight_style(theme.tabs_selected)
89        .select(0)
90        .divider("")
91        .render(tabs, buf);
92
93    let highlight_symbol = ">>";
94    let from_width = EMAILS
95        .iter()
96        .map(|e| e.from.width())
97        .max()
98        .unwrap_or_default();
99    let items = EMAILS.iter().map(|e| {
100        let from = format!("{:width$}", e.from, width = from_width).into();
101        ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
102    });
103    let mut state = ListState::default().with_selected(Some(selected_index));
104    StatefulWidget::render(
105        List::new(items)
106            .style(theme.inbox)
107            .highlight_style(theme.selected_item)
108            .highlight_symbol(highlight_symbol),
109        inbox,
110        buf,
111        &mut state,
112    );
113    let mut scrollbar_state = ScrollbarState::default()
114        .content_length(EMAILS.len())
115        .position(selected_index);
116    Scrollbar::default()
117        .begin_symbol(None)
118        .end_symbol(None)
119        .track_symbol(None)
120        .thumb_symbol("▐")
121        .render(inbox, buf, &mut scrollbar_state);
122}
123
124fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
125    let theme = THEME.email;
126    let email = EMAILS.get(selected_index);
127    let block = Block::new()
128        .style(theme.body)
129        .padding(Padding::new(2, 2, 0, 0))
130        .borders(Borders::TOP)
131        .border_type(BorderType::Thick);
132    let inner = block.inner(area);
133    block.render(area, buf);
134    if let Some(email) = email {
135        let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
136        let [headers_area, body_area] = vertical.areas(inner);
137        let headers = vec![
138            Line::from(vec![
139                "From: ".set_style(theme.header),
140                email.from.set_style(theme.header_value),
141            ]),
142            Line::from(vec![
143                "Subject: ".set_style(theme.header),
144                email.subject.set_style(theme.header_value),
145            ]),
146            "-".repeat(inner.width as usize).dim().into(),
147        ];
148        Paragraph::new(headers)
149            .style(theme.body)
150            .render(headers_area, buf);
151        let body = email.body.lines().map(Line::from).collect_vec();
152        Paragraph::new(body)
153            .style(theme.body)
154            .render(body_area, buf);
155    } else {
156        Paragraph::new("No email selected").render(inner, buf);
157    }
158}