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