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 bottom_panel_contains(&self, column: u16, row: u16) -> bool {
14 self.bottom_panel_area.is_some_and(|area| {
15 row >= area.y
16 && row < area.y.saturating_add(area.height)
17 && column >= area.x
18 && column < area.x.saturating_add(area.width)
19 })
20 }
21
22 fn shared_panel_row_index(
23 &self,
24 fixed_rows: u16,
25 visible_rows: usize,
26 column: u16,
27 row: u16,
28 ) -> Option<usize> {
29 let area = self.bottom_panel_area?;
30 if row < area.y
31 || row >= area.y.saturating_add(area.height)
32 || column < area.x
33 || column >= area.x.saturating_add(area.width)
34 {
35 return None;
36 }
37
38 let relative_row = row.saturating_sub(area.y);
39 if relative_row < fixed_rows {
40 return None;
41 }
42
43 let list_row = usize::from(relative_row - fixed_rows);
44 (list_row < visible_rows).then_some(list_row)
45 }
46
47 fn handle_modal_list_result(
48 &mut self,
49 result: modal::ModalListKeyResult,
50 events: &UnboundedSender<InlineEvent>,
51 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
52 ) -> bool {
53 match result {
54 modal::ModalListKeyResult::NotHandled => false,
55 modal::ModalListKeyResult::HandledNoRedraw => true,
56 modal::ModalListKeyResult::Redraw => {
57 self.mark_dirty();
58 true
59 }
60 modal::ModalListKeyResult::Emit(event) => {
61 self.mark_dirty();
62 self.emit_inline_event(&event, events, callback);
63 true
64 }
65 modal::ModalListKeyResult::Submit(event) | modal::ModalListKeyResult::Cancel(event) => {
66 self.close_overlay();
67 self.mark_dirty();
68 self.emit_inline_event(&event, events, callback);
69 true
70 }
71 }
72 }
73
74 fn modal_visible_index_at(&self, row: u16) -> Option<usize> {
75 let area = self.modal_list_area?;
76 if row < area.y || row >= area.y.saturating_add(area.height) {
77 return None;
78 }
79
80 let styles = render::modal_render_styles(self);
81 let content_width =
82 area.width
83 .saturating_sub(inline_list::selection_padding_width() as u16) as usize;
84 let relative_row = usize::from(row.saturating_sub(area.y));
85
86 if let Some(wizard) = self.wizard_overlay() {
87 let step = wizard.steps.get(wizard.current_step)?;
88 let offset = step.list.list_state.offset();
89 let visible_indices = &step.list.visible_indices;
90 let mut consumed_rows = 0usize;
91 for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
92 let lines = modal::modal_list_item_lines(
93 &step.list,
94 visible_index,
95 item_index,
96 &styles,
97 content_width,
98 None,
99 );
100 let height = usize::from(inline_list::row_height(&lines));
101 if relative_row < consumed_rows + height {
102 return Some(visible_index);
103 }
104 consumed_rows += height;
105 if consumed_rows >= usize::from(area.height) {
106 break;
107 }
108 }
109 return None;
110 }
111
112 let modal = self.modal_state()?;
113 let list = modal.list.as_ref()?;
114 let offset = list.list_state.offset();
115 let mut consumed_rows = 0usize;
116 for (visible_index, &item_index) in list.visible_indices.iter().enumerate().skip(offset) {
117 let lines = modal::modal_list_item_lines(
118 list,
119 visible_index,
120 item_index,
121 &styles,
122 content_width,
123 None,
124 );
125 let height = usize::from(inline_list::row_height(&lines));
126 if relative_row < consumed_rows + height {
127 return Some(visible_index);
128 }
129 consumed_rows += height;
130 if consumed_rows >= usize::from(area.height) {
131 break;
132 }
133 }
134
135 None
136 }
137
138 fn handle_active_overlay_click(
139 &mut self,
140 mouse_event: MouseEvent,
141 events: &UnboundedSender<InlineEvent>,
142 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
143 ) -> bool {
144 let column = mouse_event.column;
145 let row = mouse_event.row;
146 let in_modal_list = self.modal_list_area.is_some_and(|area| {
147 row >= area.y
148 && row < area.y.saturating_add(area.height)
149 && column >= area.x
150 && column < area.x.saturating_add(area.width)
151 });
152 if !in_modal_list {
153 return self.has_active_overlay();
154 }
155
156 let Some(visible_index) = self.modal_visible_index_at(row) else {
157 return true;
158 };
159
160 if let Some(wizard) = self.wizard_overlay_mut() {
161 let result = wizard.handle_mouse_click(visible_index);
162 return self.handle_modal_list_result(result, events, callback);
163 }
164
165 if let Some(modal) = self.modal_state_mut() {
166 let result = modal.handle_list_mouse_click(visible_index);
167 return self.handle_modal_list_result(result, events, callback);
168 }
169
170 true
171 }
172
173 fn handle_active_overlay_scroll(
174 &mut self,
175 mouse_event: MouseEvent,
176 down: bool,
177 events: &UnboundedSender<InlineEvent>,
178 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
179 ) -> bool {
180 if !self.has_active_overlay() {
181 return false;
182 }
183
184 let column = mouse_event.column;
185 let row = mouse_event.row;
186 let in_modal_list = self.modal_list_area.is_some_and(|area| {
187 row >= area.y
188 && row < area.y.saturating_add(area.height)
189 && column >= area.x
190 && column < area.x.saturating_add(area.width)
191 });
192
193 if !in_modal_list {
194 return true;
195 }
196
197 if let Some(wizard) = self.wizard_overlay_mut() {
198 let result = wizard.handle_mouse_scroll(down);
199 return self.handle_modal_list_result(result, events, callback);
200 }
201
202 if let Some(modal) = self.modal_state_mut() {
203 let result = modal.handle_list_mouse_scroll(down);
204 return self.handle_modal_list_result(result, events, callback);
205 }
206
207 true
208 }
209
210 fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
211 if self.bottom_panel_area.is_none() {
212 return false;
213 }
214
215 if self.file_palette_active {
216 let Some(palette) = self.file_palette.as_mut() else {
217 return true;
218 };
219 if down {
220 palette.move_selection_down();
221 } else {
222 palette.move_selection_up();
223 }
224 self.mark_dirty();
225 return true;
226 }
227
228 if self.history_picker_state.active {
229 if down {
230 self.history_picker_state.move_down();
231 } else {
232 self.history_picker_state.move_up();
233 }
234 self.mark_dirty();
235 return true;
236 }
237
238 if slash::slash_navigation_available(self) {
239 if down {
240 slash::move_slash_selection_down(self);
241 } else {
242 slash::move_slash_selection_up(self);
243 }
244 return true;
245 }
246
247 false
248 }
249
250 fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
251 let column = mouse_event.column;
252 let row = mouse_event.row;
253 if !self.bottom_panel_contains(column, row) {
254 return false;
255 }
256
257 if self.file_palette_active {
258 let fixed_rows = list_panel::fixed_section_rows(1, 2, true);
259 let visible_rows = self
260 .file_palette
261 .as_ref()
262 .map(|palette| palette.current_page_items().len())
263 .unwrap_or(0);
264 let local_index = self.shared_panel_row_index(fixed_rows, visible_rows, column, row);
265 let mut apply_path = None;
266 let mut should_mark_dirty = false;
267 let Some(palette) = self.file_palette.as_mut() else {
268 return true;
269 };
270 if !palette.has_files() {
271 return true;
272 }
273
274 let page_items = palette.current_page_items();
275 if let Some(local_index) = local_index
276 && let Some((global_index, entry, selected)) = page_items.get(local_index)
277 {
278 if *selected {
279 apply_path = Some(entry.relative_path.clone());
280 } else if palette.select_index(*global_index) {
281 should_mark_dirty = true;
282 }
283 }
284
285 if let Some(path) = apply_path {
286 self.insert_file_reference(&path);
287 self.close_file_palette();
288 self.mark_dirty();
289 } else if should_mark_dirty {
290 self.mark_dirty();
291 }
292 return true;
293 }
294
295 if self.history_picker_state.active {
296 let fixed_rows = list_panel::fixed_section_rows(1, 1, true);
297 let visible_rows = if self.history_picker_state.matches.is_empty() {
298 1
299 } else {
300 self.history_picker_state
301 .matches
302 .len()
303 .min(ui::INLINE_LIST_MAX_ROWS)
304 };
305 if let Some(local_index) =
306 self.shared_panel_row_index(fixed_rows, visible_rows, column, row)
307 && !self.history_picker_state.matches.is_empty()
308 {
309 let actual_index = self
310 .history_picker_state
311 .scroll_offset()
312 .saturating_add(local_index);
313 if self.history_picker_state.selected_index() == Some(actual_index) {
314 self.history_picker_state.accept(&mut self.input_manager);
315 } else if self.history_picker_state.select_index(actual_index) {
316 self.mark_dirty();
317 }
318 }
319 return true;
320 }
321
322 if slash::slash_navigation_available(self) {
323 let fixed_rows = list_panel::fixed_section_rows(1, 1, true);
324 let visible_rows = self
325 .slash_palette
326 .suggestions()
327 .len()
328 .min(ui::INLINE_LIST_MAX_ROWS);
329 if let Some(local_index) =
330 self.shared_panel_row_index(fixed_rows, visible_rows, column, row)
331 {
332 let actual_index = self
333 .slash_palette
334 .scroll_offset()
335 .saturating_add(local_index);
336 if self.slash_palette.selected_index() == Some(actual_index) {
337 slash::apply_selected_slash_suggestion(self);
338 } else {
339 slash::select_slash_suggestion_index(self, actual_index);
340 }
341 }
342 return true;
343 }
344
345 true
346 }
347
348 pub fn handle_event(
349 &mut self,
350 event: CrosstermEvent,
351 events: &UnboundedSender<InlineEvent>,
352 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
353 ) {
354 match event {
355 CrosstermEvent::Key(key) => {
356 if matches!(key.kind, KeyEventKind::Press)
359 && let Some(outbound) = events::process_key(self, key)
360 {
361 self.emit_inline_event(&outbound, events, callback);
362 }
363 }
364 CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind {
365 MouseEventKind::Moved => {
366 if self.update_transcript_file_link_hover(mouse_event.column, mouse_event.row) {
367 self.mark_dirty();
368 }
369 }
370 MouseEventKind::ScrollDown => {
371 if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
372 && !self.handle_bottom_panel_scroll(true)
373 {
374 self.scroll_line_down();
375 self.mark_dirty();
376 }
377 }
378 MouseEventKind::ScrollUp => {
379 if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
380 && !self.handle_bottom_panel_scroll(false)
381 {
382 self.scroll_line_up();
383 self.mark_dirty();
384 }
385 }
386 MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
387 if let Some(outbound) = self.transcript_file_link_event(
388 mouse_event.column,
389 mouse_event.row,
390 mouse_event.modifiers,
391 ) {
392 self.mark_dirty();
393 self.emit_inline_event(&outbound, events, callback);
394 return;
395 }
396
397 if self.has_active_overlay()
398 && self.handle_active_overlay_click(mouse_event, events, callback)
399 {
400 return;
401 }
402
403 if self.handle_bottom_panel_click(mouse_event) {
404 return;
405 }
406
407 if self.handle_input_click(mouse_event) {
408 self.mouse_drag_target = MouseDragTarget::Input;
409 self.mouse_selection.clear();
410 return;
411 }
412
413 self.mouse_drag_target = MouseDragTarget::Transcript;
414 self.mouse_selection
415 .start_selection(mouse_event.column, mouse_event.row);
416 self.mark_dirty();
417 self.handle_transcript_click(mouse_event);
418 }
419 MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
420 match self.mouse_drag_target {
421 MouseDragTarget::Input => {
422 if let Some(cursor) = self
423 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
424 && self.input_manager.cursor() != cursor
425 {
426 self.input_manager.set_cursor_with_selection(cursor);
427 self.mark_dirty();
428 }
429 }
430 MouseDragTarget::Transcript => {
431 self.mouse_selection
432 .update_selection(mouse_event.column, mouse_event.row);
433 self.mark_dirty();
434 }
435 MouseDragTarget::None => {}
436 }
437 }
438 MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
439 match self.mouse_drag_target {
440 MouseDragTarget::Input => {
441 if let Some(cursor) = self
442 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
443 && self.input_manager.cursor() != cursor
444 {
445 self.input_manager.set_cursor_with_selection(cursor);
446 self.mark_dirty();
447 }
448 }
449 MouseDragTarget::Transcript => {
450 self.mouse_selection
451 .finish_selection(mouse_event.column, mouse_event.row);
452 self.mark_dirty();
453 }
454 MouseDragTarget::None => {}
455 }
456 self.mouse_drag_target = MouseDragTarget::None;
457 }
458 _ => {}
459 },
460 CrosstermEvent::Paste(content) => {
461 events::handle_paste(self, &content);
462 }
463 CrosstermEvent::Resize(_, rows) => {
464 self.apply_view_rows(rows);
465 self.mark_dirty();
466 }
467 CrosstermEvent::FocusGained => {
468 }
470 CrosstermEvent::FocusLost => {
471 }
473 }
474 }
475
476 pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
477 if !matches!(
478 mouse_event.kind,
479 MouseEventKind::Down(crossterm::event::MouseButton::Left)
480 ) {
481 return false;
482 }
483
484 let Some(area) = self.transcript_area else {
485 return false;
486 };
487
488 if mouse_event.row < area.y
489 || mouse_event.row >= area.y.saturating_add(area.height)
490 || mouse_event.column < area.x
491 || mouse_event.column >= area.x.saturating_add(area.width)
492 {
493 return false;
494 }
495
496 if self.transcript_width == 0 || self.transcript_rows == 0 {
497 return false;
498 }
499
500 let row_in_view = (mouse_event.row - area.y) as usize;
501 if row_in_view >= self.transcript_rows as usize {
502 return false;
503 }
504
505 let viewport_rows = self.transcript_rows.max(1) as usize;
506 let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
507 let effective_padding = padding.min(viewport_rows.saturating_sub(1));
508 let total_rows = self.total_transcript_rows(self.transcript_width) + effective_padding;
509 let (top_offset, _clamped_total_rows) =
510 self.prepare_transcript_scroll(total_rows, viewport_rows);
511 let view_top = top_offset.min(self.scroll_manager.max_offset());
512 self.transcript_view_top = view_top;
513
514 let clicked_row = view_top.saturating_add(row_in_view);
515 let expanded = self.expand_collapsed_paste_at_row(self.transcript_width, clicked_row);
516 if expanded {
517 self.mark_dirty();
518 }
519 expanded
520 }
521
522 pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
523 if !matches!(
524 mouse_event.kind,
525 MouseEventKind::Down(crossterm::event::MouseButton::Left)
526 ) {
527 return false;
528 }
529
530 if !self.input_area_contains(mouse_event.column, mouse_event.row) {
531 return false;
532 }
533
534 let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
535 if self.input_compact_mode && cursor_at_end && self.input_compact_placeholder().is_some() {
536 self.input_compact_mode = false;
537 self.mark_dirty();
538 return true;
539 }
540
541 if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
542 {
543 if self.input_manager.cursor() != cursor {
544 self.input_manager.set_cursor(cursor);
545 self.mark_dirty();
546 }
547 return true;
548 }
549
550 false
551 }
552}