vtcode_tui/core_tui/session/
impl_events.rs1use super::transcript_links::TranscriptLinkClickAction;
2use super::*;
3use std::time::Instant;
4
5impl Session {
6 fn input_area_contains(&self, column: u16, row: u16) -> bool {
7 self.input_area.is_some_and(|area| {
8 row >= area.y
9 && row < area.y.saturating_add(area.height)
10 && column >= area.x
11 && column < area.x.saturating_add(area.width)
12 })
13 }
14
15 fn handle_modal_list_result(
16 &mut self,
17 result: modal::ModalListKeyResult,
18 events: &UnboundedSender<InlineEvent>,
19 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
20 ) -> bool {
21 match result {
22 modal::ModalListKeyResult::NotHandled => false,
23 modal::ModalListKeyResult::HandledNoRedraw => true,
24 modal::ModalListKeyResult::Redraw => {
25 self.mark_dirty();
26 true
27 }
28 modal::ModalListKeyResult::Emit(event) => {
29 self.mark_dirty();
30 self.emit_inline_event(&event, events, callback);
31 true
32 }
33 modal::ModalListKeyResult::Submit(event) | modal::ModalListKeyResult::Cancel(event) => {
34 self.close_overlay();
35 self.mark_dirty();
36 self.emit_inline_event(&event, events, callback);
37 true
38 }
39 }
40 }
41
42 fn handle_link_click_action(
43 &mut self,
44 action: TranscriptLinkClickAction,
45 clear_drag_target: bool,
46 events: &UnboundedSender<InlineEvent>,
47 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
48 ) -> bool {
49 match action {
50 TranscriptLinkClickAction::Open(outbound) => {
51 if clear_drag_target {
52 self.mouse_drag_target = MouseDragTarget::None;
53 }
54 self.mark_dirty();
55 self.emit_inline_event(&outbound, events, callback);
56 self.mouse_selection.clear_click_history();
57 true
58 }
59 TranscriptLinkClickAction::Consume => {
60 if clear_drag_target {
61 self.mouse_drag_target = MouseDragTarget::None;
62 }
63 self.mouse_selection.clear_click_history();
64 true
65 }
66 TranscriptLinkClickAction::Ignore => false,
67 }
68 }
69
70 fn modal_visible_index_at(&self, row: u16) -> Option<usize> {
71 let area = self.modal_list_area?;
72 if row < area.y || row >= area.y.saturating_add(area.height) {
73 return None;
74 }
75
76 let styles = render::modal_render_styles(self);
77 let content_width =
78 area.width
79 .saturating_sub(inline_list::selection_padding_width() as u16) as usize;
80 let relative_row = usize::from(row.saturating_sub(area.y));
81
82 if let Some(wizard) = self.wizard_overlay() {
83 let step = wizard.steps.get(wizard.current_step)?;
84 let offset = step.list.list_state.offset();
85 let visible_indices = &step.list.visible_indices;
86 let mut consumed_rows = 0usize;
87 for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
88 let lines = modal::modal_list_item_lines(
89 &step.list,
90 visible_index,
91 item_index,
92 &styles,
93 content_width,
94 None,
95 false,
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 false,
122 );
123 let height = usize::from(inline_list::row_height(&lines));
124 if relative_row < consumed_rows + height {
125 return Some(visible_index);
126 }
127 consumed_rows += height;
128 if consumed_rows >= usize::from(area.height) {
129 break;
130 }
131 }
132
133 None
134 }
135
136 fn mouse_in_modal_area(&self, column: u16, row: u16) -> bool {
137 self.modal_list_area.is_some_and(|area| {
138 row >= area.y
139 && row < area.y.saturating_add(area.height)
140 && column >= area.x
141 && column < area.x.saturating_add(area.width)
142 })
143 }
144
145 fn modal_text_area_contains(&self, column: u16, row: u16) -> bool {
146 self.modal_text_areas().iter().any(|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 }
153
154 fn handle_active_overlay_click(
155 &mut self,
156 mouse_event: MouseEvent,
157 events: &UnboundedSender<InlineEvent>,
158 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
159 ) -> bool {
160 if !self.mouse_in_modal_area(mouse_event.column, mouse_event.row) {
161 return false;
162 }
163
164 let Some(visible_index) = self.modal_visible_index_at(mouse_event.row) else {
165 return true;
166 };
167
168 if let Some(wizard) = self.wizard_overlay_mut() {
169 let result = wizard.handle_mouse_click(visible_index);
170 return self.handle_modal_list_result(result, events, callback);
171 }
172
173 if let Some(modal) = self.modal_state_mut() {
174 let result = modal.handle_list_mouse_click(visible_index);
175 return self.handle_modal_list_result(result, events, callback);
176 }
177
178 true
179 }
180
181 fn handle_active_overlay_scroll(
182 &mut self,
183 mouse_event: MouseEvent,
184 down: bool,
185 events: &UnboundedSender<InlineEvent>,
186 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
187 ) -> bool {
188 if !self.has_active_overlay() {
189 return false;
190 }
191
192 if !self.mouse_in_modal_area(mouse_event.column, mouse_event.row) {
193 return false;
194 }
195
196 if let Some(wizard) = self.wizard_overlay_mut() {
197 let result = wizard.handle_mouse_scroll(down);
198 return self.handle_modal_list_result(result, events, callback);
199 }
200
201 if let Some(modal) = self.modal_state_mut() {
202 let result = modal.handle_list_mouse_scroll(down);
203 return self.handle_modal_list_result(result, events, callback);
204 }
205
206 true
207 }
208
209 fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
210 let _ = down;
211 false
212 }
213
214 fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
215 let _ = mouse_event;
216 false
217 }
218
219 pub fn handle_event(
220 &mut self,
221 event: CrosstermEvent,
222 events: &UnboundedSender<InlineEvent>,
223 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
224 ) {
225 match event {
226 CrosstermEvent::Key(key) => {
227 self.update_held_key_modifiers(&key);
228 if matches!(key.kind, KeyEventKind::Press)
231 && let Some(outbound) = events::process_key(self, key)
232 {
233 self.emit_inline_event(&outbound, events, callback);
234 }
235 }
236 CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind {
237 MouseEventKind::Moved => {
238 if self.update_transcript_file_link_hover(mouse_event.column, mouse_event.row) {
239 self.mark_dirty();
240 }
241 }
242 MouseEventKind::ScrollDown => {
243 self.clear_pending_link_click();
244 self.mouse_selection.clear_click_history();
245 if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
246 && !self.handle_bottom_panel_scroll(true)
247 {
248 self.scroll_line_down();
249 self.mark_dirty();
250 }
251 }
252 MouseEventKind::ScrollUp => {
253 self.clear_pending_link_click();
254 self.mouse_selection.clear_click_history();
255 if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
256 && !self.handle_bottom_panel_scroll(false)
257 {
258 self.scroll_line_up();
259 self.mark_dirty();
260 }
261 }
262 MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
263 self.clear_pending_link_click();
264 if self.queue_link_click_action(self.transcript_file_link_click_action(
265 mouse_event.column,
266 mouse_event.row,
267 mouse_event.modifiers,
268 )) {
269 self.mouse_selection.clear_click_history();
270 return;
271 }
272
273 if self.has_active_overlay() {
274 let in_modal_list =
275 self.mouse_in_modal_area(mouse_event.column, mouse_event.row);
276 if self.queue_link_click_action(self.modal_link_click_action(
277 mouse_event.column,
278 mouse_event.row,
279 mouse_event.modifiers,
280 )) {
281 self.mouse_selection.clear_click_history();
282 return;
283 }
284
285 if self.modal_text_area_contains(mouse_event.column, mouse_event.row)
286 && !in_modal_list
287 {
288 let is_double_click = self.mouse_selection.register_click(
289 mouse_event.column,
290 mouse_event.row,
291 Instant::now(),
292 );
293 if is_double_click {
294 let modal_double_click_action = self.throttle_link_click_action(
295 self.modal_link_double_click_action(
296 mouse_event.column,
297 mouse_event.row,
298 ),
299 );
300 if !matches!(
301 modal_double_click_action,
302 TranscriptLinkClickAction::Ignore
303 ) {
304 self.clear_pending_link_click();
305 }
306 if self.handle_link_click_action(
307 modal_double_click_action,
308 true,
309 events,
310 callback,
311 ) {
312 return;
313 }
314 }
315
316 self.mouse_drag_target = MouseDragTarget::ModalText;
317 self.mouse_selection
318 .start_selection(mouse_event.column, mouse_event.row);
319 self.mark_dirty();
320 return;
321 }
322 }
323
324 if self.has_active_overlay()
325 && self.handle_active_overlay_click(mouse_event, events, callback)
326 {
327 self.mouse_selection.clear_click_history();
328 return;
329 }
330
331 if self.handle_bottom_panel_click(mouse_event) {
332 self.mouse_selection.clear_click_history();
333 return;
334 }
335
336 if self.handle_input_click(mouse_event) {
337 self.mouse_drag_target = MouseDragTarget::Input;
338 self.mouse_selection.clear();
339 return;
340 }
341
342 let is_double_click = self.mouse_selection.register_click(
343 mouse_event.column,
344 mouse_event.row,
345 Instant::now(),
346 );
347 if is_double_click {
348 let transcript_double_click_action = self.throttle_link_click_action(
349 self.transcript_file_link_double_click_action(
350 mouse_event.column,
351 mouse_event.row,
352 ),
353 );
354 if !matches!(
355 transcript_double_click_action,
356 TranscriptLinkClickAction::Ignore
357 ) {
358 self.clear_pending_link_click();
359 }
360 if self.handle_link_click_action(
361 transcript_double_click_action,
362 true,
363 events,
364 callback,
365 ) {
366 return;
367 }
368
369 self.mouse_drag_target = MouseDragTarget::None;
370 let _ = self.handle_transcript_click(mouse_event);
371 if self.select_transcript_word_at(mouse_event.column, mouse_event.row) {
372 self.mark_dirty();
373 } else {
374 self.mouse_selection.clear();
375 }
376 self.mouse_selection.clear_click_history();
377 return;
378 }
379
380 self.mouse_drag_target = MouseDragTarget::Transcript;
381 self.mouse_selection
382 .start_selection(mouse_event.column, mouse_event.row);
383 self.mark_dirty();
384 self.handle_transcript_click(mouse_event);
385 }
386 MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
387 self.clear_pending_link_click();
388 match self.mouse_drag_target {
389 MouseDragTarget::Input => {
390 if let Some(cursor) = self
391 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
392 && self.input_manager.cursor() != cursor
393 {
394 self.input_manager.set_cursor_with_selection(cursor);
395 self.mark_dirty();
396 }
397 }
398 MouseDragTarget::Transcript => {
399 self.mouse_selection
400 .update_selection(mouse_event.column, mouse_event.row);
401 self.mark_dirty();
402 }
403 MouseDragTarget::ModalText => {
404 self.mouse_selection
405 .update_selection(mouse_event.column, mouse_event.row);
406 self.mark_dirty();
407 }
408 MouseDragTarget::None => {}
409 }
410 }
411 MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
412 let transcript_link_action =
413 self.pending_link_click_action(self.transcript_file_link_click_action(
414 mouse_event.column,
415 mouse_event.row,
416 mouse_event.modifiers,
417 ));
418 let modal_link_action =
419 self.pending_link_click_action(self.modal_link_click_action(
420 mouse_event.column,
421 mouse_event.row,
422 mouse_event.modifiers,
423 ));
424 match self.mouse_drag_target {
425 MouseDragTarget::Input => {
426 if let Some(cursor) = self
427 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
428 && self.input_manager.cursor() != cursor
429 {
430 self.input_manager.set_cursor_with_selection(cursor);
431 self.mark_dirty();
432 }
433 }
434 MouseDragTarget::Transcript => {
435 self.mouse_selection
436 .finish_selection(mouse_event.column, mouse_event.row);
437 self.mark_dirty();
438 }
439 MouseDragTarget::ModalText => {
440 self.mouse_selection
441 .finish_selection(mouse_event.column, mouse_event.row);
442 self.mark_dirty();
443 }
444 MouseDragTarget::None => {}
445 }
446 self.mouse_drag_target = MouseDragTarget::None;
447 self.clear_pending_link_click();
448 if self.handle_link_click_action(
449 transcript_link_action,
450 false,
451 events,
452 callback,
453 ) {
454 return;
455 }
456 if self.handle_link_click_action(modal_link_action, false, events, callback) {}
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 self.clear_held_key_modifiers();
472 }
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 transcript_word_selection_range(
523 &mut self,
524 column: u16,
525 row: u16,
526 ) -> Option<((u16, u16), (u16, u16))> {
527 let area = self.transcript_area?;
528 if row < area.y
529 || row >= area.y.saturating_add(area.height)
530 || column < area.x
531 || column >= area.x.saturating_add(area.width)
532 {
533 return None;
534 }
535
536 if self.transcript_width == 0 || self.transcript_rows == 0 {
537 return None;
538 }
539
540 let row_in_view = usize::from(row.saturating_sub(area.y));
541 if row_in_view >= self.transcript_rows as usize {
542 return None;
543 }
544
545 let viewport_rows = self.transcript_rows.max(1) as usize;
546 let visible_lines = self.collect_transcript_window_cached(
547 self.transcript_width,
548 self.transcript_view_top,
549 viewport_rows,
550 );
551 let line = visible_lines.get(row_in_view)?;
552
553 let text = transcript_links::transcript_line_text(&line.line);
554 let local_column = column.saturating_sub(area.x);
555 let (start_col, end_col) = mouse_selection::word_selection_range(&text, local_column)?;
556
557 let start = (area.x.saturating_add(start_col), row);
558 let end = (area.x.saturating_add(end_col), row);
559 (start != end).then_some((start, end))
560 }
561
562 pub(crate) fn select_transcript_word_at(&mut self, column: u16, row: u16) -> bool {
563 let Some((start, end)) = self.transcript_word_selection_range(column, row) else {
564 return false;
565 };
566
567 self.mouse_selection.set_selection(start, end);
568 true
569 }
570
571 pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
572 if !matches!(
573 mouse_event.kind,
574 MouseEventKind::Down(crossterm::event::MouseButton::Left)
575 ) {
576 return false;
577 }
578
579 if !self.input_area_contains(mouse_event.column, mouse_event.row) {
580 return false;
581 }
582
583 let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
584 if self.input_compact_mode && cursor_at_end && self.input_compact_placeholder().is_some() {
585 self.input_compact_mode = false;
586 self.mark_dirty();
587 return true;
588 }
589
590 if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
591 {
592 if self.input_manager.cursor() != cursor {
593 self.input_manager.set_cursor(cursor);
594 self.mark_dirty();
595 }
596 return true;
597 }
598
599 false
600 }
601}