Skip to main content

vtcode_tui/core_tui/session/
impl_events.rs

1use 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                // Only process Press events to avoid duplicate character insertion
198                // Repeat events can cause characters to be inserted multiple times
199                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                // No-op: focus tracking is host/application concern.
339            }
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}