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 modal_visible_index_at(&self, row: u16) -> Option<usize> {
43 let area = self.modal_list_area?;
44 if row < area.y || row >= area.y.saturating_add(area.height) {
45 return None;
46 }
47
48 let styles = render::modal_render_styles(self);
49 let content_width =
50 area.width
51 .saturating_sub(inline_list::selection_padding_width() as u16) as usize;
52 let relative_row = usize::from(row.saturating_sub(area.y));
53
54 if let Some(wizard) = self.wizard_overlay() {
55 let step = wizard.steps.get(wizard.current_step)?;
56 let offset = step.list.list_state.offset();
57 let visible_indices = &step.list.visible_indices;
58 let mut consumed_rows = 0usize;
59 for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
60 let lines = modal::modal_list_item_lines(
61 &step.list,
62 visible_index,
63 item_index,
64 &styles,
65 content_width,
66 None,
67 );
68 let height = usize::from(inline_list::row_height(&lines));
69 if relative_row < consumed_rows + height {
70 return Some(visible_index);
71 }
72 consumed_rows += height;
73 if consumed_rows >= usize::from(area.height) {
74 break;
75 }
76 }
77 return None;
78 }
79
80 let modal = self.modal_state()?;
81 let list = modal.list.as_ref()?;
82 let offset = list.list_state.offset();
83 let mut consumed_rows = 0usize;
84 for (visible_index, &item_index) in list.visible_indices.iter().enumerate().skip(offset) {
85 let lines = modal::modal_list_item_lines(
86 list,
87 visible_index,
88 item_index,
89 &styles,
90 content_width,
91 None,
92 );
93 let height = usize::from(inline_list::row_height(&lines));
94 if relative_row < consumed_rows + height {
95 return Some(visible_index);
96 }
97 consumed_rows += height;
98 if consumed_rows >= usize::from(area.height) {
99 break;
100 }
101 }
102
103 None
104 }
105
106 fn handle_active_overlay_click(
107 &mut self,
108 mouse_event: MouseEvent,
109 events: &UnboundedSender<InlineEvent>,
110 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
111 ) -> bool {
112 let column = mouse_event.column;
113 let row = mouse_event.row;
114 let in_modal_list = self.modal_list_area.is_some_and(|area| {
115 row >= area.y
116 && row < area.y.saturating_add(area.height)
117 && column >= area.x
118 && column < area.x.saturating_add(area.width)
119 });
120 if !in_modal_list {
121 return self.has_active_overlay();
122 }
123
124 let Some(visible_index) = self.modal_visible_index_at(row) else {
125 return true;
126 };
127
128 if let Some(wizard) = self.wizard_overlay_mut() {
129 let result = wizard.handle_mouse_click(visible_index);
130 return self.handle_modal_list_result(result, events, callback);
131 }
132
133 if let Some(modal) = self.modal_state_mut() {
134 let result = modal.handle_list_mouse_click(visible_index);
135 return self.handle_modal_list_result(result, events, callback);
136 }
137
138 true
139 }
140
141 fn handle_active_overlay_scroll(
142 &mut self,
143 mouse_event: MouseEvent,
144 down: bool,
145 events: &UnboundedSender<InlineEvent>,
146 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
147 ) -> bool {
148 if !self.has_active_overlay() {
149 return false;
150 }
151
152 let column = mouse_event.column;
153 let row = mouse_event.row;
154 let in_modal_list = self.modal_list_area.is_some_and(|area| {
155 row >= area.y
156 && row < area.y.saturating_add(area.height)
157 && column >= area.x
158 && column < area.x.saturating_add(area.width)
159 });
160
161 if !in_modal_list {
162 return true;
163 }
164
165 if let Some(wizard) = self.wizard_overlay_mut() {
166 let result = wizard.handle_mouse_scroll(down);
167 return self.handle_modal_list_result(result, events, callback);
168 }
169
170 if let Some(modal) = self.modal_state_mut() {
171 let result = modal.handle_list_mouse_scroll(down);
172 return self.handle_modal_list_result(result, events, callback);
173 }
174
175 true
176 }
177
178 fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
179 let _ = down;
180 false
181 }
182
183 fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
184 let _ = mouse_event;
185 false
186 }
187
188 pub fn handle_event(
189 &mut self,
190 event: CrosstermEvent,
191 events: &UnboundedSender<InlineEvent>,
192 callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
193 ) {
194 match event {
195 CrosstermEvent::Key(key) => {
196 self.update_held_key_modifiers(&key);
197 if matches!(key.kind, KeyEventKind::Press)
200 && let Some(outbound) = events::process_key(self, key)
201 {
202 self.emit_inline_event(&outbound, events, callback);
203 }
204 }
205 CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind {
206 MouseEventKind::Moved => {
207 if self.update_transcript_file_link_hover(mouse_event.column, mouse_event.row) {
208 self.mark_dirty();
209 }
210 }
211 MouseEventKind::ScrollDown => {
212 self.mouse_selection.clear_click_history();
213 if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
214 && !self.handle_bottom_panel_scroll(true)
215 {
216 self.scroll_line_down();
217 self.mark_dirty();
218 }
219 }
220 MouseEventKind::ScrollUp => {
221 self.mouse_selection.clear_click_history();
222 if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
223 && !self.handle_bottom_panel_scroll(false)
224 {
225 self.scroll_line_up();
226 self.mark_dirty();
227 }
228 }
229 MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
230 match self.transcript_file_link_click_action(
231 mouse_event.column,
232 mouse_event.row,
233 mouse_event.modifiers,
234 ) {
235 TranscriptLinkClickAction::Open(outbound) => {
236 self.mark_dirty();
237 self.emit_inline_event(&outbound, events, callback);
238 self.mouse_selection.clear_click_history();
239 return;
240 }
241 TranscriptLinkClickAction::Consume => {
242 self.mouse_selection.clear_click_history();
243 return;
244 }
245 TranscriptLinkClickAction::Ignore => {}
246 }
247
248 if self.has_active_overlay()
249 && self.handle_active_overlay_click(mouse_event, events, callback)
250 {
251 self.mouse_selection.clear_click_history();
252 return;
253 }
254
255 if self.handle_bottom_panel_click(mouse_event) {
256 self.mouse_selection.clear_click_history();
257 return;
258 }
259
260 if self.handle_input_click(mouse_event) {
261 self.mouse_drag_target = MouseDragTarget::Input;
262 self.mouse_selection.clear();
263 return;
264 }
265
266 let is_double_click = self.mouse_selection.register_click(
267 mouse_event.column,
268 mouse_event.row,
269 Instant::now(),
270 );
271 if is_double_click {
272 self.mouse_drag_target = MouseDragTarget::None;
273 let _ = self.handle_transcript_click(mouse_event);
274 if self.select_transcript_word_at(mouse_event.column, mouse_event.row) {
275 self.mark_dirty();
276 } else {
277 self.mouse_selection.clear();
278 }
279 self.mouse_selection.clear_click_history();
280 return;
281 }
282
283 self.mouse_drag_target = MouseDragTarget::Transcript;
284 self.mouse_selection
285 .start_selection(mouse_event.column, mouse_event.row);
286 self.mark_dirty();
287 self.handle_transcript_click(mouse_event);
288 }
289 MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
290 match self.mouse_drag_target {
291 MouseDragTarget::Input => {
292 if let Some(cursor) = self
293 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
294 && self.input_manager.cursor() != cursor
295 {
296 self.input_manager.set_cursor_with_selection(cursor);
297 self.mark_dirty();
298 }
299 }
300 MouseDragTarget::Transcript => {
301 self.mouse_selection
302 .update_selection(mouse_event.column, mouse_event.row);
303 self.mark_dirty();
304 }
305 MouseDragTarget::None => {}
306 }
307 }
308 MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
309 match self.mouse_drag_target {
310 MouseDragTarget::Input => {
311 if let Some(cursor) = self
312 .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
313 && self.input_manager.cursor() != cursor
314 {
315 self.input_manager.set_cursor_with_selection(cursor);
316 self.mark_dirty();
317 }
318 }
319 MouseDragTarget::Transcript => {
320 self.mouse_selection
321 .finish_selection(mouse_event.column, mouse_event.row);
322 self.mark_dirty();
323 }
324 MouseDragTarget::None => {}
325 }
326 self.mouse_drag_target = MouseDragTarget::None;
327 }
328 _ => {}
329 },
330 CrosstermEvent::Paste(content) => {
331 events::handle_paste(self, &content);
332 }
333 CrosstermEvent::Resize(_, rows) => {
334 self.apply_view_rows(rows);
335 self.mark_dirty();
336 }
337 CrosstermEvent::FocusGained => {
338 }
340 CrosstermEvent::FocusLost => {
341 self.clear_held_key_modifiers();
342 }
343 }
344 }
345
346 pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
347 if !matches!(
348 mouse_event.kind,
349 MouseEventKind::Down(crossterm::event::MouseButton::Left)
350 ) {
351 return false;
352 }
353
354 let Some(area) = self.transcript_area else {
355 return false;
356 };
357
358 if mouse_event.row < area.y
359 || mouse_event.row >= area.y.saturating_add(area.height)
360 || mouse_event.column < area.x
361 || mouse_event.column >= area.x.saturating_add(area.width)
362 {
363 return false;
364 }
365
366 if self.transcript_width == 0 || self.transcript_rows == 0 {
367 return false;
368 }
369
370 let row_in_view = (mouse_event.row - area.y) as usize;
371 if row_in_view >= self.transcript_rows as usize {
372 return false;
373 }
374
375 let viewport_rows = self.transcript_rows.max(1) as usize;
376 let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
377 let effective_padding = padding.min(viewport_rows.saturating_sub(1));
378 let total_rows = self.total_transcript_rows(self.transcript_width) + effective_padding;
379 let (top_offset, _clamped_total_rows) =
380 self.prepare_transcript_scroll(total_rows, viewport_rows);
381 let view_top = top_offset.min(self.scroll_manager.max_offset());
382 self.transcript_view_top = view_top;
383
384 let clicked_row = view_top.saturating_add(row_in_view);
385 let expanded = self.expand_collapsed_paste_at_row(self.transcript_width, clicked_row);
386 if expanded {
387 self.mark_dirty();
388 }
389 expanded
390 }
391
392 pub(crate) fn transcript_word_selection_range(
393 &mut self,
394 column: u16,
395 row: u16,
396 ) -> Option<((u16, u16), (u16, u16))> {
397 let area = self.transcript_area?;
398 if row < area.y
399 || row >= area.y.saturating_add(area.height)
400 || column < area.x
401 || column >= area.x.saturating_add(area.width)
402 {
403 return None;
404 }
405
406 if self.transcript_width == 0 || self.transcript_rows == 0 {
407 return None;
408 }
409
410 let row_in_view = usize::from(row.saturating_sub(area.y));
411 if row_in_view >= self.transcript_rows as usize {
412 return None;
413 }
414
415 let viewport_rows = self.transcript_rows.max(1) as usize;
416 let visible_lines = self.collect_transcript_window_cached(
417 self.transcript_width,
418 self.transcript_view_top,
419 viewport_rows,
420 );
421 let line = visible_lines.get(row_in_view)?;
422
423 let text: String = line
424 .line
425 .spans
426 .iter()
427 .map(|span| span.content.as_ref())
428 .collect();
429 let local_column = column.saturating_sub(area.x);
430 let (start_col, end_col) = mouse_selection::word_selection_range(&text, local_column)?;
431
432 let start = (area.x.saturating_add(start_col), row);
433 let end = (area.x.saturating_add(end_col), row);
434 (start != end).then_some((start, end))
435 }
436
437 pub(crate) fn select_transcript_word_at(&mut self, column: u16, row: u16) -> bool {
438 let Some((start, end)) = self.transcript_word_selection_range(column, row) else {
439 return false;
440 };
441
442 self.mouse_selection.set_selection(start, end);
443 true
444 }
445
446 pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
447 if !matches!(
448 mouse_event.kind,
449 MouseEventKind::Down(crossterm::event::MouseButton::Left)
450 ) {
451 return false;
452 }
453
454 if !self.input_area_contains(mouse_event.column, mouse_event.row) {
455 return false;
456 }
457
458 let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
459 if self.input_compact_mode && cursor_at_end && self.input_compact_placeholder().is_some() {
460 self.input_compact_mode = false;
461 self.mark_dirty();
462 return true;
463 }
464
465 if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
466 {
467 if self.input_manager.cursor() != cursor {
468 self.input_manager.set_cursor(cursor);
469 self.mark_dirty();
470 }
471 return true;
472 }
473
474 false
475 }
476}