vtcode_ui/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
239 .update_transcript_file_link_hover(mouse_event.column, mouse_event.row) =>
240 {
241 self.mark_dirty();
242 }
243 MouseEventKind::ScrollDown => {
244 self.clear_pending_link_click();
245 self.mouse_selection.clear_click_history();
246 if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
247 && !self.handle_bottom_panel_scroll(true)
248 {
249 self.scroll_line_down();
250 self.mark_dirty();
251 }
252 }
253 MouseEventKind::ScrollUp => {
254 self.clear_pending_link_click();
255 self.mouse_selection.clear_click_history();
256 if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
257 && !self.handle_bottom_panel_scroll(false)
258 {
259 self.scroll_line_up();
260 self.mark_dirty();
261 }
262 }
263 MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
264 self.clear_pending_link_click();
265 if self.queue_link_click_action(self.transcript_file_link_click_action(
266 mouse_event.column,
267 mouse_event.row,
268 mouse_event.modifiers,
269 )) {
270 self.mouse_selection.clear_click_history();
271 return;
272 }
273
274 if self.has_active_overlay() {
275 let in_modal_list =
276 self.mouse_in_modal_area(mouse_event.column, mouse_event.row);
277 if self.queue_link_click_action(self.modal_link_click_action(
278 mouse_event.column,
279 mouse_event.row,
280 mouse_event.modifiers,
281 )) {
282 self.mouse_selection.clear_click_history();
283 return;
284 }
285
286 if self.modal_text_area_contains(mouse_event.column, mouse_event.row)
287 && !in_modal_list
288 {
289 let is_double_click = self.mouse_selection.register_click(
290 mouse_event.column,
291 mouse_event.row,
292 Instant::now(),
293 );
294 if is_double_click {
295 let modal_double_click_action = self.throttle_link_click_action(
296 self.modal_link_double_click_action(
297 mouse_event.column,
298 mouse_event.row,
299 ),
300 );
301 if !matches!(
302 modal_double_click_action,
303 TranscriptLinkClickAction::Ignore
304 ) {
305 self.clear_pending_link_click();
306 }
307 if self.handle_link_click_action(
308 modal_double_click_action,
309 true,
310 events,
311 callback,
312 ) {
313 return;
314 }
315 }
316
317 self.mouse_drag_target = MouseDragTarget::ModalText;
318 self.mouse_selection
319 .start_selection(mouse_event.column, mouse_event.row);
320 self.mark_dirty();
321 return;
322 }
323 }
324
325 if self.has_active_overlay()
326 && self.handle_active_overlay_click(mouse_event, events, callback)
327 {
328 self.mouse_selection.clear_click_history();
329 return;
330 }
331
332 if self.handle_bottom_panel_click(mouse_event) {
333 self.mouse_selection.clear_click_history();
334 return;
335 }
336
337 if self.handle_input_click(mouse_event) {
338 self.mouse_drag_target = MouseDragTarget::Input;
339 self.mouse_selection.clear();
340 return;
341 }
342
343 let is_double_click = self.mouse_selection.register_click(
344 mouse_event.column,
345 mouse_event.row,
346 Instant::now(),
347 );
348 if is_double_click {
349 let transcript_double_click_action = self.throttle_link_click_action(
350 self.transcript_file_link_double_click_action(
351 mouse_event.column,
352 mouse_event.row,
353 ),
354 );
355 if !matches!(
356 transcript_double_click_action,
357 TranscriptLinkClickAction::Ignore
358 ) {
359 self.clear_pending_link_click();
360 }
361 if self.handle_link_click_action(
362 transcript_double_click_action,
363 true,
364 events,
365 callback,
366 ) {
367 return;
368 }
369
370 self.mouse_drag_target = MouseDragTarget::None;
371 let _ = self.handle_transcript_click(mouse_event);
372 if self.select_transcript_word_at(mouse_event.column, mouse_event.row) {
373 self.mark_dirty();
374 } else {
375 self.mouse_selection.clear();
376 }
377 self.mouse_selection.clear_click_history();
378 return;
379 }
380
381 self.mouse_drag_target = MouseDragTarget::Transcript;
382 self.mouse_selection
383 .start_selection(mouse_event.column, mouse_event.row);
384 self.mark_dirty();
385 self.handle_transcript_click(mouse_event);
386 }
387 MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
388 self.clear_pending_link_click();
389 match self.mouse_drag_target {
390 MouseDragTarget::Input => {
391 if let Some(cursor) = self
392 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
393 && self.input_manager.cursor() != cursor
394 {
395 self.input_manager.set_cursor_with_selection(cursor);
396 self.mark_dirty();
397 }
398 }
399 MouseDragTarget::Transcript => {
400 self.mouse_selection
401 .update_selection(mouse_event.column, mouse_event.row);
402 self.mark_dirty();
403 }
404 MouseDragTarget::ModalText => {
405 self.mouse_selection
406 .update_selection(mouse_event.column, mouse_event.row);
407 self.mark_dirty();
408 }
409 MouseDragTarget::None => {}
410 }
411 }
412 MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
413 let transcript_link_action =
414 self.pending_link_click_action(self.transcript_file_link_click_action(
415 mouse_event.column,
416 mouse_event.row,
417 mouse_event.modifiers,
418 ));
419 let modal_link_action =
420 self.pending_link_click_action(self.modal_link_click_action(
421 mouse_event.column,
422 mouse_event.row,
423 mouse_event.modifiers,
424 ));
425 match self.mouse_drag_target {
426 MouseDragTarget::Input => {
427 if let Some(cursor) = self
428 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
429 && self.input_manager.cursor() != cursor
430 {
431 self.input_manager.set_cursor_with_selection(cursor);
432 self.mark_dirty();
433 }
434 }
435 MouseDragTarget::Transcript => {
436 self.mouse_selection
437 .finish_selection(mouse_event.column, mouse_event.row);
438 self.mark_dirty();
439 }
440 MouseDragTarget::ModalText => {
441 self.mouse_selection
442 .finish_selection(mouse_event.column, mouse_event.row);
443 self.mark_dirty();
444 }
445 MouseDragTarget::None => {}
446 }
447 self.mouse_drag_target = MouseDragTarget::None;
448 self.clear_pending_link_click();
449 if self.handle_link_click_action(
450 transcript_link_action,
451 false,
452 events,
453 callback,
454 ) {
455 return;
456 }
457 if self.handle_link_click_action(modal_link_action, false, events, callback) {}
458 }
459 _ => {}
460 },
461 CrosstermEvent::Paste(content) => {
462 events::handle_paste(self, &content);
463 }
464 CrosstermEvent::Resize(_, rows) => {
465 self.apply_view_rows(rows);
466 self.mark_dirty();
467 }
468 CrosstermEvent::FocusGained => {
469 }
471 CrosstermEvent::FocusLost => {
472 self.clear_held_key_modifiers();
473 }
474 }
475 }
476
477 pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
478 if !matches!(
479 mouse_event.kind,
480 MouseEventKind::Down(crossterm::event::MouseButton::Left)
481 ) {
482 return false;
483 }
484
485 let Some(area) = self.transcript_area else {
486 return false;
487 };
488
489 if mouse_event.row < area.y
490 || mouse_event.row >= area.y.saturating_add(area.height)
491 || mouse_event.column < area.x
492 || mouse_event.column >= area.x.saturating_add(area.width)
493 {
494 return false;
495 }
496
497 if self.transcript_width == 0 || self.transcript_rows == 0 {
498 return false;
499 }
500
501 let row_in_view = (mouse_event.row - area.y) as usize;
502 if row_in_view >= self.transcript_rows as usize {
503 return false;
504 }
505
506 let viewport_rows = self.transcript_rows.max(1) as usize;
507 let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
508 let effective_padding = padding.min(viewport_rows.saturating_sub(1));
509 let total_rows = self.total_transcript_rows(self.transcript_width) + effective_padding;
510 let (top_offset, _clamped_total_rows) =
511 self.prepare_transcript_scroll(total_rows, viewport_rows);
512 let view_top = top_offset.min(self.scroll_manager.max_offset());
513 self.transcript_view_top = view_top;
514
515 let clicked_row = view_top.saturating_add(row_in_view);
516 let expanded = self.expand_collapsed_paste_at_row(self.transcript_width, clicked_row);
517 if expanded {
518 self.mark_dirty();
519 }
520 expanded
521 }
522
523 pub(crate) fn transcript_word_selection_range(
524 &mut self,
525 column: u16,
526 row: u16,
527 ) -> Option<((u16, u16), (u16, u16))> {
528 let area = self.transcript_area?;
529 if row < area.y
530 || row >= area.y.saturating_add(area.height)
531 || column < area.x
532 || column >= area.x.saturating_add(area.width)
533 {
534 return None;
535 }
536
537 if self.transcript_width == 0 || self.transcript_rows == 0 {
538 return None;
539 }
540
541 let row_in_view = usize::from(row.saturating_sub(area.y));
542 if row_in_view >= self.transcript_rows as usize {
543 return None;
544 }
545
546 let viewport_rows = self.transcript_rows.max(1) as usize;
547 let visible_lines = self.collect_transcript_window_cached(
548 self.transcript_width,
549 self.transcript_view_top,
550 viewport_rows,
551 );
552 let line = visible_lines.get(row_in_view)?;
553
554 let text = transcript_links::transcript_line_text(&line.line);
555 let local_column = column.saturating_sub(area.x);
556 let (start_col, end_col) = mouse_selection::word_selection_range(&text, local_column)?;
557
558 let start = (area.x.saturating_add(start_col), row);
559 let end = (area.x.saturating_add(end_col), row);
560 (start != end).then_some((start, end))
561 }
562
563 pub(crate) fn select_transcript_word_at(&mut self, column: u16, row: u16) -> bool {
564 let Some((start, end)) = self.transcript_word_selection_range(column, row) else {
565 return false;
566 };
567
568 self.mouse_selection.set_selection(start, end);
569 true
570 }
571
572 pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
573 if !matches!(
574 mouse_event.kind,
575 MouseEventKind::Down(crossterm::event::MouseButton::Left)
576 ) {
577 return false;
578 }
579
580 if !self.input_area_contains(mouse_event.column, mouse_event.row) {
581 return false;
582 }
583
584 let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
585 if self.input_compact_mode && cursor_at_end && self.input_compact_placeholder().is_some() {
586 self.input_compact_mode = false;
587 self.mark_dirty();
588 return true;
589 }
590
591 if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
592 {
593 if self.input_manager.cursor() != cursor {
594 self.input_manager.set_cursor(cursor);
595 self.mark_dirty();
596 }
597 return true;
598 }
599
600 false
601 }
602}