vtcode_tui/core_tui/session/
impl_events.rs1use super::*;
2
3impl Session {
4 fn input_area_contains(&self, column: u16, row: u16) -> bool {
5 self.input_area.is_some_and(|area| {
6 row >= area.y
7 && row < area.y.saturating_add(area.height)
8 && column >= area.x
9 && column < area.x.saturating_add(area.width)
10 })
11 }
12
13 fn handle_modal_list_result(
14 &mut self,
15 result: modal::ModalListKeyResult,
16 events: &UnboundedSender<InlineEvent>,
17 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
18 ) -> bool {
19 match result {
20 modal::ModalListKeyResult::NotHandled => false,
21 modal::ModalListKeyResult::HandledNoRedraw => true,
22 modal::ModalListKeyResult::Redraw => {
23 self.mark_dirty();
24 true
25 }
26 modal::ModalListKeyResult::Emit(event) => {
27 self.mark_dirty();
28 self.emit_inline_event(&event, events, callback);
29 true
30 }
31 modal::ModalListKeyResult::Submit(event) | modal::ModalListKeyResult::Cancel(event) => {
32 self.close_overlay();
33 self.mark_dirty();
34 self.emit_inline_event(&event, events, callback);
35 true
36 }
37 }
38 }
39
40 fn modal_visible_index_at(&self, row: u16) -> Option<usize> {
41 let area = self.modal_list_area?;
42 if row < area.y || row >= area.y.saturating_add(area.height) {
43 return None;
44 }
45
46 let styles = render::modal_render_styles(self);
47 let content_width =
48 area.width
49 .saturating_sub(inline_list::selection_padding_width() as u16) as usize;
50 let relative_row = usize::from(row.saturating_sub(area.y));
51
52 if let Some(wizard) = self.wizard_overlay() {
53 let step = wizard.steps.get(wizard.current_step)?;
54 let offset = step.list.list_state.offset();
55 let visible_indices = &step.list.visible_indices;
56 let mut consumed_rows = 0usize;
57 for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
58 let lines = modal::modal_list_item_lines(
59 &step.list,
60 visible_index,
61 item_index,
62 &styles,
63 content_width,
64 None,
65 );
66 let height = usize::from(inline_list::row_height(&lines));
67 if relative_row < consumed_rows + height {
68 return Some(visible_index);
69 }
70 consumed_rows += height;
71 if consumed_rows >= usize::from(area.height) {
72 break;
73 }
74 }
75 return None;
76 }
77
78 let modal = self.modal_state()?;
79 let list = modal.list.as_ref()?;
80 let offset = list.list_state.offset();
81 let mut consumed_rows = 0usize;
82 for (visible_index, &item_index) in list.visible_indices.iter().enumerate().skip(offset) {
83 let lines = modal::modal_list_item_lines(
84 list,
85 visible_index,
86 item_index,
87 &styles,
88 content_width,
89 None,
90 );
91 let height = usize::from(inline_list::row_height(&lines));
92 if relative_row < consumed_rows + height {
93 return Some(visible_index);
94 }
95 consumed_rows += height;
96 if consumed_rows >= usize::from(area.height) {
97 break;
98 }
99 }
100
101 None
102 }
103
104 fn handle_active_overlay_click(
105 &mut self,
106 mouse_event: MouseEvent,
107 events: &UnboundedSender<InlineEvent>,
108 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
109 ) -> bool {
110 let column = mouse_event.column;
111 let row = mouse_event.row;
112 let in_modal_list = self.modal_list_area.is_some_and(|area| {
113 row >= area.y
114 && row < area.y.saturating_add(area.height)
115 && column >= area.x
116 && column < area.x.saturating_add(area.width)
117 });
118 if !in_modal_list {
119 return self.has_active_overlay();
120 }
121
122 let Some(visible_index) = self.modal_visible_index_at(row) else {
123 return true;
124 };
125
126 if let Some(wizard) = self.wizard_overlay_mut() {
127 let result = wizard.handle_mouse_click(visible_index);
128 return self.handle_modal_list_result(result, events, callback);
129 }
130
131 if let Some(modal) = self.modal_state_mut() {
132 let result = modal.handle_list_mouse_click(visible_index);
133 return self.handle_modal_list_result(result, events, callback);
134 }
135
136 true
137 }
138
139 fn handle_active_overlay_scroll(
140 &mut self,
141 mouse_event: MouseEvent,
142 down: bool,
143 events: &UnboundedSender<InlineEvent>,
144 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
145 ) -> bool {
146 if !self.has_active_overlay() {
147 return false;
148 }
149
150 let column = mouse_event.column;
151 let row = mouse_event.row;
152 let in_modal_list = self.modal_list_area.is_some_and(|area| {
153 row >= area.y
154 && row < area.y.saturating_add(area.height)
155 && column >= area.x
156 && column < area.x.saturating_add(area.width)
157 });
158
159 if !in_modal_list {
160 return true;
161 }
162
163 if let Some(wizard) = self.wizard_overlay_mut() {
164 let result = wizard.handle_mouse_scroll(down);
165 return self.handle_modal_list_result(result, events, callback);
166 }
167
168 if let Some(modal) = self.modal_state_mut() {
169 let result = modal.handle_list_mouse_scroll(down);
170 return self.handle_modal_list_result(result, events, callback);
171 }
172
173 true
174 }
175
176 fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
177 let _ = down;
178 false
179 }
180
181 fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
182 let _ = mouse_event;
183 false
184 }
185
186 pub fn handle_event(
187 &mut self,
188 event: CrosstermEvent,
189 events: &UnboundedSender<InlineEvent>,
190 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
191 ) {
192 match event {
193 CrosstermEvent::Key(key) => {
194 if matches!(key.kind, KeyEventKind::Press)
197 && let Some(outbound) = events::process_key(self, key)
198 {
199 self.emit_inline_event(&outbound, events, callback);
200 }
201 }
202 CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind {
203 MouseEventKind::Moved => {
204 if self.update_transcript_file_link_hover(mouse_event.column, mouse_event.row) {
205 self.mark_dirty();
206 }
207 }
208 MouseEventKind::ScrollDown => {
209 if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
210 && !self.handle_bottom_panel_scroll(true)
211 {
212 self.scroll_line_down();
213 self.mark_dirty();
214 }
215 }
216 MouseEventKind::ScrollUp => {
217 if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
218 && !self.handle_bottom_panel_scroll(false)
219 {
220 self.scroll_line_up();
221 self.mark_dirty();
222 }
223 }
224 MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
225 if let Some(outbound) = self.transcript_file_link_event(
226 mouse_event.column,
227 mouse_event.row,
228 mouse_event.modifiers,
229 ) {
230 self.mark_dirty();
231 self.emit_inline_event(&outbound, events, callback);
232 return;
233 }
234
235 if self.has_active_overlay()
236 && self.handle_active_overlay_click(mouse_event, events, callback)
237 {
238 return;
239 }
240
241 if self.handle_bottom_panel_click(mouse_event) {
242 return;
243 }
244
245 if self.handle_input_click(mouse_event) {
246 self.mouse_drag_target = MouseDragTarget::Input;
247 self.mouse_selection.clear();
248 return;
249 }
250
251 self.mouse_drag_target = MouseDragTarget::Transcript;
252 self.mouse_selection
253 .start_selection(mouse_event.column, mouse_event.row);
254 self.mark_dirty();
255 self.handle_transcript_click(mouse_event);
256 }
257 MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
258 match self.mouse_drag_target {
259 MouseDragTarget::Input => {
260 if let Some(cursor) = self
261 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
262 && self.input_manager.cursor() != cursor
263 {
264 self.input_manager.set_cursor_with_selection(cursor);
265 self.mark_dirty();
266 }
267 }
268 MouseDragTarget::Transcript => {
269 self.mouse_selection
270 .update_selection(mouse_event.column, mouse_event.row);
271 self.mark_dirty();
272 }
273 MouseDragTarget::None => {}
274 }
275 }
276 MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
277 match self.mouse_drag_target {
278 MouseDragTarget::Input => {
279 if let Some(cursor) = self
280 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
281 && self.input_manager.cursor() != cursor
282 {
283 self.input_manager.set_cursor_with_selection(cursor);
284 self.mark_dirty();
285 }
286 }
287 MouseDragTarget::Transcript => {
288 self.mouse_selection
289 .finish_selection(mouse_event.column, mouse_event.row);
290 self.mark_dirty();
291 }
292 MouseDragTarget::None => {}
293 }
294 self.mouse_drag_target = MouseDragTarget::None;
295 }
296 _ => {}
297 },
298 CrosstermEvent::Paste(content) => {
299 events::handle_paste(self, &content);
300 }
301 CrosstermEvent::Resize(_, rows) => {
302 self.apply_view_rows(rows);
303 self.mark_dirty();
304 }
305 CrosstermEvent::FocusGained => {
306 }
308 CrosstermEvent::FocusLost => {
309 }
311 }
312 }
313
314 pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
315 if !matches!(
316 mouse_event.kind,
317 MouseEventKind::Down(crossterm::event::MouseButton::Left)
318 ) {
319 return false;
320 }
321
322 let Some(area) = self.transcript_area else {
323 return false;
324 };
325
326 if mouse_event.row < area.y
327 || mouse_event.row >= area.y.saturating_add(area.height)
328 || mouse_event.column < area.x
329 || mouse_event.column >= area.x.saturating_add(area.width)
330 {
331 return false;
332 }
333
334 if self.transcript_width == 0 || self.transcript_rows == 0 {
335 return false;
336 }
337
338 let row_in_view = (mouse_event.row - area.y) as usize;
339 if row_in_view >= self.transcript_rows as usize {
340 return false;
341 }
342
343 let viewport_rows = self.transcript_rows.max(1) as usize;
344 let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
345 let effective_padding = padding.min(viewport_rows.saturating_sub(1));
346 let total_rows = self.total_transcript_rows(self.transcript_width) + effective_padding;
347 let (top_offset, _clamped_total_rows) =
348 self.prepare_transcript_scroll(total_rows, viewport_rows);
349 let view_top = top_offset.min(self.scroll_manager.max_offset());
350 self.transcript_view_top = view_top;
351
352 let clicked_row = view_top.saturating_add(row_in_view);
353 let expanded = self.expand_collapsed_paste_at_row(self.transcript_width, clicked_row);
354 if expanded {
355 self.mark_dirty();
356 }
357 expanded
358 }
359
360 pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
361 if !matches!(
362 mouse_event.kind,
363 MouseEventKind::Down(crossterm::event::MouseButton::Left)
364 ) {
365 return false;
366 }
367
368 if !self.input_area_contains(mouse_event.column, mouse_event.row) {
369 return false;
370 }
371
372 let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
373 if self.input_compact_mode && cursor_at_end && self.input_compact_placeholder().is_some() {
374 self.input_compact_mode = false;
375 self.mark_dirty();
376 return true;
377 }
378
379 if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
380 {
381 if self.input_manager.cursor() != cursor {
382 self.input_manager.set_cursor(cursor);
383 self.mark_dirty();
384 }
385 return true;
386 }
387
388 false
389 }
390}