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