Skip to main content

vtcode_tui/core_tui/session/
impl_events.rs

1use super::*;
2
3impl Session {
4    fn input_area_contains(&self, column: u16, row: u16) -> bool {
5        self.input_area.is_some_and(|area| {
6            row >= area.y
7                && row < area.y.saturating_add(area.height)
8                && column >= area.x
9                && column < area.x.saturating_add(area.width)
10        })
11    }
12
13    fn handle_modal_list_result(
14        &mut self,
15        result: modal::ModalListKeyResult,
16        events: &UnboundedSender<InlineEvent>,
17        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
18    ) -> bool {
19        match result {
20            modal::ModalListKeyResult::NotHandled => false,
21            modal::ModalListKeyResult::HandledNoRedraw => true,
22            modal::ModalListKeyResult::Redraw => {
23                self.mark_dirty();
24                true
25            }
26            modal::ModalListKeyResult::Emit(event) => {
27                self.mark_dirty();
28                self.emit_inline_event(&event, events, callback);
29                true
30            }
31            modal::ModalListKeyResult::Submit(event) | modal::ModalListKeyResult::Cancel(event) => {
32                self.close_overlay();
33                self.mark_dirty();
34                self.emit_inline_event(&event, events, callback);
35                true
36            }
37        }
38    }
39
40    fn modal_visible_index_at(&self, row: u16) -> Option<usize> {
41        let area = self.modal_list_area?;
42        if row < area.y || row >= area.y.saturating_add(area.height) {
43            return None;
44        }
45
46        let styles = render::modal_render_styles(self);
47        let content_width =
48            area.width
49                .saturating_sub(inline_list::selection_padding_width() as u16) as usize;
50        let relative_row = usize::from(row.saturating_sub(area.y));
51
52        if let Some(wizard) = self.wizard_overlay() {
53            let step = wizard.steps.get(wizard.current_step)?;
54            let offset = step.list.list_state.offset();
55            let visible_indices = &step.list.visible_indices;
56            let mut consumed_rows = 0usize;
57            for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
58                let lines = modal::modal_list_item_lines(
59                    &step.list,
60                    visible_index,
61                    item_index,
62                    &styles,
63                    content_width,
64                    None,
65                );
66                let height = usize::from(inline_list::row_height(&lines));
67                if relative_row < consumed_rows + height {
68                    return Some(visible_index);
69                }
70                consumed_rows += height;
71                if consumed_rows >= usize::from(area.height) {
72                    break;
73                }
74            }
75            return None;
76        }
77
78        let modal = self.modal_state()?;
79        let list = modal.list.as_ref()?;
80        let offset = list.list_state.offset();
81        let mut consumed_rows = 0usize;
82        for (visible_index, &item_index) in list.visible_indices.iter().enumerate().skip(offset) {
83            let lines = modal::modal_list_item_lines(
84                list,
85                visible_index,
86                item_index,
87                &styles,
88                content_width,
89                None,
90            );
91            let height = usize::from(inline_list::row_height(&lines));
92            if relative_row < consumed_rows + height {
93                return Some(visible_index);
94            }
95            consumed_rows += height;
96            if consumed_rows >= usize::from(area.height) {
97                break;
98            }
99        }
100
101        None
102    }
103
104    fn handle_active_overlay_click(
105        &mut self,
106        mouse_event: MouseEvent,
107        events: &UnboundedSender<InlineEvent>,
108        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
109    ) -> bool {
110        let column = mouse_event.column;
111        let row = mouse_event.row;
112        let in_modal_list = self.modal_list_area.is_some_and(|area| {
113            row >= area.y
114                && row < area.y.saturating_add(area.height)
115                && column >= area.x
116                && column < area.x.saturating_add(area.width)
117        });
118        if !in_modal_list {
119            return self.has_active_overlay();
120        }
121
122        let Some(visible_index) = self.modal_visible_index_at(row) else {
123            return true;
124        };
125
126        if let Some(wizard) = self.wizard_overlay_mut() {
127            let result = wizard.handle_mouse_click(visible_index);
128            return self.handle_modal_list_result(result, events, callback);
129        }
130
131        if let Some(modal) = self.modal_state_mut() {
132            let result = modal.handle_list_mouse_click(visible_index);
133            return self.handle_modal_list_result(result, events, callback);
134        }
135
136        true
137    }
138
139    fn handle_active_overlay_scroll(
140        &mut self,
141        mouse_event: MouseEvent,
142        down: bool,
143        events: &UnboundedSender<InlineEvent>,
144        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
145    ) -> bool {
146        if !self.has_active_overlay() {
147            return false;
148        }
149
150        let column = mouse_event.column;
151        let row = mouse_event.row;
152        let in_modal_list = self.modal_list_area.is_some_and(|area| {
153            row >= area.y
154                && row < area.y.saturating_add(area.height)
155                && column >= area.x
156                && column < area.x.saturating_add(area.width)
157        });
158
159        if !in_modal_list {
160            return true;
161        }
162
163        if let Some(wizard) = self.wizard_overlay_mut() {
164            let result = wizard.handle_mouse_scroll(down);
165            return self.handle_modal_list_result(result, events, callback);
166        }
167
168        if let Some(modal) = self.modal_state_mut() {
169            let result = modal.handle_list_mouse_scroll(down);
170            return self.handle_modal_list_result(result, events, callback);
171        }
172
173        true
174    }
175
176    fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
177        let _ = down;
178        false
179    }
180
181    fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
182        let _ = mouse_event;
183        false
184    }
185
186    pub fn handle_event(
187        &mut self,
188        event: CrosstermEvent,
189        events: &UnboundedSender<InlineEvent>,
190        callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
191    ) {
192        match event {
193            CrosstermEvent::Key(key) => {
194                // Only process Press events to avoid duplicate character insertion
195                // Repeat events can cause characters to be inserted multiple times
196                if matches!(key.kind, KeyEventKind::Press)
197                    && let Some(outbound) = events::process_key(self, key)
198                {
199                    self.emit_inline_event(&outbound, events, callback);
200                }
201            }
202            CrosstermEvent::Mouse(mouse_event) => match mouse_event.kind {
203                MouseEventKind::Moved => {
204                    if self.update_transcript_file_link_hover(mouse_event.column, mouse_event.row) {
205                        self.mark_dirty();
206                    }
207                }
208                MouseEventKind::ScrollDown => {
209                    if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
210                        && !self.handle_bottom_panel_scroll(true)
211                    {
212                        self.scroll_line_down();
213                        self.mark_dirty();
214                    }
215                }
216                MouseEventKind::ScrollUp => {
217                    if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
218                        && !self.handle_bottom_panel_scroll(false)
219                    {
220                        self.scroll_line_up();
221                        self.mark_dirty();
222                    }
223                }
224                MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
225                    if let Some(outbound) = self.transcript_file_link_event(
226                        mouse_event.column,
227                        mouse_event.row,
228                        mouse_event.modifiers,
229                    ) {
230                        self.mark_dirty();
231                        self.emit_inline_event(&outbound, events, callback);
232                        return;
233                    }
234
235                    if self.has_active_overlay()
236                        && self.handle_active_overlay_click(mouse_event, events, callback)
237                    {
238                        return;
239                    }
240
241                    if self.handle_bottom_panel_click(mouse_event) {
242                        return;
243                    }
244
245                    if self.handle_input_click(mouse_event) {
246                        self.mouse_drag_target = MouseDragTarget::Input;
247                        self.mouse_selection.clear();
248                        return;
249                    }
250
251                    self.mouse_drag_target = MouseDragTarget::Transcript;
252                    self.mouse_selection
253                        .start_selection(mouse_event.column, mouse_event.row);
254                    self.mark_dirty();
255                    self.handle_transcript_click(mouse_event);
256                }
257                MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
258                    match self.mouse_drag_target {
259                        MouseDragTarget::Input => {
260                            if let Some(cursor) = self
261                                .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
262                                && self.input_manager.cursor() != cursor
263                            {
264                                self.input_manager.set_cursor_with_selection(cursor);
265                                self.mark_dirty();
266                            }
267                        }
268                        MouseDragTarget::Transcript => {
269                            self.mouse_selection
270                                .update_selection(mouse_event.column, mouse_event.row);
271                            self.mark_dirty();
272                        }
273                        MouseDragTarget::None => {}
274                    }
275                }
276                MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
277                    match self.mouse_drag_target {
278                        MouseDragTarget::Input => {
279                            if let Some(cursor) = self
280                                .cursor_index_for_input_point(mouse_event.column, mouse_event.row)
281                                && self.input_manager.cursor() != cursor
282                            {
283                                self.input_manager.set_cursor_with_selection(cursor);
284                                self.mark_dirty();
285                            }
286                        }
287                        MouseDragTarget::Transcript => {
288                            self.mouse_selection
289                                .finish_selection(mouse_event.column, mouse_event.row);
290                            self.mark_dirty();
291                        }
292                        MouseDragTarget::None => {}
293                    }
294                    self.mouse_drag_target = MouseDragTarget::None;
295                }
296                _ => {}
297            },
298            CrosstermEvent::Paste(content) => {
299                events::handle_paste(self, &content);
300            }
301            CrosstermEvent::Resize(_, rows) => {
302                self.apply_view_rows(rows);
303                self.mark_dirty();
304            }
305            CrosstermEvent::FocusGained => {
306                // No-op: focus tracking is host/application concern.
307            }
308            CrosstermEvent::FocusLost => {
309                // No-op: focus tracking is host/application concern.
310            }
311        }
312    }
313
314    pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
315        if !matches!(
316            mouse_event.kind,
317            MouseEventKind::Down(crossterm::event::MouseButton::Left)
318        ) {
319            return false;
320        }
321
322        let Some(area) = self.transcript_area else {
323            return false;
324        };
325
326        if mouse_event.row < area.y
327            || mouse_event.row >= area.y.saturating_add(area.height)
328            || mouse_event.column < area.x
329            || mouse_event.column >= area.x.saturating_add(area.width)
330        {
331            return false;
332        }
333
334        if self.transcript_width == 0 || self.transcript_rows == 0 {
335            return false;
336        }
337
338        let row_in_view = (mouse_event.row - area.y) as usize;
339        if row_in_view >= self.transcript_rows as usize {
340            return false;
341        }
342
343        let viewport_rows = self.transcript_rows.max(1) as usize;
344        let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
345        let effective_padding = padding.min(viewport_rows.saturating_sub(1));
346        let total_rows = self.total_transcript_rows(self.transcript_width) + effective_padding;
347        let (top_offset, _clamped_total_rows) =
348            self.prepare_transcript_scroll(total_rows, viewport_rows);
349        let view_top = top_offset.min(self.scroll_manager.max_offset());
350        self.transcript_view_top = view_top;
351
352        let clicked_row = view_top.saturating_add(row_in_view);
353        let expanded = self.expand_collapsed_paste_at_row(self.transcript_width, clicked_row);
354        if expanded {
355            self.mark_dirty();
356        }
357        expanded
358    }
359
360    pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
361        if !matches!(
362            mouse_event.kind,
363            MouseEventKind::Down(crossterm::event::MouseButton::Left)
364        ) {
365            return false;
366        }
367
368        if !self.input_area_contains(mouse_event.column, mouse_event.row) {
369            return false;
370        }
371
372        let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
373        if self.input_compact_mode && cursor_at_end && self.input_compact_placeholder().is_some() {
374            self.input_compact_mode = false;
375            self.mark_dirty();
376            return true;
377        }
378
379        if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
380        {
381            if self.input_manager.cursor() != cursor {
382                self.input_manager.set_cursor(cursor);
383                self.mark_dirty();
384            }
385            return true;
386        }
387
388        false
389    }
390}