1use super::*;
11use crate::input::keybindings::Action;
12use crate::model::event::{ContainerId, CursorId, LeafId, SplitDirection};
13use crate::services::plugins::hooks::HookArgs;
14use crate::view::popup_mouse::{popup_areas_to_layout_info, PopupHitTester};
15use crate::view::prompt::PromptType;
16use crate::view::ui::tabs::TabHit;
17use anyhow::Result as AnyhowResult;
18use ratatui::layout::Rect;
19use rust_i18n::t;
20
21fn in_rect(col: u16, row: u16, rect: Rect) -> bool {
23 col >= rect.x && col < rect.x + rect.width && row >= rect.y && row < rect.y + rect.height
24}
25
26impl Editor {
27 pub fn handle_mouse(
30 &mut self,
31 mouse_event: crossterm::event::MouseEvent,
32 ) -> AnyhowResult<bool> {
33 use crossterm::event::{MouseButton, MouseEventKind};
34
35 let col = mouse_event.column;
36 let row = mouse_event.row;
37
38 let (is_double_click, is_triple_click) = self.detect_multi_click(&mouse_event, col, row);
39
40 if self.keybinding_editor.is_some() {
42 return self.handle_keybinding_editor_mouse(mouse_event);
43 }
44
45 if self.settings_state.as_ref().is_some_and(|s| s.visible) {
47 return self.handle_settings_mouse(mouse_event, is_double_click);
48 }
49
50 if self.calibration_wizard.is_some() {
52 return Ok(false);
53 }
54
55 if self.global_popups.top().is_some_and(|p| {
59 matches!(
60 p.resolver,
61 crate::view::popup::PopupResolver::WorkspaceTrust
62 )
63 }) {
64 return self.handle_workspace_trust_mouse(mouse_event);
65 }
66
67 let mut needs_render = false;
69 if let Some(ref prompt) = self.active_window_mut().prompt {
70 if matches!(prompt.prompt_type, PromptType::LspRename { .. }) {
71 self.cancel_prompt();
72 needs_render = true;
73 }
74 }
75
76 let cursor_moved = self.active_window_mut().mouse_cursor_position != Some((col, row));
79 self.active_window_mut().mouse_cursor_position = Some((col, row));
80 if self.active_window_mut().gpm_active && cursor_moved {
81 needs_render = true;
82 }
83
84 tracing::trace!(
85 "handle_mouse: kind={:?}, col={}, row={}",
86 mouse_event.kind,
87 col,
88 row
89 );
90
91 if let Some(result) =
94 self.active_window_mut()
95 .try_forward_mouse_to_terminal(col, row, mouse_event)
96 {
97 return result;
98 }
99
100 if self.active_window_mut().theme_info_popup.is_some() {
102 if let MouseEventKind::Down(MouseButton::Left) = mouse_event.kind {
103 if let Some((popup_rect, button_row_offset)) = self.theme_info_popup_rect() {
104 if in_rect(col, row, popup_rect) {
105 let actual_button_row = popup_rect.y + button_row_offset;
107 if row == actual_button_row {
108 let fg_key = self
109 .active_window_mut()
110 .theme_info_popup
111 .as_ref()
112 .and_then(|p| p.info.fg_key.clone());
113 self.active_window_mut().theme_info_popup = None;
114 if let Some(key) = fg_key {
115 self.fire_theme_inspect_hook(key);
116 }
117 return Ok(true);
118 }
119 return Ok(true);
121 }
122 }
123 self.active_window_mut().theme_info_popup = None;
125 needs_render = true;
126 }
127 }
128
129 match mouse_event.kind {
130 MouseEventKind::Down(MouseButton::Left) => {
131 if is_double_click || is_triple_click {
132 if let Some((buffer_id, byte_pos)) =
133 self.fold_toggle_line_at_screen_position(col, row)
134 {
135 self.active_window_mut()
136 .toggle_fold_at_byte(buffer_id, byte_pos);
137 needs_render = true;
138 return Ok(needs_render);
139 }
140 }
141 if is_triple_click {
142 self.handle_mouse_triple_click(col, row)?;
144 needs_render = true;
145 return Ok(needs_render);
146 }
147 if is_double_click {
148 self.handle_mouse_double_click(col, row)?;
150 needs_render = true;
151 return Ok(needs_render);
152 }
153 self.handle_mouse_click(col, row, mouse_event.modifiers)?;
154 needs_render = true;
155 }
156 MouseEventKind::Drag(MouseButton::Left) => {
157 self.handle_mouse_drag(col, row)?;
158 needs_render = true;
159 }
160 MouseEventKind::Up(MouseButton::Left) => {
161 let was_dragging_separator = self
163 .active_window_mut()
164 .mouse_state
165 .dragging_separator
166 .is_some();
167
168 if let Some(drag_state) = self.active_window_mut().mouse_state.dragging_tab.take() {
170 if drag_state.is_dragging() {
171 if let Some(drop_zone) = drag_state.drop_zone {
172 self.execute_tab_drop(
173 drag_state.buffer_id,
174 drag_state.source_split_id,
175 drop_zone,
176 );
177 }
178 }
179 }
180
181 self.release_widget_scrollbar();
183 self.active_window_mut().mouse_state.dragging_scrollbar = None;
184 self.active_window_mut().mouse_state.drag_start_row = None;
185 self.active_window_mut().mouse_state.drag_start_top_byte = None;
186 self.active_window_mut()
187 .mouse_state
188 .dragging_horizontal_scrollbar = None;
189 self.active_window_mut().mouse_state.drag_start_hcol = None;
190 self.active_window_mut().mouse_state.drag_start_left_column = None;
191 self.active_window_mut().mouse_state.dragging_separator = None;
192 self.active_window_mut().mouse_state.drag_start_position = None;
193 self.active_window_mut().mouse_state.drag_start_ratio = None;
194 self.active_window_mut().mouse_state.dragging_file_explorer = false;
195 self.active_window_mut()
196 .mouse_state
197 .drag_start_explorer_width = None;
198 self.active_window_mut().mouse_state.dragging_text_selection = false;
200 self.active_window_mut().mouse_state.drag_selection_split = None;
201 self.active_window_mut().mouse_state.drag_selection_anchor = None;
202 self.active_window_mut().mouse_state.drag_selection_by_words = false;
203 self.active_window_mut().mouse_state.drag_selection_word_end = None;
204 self.active_window_mut()
206 .mouse_state
207 .dragging_popup_scrollbar = None;
208 self.active_window_mut().mouse_state.drag_start_popup_scroll = None;
209 self.active_window_mut()
211 .mouse_state
212 .dragging_prompt_scrollbar = false;
213 self.active_window_mut().mouse_state.selecting_in_popup = None;
215
216 if was_dragging_separator {
218 self.active_window_mut().resize_visible_terminals();
219 }
220
221 needs_render = true;
222 }
223 MouseEventKind::Moved => {
224 {
226 let content_rect = self
228 .active_layout()
229 .split_areas
230 .iter()
231 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
232 .map(|(_, _, rect, _, _, _)| *rect);
233
234 let (content_x, content_y) = content_rect.map(|r| (r.x, r.y)).unwrap_or((0, 0));
235
236 self.plugin_manager.read().unwrap().run_hook(
237 "mouse_move",
238 HookArgs::MouseMove {
239 column: col,
240 row,
241 content_x,
242 content_y,
243 },
244 );
245 }
246
247 let hover_changed = self.update_hover_target(col, row);
250 needs_render = needs_render || hover_changed;
251
252 if let Some((popup_rect, button_row_offset)) = self.theme_info_popup_rect() {
254 let button_row = popup_rect.y + button_row_offset;
255 let new_highlighted = row == button_row
256 && col >= popup_rect.x
257 && col < popup_rect.x + popup_rect.width;
258 if let Some(ref mut popup) = self.active_window_mut().theme_info_popup {
259 if popup.button_highlighted != new_highlighted {
260 popup.button_highlighted = new_highlighted;
261 needs_render = true;
262 }
263 }
264 }
265
266 self.update_lsp_hover_state(col, row);
268 }
269 MouseEventKind::ScrollUp => {
270 self.handle_vertical_scroll(col, row, mouse_event.modifiers, -3)?;
271 needs_render = true;
272 }
273 MouseEventKind::ScrollDown => {
274 self.handle_vertical_scroll(col, row, mouse_event.modifiers, 3)?;
275 needs_render = true;
276 }
277 MouseEventKind::ScrollLeft => {
278 self.active_window_mut()
280 .handle_horizontal_scroll(col, row, -3)?;
281 needs_render = true;
282 }
283 MouseEventKind::ScrollRight => {
284 self.active_window_mut()
286 .handle_horizontal_scroll(col, row, 3)?;
287 needs_render = true;
288 }
289 MouseEventKind::Down(MouseButton::Right) => {
290 if self.overlay_prompt_active() {
294 needs_render = true;
295 } else if mouse_event
296 .modifiers
297 .contains(crossterm::event::KeyModifiers::CONTROL)
298 {
299 self.show_theme_info_popup(col, row)?;
301 needs_render = true;
302 } else {
303 self.handle_right_click(col, row)?;
305 needs_render = true;
306 }
307 }
308 _ => {
309 }
311 }
312
313 self.active_window_mut().mouse_state.last_position = Some((col, row));
314 Ok(needs_render)
315 }
316
317 fn detect_multi_click(
319 &mut self,
320 mouse_event: &crossterm::event::MouseEvent,
321 col: u16,
322 row: u16,
323 ) -> (bool, bool) {
324 use crossterm::event::{MouseButton, MouseEventKind};
325 if !matches!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left)) {
326 return (false, false);
327 }
328 let now = self.time_source.now();
329 let threshold = std::time::Duration::from_millis(self.config.editor.double_click_time_ms);
330 let is_consecutive = if let (Some(prev_time), Some(prev_pos)) = (
331 self.active_window_mut().previous_click_time,
332 self.active_window_mut().previous_click_position,
333 ) {
334 now.duration_since(prev_time) < threshold && prev_pos == (col, row)
335 } else {
336 false
337 };
338 if is_consecutive {
339 self.active_window_mut().click_count += 1;
340 } else {
341 self.active_window_mut().click_count = 1;
342 }
343 self.active_window_mut().previous_click_time = Some(now);
344 self.active_window_mut().previous_click_position = Some((col, row));
345 let is_triple = self.active_window_mut().click_count >= 3;
346 let is_double = self.active_window_mut().click_count == 2;
347 if is_triple {
348 self.active_window_mut().click_count = 0;
349 self.active_window_mut().previous_click_time = None;
350 self.active_window_mut().previous_click_position = None;
351 }
352 (is_double, is_triple)
353 }
354
355 fn handle_vertical_scroll(
358 &mut self,
359 col: u16,
360 row: u16,
361 modifiers: crossterm::event::KeyModifiers,
362 delta: i32,
363 ) -> AnyhowResult<()> {
364 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
365 self.active_window_mut()
366 .handle_horizontal_scroll(col, row, delta)?;
367 } else if self.handle_overlay_prompt_scroll(col, row, delta) {
368 } else if self.handle_prompt_scroll(delta) {
372 } else if self.is_file_open_active()
374 && self.is_mouse_over_file_browser(col, row)
375 && self.handle_file_open_scroll(delta)
376 {
377 } else if self.is_mouse_over_any_popup(col, row) {
379 self.scroll_popup(delta);
380 } else if self.floating_widget_panel.is_some() {
381 self.handle_floating_widget_panel_wheel(col, row, delta);
388 } else if self
389 .active_window()
390 .split_at_position(col, row)
391 .map(|(_, buffer_id)| self.handle_widget_panel_wheel(buffer_id, delta))
392 .unwrap_or(false)
393 {
394 } else {
396 if self.active_window().terminal_mode
397 && self
398 .active_window()
399 .is_terminal_buffer(self.active_buffer())
400 {
401 {
402 let __b = self.active_buffer();
403 self.active_window_mut().sync_terminal_to_buffer(__b);
404 };
405 self.active_window_mut().terminal_mode = false;
406 self.active_window_mut().key_context =
407 crate::input::keybindings::KeyContext::Normal;
408 }
409 self.dismiss_transient_popups();
410 self.active_window_mut()
411 .handle_mouse_scroll(col, row, delta)?;
412 }
413 Ok(())
414 }
415
416 fn handle_overlay_prompt_scroll(&mut self, col: u16, row: u16, delta: i32) -> bool {
427 if !self.overlay_prompt_active() {
428 return false;
429 }
430 let preview_area = self.active_chrome().prompt_preview_area;
431 let results_visible = self
432 .active_chrome()
433 .prompt_results_area
434 .map(|r| r.height as usize)
435 .unwrap_or(0);
436 if let Some(preview) = preview_area {
437 if in_rect(col, row, preview) {
438 self.active_window_mut()
439 .scroll_overlay_preview_by_lines(delta);
440 return true;
441 }
442 }
443 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
444 prompt.scroll_results(delta, results_visible);
445 }
446 true
447 }
448
449 pub(super) fn update_hover_target(&mut self, col: u16, row: u16) -> bool {
452 let old_target = self.active_window_mut().mouse_state.hover_target.clone();
453 let new_target = self.compute_hover_target(col, row);
454 let changed = old_target != new_target;
455 self.active_window_mut().mouse_state.hover_target = new_target.clone();
456
457 if let Some(active_menu_idx) = self.menu_state.active_menu {
460 let all_menus: Vec<crate::config::Menu> = self
461 .menus
462 .menus
463 .iter()
464 .chain(self.menu_state.plugin_menus.iter())
465 .cloned()
466 .collect();
467 if let Some(HoverTarget::MenuBarItem(hovered_menu_idx)) = new_target.clone() {
468 if hovered_menu_idx != active_menu_idx {
469 self.menu_state.open_menu(hovered_menu_idx);
470 return true; }
472 }
473
474 if let Some(HoverTarget::MenuDropdownItem(_, item_idx)) = new_target.clone() {
476 if self.menu_state.submenu_path.first() == Some(&item_idx) {
479 tracing::trace!(
480 "menu hover: staying on submenu parent item_idx={}, submenu_path={:?}",
481 item_idx,
482 self.menu_state.submenu_path
483 );
484 return changed;
485 }
486
487 if !self.menu_state.submenu_path.is_empty() {
489 tracing::trace!(
490 "menu hover: clearing submenu_path={:?} for different item_idx={}",
491 self.menu_state.submenu_path,
492 item_idx
493 );
494 self.menu_state.submenu_path.clear();
495 self.menu_state.highlighted_item = Some(item_idx);
496 return true;
497 }
498
499 if let Some(menu) = all_menus.get(active_menu_idx) {
501 if let Some(crate::config::MenuItem::Submenu { items, .. }) =
502 menu.items.get(item_idx)
503 {
504 if !items.is_empty() {
505 tracing::trace!("menu hover: opening submenu at item_idx={}", item_idx);
506 self.menu_state.submenu_path.push(item_idx);
507 self.menu_state.highlighted_item = Some(0);
508 return true;
509 }
510 }
511 }
512 if self.menu_state.highlighted_item != Some(item_idx) {
514 self.menu_state.highlighted_item = Some(item_idx);
515 return true;
516 }
517 }
518
519 if let Some(HoverTarget::SubmenuItem(depth, item_idx)) = new_target {
521 if self.menu_state.submenu_path.len() > depth
525 && self.menu_state.submenu_path.get(depth) == Some(&item_idx)
526 {
527 tracing::trace!(
528 "menu hover: staying on nested submenu parent depth={}, item_idx={}, submenu_path={:?}",
529 depth,
530 item_idx,
531 self.menu_state.submenu_path
532 );
533 return changed;
534 }
535
536 if self.menu_state.submenu_path.len() > depth {
538 tracing::trace!(
539 "menu hover: truncating submenu_path={:?} to depth={} for item_idx={}",
540 self.menu_state.submenu_path,
541 depth,
542 item_idx
543 );
544 self.menu_state.submenu_path.truncate(depth);
545 }
546
547 if let Some(items) = self
549 .menu_state
550 .get_current_items(&all_menus, active_menu_idx)
551 {
552 if let Some(crate::config::MenuItem::Submenu {
554 items: sub_items, ..
555 }) = items.get(item_idx)
556 {
557 if !sub_items.is_empty()
558 && !self.menu_state.submenu_path.contains(&item_idx)
559 {
560 tracing::trace!(
561 "menu hover: opening nested submenu at depth={}, item_idx={}",
562 depth,
563 item_idx
564 );
565 self.menu_state.submenu_path.push(item_idx);
566 self.menu_state.highlighted_item = Some(0);
567 return true;
568 }
569 }
570 if self.menu_state.highlighted_item != Some(item_idx) {
572 self.menu_state.highlighted_item = Some(item_idx);
573 return true;
574 }
575 }
576 }
577 }
578
579 if let Some(HoverTarget::TabContextMenuItem(item_idx)) = new_target.clone() {
581 if let Some(ref mut menu) = self.active_window_mut().tab_context_menu {
582 if menu.highlighted != item_idx {
583 menu.highlighted = item_idx;
584 return true;
585 }
586 }
587 }
588
589 if let Some(&HoverTarget::FileExplorerContextMenuItem(item_idx)) = new_target.as_ref() {
590 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
591 if menu.highlighted != item_idx {
592 menu.highlighted = item_idx;
593 return true;
594 }
595 }
596 }
597
598 if old_target != new_target
601 && matches!(
602 old_target,
603 Some(HoverTarget::FileExplorerStatusIndicator(_))
604 )
605 {
606 self.dismiss_file_explorer_status_tooltip();
607 }
608
609 if let Some(HoverTarget::FileExplorerStatusIndicator(ref path)) = new_target {
610 if old_target != new_target {
612 self.show_file_explorer_status_tooltip(path.clone(), col, row);
613 return true;
614 }
615 }
616
617 changed
618 }
619
620 fn update_lsp_hover_state(&mut self, col: u16, row: u16) {
629 tracing::trace!(col, row, "update_lsp_hover_state: raw mouse position");
630
631 if self.active_window_mut().theme_info_popup.is_some()
634 || self.active_window_mut().tab_context_menu.is_some()
635 || self
636 .active_window_mut()
637 .file_explorer_context_menu
638 .is_some()
639 {
640 if self
641 .active_window_mut()
642 .mouse_state
643 .lsp_hover_state
644 .is_some()
645 {
646 self.active_window_mut().mouse_state.lsp_hover_state = None;
647 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
648 self.dismiss_transient_popups();
649 }
650 return;
651 }
652
653 if self.is_mouse_over_transient_popup(col, row) {
655 return;
656 }
657
658 let split_info = self
660 .active_layout()
661 .split_areas
662 .iter()
663 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
664 .map(|(split_id, buffer_id, content_rect, _, _, _)| {
665 (*split_id, *buffer_id, *content_rect)
666 });
667
668 let Some((split_id, buffer_id, content_rect)) = split_info else {
669 if self
671 .active_window_mut()
672 .mouse_state
673 .lsp_hover_state
674 .is_some()
675 {
676 self.active_window_mut().mouse_state.lsp_hover_state = None;
677 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
678 self.dismiss_transient_popups();
679 }
680 return;
681 };
682
683 let cached_mappings = self
685 .active_layout()
686 .view_line_mappings
687 .get(&split_id)
688 .cloned();
689 let gutter_width = self
690 .buffers()
691 .get(&buffer_id)
692 .map(|s| s.margins.left_total_width() as u16)
693 .unwrap_or(0);
694 let fallback = self
695 .buffers()
696 .get(&buffer_id)
697 .map(|s| s.buffer.len())
698 .unwrap_or(0);
699
700 let compose_width = self
702 .windows
703 .get(&self.active_window)
704 .and_then(|w| w.buffers.splits())
705 .map(|(_, vs)| vs)
706 .expect("active window must have a populated split layout")
707 .get(&split_id)
708 .and_then(|vs| vs.compose_width);
709
710 let Some(byte_pos) = super::click_geometry::screen_to_buffer_position(
712 col,
713 row,
714 content_rect,
715 gutter_width,
716 &cached_mappings,
717 fallback,
718 false, compose_width,
720 ) else {
721 if self
725 .active_window_mut()
726 .mouse_state
727 .lsp_hover_state
728 .is_some()
729 {
730 self.active_window_mut().mouse_state.lsp_hover_state = None;
731 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
732 }
733 return;
734 };
735
736 let content_col = col.saturating_sub(content_rect.x);
738 let text_col = content_col.saturating_sub(gutter_width) as usize;
739 let visual_row = row.saturating_sub(content_rect.y) as usize;
740
741 let line_info = cached_mappings
742 .as_ref()
743 .and_then(|mappings| mappings.get(visual_row))
744 .map(|line_mapping| {
745 (
746 line_mapping.visual_to_char.len(),
747 line_mapping.line_end_byte,
748 )
749 });
750
751 let is_past_line_end_or_empty = line_info
752 .map(|(line_len, _)| {
753 if line_len <= 1 {
755 return true;
756 }
757 text_col >= line_len
758 })
759 .unwrap_or(true);
761
762 tracing::trace!(
763 col,
764 row,
765 content_col,
766 text_col,
767 visual_row,
768 gutter_width,
769 byte_pos,
770 ?line_info,
771 is_past_line_end_or_empty,
772 "update_lsp_hover_state: position check"
773 );
774
775 if is_past_line_end_or_empty {
776 tracing::trace!(
777 "update_lsp_hover_state: mouse past line end or empty line, clearing hover"
778 );
779 if self
784 .active_window_mut()
785 .mouse_state
786 .lsp_hover_state
787 .is_some()
788 {
789 self.active_window_mut().mouse_state.lsp_hover_state = None;
790 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
791 }
792 return;
793 }
794
795 if let Some((start, end)) = self.active_window_mut().hover.symbol_range() {
797 if byte_pos >= start && byte_pos < end {
798 return;
800 }
801 }
802
803 if let Some((old_pos, _, _, _)) = self.active_window_mut().mouse_state.lsp_hover_state {
805 if old_pos == byte_pos {
806 return;
808 }
809 }
815
816 self.active_window_mut().mouse_state.lsp_hover_state =
818 Some((byte_pos, std::time::Instant::now(), col, row));
819 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
820 }
821
822 fn is_mouse_over_transient_popup(&self, col: u16, row: u16) -> bool {
824 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
825 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
826 hit_tester.is_over_transient_popup(col, row)
827 }
828
829 fn is_mouse_over_any_popup(&self, col: u16, row: u16) -> bool {
831 for (_, popup_area, _, _, _) in &self.active_chrome().global_popup_areas {
834 if in_rect(col, row, *popup_area) {
835 return true;
836 }
837 }
838 if let Some(outer) = self.active_chrome().suggestions_outer_area {
842 if in_rect(col, row, outer) {
843 return true;
844 }
845 }
846 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
847 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
848 hit_tester.is_over_popup(col, row)
849 }
850
851 fn is_mouse_over_file_browser(&self, col: u16, row: u16) -> bool {
853 self.active_window()
854 .file_browser_layout
855 .as_ref()
856 .is_some_and(|layout| layout.contains(col, row))
857 }
858
859 fn compute_hover_target(&self, col: u16, row: u16) -> Option<HoverTarget> {
864 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
865 let (menu_x, menu_y) = menu.clamped_position(
866 self.active_chrome().last_frame_width,
867 self.active_chrome().last_frame_height,
868 );
869 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
870 let menu_height = menu.height();
871
872 if col >= menu_x
873 && col < menu_x + menu_width
874 && row > menu_y
875 && row < menu_y + menu_height - 1
876 {
877 let item_idx = (row - menu_y - 1) as usize;
878 if item_idx < menu.items().len() {
879 return Some(HoverTarget::FileExplorerContextMenuItem(item_idx));
880 }
881 }
882 }
883
884 if let Some(ref menu) = self.active_window().tab_context_menu {
886 let menu_x = menu.position.0;
887 let menu_y = menu.position.1;
888 let menu_width = 22u16;
889 let items = super::types::TabContextMenuItem::all();
890 let menu_height = items.len() as u16 + 2;
891
892 if col >= menu_x
893 && col < menu_x + menu_width
894 && row > menu_y
895 && row < menu_y + menu_height - 1
896 {
897 let item_idx = (row - menu_y - 1) as usize;
898 if item_idx < items.len() {
899 return Some(HoverTarget::TabContextMenuItem(item_idx));
900 }
901 }
902 }
903
904 if let Some((inner_rect, start_idx, _visible_count, total_count)) =
906 &self.active_chrome().suggestions_area
907 {
908 if in_rect(col, row, *inner_rect) {
909 let relative_row = (row - inner_rect.y) as usize;
910 let item_idx = start_idx + relative_row;
911
912 if item_idx < *total_count {
913 return Some(HoverTarget::SuggestionItem(item_idx));
914 }
915 }
916 }
917
918 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
921 self.active_chrome().popup_areas.iter().rev()
922 {
923 if in_rect(col, row, *inner_rect) && *num_items > 0 {
924 let relative_row = (row - inner_rect.y) as usize;
926 let item_idx = scroll_offset + relative_row;
927
928 if item_idx < *num_items {
929 return Some(HoverTarget::PopupListItem(*popup_idx, item_idx));
930 }
931 }
932 }
933
934 if self.is_file_open_active() {
936 if let Some(hover) = self.compute_file_browser_hover(col, row) {
937 return Some(hover);
938 }
939 }
940
941 if self.active_window().menu_bar_visible {
944 if let Some(ref menu_layout) = self.active_chrome().menu_layout {
945 if let Some(menu_idx) = menu_layout.menu_at(col, row) {
946 return Some(HoverTarget::MenuBarItem(menu_idx));
947 }
948 }
949 }
950
951 if let Some(active_idx) = self.menu_state.active_menu {
953 if let Some(hover) = self.compute_menu_dropdown_hover(col, row, active_idx) {
954 return Some(hover);
955 }
956 }
957
958 if let Some(explorer_area) = self.active_layout().file_explorer_area {
960 let close_button_x = explorer_area.x + explorer_area.width.saturating_sub(3);
962 if row == explorer_area.y
963 && col >= close_button_x
964 && col < explorer_area.x + explorer_area.width
965 {
966 return Some(HoverTarget::FileExplorerCloseButton);
967 }
968
969 let content_start_y = explorer_area.y + 1; let content_end_y = explorer_area.y + explorer_area.height.saturating_sub(1); let status_indicator_x = explorer_area.x + explorer_area.width.saturating_sub(3); if row >= content_start_y
976 && row < content_end_y
977 && col >= status_indicator_x
978 && col < explorer_area.x + explorer_area.width.saturating_sub(1)
979 {
980 if let Some(explorer) = self.file_explorer().as_ref() {
982 let relative_row = row.saturating_sub(content_start_y) as usize;
983 let scroll_offset = explorer.get_scroll_offset();
984 let item_index = relative_row + scroll_offset;
985 let display_nodes = explorer.get_display_nodes();
986
987 if item_index < display_nodes.len() {
988 let (node_id, _indent) = display_nodes[item_index];
989 if let Some(node) = explorer.tree().get_node(node_id) {
990 return Some(HoverTarget::FileExplorerStatusIndicator(
991 node.entry.path.clone(),
992 ));
993 }
994 }
995 }
996 }
997
998 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1001 if col == border_x
1002 && row >= explorer_area.y
1003 && row < explorer_area.y + explorer_area.height
1004 {
1005 return Some(HoverTarget::FileExplorerBorder);
1006 }
1007 }
1008
1009 for (split_id, direction, sep_x, sep_y, sep_length) in &self.active_layout().separator_areas
1011 {
1012 let is_on_separator = match direction {
1013 SplitDirection::Horizontal => {
1014 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
1015 }
1016 SplitDirection::Vertical => {
1017 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
1018 }
1019 };
1020
1021 if is_on_separator {
1022 return Some(HoverTarget::SplitSeparator(*split_id, *direction));
1023 }
1024 }
1025
1026 for (split_id, btn_row, start_col, end_col) in &self.active_layout().close_split_areas {
1029 if row == *btn_row && col >= *start_col && col < *end_col {
1030 return Some(HoverTarget::CloseSplitButton(*split_id));
1031 }
1032 }
1033
1034 for (split_id, btn_row, start_col, end_col) in &self.active_layout().maximize_split_areas {
1035 if row == *btn_row && col >= *start_col && col < *end_col {
1036 return Some(HoverTarget::MaximizeSplitButton(*split_id));
1037 }
1038 }
1039
1040 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
1041 match tab_layout.hit_test(col, row) {
1042 Some(TabHit::CloseButton(target)) => {
1043 return Some(HoverTarget::TabCloseButton(target, *split_id));
1044 }
1045 Some(TabHit::TabName(target)) => {
1046 return Some(HoverTarget::TabName(target, *split_id));
1047 }
1048 Some(TabHit::ScrollLeft)
1049 | Some(TabHit::ScrollRight)
1050 | Some(TabHit::BarBackground)
1051 | None => {}
1052 }
1053 }
1054
1055 for (split_id, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1057 &self.active_layout().split_areas
1058 {
1059 if in_rect(col, row, *scrollbar_rect) {
1060 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
1061 let is_on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
1062
1063 if is_on_thumb {
1064 return Some(HoverTarget::ScrollbarThumb(*split_id));
1065 } else {
1066 return Some(HoverTarget::ScrollbarTrack(*split_id, relative_row as u16));
1067 }
1068 }
1069 }
1070
1071 if let Some((status_row, _status_x, _status_width)) = self.active_chrome().status_bar_area {
1073 if row == status_row {
1074 let indicators = [
1075 (
1076 self.active_chrome().status_bar_line_ending_area,
1077 HoverTarget::StatusBarLineEndingIndicator,
1078 ),
1079 (
1080 self.active_chrome().status_bar_encoding_area,
1081 HoverTarget::StatusBarEncodingIndicator,
1082 ),
1083 (
1084 self.active_chrome().status_bar_language_area,
1085 HoverTarget::StatusBarLanguageIndicator,
1086 ),
1087 (
1088 self.active_chrome().status_bar_lsp_area,
1089 HoverTarget::StatusBarLspIndicator,
1090 ),
1091 (
1092 self.active_chrome().status_bar_remote_area,
1093 HoverTarget::StatusBarRemoteIndicator,
1094 ),
1095 (
1096 self.active_chrome().status_bar_warning_area,
1097 HoverTarget::StatusBarWarningBadge,
1098 ),
1099 ];
1100 for (area, target) in indicators {
1101 if let Some((indicator_row, start, end)) = area {
1102 if row == indicator_row && col >= start && col < end {
1103 return Some(target);
1104 }
1105 }
1106 }
1107 }
1108 }
1109
1110 if let Some(ref layout) = self.active_chrome().search_options_layout {
1112 use crate::view::ui::status_bar::SearchOptionsHover;
1113 if let Some(hover) = layout.checkbox_at(col, row) {
1114 return Some(match hover {
1115 SearchOptionsHover::CaseSensitive => HoverTarget::SearchOptionCaseSensitive,
1116 SearchOptionsHover::WholeWord => HoverTarget::SearchOptionWholeWord,
1117 SearchOptionsHover::Regex => HoverTarget::SearchOptionRegex,
1118 SearchOptionsHover::ConfirmEach => HoverTarget::SearchOptionConfirmEach,
1119 SearchOptionsHover::None => return None,
1120 });
1121 }
1122 }
1123
1124 None
1126 }
1127
1128 pub(super) fn handle_mouse_double_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1131 tracing::debug!("handle_mouse_double_click at col={}, row={}", col, row);
1132
1133 if let Some(r) = self.handle_click_suggestions_confirm(col, row) {
1137 return r;
1138 }
1139
1140 if self.overlay_prompt_active() {
1143 return Ok(());
1144 }
1145
1146 if self.is_mouse_over_any_popup(col, row) {
1148 return Ok(());
1150 } else {
1151 self.dismiss_transient_popups();
1153 }
1154
1155 if self.handle_file_open_double_click(col, row) {
1157 return Ok(());
1158 }
1159
1160 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1162 if col >= explorer_area.x
1163 && col < explorer_area.x + explorer_area.width
1164 && row > explorer_area.y && row < explorer_area.y + explorer_area.height
1166 {
1167 self.file_explorer_open_file()?;
1169 return Ok(());
1170 }
1171 }
1172
1173 let split_areas = self.active_layout().split_areas.clone();
1175 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1176 &split_areas
1177 {
1178 if in_rect(col, row, *content_rect) {
1179 if self.active_window().is_terminal_buffer(*buffer_id) {
1181 self.active_window_mut().key_context =
1182 crate::input::keybindings::KeyContext::Terminal;
1183 return Ok(());
1185 }
1186
1187 self.active_window_mut().key_context =
1188 crate::input::keybindings::KeyContext::Normal;
1189
1190 self.handle_editor_double_click(col, row, *split_id, *buffer_id, *content_rect)?;
1192 return Ok(());
1193 }
1194 }
1195
1196 Ok(())
1197 }
1198
1199 fn handle_editor_double_click(
1201 &mut self,
1202 col: u16,
1203 row: u16,
1204 split_id: LeafId,
1205 buffer_id: BufferId,
1206 content_rect: ratatui::layout::Rect,
1207 ) -> AnyhowResult<()> {
1208 use crate::model::event::Event;
1209
1210 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1214 return Ok(());
1215 }
1216
1217 self.focus_split(split_id, buffer_id);
1219
1220 let cached_mappings = self
1222 .active_layout()
1223 .view_line_mappings
1224 .get(&split_id)
1225 .cloned();
1226
1227 let leaf_id = split_id;
1229 let fallback = self
1230 .windows
1231 .get(&self.active_window)
1232 .and_then(|w| w.buffers.splits())
1233 .map(|(_, vs)| vs)
1234 .expect("active window must have a populated split layout")
1235 .get(&leaf_id)
1236 .map(|vs| vs.viewport.top_byte)
1237 .unwrap_or(0);
1238
1239 let compose_width = self
1241 .windows
1242 .get(&self.active_window)
1243 .and_then(|w| w.buffers.splits())
1244 .map(|(_, vs)| vs)
1245 .expect("active window must have a populated split layout")
1246 .get(&leaf_id)
1247 .and_then(|vs| vs.compose_width);
1248
1249 let gutter_width = self
1253 .active_window()
1254 .buffers
1255 .get(&buffer_id)
1256 .map(|s| s.margins.left_total_width() as u16)
1257 .unwrap_or(0);
1258
1259 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1260 col,
1261 row,
1262 content_rect,
1263 gutter_width,
1264 &cached_mappings,
1265 fallback,
1266 true, compose_width,
1268 ) else {
1269 return Ok(());
1270 };
1271
1272 let primary_cursor_id = self
1273 .active_window()
1274 .buffers
1275 .splits()
1276 .and_then(|(_, vs)| vs.get(&leaf_id))
1277 .map(|vs| vs.cursors.primary_id())
1278 .unwrap_or(CursorId(0));
1279 let event = Event::MoveCursor {
1280 cursor_id: primary_cursor_id,
1281 old_position: 0,
1282 new_position: target_position,
1283 old_anchor: None,
1284 new_anchor: None,
1285 old_sticky_column: 0,
1286 new_sticky_column: 0,
1287 };
1288
1289 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1290 event_log.append(event.clone());
1291 }
1292 self.active_window_mut()
1293 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1294
1295 self.handle_action(Action::SelectWord)?;
1297
1298 if let Some(cursor) = self
1300 .windows
1301 .get(&self.active_window)
1302 .and_then(|w| w.buffers.splits())
1303 .map(|(_, vs)| vs)
1304 .expect("active window must have a populated split layout")
1305 .get(&leaf_id)
1306 .map(|vs| vs.cursors.primary())
1307 {
1308 let sel_start = cursor.selection_start();
1311 let sel_end = cursor.selection_end();
1312 self.active_window_mut().mouse_state.dragging_text_selection = true;
1313 self.active_window_mut().mouse_state.drag_selection_split = Some(split_id);
1314 self.active_window_mut().mouse_state.drag_selection_anchor = Some(sel_start);
1315 self.active_window_mut().mouse_state.drag_selection_by_words = true;
1316 self.active_window_mut().mouse_state.drag_selection_word_end = Some(sel_end);
1317 }
1318
1319 Ok(())
1320 }
1321 pub(super) fn handle_mouse_triple_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1324 tracing::debug!("handle_mouse_triple_click at col={}, row={}", col, row);
1325
1326 if self.overlay_prompt_active() {
1329 return Ok(());
1330 }
1331
1332 if self.is_mouse_over_any_popup(col, row) {
1334 return Ok(());
1335 } else {
1336 self.dismiss_transient_popups();
1337 }
1338
1339 let split_areas = self.active_layout().split_areas.clone();
1341 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1342 &split_areas
1343 {
1344 if in_rect(col, row, *content_rect) {
1345 if self.active_window().is_terminal_buffer(*buffer_id) {
1346 return Ok(());
1347 }
1348
1349 self.active_window_mut().key_context =
1350 crate::input::keybindings::KeyContext::Normal;
1351
1352 self.handle_editor_triple_click(col, row, *split_id, *buffer_id, *content_rect)?;
1355 return Ok(());
1356 }
1357 }
1358
1359 Ok(())
1360 }
1361
1362 fn handle_editor_triple_click(
1364 &mut self,
1365 col: u16,
1366 row: u16,
1367 split_id: LeafId,
1368 buffer_id: BufferId,
1369 content_rect: ratatui::layout::Rect,
1370 ) -> AnyhowResult<()> {
1371 use crate::model::event::Event;
1372
1373 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1374 return Ok(());
1375 }
1376
1377 self.focus_split(split_id, buffer_id);
1379
1380 let cached_mappings = self
1382 .active_layout()
1383 .view_line_mappings
1384 .get(&split_id)
1385 .cloned();
1386
1387 let leaf_id = split_id;
1388 let fallback = self
1389 .windows
1390 .get(&self.active_window)
1391 .and_then(|w| w.buffers.splits())
1392 .map(|(_, vs)| vs)
1393 .expect("active window must have a populated split layout")
1394 .get(&leaf_id)
1395 .map(|vs| vs.viewport.top_byte)
1396 .unwrap_or(0);
1397
1398 let compose_width = self
1400 .windows
1401 .get(&self.active_window)
1402 .and_then(|w| w.buffers.splits())
1403 .map(|(_, vs)| vs)
1404 .expect("active window must have a populated split layout")
1405 .get(&leaf_id)
1406 .and_then(|vs| vs.compose_width);
1407
1408 let gutter_width = self
1412 .active_window()
1413 .buffers
1414 .get(&buffer_id)
1415 .map(|s| s.margins.left_total_width() as u16)
1416 .unwrap_or(0);
1417
1418 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1419 col,
1420 row,
1421 content_rect,
1422 gutter_width,
1423 &cached_mappings,
1424 fallback,
1425 true,
1426 compose_width,
1427 ) else {
1428 return Ok(());
1429 };
1430
1431 let primary_cursor_id = self
1432 .active_window()
1433 .buffers
1434 .splits()
1435 .and_then(|(_, vs)| vs.get(&leaf_id))
1436 .map(|vs| vs.cursors.primary_id())
1437 .unwrap_or(CursorId(0));
1438 let event = Event::MoveCursor {
1439 cursor_id: primary_cursor_id,
1440 old_position: 0,
1441 new_position: target_position,
1442 old_anchor: None,
1443 new_anchor: None,
1444 old_sticky_column: 0,
1445 new_sticky_column: 0,
1446 };
1447
1448 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1449 event_log.append(event.clone());
1450 }
1451 self.active_window_mut()
1452 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1453
1454 self.handle_action(Action::SelectLine)?;
1456
1457 Ok(())
1458 }
1459
1460 pub(super) fn overlay_prompt_active(&self) -> bool {
1468 self.active_window()
1469 .prompt
1470 .as_ref()
1471 .is_some_and(|p| p.overlay)
1472 }
1473
1474 pub(super) fn handle_mouse_click(
1475 &mut self,
1476 col: u16,
1477 row: u16,
1478 modifiers: crossterm::event::KeyModifiers,
1479 ) -> AnyhowResult<()> {
1480 if self.floating_widget_panel.is_some() {
1486 self.handle_floating_widget_click(col, row);
1487 return Ok(());
1488 }
1489 if let Some(r) = self.handle_click_context_menus(col, row) {
1490 return r;
1491 }
1492 if !self.is_mouse_over_any_popup(col, row) {
1493 self.dismiss_transient_popups();
1494 }
1495 if let Some(r) = self.handle_click_suggestions(col, row) {
1496 return r;
1497 }
1498 if let Some(r) = self.handle_click_prompt_scrollbar(col, row) {
1499 return r;
1500 }
1501 if let Some(r) = self.handle_click_popup_scrollbar(col, row) {
1502 return r;
1503 }
1504 if let Some(r) = self.handle_click_global_popups(col, row) {
1505 return r;
1506 }
1507 if let Some(r) = self.handle_click_buffer_popups(col, row) {
1508 return r;
1509 }
1510 if self.is_mouse_over_any_popup(col, row) {
1511 return Ok(());
1512 }
1513 if self.is_file_open_active() && self.handle_file_open_click(col, row) {
1514 return Ok(());
1515 }
1516 if let Some(r) = self.handle_click_menu_bar(col, row) {
1517 return r;
1518 }
1519 if let Some(r) = self.handle_click_file_explorer_area(col, row) {
1520 return r;
1521 }
1522 if let Some(r) = self.handle_click_scrollbar(col, row) {
1523 return r;
1524 }
1525 if let Some(r) = self.handle_click_horizontal_scrollbar(col, row) {
1526 return r;
1527 }
1528 if let Some(r) = self.handle_click_status_bar(col, row) {
1529 return r;
1530 }
1531 if let Some(r) = self.handle_click_search_options(col, row) {
1532 return r;
1533 }
1534 if let Some(r) = self.handle_click_split_separator(col, row) {
1535 return r;
1536 }
1537 if let Some(r) = self.handle_click_split_controls(col, row) {
1538 return r;
1539 }
1540 if let Some(r) = self.handle_click_tab_bar(col, row) {
1541 return r;
1542 }
1543
1544 if self.overlay_prompt_active() {
1551 let hit = self
1552 .active_chrome()
1553 .prompt_toolbar_hits
1554 .iter()
1555 .find(|(_, r)| in_rect(col, row, *r))
1556 .map(|(k, _)| k.clone());
1557 if let Some(widget_key) = hit {
1558 if let Some(p) = self.active_window_mut().prompt.as_mut() {
1562 p.toolbar_focus = Some(widget_key.clone());
1563 }
1564 self.toggle_overlay_toolbar_widget(&widget_key);
1565 }
1566 return Ok(());
1567 }
1568
1569 tracing::debug!(
1571 "handle_mouse_click: checking {} split_areas for click at ({}, {})",
1572 self.active_layout().split_areas.len(),
1573 col,
1574 row
1575 );
1576 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1577 &self.active_layout().split_areas
1578 {
1579 tracing::debug!(
1580 " split_id={:?}, content_rect=({}, {}, {}x{})",
1581 split_id,
1582 content_rect.x,
1583 content_rect.y,
1584 content_rect.width,
1585 content_rect.height
1586 );
1587 if in_rect(col, row, *content_rect) {
1588 tracing::debug!(" -> HIT! calling handle_editor_click");
1590 self.handle_editor_click(
1591 col,
1592 row,
1593 *split_id,
1594 *buffer_id,
1595 *content_rect,
1596 modifiers,
1597 )?;
1598 return Ok(());
1599 }
1600 }
1601 tracing::debug!(" -> No split area hit");
1602
1603 Ok(())
1604 }
1605
1606 fn handle_click_context_menus(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1610 if self
1611 .active_window_mut()
1612 .file_explorer_context_menu
1613 .is_some()
1614 {
1615 if let Some(result) = self.handle_file_explorer_context_menu_click(col, row) {
1616 return Some(result);
1617 }
1618 }
1619 if self.active_window_mut().tab_context_menu.is_some() {
1620 if let Some(result) = self.handle_tab_context_menu_click(col, row) {
1621 return Some(result);
1622 }
1623 }
1624 None
1625 }
1626
1627 fn suggestion_at(&self, col: u16, row: u16) -> Option<usize> {
1631 let (inner_rect, start_idx, _visible_count, total_count) =
1632 self.active_chrome().suggestions_area?;
1633 if col < inner_rect.x
1634 || col >= inner_rect.x + inner_rect.width
1635 || row < inner_rect.y
1636 || row >= inner_rect.y + inner_rect.height
1637 {
1638 return None;
1639 }
1640 let relative_row = (row - inner_rect.y) as usize;
1641 let item_idx = start_idx + relative_row;
1642 if item_idx < total_count {
1643 Some(item_idx)
1644 } else {
1645 None
1646 }
1647 }
1648
1649 fn handle_click_suggestions(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1650 let item_idx = self.suggestion_at(col, row)?;
1651 let prompt = self.active_window_mut().prompt.as_mut()?;
1652 prompt.selected_suggestion = Some(item_idx);
1653 let confirms = prompt.prompt_type.click_confirms();
1654 if !confirms {
1655 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1659 prompt.input = suggestion.get_value().to_string();
1660 prompt.cursor_pos = prompt.input.len();
1661 }
1662 }
1663 if confirms {
1664 return Some(self.handle_action(Action::PromptConfirm));
1665 }
1666 Some(Ok(()))
1667 }
1668
1669 fn handle_click_suggestions_confirm(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1673 let item_idx = self.suggestion_at(col, row)?;
1674 let prompt = self.active_window_mut().prompt.as_mut()?;
1675 prompt.selected_suggestion = Some(item_idx);
1676 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1677 prompt.input = suggestion.get_value().to_string();
1678 prompt.cursor_pos = prompt.input.len();
1679 }
1680 Some(self.handle_action(Action::PromptConfirm))
1681 }
1682
1683 fn handle_click_prompt_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1689 use crate::view::ui::scrollbar::ScrollbarState;
1690 let sb_rect = self.active_chrome().suggestions_scrollbar_rect?;
1691 if col < sb_rect.x
1692 || col >= sb_rect.x + sb_rect.width
1693 || row < sb_rect.y
1694 || row >= sb_rect.y + sb_rect.height
1695 {
1696 return None;
1697 }
1698 let suggestions_area_visible = self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
1706 let active_window_id = self.active_window;
1707 let prompt = self
1708 .windows
1709 .get_mut(&active_window_id)
1710 .and_then(|w| w.prompt.as_mut())?;
1711 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
1712 let total = prompt.suggestions.len();
1713 let track_height = sb_rect.height as usize;
1714 let click_row = row.saturating_sub(sb_rect.y) as usize;
1715 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
1716 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
1717 self.active_window_mut()
1720 .mouse_state
1721 .dragging_prompt_scrollbar = true;
1722 Some(Ok(()))
1723 }
1724
1725 fn handle_click_popup_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1726 let scrollbar_info: Option<(usize, i32)> =
1728 self.active_chrome().popup_areas.iter().rev().find_map(
1729 |(popup_idx, _popup_rect, inner_rect, _scroll, _n, scrollbar_rect, total_lines)| {
1730 let sb_rect = scrollbar_rect.as_ref()?;
1731 if col >= sb_rect.x
1732 && col < sb_rect.x + sb_rect.width
1733 && row >= sb_rect.y
1734 && row < sb_rect.y + sb_rect.height
1735 {
1736 let relative_row = (row - sb_rect.y) as usize;
1737 let track_height = sb_rect.height as usize;
1738 let visible_lines = inner_rect.height as usize;
1739 if track_height > 0 && *total_lines > visible_lines {
1740 let max_scroll = total_lines.saturating_sub(visible_lines);
1741 let target = if track_height > 1 {
1742 (relative_row * max_scroll) / (track_height.saturating_sub(1))
1743 } else {
1744 0
1745 };
1746 Some((*popup_idx, target as i32))
1747 } else {
1748 Some((*popup_idx, 0))
1749 }
1750 } else {
1751 None
1752 }
1753 },
1754 );
1755 let (popup_idx, target_scroll) = scrollbar_info?;
1756 self.active_window_mut()
1757 .mouse_state
1758 .dragging_popup_scrollbar = Some(popup_idx);
1759 self.active_window_mut().mouse_state.drag_start_row = Some(row);
1760 let current_scroll = self
1761 .active_state()
1762 .popups
1763 .get(popup_idx)
1764 .map(|p| p.scroll_offset)
1765 .unwrap_or(0);
1766 self.active_window_mut().mouse_state.drag_start_popup_scroll = Some(current_scroll);
1767 let state = self.active_state_mut();
1768 if let Some(popup) = state.popups.get_mut(popup_idx) {
1769 popup.scroll_by(target_scroll - current_scroll as i32);
1770 }
1771 Some(Ok(()))
1772 }
1773
1774 fn handle_workspace_trust_mouse(
1780 &mut self,
1781 mouse_event: crossterm::event::MouseEvent,
1782 ) -> AnyhowResult<bool> {
1783 use crossterm::event::{MouseButton, MouseEventKind};
1784 let col = mouse_event.column;
1785 let row = mouse_event.row;
1786 let layout = self.active_chrome().workspace_trust_dialog.clone();
1787
1788 match mouse_event.kind {
1789 MouseEventKind::ScrollUp => {
1790 self.workspace_trust_scroll = self.workspace_trust_scroll.saturating_sub(2);
1791 }
1792 MouseEventKind::ScrollDown => {
1793 let max = layout.as_ref().map(|l| l.max_scroll).unwrap_or(0);
1794 self.workspace_trust_scroll = (self.workspace_trust_scroll + 2).min(max);
1795 }
1796 MouseEventKind::Down(MouseButton::Left) => {
1797 if let Some(layout) = layout {
1798 let hit = |r: ratatui::layout::Rect| in_rect(col, row, r);
1799 if hit(layout.ok) {
1800 let idx = self.current_workspace_trust_selection();
1801 self.confirm_workspace_trust(idx);
1802 } else if hit(layout.quit) {
1803 self.hide_popup();
1806 if !self.workspace_trust_prompt_cancellable {
1807 self.should_quit = true;
1808 }
1809 } else if let Some(i) = layout.radios.iter().position(|r| hit(*r)) {
1810 self.confirm_workspace_trust(i);
1811 }
1812 }
1814 }
1815 _ => {}
1817 }
1818 Ok(true)
1819 }
1820
1821 fn handle_click_global_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1822 for (popup_idx, popup_rect, inner_rect, scroll_offset, num_items) in self
1823 .active_chrome()
1824 .global_popup_areas
1825 .clone()
1826 .into_iter()
1827 .rev()
1828 {
1829 if popup_rect.width >= 5 {
1830 let cb_x = popup_rect.x + popup_rect.width - 4;
1831 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
1832 return Some(self.handle_action(Action::PopupCancel));
1833 }
1834 }
1835 if in_rect(col, row, inner_rect) && num_items > 0 {
1836 let relative_row = (row - inner_rect.y) as usize;
1837 let item_idx = scroll_offset + relative_row;
1838 if item_idx < num_items {
1839 if let Some(popup) = self.global_popups.get_mut(popup_idx) {
1840 if let crate::view::popup::PopupContent::List { items: _, selected } =
1841 &mut popup.content
1842 {
1843 *selected = item_idx;
1844 }
1845 }
1846 return Some(self.handle_action(Action::PopupConfirm));
1847 }
1848 }
1849 }
1850 None
1851 }
1852
1853 fn handle_click_buffer_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1854 let close_hit = self.active_chrome().popup_areas.iter().rev().find_map(
1856 |(_idx, popup_rect, _inner, _scroll, _n, _sb, _tl)| {
1857 if popup_rect.width < 5 {
1858 return None;
1859 }
1860 let cb_x = popup_rect.x + popup_rect.width - 4;
1861 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
1862 Some(())
1863 } else {
1864 None
1865 }
1866 },
1867 );
1868 if close_hit.is_some() {
1869 return Some(self.handle_action(Action::PopupCancel));
1870 }
1871
1872 let popup_areas = self.active_chrome().popup_areas.clone();
1874 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
1875 popup_areas.iter().rev()
1876 {
1877 if !in_rect(col, row, *inner_rect) {
1878 continue;
1879 }
1880 let relative_col = (col - inner_rect.x) as usize;
1881 let relative_row = (row - inner_rect.y) as usize;
1882
1883 let link_url = {
1884 let state = self.active_state();
1885 state
1886 .popups
1887 .top()
1888 .and_then(|p| p.link_at_position(relative_col, relative_row))
1889 };
1890 if let Some(url) = link_url {
1891 #[cfg(feature = "runtime")]
1892 if let Err(e) = open::that(&url) {
1893 self.set_status_message(format!("Failed to open URL: {}", e));
1894 } else {
1895 self.set_status_message(format!("Opening: {}", url));
1896 }
1897 return Some(Ok(()));
1898 }
1899
1900 if *num_items > 0 {
1901 let item_idx = scroll_offset + relative_row;
1902 if item_idx < *num_items {
1903 let state = self.active_state_mut();
1904 if let Some(popup) = state.popups.top_mut() {
1905 if let crate::view::popup::PopupContent::List { items: _, selected } =
1906 &mut popup.content
1907 {
1908 *selected = item_idx;
1909 }
1910 }
1911 return Some(self.handle_action(Action::PopupConfirm));
1912 }
1913 }
1914
1915 let is_text_popup = {
1916 let state = self.active_state();
1917 state.popups.top().is_some_and(|p| {
1918 matches!(
1919 p.content,
1920 crate::view::popup::PopupContent::Text(_)
1921 | crate::view::popup::PopupContent::Markdown(_)
1922 )
1923 })
1924 };
1925 if is_text_popup {
1926 let line = scroll_offset + relative_row;
1927 let popup_idx_copy = *popup_idx;
1928 let state = self.active_state_mut();
1929 if let Some(popup) = state.popups.top_mut() {
1930 popup.start_selection(line, relative_col);
1931 }
1932 self.active_window_mut().mouse_state.selecting_in_popup = Some(popup_idx_copy);
1933 return Some(Ok(()));
1934 }
1935 }
1936 None
1937 }
1938
1939 fn handle_click_menu_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1940 if self.active_window_mut().menu_bar_visible {
1941 let hit = self
1943 .active_chrome()
1944 .menu_layout
1945 .as_ref()
1946 .and_then(|ml| ml.menu_at(col, row));
1947 let layout_exists = self.active_chrome().menu_layout.is_some();
1948 if layout_exists {
1949 if let Some(menu_idx) = hit {
1950 if self.menu_state.active_menu == Some(menu_idx) {
1951 self.close_menu_with_auto_hide();
1952 } else {
1953 self.active_window_mut().on_editor_focus_lost();
1954 self.menu_state.open_menu(menu_idx);
1955 }
1956 return Some(Ok(()));
1957 } else if row == 0 {
1958 self.close_menu_with_auto_hide();
1959 return Some(Ok(()));
1960 }
1961 }
1962 }
1963
1964 if let Some(active_idx) = self.menu_state.active_menu {
1965 let all_menus: Vec<crate::config::Menu> = self
1966 .menus
1967 .menus
1968 .iter()
1969 .chain(self.menu_state.plugin_menus.iter())
1970 .cloned()
1971 .collect();
1972 if let Some(menu) = all_menus.get(active_idx) {
1973 match self.handle_menu_dropdown_click(col, row, menu) {
1974 Ok(Some(click_result)) => return Some(click_result),
1975 Ok(None) => {}
1976 Err(e) => return Some(Err(e)),
1977 }
1978 }
1979 self.close_menu_with_auto_hide();
1980 return Some(Ok(()));
1981 }
1982
1983 None
1984 }
1985
1986 fn handle_click_file_explorer_area(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1987 let explorer_area = self.active_layout().file_explorer_area?;
1988 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1989 if col == border_x && row >= explorer_area.y && row < explorer_area.y + explorer_area.height
1990 {
1991 self.active_window_mut().mouse_state.dragging_file_explorer = true;
1992 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
1993 self.active_window_mut()
1994 .mouse_state
1995 .drag_start_explorer_width = Some(self.active_window().file_explorer_width);
1996 return Some(Ok(()));
1997 }
1998 if in_rect(col, row, explorer_area) {
1999 return Some(self.handle_file_explorer_click(col, row, explorer_area));
2000 }
2001 None
2002 }
2003
2004 fn handle_click_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2005 let (split_id, buffer_id, scrollbar_rect, is_on_thumb) =
2006 self.active_layout().split_areas.iter().find_map(
2007 |(split_id, buffer_id, _content, scrollbar_rect, thumb_start, thumb_end)| {
2008 if in_rect(col, row, *scrollbar_rect) {
2009 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
2010 let on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
2011 Some((*split_id, *buffer_id, *scrollbar_rect, on_thumb))
2012 } else {
2013 None
2014 }
2015 },
2016 )?;
2017
2018 self.focus_split(split_id, buffer_id);
2019 if is_on_thumb {
2020 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2021 self.active_window_mut().mouse_state.drag_start_row = Some(row);
2022 if self.active_window().is_composite_buffer(buffer_id) {
2023 if let Some(vs) = self
2024 .active_window()
2025 .composite_view_states
2026 .get(&(split_id, buffer_id))
2027 {
2028 self.active_window_mut()
2029 .mouse_state
2030 .drag_start_composite_scroll_row = Some(vs.scroll_row);
2031 }
2032 } else {
2033 let snap = self
2034 .windows
2035 .get(&self.active_window)
2036 .and_then(|w| w.buffers.splits())
2037 .map(|(_, vs)| vs)
2038 .expect("active window must have a populated split layout")
2039 .get(&split_id)
2040 .map(|vs| (vs.viewport.top_byte, vs.viewport.top_view_line_offset));
2041 if let Some((top_byte, top_view_line_offset)) = snap {
2042 let ms = &mut self.active_window_mut().mouse_state;
2043 ms.drag_start_top_byte = Some(top_byte);
2044 ms.drag_start_view_line_offset = Some(top_view_line_offset);
2045 }
2046 }
2047 } else {
2048 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2049 if let Err(e) = self.active_window_mut().handle_scrollbar_jump(
2050 col,
2051 row,
2052 split_id,
2053 buffer_id,
2054 scrollbar_rect,
2055 ) {
2056 return Some(Err(e));
2057 }
2058 self.active_window_mut().mouse_state.hover_target =
2059 Some(HoverTarget::ScrollbarThumb(split_id));
2060 }
2061 Some(Ok(()))
2062 }
2063
2064 fn handle_click_horizontal_scrollbar(
2065 &mut self,
2066 col: u16,
2067 row: u16,
2068 ) -> Option<AnyhowResult<()>> {
2069 let (split_id, buffer_id, hscrollbar_rect, max_content_width, is_on_thumb) = self
2070 .active_layout()
2071 .horizontal_scrollbar_areas
2072 .iter()
2073 .find_map(
2074 |(
2075 split_id,
2076 buffer_id,
2077 hscrollbar_rect,
2078 max_content_width,
2079 thumb_start,
2080 thumb_end,
2081 )| {
2082 if col >= hscrollbar_rect.x
2083 && col < hscrollbar_rect.x + hscrollbar_rect.width
2084 && row >= hscrollbar_rect.y
2085 && row < hscrollbar_rect.y + hscrollbar_rect.height
2086 {
2087 let relative_col = col.saturating_sub(hscrollbar_rect.x) as usize;
2088 let on_thumb = relative_col >= *thumb_start && relative_col < *thumb_end;
2089 Some((
2090 *split_id,
2091 *buffer_id,
2092 *hscrollbar_rect,
2093 *max_content_width,
2094 on_thumb,
2095 ))
2096 } else {
2097 None
2098 }
2099 },
2100 )?;
2101
2102 self.focus_split(split_id, buffer_id);
2103 self.active_window_mut()
2104 .mouse_state
2105 .dragging_horizontal_scrollbar = Some(split_id);
2106 if is_on_thumb {
2107 self.active_window_mut().mouse_state.drag_start_hcol = Some(col);
2108 if let Some(vs) = self
2109 .windows
2110 .get(&self.active_window)
2111 .and_then(|w| w.buffers.splits())
2112 .map(|(_, vs)| vs)
2113 .expect("active window must have a populated split layout")
2114 .get(&split_id)
2115 {
2116 self.active_window_mut().mouse_state.drag_start_left_column =
2117 Some(vs.viewport.left_column);
2118 }
2119 } else {
2120 self.active_window_mut().mouse_state.drag_start_hcol = None;
2121 self.active_window_mut().mouse_state.drag_start_left_column = None;
2122 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2123 let track_width = hscrollbar_rect.width as f64;
2124 let ratio = if track_width > 1.0 {
2125 (relative_col / (track_width - 1.0)).clamp(0.0, 1.0)
2126 } else {
2127 0.0
2128 };
2129 if let Some(vs) = self
2130 .windows
2131 .get_mut(&self.active_window)
2132 .and_then(|w| w.split_view_states_mut())
2133 .expect("active window must have a populated split layout")
2134 .get_mut(&split_id)
2135 {
2136 let visible_width = vs.viewport.width as usize;
2137 let max_scroll = max_content_width.saturating_sub(visible_width);
2138 let target_col = (ratio * max_scroll as f64).round() as usize;
2139 vs.viewport.left_column = target_col.min(max_scroll);
2140 vs.viewport.set_skip_ensure_visible();
2141 }
2142 }
2143 Some(Ok(()))
2144 }
2145
2146 fn handle_click_status_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2147 let (status_row, _status_x, _status_width) = self.active_chrome().status_bar_area?;
2148 if row != status_row {
2149 return None;
2150 }
2151 if let Some((r, s, e)) = self.active_chrome().status_bar_line_ending_area {
2161 if row == r && col >= s && col < e {
2162 self.dismiss_menu_popups_for_prompt();
2163 return Some(self.handle_action(Action::SetLineEnding));
2164 }
2165 }
2166 if let Some((r, s, e)) = self.active_chrome().status_bar_encoding_area {
2167 if row == r && col >= s && col < e {
2168 self.dismiss_menu_popups_for_prompt();
2169 return Some(self.handle_action(Action::SetEncoding));
2170 }
2171 }
2172 if let Some((r, s, e)) = self.active_chrome().status_bar_language_area {
2173 if row == r && col >= s && col < e {
2174 self.dismiss_menu_popups_for_prompt();
2175 return Some(self.handle_action(Action::SetLanguage));
2176 }
2177 }
2178 if let Some((r, s, e)) = self.active_chrome().status_bar_lsp_area {
2179 if row == r && col >= s && col < e {
2180 return Some(self.handle_action(Action::ShowLspStatus));
2183 }
2184 }
2185 if let Some((r, s, e)) = self.active_chrome().status_bar_remote_area {
2186 if row == r && col >= s && col < e {
2187 self.dismiss_menu_popups_for_prompt();
2188 return Some(self.handle_action(Action::ShowRemoteIndicatorMenu));
2189 }
2190 }
2191 if let Some((r, s, e)) = self.active_chrome().status_bar_warning_area {
2192 if row == r && col >= s && col < e {
2193 self.dismiss_menu_popups_for_prompt();
2194 return Some(self.handle_action(Action::ShowWarnings));
2195 }
2196 }
2197 if let Some((r, s, e)) = self.active_chrome().status_bar_message_area {
2198 if row == r && col >= s && col < e {
2199 return Some(self.handle_action(Action::ShowStatusLog));
2200 }
2201 }
2202 None
2203 }
2204
2205 fn handle_click_search_options(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2206 use crate::view::ui::status_bar::SearchOptionsHover;
2207 let layout = self.active_chrome().search_options_layout.clone()?;
2208 match layout.checkbox_at(col, row)? {
2209 SearchOptionsHover::CaseSensitive => {
2210 Some(self.handle_action(Action::ToggleSearchCaseSensitive))
2211 }
2212 SearchOptionsHover::WholeWord => {
2213 Some(self.handle_action(Action::ToggleSearchWholeWord))
2214 }
2215 SearchOptionsHover::Regex => Some(self.handle_action(Action::ToggleSearchRegex)),
2216 SearchOptionsHover::ConfirmEach => {
2217 Some(self.handle_action(Action::ToggleSearchConfirmEach))
2218 }
2219 SearchOptionsHover::None => None,
2220 }
2221 }
2222
2223 fn handle_click_split_separator(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2224 let separator_areas = self.active_layout().separator_areas.clone();
2225 for (split_id, direction, sep_x, sep_y, sep_length) in &separator_areas {
2226 let is_on_separator = match direction {
2227 SplitDirection::Horizontal => {
2228 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
2229 }
2230 SplitDirection::Vertical => {
2231 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
2232 }
2233 };
2234 if is_on_separator {
2235 self.active_window_mut().mouse_state.dragging_separator =
2236 Some((*split_id, *direction));
2237 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2238 let ratio = self
2239 .split_manager_mut()
2240 .get_ratio((*split_id).into())
2241 .or_else(|| self.grouped_split_ratio(*split_id));
2242 if let Some(ratio) = ratio {
2243 self.active_window_mut().mouse_state.drag_start_ratio = Some(ratio);
2244 }
2245 return Some(Ok(()));
2246 }
2247 }
2248 None
2249 }
2250
2251 fn handle_click_split_controls(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2252 let close_split_id = self
2253 .active_layout()
2254 .close_split_areas
2255 .iter()
2256 .find(|(_, btn_row, start_col, end_col)| {
2257 row == *btn_row && col >= *start_col && col < *end_col
2258 })
2259 .map(|(split_id, _, _, _)| *split_id);
2260 if let Some(split_id) = close_split_id {
2261 if let Err(e) = self
2262 .windows
2263 .get_mut(&self.active_window)
2264 .and_then(|w| w.split_manager_mut())
2265 .expect("active window must have a populated split layout")
2266 .close_split(split_id)
2267 {
2268 self.set_status_message(
2269 t!("error.cannot_close_split", error = e.to_string()).to_string(),
2270 );
2271 } else {
2272 let new_active = self
2273 .windows
2274 .get(&self.active_window)
2275 .and_then(|w| w.buffers.splits())
2276 .map(|(mgr, _)| mgr)
2277 .expect("active window must have a populated split layout")
2278 .active_split();
2279 if let Some(buffer_id) = self
2280 .windows
2281 .get(&self.active_window)
2282 .and_then(|w| w.buffers.splits())
2283 .map(|(mgr, _)| mgr)
2284 .expect("active window must have a populated split layout")
2285 .buffer_for_split(new_active)
2286 {
2287 self.set_active_buffer(buffer_id);
2288 }
2289 self.set_status_message(t!("split.closed").to_string());
2290 }
2291 return Some(Ok(()));
2292 }
2293
2294 let maximize_target = self
2295 .active_layout()
2296 .maximize_split_areas
2297 .iter()
2298 .find(|(_, btn_row, start_col, end_col)| {
2299 row == *btn_row && col >= *start_col && col < *end_col
2300 })
2301 .map(|(split_id, _, _, _)| *split_id);
2302 if let Some(target) = maximize_target {
2303 let already_maximized = self
2310 .windows
2311 .get(&self.active_window)
2312 .and_then(|w| w.buffers.splits())
2313 .map(|(mgr, _)| mgr.is_maximized())
2314 .unwrap_or(false);
2315 if !already_maximized {
2316 if let Some(buffer_id) = self
2317 .windows
2318 .get(&self.active_window)
2319 .and_then(|w| w.buffers.splits())
2320 .map(|(mgr, _)| mgr)
2321 .expect("active window must have a populated split layout")
2322 .buffer_for_split(target)
2323 {
2324 self.focus_split(target, buffer_id);
2325 }
2326 }
2327 match self
2328 .windows
2329 .get_mut(&self.active_window)
2330 .and_then(|w| w.split_manager_mut())
2331 .expect("active window must have a populated split layout")
2332 .toggle_maximize_for(target)
2333 {
2334 Ok(maximized) => {
2335 let msg = if maximized {
2336 t!("split.maximized").to_string()
2337 } else {
2338 t!("split.restored").to_string()
2339 };
2340 self.set_status_message(msg);
2341 }
2342 Err(e) => self.set_status_message(e),
2343 }
2344 return Some(Ok(()));
2345 }
2346
2347 None
2348 }
2349
2350 fn handle_click_tab_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2351 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
2352 tracing::debug!(
2353 "Tab layout for split {:?}: bar_area={:?}, left_scroll={:?}, right_scroll={:?}",
2354 split_id,
2355 tab_layout.bar_area,
2356 tab_layout.left_scroll_area,
2357 tab_layout.right_scroll_area
2358 );
2359 }
2360 let tab_hit = self
2361 .active_layout()
2362 .tab_layouts
2363 .iter()
2364 .find_map(|(split_id, tab_layout)| {
2365 let hit = tab_layout.hit_test(col, row);
2366 tracing::debug!(
2367 "Tab hit_test at ({}, {}) for split {:?} returned {:?}",
2368 col,
2369 row,
2370 split_id,
2371 hit
2372 );
2373 hit.map(|h| (*split_id, h))
2374 });
2375 let (split_id, hit) = tab_hit?;
2376 match hit {
2377 TabHit::CloseButton(target) => {
2378 match target {
2379 crate::view::split::TabTarget::Buffer(buffer_id) => {
2380 self.focus_split(split_id, buffer_id);
2381 self.close_tab_in_split(buffer_id, split_id);
2382 }
2383 crate::view::split::TabTarget::Group(group_leaf) => {
2384 self.close_buffer_group_by_leaf(group_leaf);
2385 }
2386 }
2387 Some(Ok(()))
2388 }
2389 TabHit::TabName(target) => {
2390 let direction = self
2391 .windows
2392 .get(&self.active_window)
2393 .and_then(|w| w.buffers.splits())
2394 .map(|(_, vs)| vs)
2395 .expect("active window must have a populated split layout")
2396 .get(&split_id)
2397 .map(|vs| {
2398 let open = &vs.open_buffers;
2399 let cur = vs.active_target();
2400 let cur_idx = open.iter().position(|t| *t == cur);
2401 let new_idx = open.iter().position(|t| *t == target);
2402 match (cur_idx, new_idx) {
2403 (Some(c), Some(n)) if n > c => 1,
2404 (Some(c), Some(n)) if n < c => -1,
2405 _ => 0,
2406 }
2407 })
2408 .unwrap_or(0);
2409 self.active_window_mut()
2410 .animate_tab_switch(split_id, direction);
2411 match target {
2412 crate::view::split::TabTarget::Buffer(buffer_id) => {
2413 self.focus_split(split_id, buffer_id);
2414 self.active_window_mut()
2415 .promote_buffer_from_preview(buffer_id);
2416 self.active_window_mut().mouse_state.dragging_tab = Some(
2417 super::types::TabDragState::new(buffer_id, split_id, (col, row)),
2418 );
2419 }
2420 crate::view::split::TabTarget::Group(group_leaf) => {
2421 self.activate_group_tab(split_id, group_leaf);
2422 }
2423 }
2424 Some(Ok(()))
2425 }
2426 TabHit::ScrollLeft => {
2427 self.set_status_message("ScrollLeft clicked!".to_string());
2428 if let Some(vs) = self
2429 .windows
2430 .get_mut(&self.active_window)
2431 .and_then(|w| w.split_view_states_mut())
2432 .expect("active window must have a populated split layout")
2433 .get_mut(&split_id)
2434 {
2435 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_sub(10);
2436 }
2437 Some(Ok(()))
2438 }
2439 TabHit::ScrollRight => {
2440 self.set_status_message("ScrollRight clicked!".to_string());
2441 if let Some(vs) = self
2442 .windows
2443 .get_mut(&self.active_window)
2444 .and_then(|w| w.split_view_states_mut())
2445 .expect("active window must have a populated split layout")
2446 .get_mut(&split_id)
2447 {
2448 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_add(10);
2449 }
2450 Some(Ok(()))
2451 }
2452 TabHit::BarBackground => None,
2453 }
2454 }
2455
2456 pub(super) fn handle_mouse_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2458 if self.try_widget_scrollbar_drag(row) {
2461 let _ = col;
2462 return Ok(());
2463 }
2464 if self.overlay_prompt_active()
2469 && !self.active_window().mouse_state.dragging_prompt_scrollbar
2470 {
2471 return Ok(());
2472 }
2473
2474 if let Some(dragging_split_id) = self.active_window_mut().mouse_state.dragging_scrollbar {
2476 let split_areas = self.active_layout().split_areas.clone();
2479 for (split_id, buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2480 &split_areas
2481 {
2482 if *split_id == dragging_split_id {
2483 if self.active_window().mouse_state.drag_start_row.is_some() {
2485 self.active_window_mut().handle_scrollbar_drag_relative(
2487 row,
2488 *split_id,
2489 *buffer_id,
2490 *scrollbar_rect,
2491 )?;
2492 } else {
2493 self.active_window_mut().handle_scrollbar_jump(
2495 col,
2496 row,
2497 *split_id,
2498 *buffer_id,
2499 *scrollbar_rect,
2500 )?;
2501 }
2502 return Ok(());
2503 }
2504 }
2505 }
2506
2507 if let Some(dragging_split_id) = self
2509 .active_window_mut()
2510 .mouse_state
2511 .dragging_horizontal_scrollbar
2512 {
2513 let hscrollbar_areas = self.active_layout().horizontal_scrollbar_areas.clone();
2518 for (
2519 split_id,
2520 _buffer_id,
2521 hscrollbar_rect,
2522 max_content_width,
2523 thumb_start,
2524 thumb_end,
2525 ) in &hscrollbar_areas
2526 {
2527 if *split_id == dragging_split_id {
2528 let track_width = hscrollbar_rect.width as f64;
2529 if track_width <= 1.0 {
2530 break;
2531 }
2532
2533 if let (Some(drag_start_hcol), Some(drag_start_left_column)) = (
2534 self.active_window_mut().mouse_state.drag_start_hcol,
2535 self.active_window_mut().mouse_state.drag_start_left_column,
2536 ) {
2537 let col_offset = (col as i32) - (drag_start_hcol as i32);
2540 if let Some(view_state) = self
2541 .windows
2542 .get_mut(&self.active_window)
2543 .and_then(|w| w.split_view_states_mut())
2544 .expect("active window must have a populated split layout")
2545 .get_mut(&dragging_split_id)
2546 {
2547 let visible_width = view_state.viewport.width as usize;
2548 let max_scroll = max_content_width.saturating_sub(visible_width);
2549 if max_scroll > 0 {
2550 let thumb_size = thumb_end.saturating_sub(*thumb_start).max(1);
2551 let track_travel = (track_width - thumb_size as f64).max(1.0);
2552 let scroll_per_pixel = max_scroll as f64 / track_travel;
2553 let scroll_offset =
2554 (col_offset as f64 * scroll_per_pixel).round() as i64;
2555 let new_left =
2556 (drag_start_left_column as i64 + scroll_offset).max(0) as usize;
2557 view_state.viewport.left_column = new_left.min(max_scroll);
2558 view_state.viewport.set_skip_ensure_visible();
2559 }
2560 }
2561 } else {
2562 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2564 let ratio = (relative_col / (track_width - 1.0)).clamp(0.0, 1.0);
2565
2566 if let Some(view_state) = self
2567 .windows
2568 .get_mut(&self.active_window)
2569 .and_then(|w| w.split_view_states_mut())
2570 .expect("active window must have a populated split layout")
2571 .get_mut(&dragging_split_id)
2572 {
2573 let visible_width = view_state.viewport.width as usize;
2574 let max_scroll = max_content_width.saturating_sub(visible_width);
2575 let target_col = (ratio * max_scroll as f64).round() as usize;
2576 view_state.viewport.left_column = target_col.min(max_scroll);
2577 view_state.viewport.set_skip_ensure_visible();
2578 }
2579 }
2580
2581 return Ok(());
2582 }
2583 }
2584 }
2585
2586 if let Some(popup_idx) = self.active_window_mut().mouse_state.selecting_in_popup {
2588 if let Some((_, _, inner_rect, scroll_offset, _, _, _)) = self
2590 .active_chrome()
2591 .popup_areas
2592 .iter()
2593 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2594 {
2595 if col >= inner_rect.x
2597 && col < inner_rect.x + inner_rect.width
2598 && row >= inner_rect.y
2599 && row < inner_rect.y + inner_rect.height
2600 {
2601 let relative_col = (col - inner_rect.x) as usize;
2602 let relative_row = (row - inner_rect.y) as usize;
2603 let line = scroll_offset + relative_row;
2604
2605 let state = self.active_state_mut();
2606 if let Some(popup) = state.popups.get_mut(popup_idx) {
2607 popup.extend_selection(line, relative_col);
2608 }
2609 }
2610 }
2611 return Ok(());
2612 }
2613
2614 if self
2619 .active_window_mut()
2620 .mouse_state
2621 .dragging_prompt_scrollbar
2622 {
2623 use crate::view::ui::scrollbar::ScrollbarState;
2624 let sb_rect = self.active_chrome().suggestions_scrollbar_rect;
2627 let suggestions_area_visible =
2628 self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
2629 let active_window_id = self.active_window;
2630 if let (Some(sb_rect), Some(prompt)) = (
2631 sb_rect,
2632 self.windows
2633 .get_mut(&active_window_id)
2634 .and_then(|w| w.prompt.as_mut()),
2635 ) {
2636 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
2637 let total = prompt.suggestions.len();
2638 let track_height = sb_rect.height as usize;
2639 let clamped_row =
2643 row.clamp(sb_rect.y, sb_rect.y + sb_rect.height.saturating_sub(1));
2644 let click_row = clamped_row.saturating_sub(sb_rect.y) as usize;
2645 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
2646 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
2647 }
2648 return Ok(());
2649 }
2650
2651 if let Some(popup_idx) = self
2653 .active_window_mut()
2654 .mouse_state
2655 .dragging_popup_scrollbar
2656 {
2657 if let Some((_, _, inner_rect, _, _, Some(sb_rect), total_lines)) = self
2659 .active_chrome()
2660 .popup_areas
2661 .iter()
2662 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2663 {
2664 let track_height = sb_rect.height as usize;
2665 let visible_lines = inner_rect.height as usize;
2666
2667 if track_height > 0 && *total_lines > visible_lines {
2668 let relative_row = row.saturating_sub(sb_rect.y) as usize;
2669 let max_scroll = total_lines.saturating_sub(visible_lines);
2670 let target_scroll = if track_height > 1 {
2671 (relative_row * max_scroll) / (track_height.saturating_sub(1))
2672 } else {
2673 0
2674 };
2675
2676 let state = self.active_state_mut();
2677 if let Some(popup) = state.popups.get_mut(popup_idx) {
2678 let current_scroll = popup.scroll_offset as i32;
2679 let delta = target_scroll as i32 - current_scroll;
2680 popup.scroll_by(delta);
2681 }
2682 }
2683 }
2684 return Ok(());
2685 }
2686
2687 if let Some((split_id, direction)) = self.active_window_mut().mouse_state.dragging_separator
2689 {
2690 self.handle_separator_drag(col, row, split_id, direction)?;
2691 return Ok(());
2692 }
2693
2694 if self.active_window_mut().mouse_state.dragging_file_explorer {
2696 self.handle_file_explorer_border_drag(col)?;
2697 return Ok(());
2698 }
2699
2700 if self.active_window_mut().mouse_state.dragging_text_selection {
2702 self.handle_text_selection_drag(col, row)?;
2703 return Ok(());
2704 }
2705
2706 if self.active_window_mut().mouse_state.dragging_tab.is_some() {
2708 self.handle_tab_drag(col, row)?;
2709 return Ok(());
2710 }
2711
2712 Ok(())
2713 }
2714
2715 fn handle_text_selection_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2717 use crate::model::event::Event;
2718 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2719
2720 let Some(split_id) = self.active_window_mut().mouse_state.drag_selection_split else {
2721 return Ok(());
2722 };
2723 let Some(anchor_position) = self.active_window_mut().mouse_state.drag_selection_anchor
2724 else {
2725 return Ok(());
2726 };
2727
2728 let Some((buffer_id, content_rect)) = self
2730 .active_layout()
2731 .split_areas
2732 .iter()
2733 .find(|(sid, _, _, _, _, _)| *sid == split_id)
2734 .map(|(_, bid, rect, _, _, _)| (*bid, *rect))
2735 else {
2736 return Ok(());
2737 };
2738
2739 let cached_mappings = self
2741 .active_layout()
2742 .view_line_mappings
2743 .get(&split_id)
2744 .cloned();
2745
2746 let leaf_id = split_id;
2747
2748 let fallback = self
2750 .windows
2751 .get(&self.active_window)
2752 .and_then(|w| w.buffers.splits())
2753 .map(|(_, vs)| vs)
2754 .expect("active window must have a populated split layout")
2755 .get(&leaf_id)
2756 .map(|vs| vs.viewport.top_byte)
2757 .unwrap_or(0);
2758
2759 let compose_width = self
2761 .windows
2762 .get(&self.active_window)
2763 .and_then(|w| w.buffers.splits())
2764 .map(|(_, vs)| vs)
2765 .expect("active window must have a populated split layout")
2766 .get(&leaf_id)
2767 .and_then(|vs| vs.compose_width);
2768
2769 let drag_by_words = self.active_window_mut().mouse_state.drag_selection_by_words;
2773 let drag_word_end = self.active_window_mut().mouse_state.drag_selection_word_end;
2774
2775 let Some((target_position, new_position, anchor_position, new_sticky_column)) = self
2776 .active_window()
2777 .buffers
2778 .get(&buffer_id)
2779 .and_then(|state| {
2780 let gutter_width = state.margins.left_total_width() as u16;
2781 let target_position = super::click_geometry::screen_to_buffer_position(
2782 col,
2783 row,
2784 content_rect,
2785 gutter_width,
2786 &cached_mappings,
2787 fallback,
2788 true, compose_width,
2790 )?;
2791 let (new_position, anchor_pos) = if drag_by_words {
2792 if target_position >= anchor_position {
2793 (
2794 find_word_end(&state.buffer, target_position),
2795 anchor_position,
2796 )
2797 } else {
2798 let word_end = drag_word_end.unwrap_or(anchor_position);
2799 (find_word_start(&state.buffer, target_position), word_end)
2800 }
2801 } else {
2802 (target_position, anchor_position)
2803 };
2804 let new_sticky_column = state
2805 .buffer
2806 .offset_to_position(new_position)
2807 .map(|pos| pos.column);
2808 Some((target_position, new_position, anchor_pos, new_sticky_column))
2809 })
2810 else {
2811 return Ok(());
2812 };
2813 let _ = target_position;
2814
2815 let (primary_cursor_id, old_position, old_anchor, old_sticky_column) = self
2816 .active_window()
2817 .buffers
2818 .splits()
2819 .and_then(|(_, vs)| vs.get(&leaf_id))
2820 .map(|vs| {
2821 let cursor = vs.cursors.primary();
2822 (
2823 vs.cursors.primary_id(),
2824 cursor.position,
2825 cursor.anchor,
2826 cursor.sticky_column,
2827 )
2828 })
2829 .unwrap_or((CursorId(0), 0, None, 0));
2830
2831 let event = Event::MoveCursor {
2832 cursor_id: primary_cursor_id,
2833 old_position,
2834 new_position,
2835 old_anchor,
2836 new_anchor: Some(anchor_position),
2837 old_sticky_column,
2838 new_sticky_column: new_sticky_column.unwrap_or(old_sticky_column),
2839 };
2840
2841 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
2842 event_log.append(event.clone());
2843 }
2844 self.active_window_mut()
2845 .apply_event_to_buffer(buffer_id, leaf_id, &event);
2846
2847 Ok(())
2848 }
2849
2850 pub(super) fn handle_file_explorer_border_drag(&mut self, col: u16) -> AnyhowResult<()> {
2852 let Some((start_col, _start_row)) =
2853 self.active_window_mut().mouse_state.drag_start_position
2854 else {
2855 return Ok(());
2856 };
2857 let Some(start_width) = self
2858 .active_window_mut()
2859 .mouse_state
2860 .drag_start_explorer_width
2861 else {
2862 return Ok(());
2863 };
2864
2865 let delta = col as i32 - start_col as i32;
2866 let total_width = self.terminal_width as i32;
2867
2868 if total_width > 0 {
2872 use crate::config::ExplorerWidth;
2873 self.active_window_mut().file_explorer_width = match start_width {
2874 ExplorerWidth::Percent(start_pct) => {
2875 let percent_delta = (delta * 100) / total_width;
2876 let new_pct = (start_pct as i32 + percent_delta).clamp(0, 100) as u8;
2877 ExplorerWidth::Percent(new_pct)
2878 }
2879 ExplorerWidth::Columns(start_cols) => {
2880 let new_cols = (start_cols as i32 + delta).clamp(0, total_width) as u16;
2881 ExplorerWidth::Columns(new_cols)
2882 }
2883 };
2884 }
2885
2886 Ok(())
2887 }
2888
2889 pub(super) fn handle_separator_drag(
2891 &mut self,
2892 col: u16,
2893 row: u16,
2894 split_id: ContainerId,
2895 direction: SplitDirection,
2896 ) -> AnyhowResult<()> {
2897 let Some((start_col, start_row)) = self.active_window_mut().mouse_state.drag_start_position
2898 else {
2899 return Ok(());
2900 };
2901 let Some(start_ratio) = self.active_window_mut().mouse_state.drag_start_ratio else {
2902 return Ok(());
2903 };
2904 let Some(editor_area) = self.active_layout().editor_content_area else {
2905 return Ok(());
2906 };
2907
2908 let (delta, total_size) = match direction {
2910 SplitDirection::Horizontal => {
2911 let delta = row as i32 - start_row as i32;
2913 let total = editor_area.height as i32;
2914 (delta, total)
2915 }
2916 SplitDirection::Vertical => {
2917 let delta = col as i32 - start_col as i32;
2919 let total = editor_area.width as i32;
2920 (delta, total)
2921 }
2922 };
2923
2924 if total_size > 0 {
2927 let ratio_delta = delta as f32 / total_size as f32;
2928 let new_ratio = (start_ratio + ratio_delta).clamp(0.1, 0.9);
2929
2930 if self
2935 .windows
2936 .get(&self.active_window)
2937 .and_then(|w| w.buffers.splits())
2938 .map(|(mgr, _)| mgr)
2939 .expect("active window must have a populated split layout")
2940 .get_ratio(split_id.into())
2941 .is_some()
2942 {
2943 self.windows
2944 .get_mut(&self.active_window)
2945 .and_then(|w| w.split_manager_mut())
2946 .expect("active window must have a populated split layout")
2947 .set_ratio(split_id, new_ratio);
2948 } else {
2949 self.set_grouped_split_ratio(split_id, new_ratio);
2950 }
2951 }
2952
2953 Ok(())
2954 }
2955
2956 pub(super) fn handle_right_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2958 let frame_w = self.active_chrome().last_frame_width;
2959 let frame_h = self.active_chrome().last_frame_height;
2960 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
2961 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
2962 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
2963 let menu_height = menu.height();
2964 if col >= menu_x
2965 && col < menu_x + menu_width
2966 && row >= menu_y
2967 && row < menu_y + menu_height
2968 {
2969 return Ok(());
2970 }
2971 }
2972
2973 if let Some(ref menu) = self.active_window_mut().tab_context_menu {
2975 let menu_x = menu.position.0;
2976 let menu_y = menu.position.1;
2977 let menu_width = 22u16; let menu_height = super::types::TabContextMenuItem::all().len() as u16 + 2; if col >= menu_x
2982 && col < menu_x + menu_width
2983 && row >= menu_y
2984 && row < menu_y + menu_height
2985 {
2986 return Ok(());
2988 }
2989 }
2990
2991 if let Some(explorer_area) = self.active_layout().file_explorer_area {
2992 if col >= explorer_area.x
2993 && col < explorer_area.x + explorer_area.width
2994 && row < explorer_area.y + explorer_area.height
2995 && row > explorer_area.y
2996 {
2998 let relative_row = row.saturating_sub(explorer_area.y + 1);
2999 let (is_multi, is_root_selected) =
3000 if let Some(explorer) = self.file_explorer_mut().as_mut() {
3001 let display_nodes = explorer.get_display_nodes();
3002 let scroll_offset = explorer.get_scroll_offset();
3003 let clicked_index = (relative_row as usize) + scroll_offset;
3004 let mut clicked_is_root = false;
3005 if clicked_index < display_nodes.len() {
3006 let (node_id, _) = display_nodes[clicked_index];
3007 explorer.set_selected(Some(node_id));
3008 clicked_is_root = node_id == explorer.tree().root_id();
3009 }
3010 (explorer.has_multi_selection(), clicked_is_root)
3011 } else {
3012 (false, false)
3013 };
3014 self.active_window_mut().key_context =
3015 crate::input::keybindings::KeyContext::FileExplorer;
3016 self.active_window_mut().tab_context_menu = None;
3017 self.active_window_mut().file_explorer_context_menu =
3018 Some(super::types::FileExplorerContextMenu::new(
3019 col,
3020 row + 1,
3021 is_multi,
3022 is_root_selected,
3023 ));
3024 return Ok(());
3025 }
3026 }
3027
3028 self.active_window_mut().file_explorer_context_menu = None;
3029
3030 let tab_hit = self
3032 .active_layout()
3033 .tab_layouts
3034 .iter()
3035 .find_map(
3036 |(split_id, tab_layout)| match tab_layout.hit_test(col, row) {
3037 Some(TabHit::TabName(target) | TabHit::CloseButton(target)) => {
3038 target.as_buffer().map(|bid| (*split_id, bid))
3041 }
3042 _ => None,
3043 },
3044 );
3045
3046 if let Some((split_id, buffer_id)) = tab_hit {
3047 self.active_window_mut().tab_context_menu =
3049 Some(TabContextMenu::new(buffer_id, split_id, col, row + 1));
3050 } else {
3051 self.active_window_mut().tab_context_menu = None;
3053 }
3054
3055 Ok(())
3056 }
3057
3058 pub(super) fn handle_tab_context_menu_click(
3060 &mut self,
3061 col: u16,
3062 row: u16,
3063 ) -> Option<AnyhowResult<()>> {
3064 let menu = self.active_window_mut().tab_context_menu.as_ref()?;
3065 let menu_x = menu.position.0;
3066 let menu_y = menu.position.1;
3067 let menu_width = 22u16;
3068 let items = super::types::TabContextMenuItem::all();
3069 let menu_height = items.len() as u16 + 2; if col < menu_x || col >= menu_x + menu_width || row < menu_y || row >= menu_y + menu_height
3073 {
3074 self.active_window_mut().tab_context_menu = None;
3076 return Some(Ok(()));
3077 }
3078
3079 if row == menu_y || row == menu_y + menu_height - 1 {
3081 return Some(Ok(()));
3082 }
3083
3084 let item_idx = (row - menu_y - 1) as usize;
3086 if item_idx >= items.len() {
3087 return Some(Ok(()));
3088 }
3089
3090 let buffer_id = menu.buffer_id;
3092 let split_id = menu.split_id;
3093 let item = items[item_idx];
3094
3095 self.active_window_mut().tab_context_menu = None;
3097
3098 Some(self.execute_tab_context_menu_action(item, buffer_id, split_id))
3100 }
3101
3102 fn execute_tab_context_menu_action(
3104 &mut self,
3105 item: super::types::TabContextMenuItem,
3106 buffer_id: BufferId,
3107 leaf_id: LeafId,
3108 ) -> AnyhowResult<()> {
3109 use super::types::TabContextMenuItem;
3110 match item {
3111 TabContextMenuItem::Close => {
3112 self.close_tab_in_split(buffer_id, leaf_id);
3113 }
3114 TabContextMenuItem::CloseOthers => {
3115 self.close_other_tabs_in_split(buffer_id, leaf_id);
3116 }
3117 TabContextMenuItem::CloseToRight => {
3118 self.close_tabs_to_right_in_split(buffer_id, leaf_id);
3119 }
3120 TabContextMenuItem::CloseToLeft => {
3121 self.close_tabs_to_left_in_split(buffer_id, leaf_id);
3122 }
3123 TabContextMenuItem::CloseAll => {
3124 self.close_all_tabs_in_split(leaf_id);
3125 }
3126 TabContextMenuItem::CopyRelativePath => {
3127 self.copy_buffer_path(buffer_id, true);
3128 }
3129 TabContextMenuItem::CopyFullPath => {
3130 self.copy_buffer_path(buffer_id, false);
3131 }
3132 }
3133
3134 Ok(())
3135 }
3136
3137 pub(super) fn handle_file_explorer_context_menu_key(
3140 &mut self,
3141 code: crossterm::event::KeyCode,
3142 modifiers: crossterm::event::KeyModifiers,
3143 ) -> Option<AnyhowResult<()>> {
3144 use crossterm::event::KeyCode;
3145 use crossterm::event::KeyModifiers;
3146
3147 if modifiers != KeyModifiers::NONE {
3148 return None;
3149 }
3150
3151 match code {
3152 KeyCode::Up => {
3153 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3154 menu.prev_item();
3155 }
3156 Some(Ok(()))
3157 }
3158 KeyCode::Down => {
3159 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3160 menu.next_item();
3161 }
3162 Some(Ok(()))
3163 }
3164 KeyCode::Enter => {
3165 let item = {
3166 let menu = self
3167 .active_window_mut()
3168 .file_explorer_context_menu
3169 .as_ref()?;
3170 menu.items()[menu.highlighted]
3171 };
3172 self.active_window_mut().file_explorer_context_menu = None;
3173 self.execute_file_explorer_context_menu_action(item);
3174 Some(Ok(()))
3175 }
3176 KeyCode::Esc => {
3177 self.active_window_mut().file_explorer_context_menu = None;
3178 Some(Ok(()))
3179 }
3180 _ => None,
3181 }
3182 }
3183
3184 pub(super) fn handle_file_explorer_context_menu_click(
3186 &mut self,
3187 col: u16,
3188 row: u16,
3189 ) -> Option<AnyhowResult<()>> {
3190 let frame_w = self.active_chrome().last_frame_width;
3192 let frame_h = self.active_chrome().last_frame_height;
3193 let clicked_item: Option<super::types::FileExplorerContextMenuItem> = {
3194 let menu = self.active_window().file_explorer_context_menu.as_ref()?;
3195 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3196 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3197 let menu_height = menu.height();
3198
3199 if col < menu_x
3200 || col >= menu_x + menu_width
3201 || row < menu_y
3202 || row >= menu_y + menu_height
3203 {
3204 self.active_window_mut().file_explorer_context_menu = None;
3205 return Some(Ok(()));
3206 }
3207
3208 if row == menu_y || row == menu_y + menu_height - 1 {
3209 return Some(Ok(()));
3210 }
3211
3212 let item_idx = (row - menu_y - 1) as usize;
3213 menu.items().get(item_idx).copied()
3214 };
3215
3216 self.active_window_mut().file_explorer_context_menu = None;
3217 if let Some(item) = clicked_item {
3218 self.execute_file_explorer_context_menu_action(item);
3219 }
3220 Some(Ok(()))
3221 }
3222
3223 fn execute_file_explorer_context_menu_action(
3224 &mut self,
3225 item: super::types::FileExplorerContextMenuItem,
3226 ) {
3227 use super::types::FileExplorerContextMenuItem;
3228 match item {
3229 FileExplorerContextMenuItem::NewFile => self.file_explorer_new_file(),
3230 FileExplorerContextMenuItem::NewDirectory => self.file_explorer_new_directory(),
3231 FileExplorerContextMenuItem::Rename => self.file_explorer_rename(),
3232 FileExplorerContextMenuItem::Cut => self.active_window_mut().file_explorer_cut(),
3233 FileExplorerContextMenuItem::Copy => self.active_window_mut().file_explorer_copy(),
3234 FileExplorerContextMenuItem::Paste => self.file_explorer_paste(),
3235 FileExplorerContextMenuItem::Duplicate => self.file_explorer_duplicate(),
3236 FileExplorerContextMenuItem::Delete => self.file_explorer_delete(),
3237 FileExplorerContextMenuItem::CopyFullPath => self.file_explorer_copy_path(false),
3238 FileExplorerContextMenuItem::CopyRelativePath => self.file_explorer_copy_path(true),
3239 }
3240 }
3241
3242 fn show_file_explorer_status_tooltip(&mut self, path: std::path::PathBuf, col: u16, row: u16) {
3244 use crate::view::popup::{Popup, PopupPosition};
3245 use ratatui::style::Style;
3246
3247 let is_directory = path.is_dir();
3248
3249 let decoration = self
3251 .active_window()
3252 .file_explorer_decoration_cache
3253 .direct_for_path(&path)
3254 .cloned();
3255
3256 let bubbled_decoration = if is_directory && decoration.is_none() {
3258 self.active_window()
3259 .file_explorer_decoration_cache
3260 .bubbled_for_path(&path)
3261 .cloned()
3262 } else {
3263 None
3264 };
3265
3266 let has_unsaved_changes = if is_directory {
3268 self.windows
3270 .get(&self.active_window)
3271 .map(|w| &w.buffers)
3272 .expect("active window present")
3273 .iter()
3274 .any(|(buffer_id, state)| {
3275 if state.buffer.is_modified() {
3276 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3277 {
3278 if let Some(file_path) = metadata.file_path() {
3279 return file_path.starts_with(&path);
3280 }
3281 }
3282 }
3283 false
3284 })
3285 } else {
3286 self.windows
3287 .get(&self.active_window)
3288 .map(|w| &w.buffers)
3289 .expect("active window present")
3290 .iter()
3291 .any(|(buffer_id, state)| {
3292 if state.buffer.is_modified() {
3293 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3294 {
3295 return metadata.file_path() == Some(&path);
3296 }
3297 }
3298 false
3299 })
3300 };
3301
3302 let mut lines: Vec<String> = Vec::new();
3304
3305 if let Some(decoration) = &decoration {
3306 let symbol = &decoration.symbol;
3307 let explanation = match symbol.as_str() {
3308 "U" => "Untracked - File is not tracked by git",
3309 "M" => "Modified - File has unstaged changes",
3310 "A" => "Added - File is staged for commit",
3311 "D" => "Deleted - File is staged for deletion",
3312 "R" => "Renamed - File has been renamed",
3313 "C" => "Copied - File has been copied",
3314 "!" => "Conflicted - File has merge conflicts",
3315 "●" => "Has changes - Contains modified files",
3316 _ => "Unknown status",
3317 };
3318 lines.push(format!("{} - {}", symbol, explanation));
3319 } else if bubbled_decoration.is_some() {
3320 lines.push("● - Contains modified files".to_string());
3321 } else if has_unsaved_changes {
3322 if is_directory {
3323 lines.push("● - Contains unsaved changes".to_string());
3324 } else {
3325 lines.push("● - Unsaved changes in editor".to_string());
3326 }
3327 } else {
3328 return; }
3330
3331 if is_directory {
3333 if let Some(modified_files) = self.get_modified_files_in_directory(&path) {
3335 lines.push(String::new()); lines.push("Modified files:".to_string());
3337 let resolved_path = path.canonicalize().unwrap_or_else(|_| path.clone());
3339 const MAX_FILES: usize = 8;
3340 for (i, file) in modified_files.iter().take(MAX_FILES).enumerate() {
3341 let display_name = file
3343 .strip_prefix(&resolved_path)
3344 .unwrap_or(file)
3345 .to_string_lossy()
3346 .to_string();
3347 lines.push(format!(" {}", display_name));
3348 if i == MAX_FILES - 1 && modified_files.len() > MAX_FILES {
3349 lines.push(format!(
3350 " ... and {} more",
3351 modified_files.len() - MAX_FILES
3352 ));
3353 break;
3354 }
3355 }
3356 }
3357 } else {
3358 if let Some(stats) = self.get_git_diff_stats(&path) {
3360 lines.push(String::new()); lines.push(stats);
3362 }
3363 }
3364
3365 if lines.is_empty() {
3366 return;
3367 }
3368
3369 let mut popup = Popup::text(lines, &*self.theme.read().unwrap());
3371 popup.title = Some("Git Status".to_string());
3372 popup.transient = true;
3373 popup.position = PopupPosition::Fixed { x: col, y: row + 1 };
3374 popup.width = 50;
3375 popup.max_height = 15;
3376 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
3377 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
3378
3379 let __buffer_id = self.active_buffer();
3381 if let Some(state) = self
3382 .windows
3383 .get_mut(&self.active_window)
3384 .map(|w| &mut w.buffers)
3385 .expect("active window present")
3386 .get_mut(&__buffer_id)
3387 {
3388 state.popups.show(popup);
3389 }
3390 }
3391
3392 fn dismiss_file_explorer_status_tooltip(&mut self) {
3394 let __buffer_id = self.active_buffer();
3396 if let Some(state) = self
3397 .windows
3398 .get_mut(&self.active_window)
3399 .map(|w| &mut w.buffers)
3400 .expect("active window present")
3401 .get_mut(&__buffer_id)
3402 {
3403 state.popups.dismiss_transient();
3404 }
3405 }
3406
3407 fn get_git_diff_stats(&self, path: &std::path::Path) -> Option<String> {
3409 use crate::services::process_hidden::HideWindow;
3410 use std::process::Command;
3411
3412 let output = Command::new("git")
3414 .args(["diff", "--numstat", "--"])
3415 .arg(path)
3416 .current_dir(self.working_dir())
3417 .hide_window()
3418 .output()
3419 .ok()?;
3420
3421 if !output.status.success() {
3422 return None;
3423 }
3424
3425 let stdout = String::from_utf8_lossy(&output.stdout);
3426 let line = stdout.lines().next()?;
3427 let parts: Vec<&str> = line.split('\t').collect();
3428
3429 if parts.len() >= 2 {
3430 let insertions = parts[0];
3431 let deletions = parts[1];
3432
3433 if insertions == "-" && deletions == "-" {
3435 return Some("Binary file changed".to_string());
3436 }
3437
3438 let ins: i32 = insertions.parse().unwrap_or(0);
3439 let del: i32 = deletions.parse().unwrap_or(0);
3440
3441 if ins > 0 || del > 0 {
3442 return Some(format!("+{} -{} lines", ins, del));
3443 }
3444 }
3445
3446 let staged_output = Command::new("git")
3448 .args(["diff", "--numstat", "--cached", "--"])
3449 .arg(path)
3450 .current_dir(self.working_dir())
3451 .hide_window()
3452 .output()
3453 .ok()?;
3454
3455 if staged_output.status.success() {
3456 let staged_stdout = String::from_utf8_lossy(&staged_output.stdout);
3457 if let Some(line) = staged_stdout.lines().next() {
3458 let parts: Vec<&str> = line.split('\t').collect();
3459 if parts.len() >= 2 {
3460 let insertions = parts[0];
3461 let deletions = parts[1];
3462
3463 if insertions == "-" && deletions == "-" {
3464 return Some("Binary file staged".to_string());
3465 }
3466
3467 let ins: i32 = insertions.parse().unwrap_or(0);
3468 let del: i32 = deletions.parse().unwrap_or(0);
3469
3470 if ins > 0 || del > 0 {
3471 return Some(format!("+{} -{} lines (staged)", ins, del));
3472 }
3473 }
3474 }
3475 }
3476
3477 None
3478 }
3479
3480 fn get_modified_files_in_directory(
3482 &self,
3483 dir_path: &std::path::Path,
3484 ) -> Option<Vec<std::path::PathBuf>> {
3485 use crate::services::process_hidden::HideWindow;
3486 use std::process::Command;
3487
3488 let resolved_path = dir_path
3490 .canonicalize()
3491 .unwrap_or_else(|_| dir_path.to_path_buf());
3492
3493 let output = Command::new("git")
3495 .args(["status", "--porcelain", "--"])
3496 .arg(&resolved_path)
3497 .current_dir(self.working_dir())
3498 .hide_window()
3499 .output()
3500 .ok()?;
3501
3502 if !output.status.success() {
3503 return None;
3504 }
3505
3506 let stdout = String::from_utf8_lossy(&output.stdout);
3507 let modified_files: Vec<std::path::PathBuf> = stdout
3508 .lines()
3509 .filter_map(|line| {
3510 if line.len() > 3 {
3513 let file_part = &line[3..];
3514 let file_name = if file_part.contains(" -> ") {
3516 file_part.split(" -> ").last().unwrap_or(file_part)
3517 } else {
3518 file_part
3519 };
3520 Some(self.working_dir().join(file_name))
3521 } else {
3522 None
3523 }
3524 })
3525 .collect();
3526
3527 if modified_files.is_empty() {
3528 None
3529 } else {
3530 Some(modified_files)
3531 }
3532 }
3533
3534 fn handle_floating_widget_panel_wheel(&mut self, col: u16, row: u16, delta: i32) -> bool {
3546 let inner = match self.floating_widget_panel.as_ref() {
3547 Some(fwp) => match fwp.last_inner_rect {
3548 Some(rect) => rect,
3549 None => return false,
3550 },
3551 None => return false,
3552 };
3553 if col < inner.x || col >= inner.x + inner.width {
3554 return false;
3555 }
3556 if row < inner.y || row >= inner.y + inner.height {
3557 return false;
3558 }
3559 self.handle_widget_panel_wheel(super::FLOATING_PANEL_BUFFER_ID, delta)
3560 }
3561
3562 fn try_widget_scrollbar_press(&mut self, col: u16, row: u16) -> bool {
3567 use crate::view::ui::scrollbar::ScrollbarState;
3568 let (panel_id, tracks) = match self.floating_widget_panel.as_ref() {
3569 Some(fwp) => (fwp.panel_id, fwp.scrollbar_tracks.clone()),
3570 None => return false,
3571 };
3572 for t in &tracks {
3573 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
3574 let pressed = self
3575 .floating_widget_panel
3576 .as_mut()
3577 .and_then(|fwp| fwp.scrollbar_mouse.press(state, t.rect, col, row));
3578 if let Some(new_offset) = pressed {
3579 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3580 fwp.scrollbar_drag_key = Some(t.list_key.clone());
3581 }
3582 self.apply_widget_scroll(panel_id, &t.list_key, new_offset, t.visible);
3583 return true;
3584 }
3585 }
3586 false
3587 }
3588
3589 fn try_widget_scrollbar_drag(&mut self, row: u16) -> bool {
3592 use crate::view::ui::scrollbar::ScrollbarState;
3593 let (panel_id, key) = match self.floating_widget_panel.as_ref() {
3594 Some(fwp) => match &fwp.scrollbar_drag_key {
3595 Some(k) => (fwp.panel_id, k.clone()),
3596 None => return false,
3597 },
3598 None => return false,
3599 };
3600 let track = self.floating_widget_panel.as_ref().and_then(|fwp| {
3603 fwp.scrollbar_tracks
3604 .iter()
3605 .find(|t| t.list_key == key)
3606 .cloned()
3607 });
3608 let Some(t) = track else {
3609 return false;
3610 };
3611 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
3612 let new_offset = self
3613 .floating_widget_panel
3614 .as_mut()
3615 .and_then(|fwp| fwp.scrollbar_mouse.drag(state, t.rect, row));
3616 if let Some(off) = new_offset {
3617 self.apply_widget_scroll(panel_id, &key, off, t.visible);
3618 }
3619 true
3620 }
3621
3622 pub(super) fn release_widget_scrollbar(&mut self) {
3624 if let Some(fwp) = self.floating_widget_panel.as_mut() {
3625 fwp.scrollbar_mouse.release();
3626 fwp.scrollbar_drag_key = None;
3627 }
3628 }
3629
3630 fn apply_widget_scroll(
3636 &mut self,
3637 panel_id: u64,
3638 list_key: &str,
3639 new_offset: usize,
3640 visible: usize,
3641 ) {
3642 let moved_sel = self.widget_registry.set_list_scroll(
3643 panel_id,
3644 list_key,
3645 new_offset as u32,
3646 visible as u32,
3647 );
3648 self.rerender_widget_panel(panel_id);
3649 if let Some(sel) = moved_sel {
3650 if self
3651 .plugin_manager
3652 .read()
3653 .unwrap()
3654 .has_hook_handlers("widget_event")
3655 {
3656 self.plugin_manager.read().unwrap().run_hook(
3657 "widget_event",
3658 crate::services::plugins::hooks::HookArgs::WidgetEvent {
3659 panel_id,
3660 widget_key: list_key.to_string(),
3661 event_type: "select".to_string(),
3662 payload: serde_json::json!({ "index": sel as i64 }),
3663 },
3664 );
3665 }
3666 }
3667 }
3668
3669 fn handle_floating_widget_click(&mut self, col: u16, row: u16) {
3672 if self.try_widget_scrollbar_press(col, row) {
3675 return;
3676 }
3677 let (panel_id, inner) = match self.floating_widget_panel.as_ref() {
3678 Some(fwp) => match fwp.last_inner_rect {
3679 Some(rect) => (fwp.panel_id, rect),
3680 None => return,
3681 },
3682 None => return,
3683 };
3684 if col < inner.x || col >= inner.x + inner.width {
3685 return;
3686 }
3687 if row < inner.y || row >= inner.y + inner.height {
3688 return;
3689 }
3690 let brow = (row - inner.y) as u32;
3691 let entries = self
3692 .floating_widget_panel
3693 .as_ref()
3694 .map(|f| f.entries.clone())
3695 .unwrap_or_default();
3696 let local_screen_col = (col - inner.x) as usize;
3697 let bcol = match entries.get(brow as usize) {
3698 Some(entry) => screen_col_to_byte(&entry.text, local_screen_col),
3699 None => return,
3700 };
3701 let (hit_payload, hit_event, hit_key, hit_kind) =
3702 match self
3703 .widget_registry
3704 .hit_test(super::FLOATING_PANEL_BUFFER_ID, brow, bcol as u32)
3705 {
3706 Some((_, hit)) => (
3707 hit.payload.clone(),
3708 hit.event_type.to_string(),
3709 hit.widget_key.clone(),
3710 hit.widget_kind,
3711 ),
3712 None => return,
3713 };
3714 if !hit_key.is_empty() {
3715 let tabbable = self
3716 .widget_registry
3717 .get(panel_id)
3718 .map(|p| p.tabbable.iter().any(|k| k == &hit_key))
3719 .unwrap_or(false);
3720 if tabbable {
3721 self.set_panel_focus_and_notify(panel_id, hit_key.clone());
3722 }
3723 self.rerender_widget_panel(panel_id);
3724 }
3725 let handled_specially = if hit_kind == "tree" && hit_event == "expand" {
3726 if let Some(item_key) = hit_payload.get("key").and_then(|v| v.as_str()) {
3727 self.handle_widget_tree_expand_toggle(panel_id, &hit_key, item_key);
3728 true
3729 } else {
3730 false
3731 }
3732 } else {
3733 false
3734 };
3735 if !handled_specially
3736 && self
3737 .plugin_manager
3738 .read()
3739 .unwrap()
3740 .has_hook_handlers("widget_event")
3741 {
3742 self.plugin_manager.read().unwrap().run_hook(
3743 "widget_event",
3744 crate::services::plugins::hooks::HookArgs::WidgetEvent {
3745 panel_id,
3746 widget_key: hit_key,
3747 event_type: hit_event,
3748 payload: hit_payload,
3749 },
3750 );
3751 }
3752 }
3753}
3754
3755fn screen_col_to_byte(text: &str, target_col: usize) -> usize {
3760 use unicode_width::UnicodeWidthChar;
3761 let mut byte = 0;
3762 let mut col = 0usize;
3763 for ch in text.chars() {
3764 let w = UnicodeWidthChar::width(ch).unwrap_or(0);
3765 if col + w > target_col {
3766 return byte;
3767 }
3768 col += w;
3769 byte += ch.len_utf8();
3770 }
3771 byte
3772}