Skip to main content

stynx_code_tui/render/
renderer.rs

1use ratatui::Frame;
2use ratatui::style::Style;
3use ratatui::widgets::Block;
4
5use crate::layout::MainLayout;
6use crate::state::{AppState, ModalKind};
7use crate::theme;
8use crate::widgets::{DialogSelect, Footer, InfoDialog, InputBox, InputDialog, MessageList, PermissionDialog, Sidebar, SlashPopover, ThinkingPanel, ToastStack};
9
10pub struct Renderer;
11
12impl Renderer {
13    pub fn new() -> Self { Self }
14
15    pub fn draw(frame: &mut Frame, state: &mut AppState) {
16        let full = frame.area();
17        frame.render_widget(
18            Block::default().style(Style::default().bg(theme::BACKGROUND())),
19            full,
20        );
21
22        let thinking_lines = if state.is_streaming && !state.live_thinking.trim().is_empty() {
23            state
24                .live_thinking
25                .lines()
26                .filter(|l| !l.trim().is_empty())
27                .count()
28        } else {
29            0
30        };
31
32        let layout = MainLayout::split(
33            full,
34            state.sidebar.visible,
35            state.input.line_count(),
36            thinking_lines,
37        );
38
39        if let Some(sidebar_area) = layout.sidebar {
40            frame.render_widget(Sidebar::new(&state.sidebar), sidebar_area);
41        }
42
43        frame.render_widget(
44            MessageList::new(&mut state.conversation, state.spinner_frame)
45                .with_tool_details(state.tool_details),
46            layout.messages,
47        );
48        if let Some(thinking_area) = layout.thinking {
49            frame.render_widget(
50                ThinkingPanel::new(&state.live_thinking, state.spinner_frame),
51                thinking_area,
52            );
53        }
54        frame.render_widget(InputBox::new(&state.input, !state.is_streaming), layout.input);
55        if !state.input.slash_matches.is_empty() {
56            frame.render_widget(SlashPopover::new(&state.input, layout.input), full);
57        }
58        frame.render_widget(
59            Footer {
60                cwd: &state.cwd,
61                model: &state.model_name,
62                mode: &state.permission_mode,
63                cost: state.total_cost,
64                git_branch: state.git_branch.as_deref(),
65                is_streaming: state.is_streaming,
66                spinner_frame: state.spinner_frame,
67            },
68            layout.footer,
69        );
70
71        frame.render_widget(ToastStack::new(&state.toasts), full);
72
73        match &state.modal.active {
74            Some(ModalKind::Permission { tool_name, description, choice }) => {
75                frame.render_widget(
76                    PermissionDialog::new(tool_name, description, *choice),
77                    full,
78                );
79            }
80            Some(ModalKind::Select {
81                title,
82                query,
83                options,
84                selected,
85                current_value,
86                footer_hint,
87                ..
88            }) => {
89                frame.render_widget(
90                    DialogSelect::new(title, query, options, *selected)
91                        .with_current(current_value.as_deref())
92                        .with_footer(footer_hint.as_deref()),
93                    full,
94                );
95            }
96            Some(ModalKind::Info { title, rows }) => {
97                frame.render_widget(InfoDialog::new(title, rows), full);
98            }
99            Some(ModalKind::Input { title, prompt, buffer, .. }) => {
100                frame.render_widget(InputDialog::new(title, prompt, buffer), full);
101            }
102            Some(ModalKind::QuitConfirm) => {}
103            None => {}
104        }
105    }
106}
107
108impl Default for Renderer {
109    fn default() -> Self { Self }
110}