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