Skip to main content

mxr_tui/app/
draw.rs

1use super::*;
2
3fn tab_titles() -> [&'static str; 5] {
4    [
5        "1 Mailbox",
6        "2 Search",
7        "3 Rules",
8        "4 Accounts",
9        "5 Diagnostics",
10    ]
11}
12
13fn selected_tab(screen: Screen) -> usize {
14    match screen {
15        Screen::Mailbox => 0,
16        Screen::Search => 1,
17        Screen::Rules => 2,
18        Screen::Accounts => 3,
19        Screen::Diagnostics => 4,
20    }
21}
22
23impl App {
24    pub fn draw(&mut self, frame: &mut Frame) {
25        let theme = &self.theme;
26        let area = frame.area();
27
28        // Layout: tabs (1 line) | hint bar (2 lines) | content | status bar (1 line)
29        let outer_chunks = Layout::default()
30            .direction(Direction::Vertical)
31            .constraints([
32                Constraint::Length(1), // tab bar
33                Constraint::Length(2), // hint bar
34                Constraint::Min(0),    // content
35                Constraint::Length(1), // status bar
36            ])
37            .split(area);
38
39        let tab_bar_area = outer_chunks[0];
40        let hint_bar_area = outer_chunks[1];
41        let content_area = outer_chunks[2];
42        // Update visible height based on actual terminal size (subtract borders/header)
43        self.visible_height = content_area.height.saturating_sub(2) as usize;
44        let bottom_bar_area = outer_chunks[3];
45
46        // Tab bar
47        let tabs = ratatui::widgets::Tabs::new(tab_titles())
48            .select(selected_tab(self.screen))
49            .style(Style::default().fg(theme.text_muted))
50            .highlight_style(Style::default().fg(theme.accent).bold())
51            .divider(Span::styled(" | ", Style::default().fg(theme.text_muted)));
52        frame.render_widget(tabs, tab_bar_area);
53
54        // Hint bar
55        ui::hint_bar::draw(
56            frame,
57            hint_bar_area,
58            ui::hint_bar::HintBarState {
59                screen: self.screen,
60                active_pane: &self.active_pane,
61                search_active: self.search_bar.active,
62                help_modal_open: self.help_modal_open,
63                selected_count: self.selected_set.len(),
64                bulk_confirm_open: self.pending_bulk_confirm.is_some(),
65                sync_status: self.last_sync_status.clone(),
66            },
67            theme,
68        );
69
70        match self.screen {
71            Screen::Mailbox => match self.layout_mode {
72                LayoutMode::TwoPane => {
73                    let chunks = Layout::default()
74                        .direction(Direction::Horizontal)
75                        .constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
76                        .split(content_area);
77
78                    ui::sidebar::draw(
79                        frame,
80                        chunks[0],
81                        &ui::sidebar::SidebarView {
82                            labels: &self.labels,
83                            active_pane: &self.active_pane,
84                            saved_searches: &self.saved_searches,
85                            sidebar_selected: self.sidebar_selected,
86                            all_mail_active: !self.search_active
87                                && self.mailbox_view == MailboxView::Messages
88                                && self.active_label.is_none()
89                                && self.pending_active_label.is_none(),
90                            subscriptions_active: self.mailbox_view == MailboxView::Subscriptions,
91                            subscription_count: self.subscriptions_page.entries.len(),
92                            system_expanded: self.sidebar_system_expanded,
93                            user_expanded: self.sidebar_user_expanded,
94                            saved_searches_expanded: self.sidebar_saved_searches_expanded,
95                            active_label: self
96                                .pending_active_label
97                                .as_ref()
98                                .or(self.active_label.as_ref()),
99                        },
100                        theme,
101                    );
102
103                    if self.mailbox_view == MailboxView::Subscriptions {
104                        ui::subscriptions_page::draw(
105                            frame,
106                            chunks[1],
107                            &ui::subscriptions_page::SubscriptionsPageView {
108                                entries: &self.subscriptions_page.entries,
109                                selected_index: self.selected_index,
110                                scroll_offset: self.scroll_offset,
111                                active_pane: &self.active_pane,
112                                preview_blocks: &self.thread_message_blocks(),
113                                message_scroll_offset: self.message_scroll_offset,
114                            },
115                            theme,
116                        );
117                    } else {
118                        let mail_title = self.mail_list_title();
119                        ui::mail_list::draw_view(
120                            frame,
121                            chunks[1],
122                            &ui::mail_list::MailListView {
123                                rows: &self.mail_list_rows(),
124                                selected_index: self.selected_index,
125                                scroll_offset: self.scroll_offset,
126                                active_pane: &self.active_pane,
127                                title: &mail_title,
128                                selected_set: &self.selected_set,
129                                mode: self.mail_list_mode,
130                            },
131                            theme,
132                        );
133                    }
134                }
135                LayoutMode::ThreePane => {
136                    let chunks = Layout::default()
137                        .direction(Direction::Horizontal)
138                        .constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
139                        .split(content_area);
140
141                    ui::sidebar::draw(
142                        frame,
143                        chunks[0],
144                        &ui::sidebar::SidebarView {
145                            labels: &self.labels,
146                            active_pane: &self.active_pane,
147                            saved_searches: &self.saved_searches,
148                            sidebar_selected: self.sidebar_selected,
149                            all_mail_active: !self.search_active
150                                && self.mailbox_view == MailboxView::Messages
151                                && self.active_label.is_none()
152                                && self.pending_active_label.is_none(),
153                            subscriptions_active: self.mailbox_view == MailboxView::Subscriptions,
154                            subscription_count: self.subscriptions_page.entries.len(),
155                            system_expanded: self.sidebar_system_expanded,
156                            user_expanded: self.sidebar_user_expanded,
157                            saved_searches_expanded: self.sidebar_saved_searches_expanded,
158                            active_label: self
159                                .pending_active_label
160                                .as_ref()
161                                .or(self.active_label.as_ref()),
162                        },
163                        theme,
164                    );
165
166                    if self.mailbox_view == MailboxView::Subscriptions {
167                        ui::subscriptions_page::draw(
168                            frame,
169                            chunks[1],
170                            &ui::subscriptions_page::SubscriptionsPageView {
171                                entries: &self.subscriptions_page.entries,
172                                selected_index: self.selected_index,
173                                scroll_offset: self.scroll_offset,
174                                active_pane: &self.active_pane,
175                                preview_blocks: &self.thread_message_blocks(),
176                                message_scroll_offset: self.message_scroll_offset,
177                            },
178                            theme,
179                        );
180                    } else {
181                        let inner = Layout::default()
182                            .direction(Direction::Horizontal)
183                            .constraints([Constraint::Percentage(41), Constraint::Percentage(59)])
184                            .split(chunks[1]);
185                        let mail_title = self.mail_list_title();
186                        ui::mail_list::draw_view(
187                            frame,
188                            inner[0],
189                            &ui::mail_list::MailListView {
190                                rows: &self.mail_list_rows(),
191                                selected_index: self.selected_index,
192                                scroll_offset: self.scroll_offset,
193                                active_pane: &self.active_pane,
194                                title: &mail_title,
195                                selected_set: &self.selected_set,
196                                mode: self.mail_list_mode,
197                            },
198                            theme,
199                        );
200                        ui::message_view::draw(
201                            frame,
202                            inner[1],
203                            &self.thread_message_blocks(),
204                            self.message_scroll_offset,
205                            &self.active_pane,
206                            theme,
207                        );
208                    }
209                }
210                LayoutMode::FullScreen => {
211                    if self.mailbox_view == MailboxView::Subscriptions {
212                        ui::subscriptions_page::draw(
213                            frame,
214                            content_area,
215                            &ui::subscriptions_page::SubscriptionsPageView {
216                                entries: &self.subscriptions_page.entries,
217                                selected_index: self.selected_index,
218                                scroll_offset: self.scroll_offset,
219                                active_pane: &self.active_pane,
220                                preview_blocks: &self.thread_message_blocks(),
221                                message_scroll_offset: self.message_scroll_offset,
222                            },
223                            theme,
224                        );
225                    } else {
226                        ui::message_view::draw(
227                            frame,
228                            content_area,
229                            &self.thread_message_blocks(),
230                            self.message_scroll_offset,
231                            &self.active_pane,
232                            theme,
233                        );
234                    }
235                }
236            },
237            Screen::Search => {
238                let rows = self.search_mail_list_rows();
239                ui::search_page::draw(
240                    frame,
241                    content_area,
242                    &self.search_page,
243                    &rows,
244                    self.mail_list_mode,
245                    &self.thread_message_blocks(),
246                    self.message_scroll_offset,
247                    theme,
248                );
249            }
250            Screen::Rules => {
251                ui::rules_page::draw(frame, content_area, &self.rules_page, theme);
252            }
253            Screen::Diagnostics => {
254                ui::diagnostics_page::draw(frame, content_area, &self.diagnostics_page, theme);
255            }
256            Screen::Accounts => {
257                ui::accounts_page::draw(frame, content_area, &self.accounts_page, theme);
258            }
259        }
260
261        let status_bar = self.status_bar_state();
262        ui::status_bar::draw(frame, bottom_bar_area, &status_bar, theme);
263
264        if self.search_bar.active {
265            ui::search_bar::draw(frame, area, &self.search_bar, theme);
266        }
267
268        // Command palette overlay
269        ui::command_palette::draw(frame, area, &self.command_palette, theme);
270
271        // Label picker overlay
272        ui::label_picker::draw(frame, area, &self.label_picker, theme);
273
274        // Compose picker overlay
275        ui::compose_picker::draw(frame, area, &self.compose_picker, theme);
276
277        // Attachment overlay
278        ui::attachment_modal::draw(frame, area, &self.attachment_panel, theme);
279
280        // URL picker overlay
281        ui::url_modal::draw(frame, area, self.url_modal.as_ref(), theme);
282
283        // Snooze overlay
284        ui::snooze_modal::draw(frame, area, &self.snooze_panel, &self.snooze_config, theme);
285
286        // Send confirmation overlay
287        ui::send_confirm_modal::draw(frame, area, self.pending_send_confirm.as_ref(), theme);
288
289        // Bulk confirmation overlay
290        ui::bulk_confirm_modal::draw(frame, area, self.pending_bulk_confirm.as_ref(), theme);
291
292        // Error overlay
293        ui::error_modal::draw(frame, area, self.error_modal.as_ref(), theme);
294
295        // Unsubscribe confirmation overlay
296        ui::unsubscribe_modal::draw(
297            frame,
298            area,
299            self.pending_unsubscribe_confirm.as_ref(),
300            theme,
301        );
302
303        // Help overlay
304        ui::help_modal::draw(
305            frame,
306            area,
307            ui::help_modal::HelpModalState {
308                open: self.help_modal_open,
309                screen: self.screen,
310                active_pane: &self.active_pane,
311                selected_count: self.selected_set.len(),
312                scroll_offset: self.help_scroll_offset,
313            },
314            theme,
315        );
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::tab_titles;
322
323    #[test]
324    fn tab_titles_include_numeric_shortcuts() {
325        assert_eq!(
326            tab_titles(),
327            [
328                "1 Mailbox",
329                "2 Search",
330                "3 Rules",
331                "4 Accounts",
332                "5 Diagnostics",
333            ]
334        );
335    }
336}