vtcode_tui/core_tui/session/
impl_render.rs1use super::*;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4enum BottomPanelKind {
5 None,
6 InlineModal,
7 FilePalette,
8 HistoryPicker,
9 SlashPalette,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13struct BottomPanelSpec {
14 kind: BottomPanelKind,
15 height: u16,
16}
17
18impl Session {
19 pub fn render(&mut self, frame: &mut Frame<'_>) {
20 let viewport = frame.area();
21 if viewport.height == 0 || viewport.width == 0 {
22 return;
23 }
24
25 if self.needs_full_clear {
27 frame.render_widget(Clear, viewport);
28 self.needs_full_clear = false;
29 }
30
31 let header_lines = self.header_lines();
33 let header_height = self.header_height_from_lines(viewport.width, &header_lines);
34 if header_height != self.header_rows {
35 self.header_rows = header_height;
36 self.recalculate_transcript_rows();
37 }
38
39 let inner_width = viewport.width.saturating_sub(2);
40 let desired_lines = self.desired_input_lines(inner_width);
41 let block_height = Self::input_block_height_for_lines(desired_lines);
42 let status_height = ui::INLINE_INPUT_STATUS_HEIGHT;
43 let input_core_height = block_height.saturating_add(status_height);
44 let panel = resolve_bottom_panel_spec(self, viewport, header_height, input_core_height);
45 let input_height = input_core_height.saturating_add(panel.height);
46 self.apply_input_height(input_height);
47
48 let mut constraints = vec![Constraint::Length(header_height), Constraint::Min(1)];
49 constraints.push(Constraint::Length(input_height));
50
51 let segments = Layout::vertical(constraints).split(viewport);
52
53 let header_area = segments[0];
54 let main_area = segments[1];
55 let input_index = segments.len().saturating_sub(1);
56 let input_area = segments[input_index];
57
58 let _available_width = main_area.width;
59 let _horizontal_minimum = ui::INLINE_CONTENT_MIN_WIDTH + ui::INLINE_NAVIGATION_MIN_WIDTH;
60
61 let modal_in_bottom = matches!(panel.kind, BottomPanelKind::InlineModal);
62 let (transcript_area, modal_area) = if modal_in_bottom {
63 (main_area, None)
64 } else {
65 render::split_inline_modal_area(self, main_area)
66 };
67 let (input_area, bottom_panel_area) =
68 split_input_and_bottom_panel_area(input_area, panel.height);
69 let navigation_area = Rect::new(main_area.x, main_area.y, 0, 0); SessionWidget::new(self)
73 .header_lines(header_lines.clone())
74 .header_area(header_area)
75 .transcript_area(transcript_area)
76 .navigation_area(navigation_area) .render(viewport, frame.buffer_mut());
78
79 self.render_input(frame, input_area);
83 let mut rendered_bottom_modal = false;
84 if !modal_in_bottom {
85 if let Some(modal_area) = modal_area {
86 render::render_modal(self, frame, modal_area);
87 } else {
88 render::render_modal(self, frame, viewport);
89 }
90 }
91 if let Some(panel_area) = bottom_panel_area {
92 match panel.kind {
93 BottomPanelKind::InlineModal => {
94 frame.render_widget(Clear, panel_area);
95 render::render_modal(self, frame, panel_area);
96 rendered_bottom_modal = true;
97 }
98 BottomPanelKind::FilePalette => {
99 render::render_file_palette(self, frame, panel_area);
100 }
101 BottomPanelKind::HistoryPicker => {
102 render::render_history_picker(self, frame, panel_area);
103 }
104 BottomPanelKind::SlashPalette => {
105 slash::render_slash_palette(self, frame, panel_area);
106 }
107 BottomPanelKind::None => {
108 frame.render_widget(Clear, panel_area);
109 }
110 }
111 }
112 if modal_in_bottom && !rendered_bottom_modal {
113 render::render_modal(self, frame, viewport);
114 }
115
116 if self.diff_preview.is_some() {
118 diff_preview::render_diff_preview(self, frame, viewport);
119 }
120
121 if self.mouse_selection.has_selection || self.mouse_selection.is_selecting {
123 self.mouse_selection
124 .apply_highlight(frame.buffer_mut(), viewport);
125
126 if self.mouse_selection.needs_copy() {
128 let text = self
129 .mouse_selection
130 .extract_text(frame.buffer_mut(), viewport);
131 if !text.is_empty() {
132 MouseSelectionState::copy_to_clipboard(&text);
133 }
134 self.mouse_selection.mark_copied();
135 }
136 }
137 }
138
139 #[allow(dead_code)]
140 pub(crate) fn render_message_spans(&self, index: usize) -> Vec<Span<'static>> {
141 let Some(line) = self.lines.get(index) else {
142 return vec![Span::raw(String::new())];
143 };
144 message_renderer::render_message_spans(
145 line,
146 &self.theme,
147 &self.labels,
148 |kind| self.prefix_text(kind),
149 |line| self.prefix_style(line),
150 |kind| self.text_fallback(kind),
151 )
152 }
153}
154
155fn resolve_bottom_panel_spec(
156 session: &mut Session,
157 viewport: Rect,
158 header_height: u16,
159 input_reserved_height: u16,
160) -> BottomPanelSpec {
161 let max_panel_height = viewport
162 .height
163 .saturating_sub(header_height)
164 .saturating_sub(input_reserved_height)
165 .saturating_sub(1);
166 if max_panel_height == 0 || viewport.width == 0 {
167 return BottomPanelSpec {
168 kind: BottomPanelKind::None,
169 height: 0,
170 };
171 }
172
173 if session.inline_lists_visible() {
174 let split_context = SplitContext {
175 width: viewport.width,
176 max_panel_height,
177 };
178 if modal_eligible_for_inline_bottom(session) {
179 if let Some(panel) = panel_from_split(
180 session,
181 split_context,
182 BottomPanelKind::InlineModal,
183 split_inline_modal_area_probe,
184 ) {
185 return panel;
186 }
187 } else if session.file_palette_active {
188 if let Some(panel) = panel_from_split(
189 session,
190 split_context,
191 BottomPanelKind::FilePalette,
192 render::split_inline_file_palette_area,
193 ) {
194 return panel;
195 }
196 } else if session.history_picker_state.active {
197 if let Some(panel) = panel_from_split(
198 session,
199 split_context,
200 BottomPanelKind::HistoryPicker,
201 render::split_inline_history_picker_area,
202 ) {
203 return panel;
204 }
205 } else if !session.slash_palette.is_empty()
206 && let Some(panel) = panel_from_split(
207 session,
208 split_context,
209 BottomPanelKind::SlashPalette,
210 slash::split_inline_slash_area,
211 )
212 {
213 return panel;
214 }
215 }
216
217 BottomPanelSpec {
218 kind: BottomPanelKind::None,
219 height: 0,
220 }
221}
222
223#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224struct SplitContext {
225 width: u16,
226 max_panel_height: u16,
227}
228
229fn panel_from_split(
230 session: &mut Session,
231 ctx: SplitContext,
232 kind: BottomPanelKind,
233 split_fn: fn(&mut Session, Rect) -> (Rect, Option<Rect>),
234) -> Option<BottomPanelSpec> {
235 let height = probe_panel_height(session, ctx, split_fn);
236 if height == 0 {
237 None
238 } else {
239 Some(BottomPanelSpec {
240 kind,
241 height: normalize_panel_height(height, ctx.max_panel_height),
242 })
243 }
244}
245
246fn normalize_panel_height(raw_height: u16, max_panel_height: u16) -> u16 {
247 if raw_height == 0 || max_panel_height == 0 {
248 return 0;
249 }
250
251 let min_floor = ui::INLINE_LIST_PANEL_MIN_HEIGHT
252 .min(max_panel_height)
253 .max(1);
254 raw_height.max(min_floor).min(max_panel_height)
255}
256
257fn modal_eligible_for_inline_bottom(session: &Session) -> bool {
258 session.wizard_modal.is_some()
259 || session
260 .modal
261 .as_ref()
262 .is_some_and(|modal| modal.list.is_some())
263}
264
265fn split_inline_modal_area_probe(session: &mut Session, area: Rect) -> (Rect, Option<Rect>) {
266 render::split_inline_modal_area(session, area)
267}
268
269fn probe_panel_height(
270 session: &mut Session,
271 ctx: SplitContext,
272 split_fn: fn(&mut Session, Rect) -> (Rect, Option<Rect>),
273) -> u16 {
274 if ctx.width == 0 || ctx.max_panel_height == 0 {
275 return 0;
276 }
277
278 let probe_area = Rect::new(0, 0, ctx.width, ctx.max_panel_height.saturating_add(1));
279 let (_, panel_area) = split_fn(session, probe_area);
280 panel_area.map(|area| area.height).unwrap_or(0)
281}
282
283fn split_input_and_bottom_panel_area(area: Rect, panel_height: u16) -> (Rect, Option<Rect>) {
284 if area.height == 0 || panel_height == 0 || area.height <= 1 {
285 return (area, None);
286 }
287
288 let resolved_panel = panel_height.min(area.height.saturating_sub(1));
289 if resolved_panel == 0 {
290 return (area, None);
291 }
292
293 let input_height = area.height.saturating_sub(resolved_panel);
294 let chunks = Layout::vertical([
295 Constraint::Length(input_height.max(1)),
296 Constraint::Length(resolved_panel),
297 ])
298 .split(area);
299 (chunks[0], Some(chunks[1]))
300}