Skip to main content

vtcode_tui/core_tui/session/
impl_events.rs

1use super::*;
2
3impl Session {
4    pub fn handle_event(
5        &mut self,
6        event: CrosstermEvent,
7        events: &UnboundedSender<InlineEvent>,
8        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
9    ) {
10        match event {
11            CrosstermEvent::Key(key) => {
12                // Only process Press events to avoid duplicate character insertion
13                // Repeat events can cause characters to be inserted multiple times
14                if matches!(key.kind, KeyEventKind::Press)
15                    && let Some(outbound) = events::process_key(self, key)
16                {
17                    self.emit_inline_event(&outbound, events, callback);
18                }
19            }
20            CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind {
21                MouseEventKind::ScrollDown => {
22                    // Check if history picker is active - delegate scrolling to picker
23                    if self.history_picker_state.active {
24                        self.history_picker_state.move_down();
25                        self.mark_dirty();
26                    } else {
27                        self.scroll_line_down();
28                        self.mark_dirty();
29                    }
30                }
31                MouseEventKind::ScrollUp => {
32                    // Check if history picker is active - delegate scrolling to picker
33                    if self.history_picker_state.active {
34                        self.history_picker_state.move_up();
35                        self.mark_dirty();
36                    } else {
37                        self.scroll_line_up();
38                        self.mark_dirty();
39                    }
40                }
41                MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
42                    // Start mouse text selection
43                    self.mouse_selection
44                        .start_selection(mouse_event.column, mouse_event.row);
45                    self.mark_dirty();
46                    if !self.handle_input_click(mouse_event) {
47                        self.handle_transcript_click(mouse_event);
48                    }
49                }
50                MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
51                    self.mouse_selection
52                        .update_selection(mouse_event.column, mouse_event.row);
53                    self.mark_dirty();
54                }
55                MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
56                    self.mouse_selection
57                        .finish_selection(mouse_event.column, mouse_event.row);
58                    self.mark_dirty();
59                }
60                _ => {}
61            },
62            CrosstermEvent::Paste(content) => {
63                events::handle_paste(self, &content);
64            }
65            CrosstermEvent::Resize(_, rows) => {
66                self.apply_view_rows(rows);
67                self.mark_dirty();
68            }
69            CrosstermEvent::FocusGained => {
70                // No-op: focus tracking is host/application concern.
71            }
72            CrosstermEvent::FocusLost => {
73                // No-op: focus tracking is host/application concern.
74            }
75        }
76    }
77
78    pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
79        if !matches!(
80            mouse_event.kind,
81            MouseEventKind::Down(crossterm::event::MouseButton::Left)
82        ) {
83            return false;
84        }
85
86        let Some(area) = self.transcript_area else {
87            return false;
88        };
89
90        if mouse_event.row < area.y
91            || mouse_event.row >= area.y.saturating_add(area.height)
92            || mouse_event.column < area.x
93            || mouse_event.column >= area.x.saturating_add(area.width)
94        {
95            return false;
96        }
97
98        if self.transcript_width == 0 || self.transcript_rows == 0 {
99            return false;
100        }
101
102        let row_in_view = (mouse_event.row - area.y) as usize;
103        if row_in_view >= self.transcript_rows as usize {
104            return false;
105        }
106
107        let viewport_rows = self.transcript_rows.max(1) as usize;
108        let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
109        let effective_padding = padding.min(viewport_rows.saturating_sub(1));
110        let total_rows = self.total_transcript_rows(self.transcript_width) + effective_padding;
111        let (top_offset, _clamped_total_rows) =
112            self.prepare_transcript_scroll(total_rows, viewport_rows);
113        let view_top = top_offset.min(self.scroll_manager.max_offset());
114        self.transcript_view_top = view_top;
115
116        let clicked_row = view_top.saturating_add(row_in_view);
117        let expanded = self.expand_collapsed_paste_at_row(self.transcript_width, clicked_row);
118        if expanded {
119            self.mark_dirty();
120        }
121        expanded
122    }
123
124    pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
125        if !matches!(
126            mouse_event.kind,
127            MouseEventKind::Down(crossterm::event::MouseButton::Left)
128        ) {
129            return false;
130        }
131
132        let Some(area) = self.input_area else {
133            return false;
134        };
135
136        if mouse_event.row < area.y
137            || mouse_event.row >= area.y.saturating_add(area.height)
138            || mouse_event.column < area.x
139            || mouse_event.column >= area.x.saturating_add(area.width)
140        {
141            return false;
142        }
143
144        let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
145        if !self.input_compact_mode || !cursor_at_end {
146            return false;
147        }
148
149        if self.input_compact_placeholder().is_none() {
150            return false;
151        }
152
153        self.input_compact_mode = false;
154        self.mark_dirty();
155        true
156    }
157}