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 fn dispatch_modal_mouse(
37 &mut self,
38 mouse_event: crossterm::event::MouseEvent,
39 is_double_click: bool,
40 ) -> Option<AnyhowResult<bool>> {
41 use crate::app::overlay::LayerKind;
42
43 let capturing_kind = self.overlay_layers().iter().find_map(|l| match l.kind {
46 LayerKind::Settings
47 | LayerKind::KeybindingEditor
48 | LayerKind::CalibrationWizard
49 | LayerKind::WorkspaceTrust
50 | LayerKind::FloatingModal => Some(l.kind),
51 _ => None,
52 })?;
53 Some(match capturing_kind {
54 LayerKind::KeybindingEditor => self.handle_keybinding_editor_mouse(mouse_event),
55 LayerKind::Settings => self.handle_settings_mouse(mouse_event, is_double_click),
56 LayerKind::CalibrationWizard => Ok(false),
60 LayerKind::WorkspaceTrust => self.handle_workspace_trust_mouse(mouse_event),
61 LayerKind::FloatingModal => self.handle_floating_modal_mouse(mouse_event),
69 _ => unreachable!("find_map only returns capturing kinds"),
70 })
71 }
72
73 fn handle_floating_modal_mouse(
81 &mut self,
82 mouse_event: crossterm::event::MouseEvent,
83 ) -> AnyhowResult<bool> {
84 use crossterm::event::{MouseButton, MouseEventKind};
85 let (col, row) = (mouse_event.column, mouse_event.row);
86 match mouse_event.kind {
87 MouseEventKind::Down(MouseButton::Left) => {
88 if self.floating_panel_is_anchored()
93 && !self.point_in_floating_panel(super::PanelSlot::Floating, col, row)
94 {
95 self.dismiss_floating_panel_with_cancel(super::PanelSlot::Floating);
96 return Ok(true);
97 }
98 self.handle_floating_widget_click(super::PanelSlot::Floating, col, row);
101 }
102 MouseEventKind::Drag(MouseButton::Left) => {
103 self.try_widget_scrollbar_drag(super::PanelSlot::Floating, row);
106 }
107 MouseEventKind::Up(MouseButton::Left) => {
108 self.release_widget_scrollbar();
109 }
110 MouseEventKind::ScrollUp => {
111 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, -3);
112 }
113 MouseEventKind::ScrollDown => {
114 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, 3);
115 }
116 _ => {}
119 }
120 Ok(true)
121 }
122
123 pub fn handle_mouse(
126 &mut self,
127 mouse_event: crossterm::event::MouseEvent,
128 ) -> AnyhowResult<bool> {
129 use crossterm::event::{MouseButton, MouseEventKind};
130
131 let col = mouse_event.column;
132 let row = mouse_event.row;
133
134 let (is_double_click, is_triple_click) = self.detect_multi_click(&mouse_event, col, row);
135
136 if let Some(result) = self.dispatch_modal_mouse(mouse_event, is_double_click) {
142 return result;
143 }
144
145 let mut needs_render = false;
147 if let Some(ref prompt) = self.active_window_mut().prompt {
148 if matches!(prompt.prompt_type, PromptType::LspRename { .. }) {
149 self.cancel_prompt();
150 needs_render = true;
151 }
152 }
153
154 let cursor_moved = self.active_window_mut().mouse_cursor_position != Some((col, row));
157 self.active_window_mut().mouse_cursor_position = Some((col, row));
158 if self.active_window_mut().gpm_active && cursor_moved {
159 needs_render = true;
160 }
161
162 tracing::trace!(
163 "handle_mouse: kind={:?}, col={}, row={}",
164 mouse_event.kind,
165 col,
166 row
167 );
168
169 let chrome_drag_active = self.dock_resizing || {
181 let ms = &self.active_window().mouse_state;
182 ms.dragging_separator.is_some() || ms.drag_start_explorer_width.is_some()
183 };
184 if !chrome_drag_active {
185 if let Some(result) =
186 self.active_window_mut()
187 .try_forward_mouse_to_terminal(col, row, mouse_event)
188 {
189 return result;
190 }
191 }
192
193 if let Some(result) = self.try_open_terminal_link(col, row, mouse_event) {
197 return result;
198 }
199
200 if self.active_window_mut().theme_info_popup.is_some() {
202 if let MouseEventKind::Down(MouseButton::Left) = mouse_event.kind {
203 if let Some((popup_rect, button_row_offset)) = self.theme_info_popup_rect() {
204 if in_rect(col, row, popup_rect) {
205 if let Some(offset) = button_row_offset {
209 let actual_button_row = popup_rect.y + offset;
210 if row == actual_button_row {
211 let key =
212 self.active_window_mut().theme_info_popup.as_ref().and_then(
213 |p| p.info.fg_key.clone().or_else(|| p.info.bg_key.clone()),
214 );
215 self.active_window_mut().theme_info_popup = None;
216 if let Some(key) = key {
217 self.fire_theme_inspect_hook(key);
218 }
219 return Ok(true);
220 }
221 }
222 return Ok(true);
224 }
225 }
226 self.active_window_mut().theme_info_popup = None;
228 needs_render = true;
229 }
230 }
231
232 match mouse_event.kind {
233 MouseEventKind::Down(MouseButton::Left) => {
234 if is_double_click || is_triple_click {
235 if let Some((buffer_id, byte_pos)) =
236 self.fold_toggle_line_at_screen_position(col, row)
237 {
238 self.active_window_mut()
239 .toggle_fold_at_byte(buffer_id, byte_pos);
240 needs_render = true;
241 return Ok(needs_render);
242 }
243 }
244 if is_triple_click {
245 self.handle_mouse_triple_click(col, row)?;
247 needs_render = true;
248 return Ok(needs_render);
249 }
250 if is_double_click {
251 self.handle_mouse_double_click(col, row)?;
253 needs_render = true;
254 return Ok(needs_render);
255 }
256 self.handle_mouse_click(col, row, mouse_event.modifiers)?;
257 needs_render = true;
258 }
259 MouseEventKind::Drag(MouseButton::Left) => {
260 self.handle_mouse_drag(col, row)?;
261 needs_render = true;
262 }
263 MouseEventKind::Up(MouseButton::Left) => {
264 if self.dock_resizing {
267 self.dock_resizing = false;
268 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
269 self.dock.as_ref().map(|f| f.placement)
270 {
271 self.dock_width = Some(width_cols);
272 }
273 return Ok(true);
274 }
275 let was_dragging_separator = self
277 .active_window_mut()
278 .mouse_state
279 .dragging_separator
280 .is_some();
281
282 if let Some(drag_state) = self.active_window_mut().mouse_state.dragging_tab.take() {
284 if drag_state.is_dragging() {
285 if let Some(drop_zone) = drag_state.drop_zone {
286 self.execute_tab_drop(
287 drag_state.buffer_id,
288 drag_state.source_split_id,
289 drop_zone,
290 );
291 }
292 }
293 }
294
295 self.release_widget_scrollbar();
297 self.clear_active_window_drag_state();
298
299 if was_dragging_separator {
302 self.relayout();
303 }
304
305 needs_render = true;
306 }
307 MouseEventKind::Moved => {
308 {
310 let content_rect = self
312 .active_layout()
313 .split_areas
314 .iter()
315 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
316 .map(|(_, _, rect, _, _, _)| *rect);
317
318 let (content_x, content_y) = content_rect.map(|r| (r.x, r.y)).unwrap_or((0, 0));
319
320 self.plugin_manager.read().unwrap().run_hook(
321 "mouse_move",
322 HookArgs::MouseMove {
323 column: col,
324 row,
325 content_x,
326 content_y,
327 },
328 );
329 }
330
331 let hover_changed = self.update_hover_target(col, row);
334 needs_render = needs_render || hover_changed;
335
336 let term_link_changed =
339 self.update_terminal_link_hover(col, row, mouse_event.modifiers);
340 needs_render = needs_render || term_link_changed;
341
342 if let Some((popup_rect, Some(button_row_offset))) = self.theme_info_popup_rect() {
346 let button_row = popup_rect.y + button_row_offset;
347 let new_highlighted = row == button_row
348 && col >= popup_rect.x
349 && col < popup_rect.x + popup_rect.width;
350 if let Some(ref mut popup) = self.active_window_mut().theme_info_popup {
351 if popup.button_highlighted != new_highlighted {
352 popup.button_highlighted = new_highlighted;
353 needs_render = true;
354 }
355 }
356 }
357
358 self.update_lsp_hover_state(col, row);
360
361 let now_over = self
369 .dock
370 .as_ref()
371 .map(|d| {
372 d.scrollbar_hover_zones.iter().any(|z| {
373 col >= z.x && col < z.x + z.width && row >= z.y && row < z.y + z.height
374 })
375 })
376 .unwrap_or(false);
377 if let Some(d) = self.dock.as_mut() {
378 if d.scrollbar_zone_hovered != now_over {
379 d.scrollbar_zone_hovered = now_over;
380 needs_render = true;
381 }
382 }
383 }
384 MouseEventKind::ScrollUp => {
385 self.handle_vertical_scroll(col, row, mouse_event.modifiers, -3)?;
386 needs_render = true;
387 }
388 MouseEventKind::ScrollDown => {
389 self.handle_vertical_scroll(col, row, mouse_event.modifiers, 3)?;
390 needs_render = true;
391 }
392 MouseEventKind::ScrollLeft => {
393 self.active_window_mut()
395 .handle_horizontal_scroll(col, row, -3)?;
396 needs_render = true;
397 }
398 MouseEventKind::ScrollRight => {
399 self.active_window_mut()
401 .handle_horizontal_scroll(col, row, 3)?;
402 needs_render = true;
403 }
404 MouseEventKind::Down(MouseButton::Right) => {
405 if self.overlay_prompt_active() {
409 needs_render = true;
410 } else if mouse_event
411 .modifiers
412 .contains(crossterm::event::KeyModifiers::CONTROL)
413 {
414 self.show_theme_info_popup(col, row)?;
416 needs_render = true;
417 } else {
418 self.handle_right_click(col, row)?;
420 needs_render = true;
421 }
422 }
423 _ => {
424 }
426 }
427
428 self.active_window_mut().mouse_state.last_position = Some((col, row));
429 Ok(needs_render)
430 }
431
432 fn detect_multi_click(
434 &mut self,
435 mouse_event: &crossterm::event::MouseEvent,
436 col: u16,
437 row: u16,
438 ) -> (bool, bool) {
439 use crossterm::event::{MouseButton, MouseEventKind};
440 if !matches!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left)) {
441 return (false, false);
442 }
443 let now = self.time_source.now();
444 let threshold = std::time::Duration::from_millis(self.config.editor.double_click_time_ms);
445 let is_consecutive = if let (Some(prev_time), Some(prev_pos)) = (
446 self.active_window_mut().previous_click_time,
447 self.active_window_mut().previous_click_position,
448 ) {
449 now.duration_since(prev_time) < threshold && prev_pos == (col, row)
450 } else {
451 false
452 };
453 if is_consecutive {
454 self.active_window_mut().click_count += 1;
455 } else {
456 self.active_window_mut().click_count = 1;
457 }
458 self.active_window_mut().previous_click_time = Some(now);
459 self.active_window_mut().previous_click_position = Some((col, row));
460 let is_triple = self.active_window_mut().click_count >= 3;
461 let is_double = self.active_window_mut().click_count == 2;
462 if is_triple {
463 self.active_window_mut().click_count = 0;
464 self.active_window_mut().previous_click_time = None;
465 self.active_window_mut().previous_click_position = None;
466 }
467 (is_double, is_triple)
468 }
469
470 fn handle_vertical_scroll(
473 &mut self,
474 col: u16,
475 row: u16,
476 modifiers: crossterm::event::KeyModifiers,
477 delta: i32,
478 ) -> AnyhowResult<()> {
479 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
480 self.active_window_mut()
481 .handle_horizontal_scroll(col, row, delta)?;
482 } else if self.handle_overlay_prompt_scroll(col, row, delta) {
483 } else if self.handle_prompt_scroll(delta) {
487 } else if self.is_file_open_active()
489 && self.is_mouse_over_file_browser(col, row)
490 && self.handle_file_open_scroll(delta)
491 {
492 } else if self.is_mouse_over_any_popup(col, row) {
494 self.scroll_popup(delta);
495 } else if self.floating_widget_panel.is_some() {
496 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, delta);
502 } else if self.dock.is_some()
503 && self.handle_floating_widget_panel_wheel(super::PanelSlot::Dock, col, row, delta)
504 {
505 } else if self
508 .active_window()
509 .split_at_position(col, row)
510 .map(|(_, buffer_id)| self.handle_widget_panel_wheel(buffer_id, delta))
511 .unwrap_or(false)
512 {
513 } else {
515 if self.active_window().terminal_mode
516 && self
517 .active_window()
518 .is_terminal_buffer(self.active_buffer())
519 {
520 let __b = self.active_buffer();
523 self.active_window_mut().set_terminal_interaction_mode(
524 __b,
525 crate::app::window::TerminalInteractionMode::Scrollback,
526 );
527 self.active_window_mut().sync_terminal_to_buffer(__b);
528 self.active_window_mut().terminal_mode = false;
529 self.active_window_mut().key_context =
530 crate::input::keybindings::KeyContext::Normal;
531 }
532 self.dismiss_transient_popups();
533 self.active_window_mut()
534 .handle_mouse_scroll(col, row, delta)?;
535 }
536 Ok(())
537 }
538
539 fn handle_overlay_prompt_scroll(&mut self, col: u16, row: u16, delta: i32) -> bool {
550 if !self.overlay_prompt_active() {
551 return false;
552 }
553 let preview_area = self.active_chrome().prompt_preview_area;
554 let results_visible = self
555 .active_chrome()
556 .prompt_results_area
557 .map(|r| r.height as usize)
558 .unwrap_or(0);
559 if let Some(preview) = preview_area {
560 if in_rect(col, row, preview) {
561 self.active_window_mut()
562 .scroll_overlay_preview_by_lines(delta);
563 return true;
564 }
565 }
566 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
567 prompt.scroll_results(delta, results_visible);
568 }
569 true
570 }
571
572 pub(super) fn update_hover_target(&mut self, col: u16, row: u16) -> bool {
575 let old_target = self.active_window_mut().mouse_state.hover_target.clone();
576 let new_target = self.compute_hover_target(col, row);
577 let changed = old_target != new_target;
578 self.active_window_mut().mouse_state.hover_target = new_target.clone();
579
580 if let Some(active_menu_idx) = self.menu_state.active_menu {
583 let all_menus: Vec<crate::config::Menu> = self
584 .menus
585 .menus
586 .iter()
587 .chain(self.menu_state.plugin_menus.iter())
588 .cloned()
589 .collect();
590 if let Some(HoverTarget::MenuBarItem(hovered_menu_idx)) = new_target.clone() {
591 if hovered_menu_idx != active_menu_idx {
592 self.menu_state.open_menu(hovered_menu_idx);
593 return true; }
595 }
596
597 if let Some(HoverTarget::MenuDropdownItem(_, item_idx)) = new_target.clone() {
599 if self.menu_state.submenu_path.first() == Some(&item_idx) {
602 tracing::trace!(
603 "menu hover: staying on submenu parent item_idx={}, submenu_path={:?}",
604 item_idx,
605 self.menu_state.submenu_path
606 );
607 return changed;
608 }
609
610 if !self.menu_state.submenu_path.is_empty() {
612 tracing::trace!(
613 "menu hover: clearing submenu_path={:?} for different item_idx={}",
614 self.menu_state.submenu_path,
615 item_idx
616 );
617 self.menu_state.submenu_path.clear();
618 self.menu_state.highlighted_item = Some(item_idx);
619 return true;
620 }
621
622 if let Some(menu) = all_menus.get(active_menu_idx) {
624 if let Some(crate::config::MenuItem::Submenu { items, .. }) =
625 menu.items.get(item_idx)
626 {
627 if !items.is_empty() {
628 tracing::trace!("menu hover: opening submenu at item_idx={}", item_idx);
629 self.menu_state.submenu_path.push(item_idx);
630 self.menu_state.highlighted_item = Some(0);
631 return true;
632 }
633 }
634 }
635 if self.menu_state.highlighted_item != Some(item_idx) {
637 self.menu_state.highlighted_item = Some(item_idx);
638 return true;
639 }
640 }
641
642 if let Some(HoverTarget::SubmenuItem(depth, item_idx)) = new_target {
644 if self.menu_state.submenu_path.len() > depth
648 && self.menu_state.submenu_path.get(depth) == Some(&item_idx)
649 {
650 tracing::trace!(
651 "menu hover: staying on nested submenu parent depth={}, item_idx={}, submenu_path={:?}",
652 depth,
653 item_idx,
654 self.menu_state.submenu_path
655 );
656 return changed;
657 }
658
659 if self.menu_state.submenu_path.len() > depth {
661 tracing::trace!(
662 "menu hover: truncating submenu_path={:?} to depth={} for item_idx={}",
663 self.menu_state.submenu_path,
664 depth,
665 item_idx
666 );
667 self.menu_state.submenu_path.truncate(depth);
668 }
669
670 if let Some(items) = self
672 .menu_state
673 .get_current_items(&all_menus, active_menu_idx)
674 {
675 if let Some(crate::config::MenuItem::Submenu {
677 items: sub_items, ..
678 }) = items.get(item_idx)
679 {
680 if !sub_items.is_empty()
681 && !self.menu_state.submenu_path.contains(&item_idx)
682 {
683 tracing::trace!(
684 "menu hover: opening nested submenu at depth={}, item_idx={}",
685 depth,
686 item_idx
687 );
688 self.menu_state.submenu_path.push(item_idx);
689 self.menu_state.highlighted_item = Some(0);
690 return true;
691 }
692 }
693 if self.menu_state.highlighted_item != Some(item_idx) {
695 self.menu_state.highlighted_item = Some(item_idx);
696 return true;
697 }
698 }
699 }
700 }
701
702 if let Some(HoverTarget::TabContextMenuItem(item_idx)) = new_target.clone() {
704 if let Some(ref mut menu) = self.active_window_mut().tab_context_menu {
705 if menu.highlighted != item_idx {
706 menu.highlighted = item_idx;
707 return true;
708 }
709 }
710 }
711
712 if let Some(&HoverTarget::FileExplorerContextMenuItem(item_idx)) = new_target.as_ref() {
713 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
714 if menu.highlighted != item_idx {
715 menu.highlighted = item_idx;
716 return true;
717 }
718 }
719 }
720
721 if let Some(HoverTarget::NewTabMenuItem(item_idx)) = new_target.clone() {
723 if let Some(ref mut menu) = self.active_window_mut().new_tab_menu {
724 if menu.highlighted != item_idx {
725 menu.highlighted = item_idx;
726 return true;
727 }
728 }
729 }
730
731 if old_target != new_target
734 && matches!(
735 old_target,
736 Some(HoverTarget::FileExplorerStatusIndicator(_))
737 )
738 {
739 self.dismiss_file_explorer_status_tooltip();
740 }
741
742 if let Some(HoverTarget::FileExplorerStatusIndicator(ref path)) = new_target {
743 if old_target != new_target {
745 self.show_file_explorer_status_tooltip(path.clone(), col, row);
746 return true;
747 }
748 }
749
750 changed
751 }
752
753 fn update_lsp_hover_state(&mut self, col: u16, row: u16) {
762 tracing::trace!(col, row, "update_lsp_hover_state: raw mouse position");
763
764 if self.active_window_mut().theme_info_popup.is_some()
768 || self.active_window_mut().tab_context_menu.is_some()
769 || self.active_window_mut().new_tab_menu.is_some()
770 || self
771 .active_window_mut()
772 .file_explorer_context_menu
773 .is_some()
774 || self.is_lsp_status_popup_open()
775 {
776 if self
777 .active_window_mut()
778 .mouse_state
779 .lsp_hover_state
780 .is_some()
781 {
782 self.active_window_mut().mouse_state.lsp_hover_state = None;
783 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
784 self.dismiss_transient_popups();
785 }
786 return;
787 }
788
789 if self.is_mouse_over_transient_popup(col, row) {
791 return;
792 }
793
794 let split_info = self
796 .active_layout()
797 .split_areas
798 .iter()
799 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
800 .map(|(split_id, buffer_id, content_rect, _, _, _)| {
801 (*split_id, *buffer_id, *content_rect)
802 });
803
804 let Some((split_id, buffer_id, content_rect)) = split_info else {
805 if self
807 .active_window_mut()
808 .mouse_state
809 .lsp_hover_state
810 .is_some()
811 {
812 self.active_window_mut().mouse_state.lsp_hover_state = None;
813 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
814 self.dismiss_transient_popups();
815 }
816 return;
817 };
818
819 let cached_mappings = self
821 .active_layout()
822 .view_line_mappings
823 .get(&split_id)
824 .cloned();
825 let gutter_width = self
826 .buffers()
827 .get(&buffer_id)
828 .map(|s| s.margins.left_total_width() as u16)
829 .unwrap_or(0);
830 let fallback = self
831 .buffers()
832 .get(&buffer_id)
833 .map(|s| s.buffer.len())
834 .unwrap_or(0);
835
836 let compose_width = self
838 .windows
839 .get(&self.active_window)
840 .and_then(|w| w.buffers.splits())
841 .map(|(_, vs)| vs)
842 .expect("active window must have a populated split layout")
843 .get(&split_id)
844 .and_then(|vs| vs.compose_width);
845
846 let Some(byte_pos) = super::click_geometry::screen_to_buffer_position(
848 col,
849 row,
850 content_rect,
851 gutter_width,
852 &cached_mappings,
853 fallback,
854 false, compose_width,
856 ) else {
857 if self
861 .active_window_mut()
862 .mouse_state
863 .lsp_hover_state
864 .is_some()
865 {
866 self.active_window_mut().mouse_state.lsp_hover_state = None;
867 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
868 }
869 return;
870 };
871
872 let content_col = col.saturating_sub(content_rect.x);
874 let text_col = content_col.saturating_sub(gutter_width) as usize;
875 let visual_row = row.saturating_sub(content_rect.y) as usize;
876
877 let line_info = cached_mappings
878 .as_ref()
879 .and_then(|mappings| mappings.get(visual_row))
880 .map(|line_mapping| {
881 (
882 line_mapping.visual_to_char.len(),
883 line_mapping.line_end_byte,
884 )
885 });
886
887 let is_past_line_end_or_empty = line_info
888 .map(|(line_len, _)| {
889 if line_len <= 1 {
891 return true;
892 }
893 text_col >= line_len
894 })
895 .unwrap_or(true);
897
898 tracing::trace!(
899 col,
900 row,
901 content_col,
902 text_col,
903 visual_row,
904 gutter_width,
905 byte_pos,
906 ?line_info,
907 is_past_line_end_or_empty,
908 "update_lsp_hover_state: position check"
909 );
910
911 if is_past_line_end_or_empty {
912 tracing::trace!(
913 "update_lsp_hover_state: mouse past line end or empty line, clearing hover"
914 );
915 if self
920 .active_window_mut()
921 .mouse_state
922 .lsp_hover_state
923 .is_some()
924 {
925 self.active_window_mut().mouse_state.lsp_hover_state = None;
926 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
927 }
928 return;
929 }
930
931 if let Some((start, end)) = self.active_window_mut().hover.symbol_range() {
933 if byte_pos >= start && byte_pos < end {
934 return;
936 }
937 }
938
939 if let Some((old_pos, _, _, _)) = self.active_window_mut().mouse_state.lsp_hover_state {
941 if old_pos == byte_pos {
942 return;
944 }
945 }
951
952 self.active_window_mut().mouse_state.lsp_hover_state =
954 Some((byte_pos, std::time::Instant::now(), col, row));
955 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
956 }
957
958 fn is_mouse_over_transient_popup(&self, col: u16, row: u16) -> bool {
960 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
961 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
962 hit_tester.is_over_transient_popup(col, row)
963 }
964
965 fn is_mouse_over_any_popup(&self, col: u16, row: u16) -> bool {
967 for (_, popup_area, _, _, _) in &self.active_chrome().global_popup_areas {
970 if in_rect(col, row, *popup_area) {
971 return true;
972 }
973 }
974 if let Some(outer) = self.active_chrome().suggestions_outer_area {
978 if in_rect(col, row, outer) {
979 return true;
980 }
981 }
982 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
983 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
984 hit_tester.is_over_popup(col, row)
985 }
986
987 fn is_mouse_over_file_browser(&self, col: u16, row: u16) -> bool {
989 self.active_window()
990 .file_browser_layout
991 .as_ref()
992 .is_some_and(|layout| layout.contains(col, row))
993 }
994
995 fn compute_hover_target(&self, col: u16, row: u16) -> Option<HoverTarget> {
1000 self.hover_target_in_floating_overlays(col, row)
1001 .or_else(|| self.hover_target_in_chrome(col, row))
1002 }
1003
1004 fn hover_target_in_floating_overlays(&self, col: u16, row: u16) -> Option<HoverTarget> {
1008 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
1009 let (menu_x, menu_y) = menu.clamped_position(
1010 self.active_chrome().last_frame.width,
1011 self.active_chrome().last_frame.height,
1012 );
1013 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
1014 let menu_height = menu.height();
1015
1016 if col >= menu_x
1017 && col < menu_x + menu_width
1018 && row > menu_y
1019 && row < menu_y + menu_height - 1
1020 {
1021 let item_idx = (row - menu_y - 1) as usize;
1022 if item_idx < menu.items().len() {
1023 return Some(HoverTarget::FileExplorerContextMenuItem(item_idx));
1024 }
1025 }
1026 }
1027
1028 if let Some(ref menu) = self.active_window().new_tab_menu {
1030 let menu_x = menu.position.0;
1031 let menu_y = menu.position.1;
1032 let menu_width = super::types::NEW_TAB_MENU_WIDTH;
1033 let items = super::types::NewTabMenuItem::all();
1034 let menu_height = items.len() as u16 + 2;
1035
1036 if col >= menu_x
1037 && col < menu_x + menu_width
1038 && row > menu_y
1039 && row < menu_y + menu_height - 1
1040 {
1041 let item_idx = (row - menu_y - 1) as usize;
1042 if item_idx < items.len() {
1043 return Some(HoverTarget::NewTabMenuItem(item_idx));
1044 }
1045 }
1046 }
1047
1048 if let Some(ref menu) = self.active_window().tab_context_menu {
1050 let menu_x = menu.position.0;
1051 let menu_y = menu.position.1;
1052 let menu_width = 22u16;
1053 let items = super::types::TabContextMenuItem::all();
1054 let menu_height = items.len() as u16 + 2;
1055
1056 if col >= menu_x
1057 && col < menu_x + menu_width
1058 && row > menu_y
1059 && row < menu_y + menu_height - 1
1060 {
1061 let item_idx = (row - menu_y - 1) as usize;
1062 if item_idx < items.len() {
1063 return Some(HoverTarget::TabContextMenuItem(item_idx));
1064 }
1065 }
1066 }
1067
1068 if let Some((inner_rect, start_idx, _visible_count, total_count)) =
1070 &self.active_chrome().suggestions_area
1071 {
1072 if in_rect(col, row, *inner_rect) {
1073 let relative_row = (row - inner_rect.y) as usize;
1074 let item_idx = start_idx + relative_row;
1075
1076 if item_idx < *total_count {
1077 return Some(HoverTarget::SuggestionItem(item_idx));
1078 }
1079 }
1080 }
1081
1082 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
1085 self.active_chrome().popup_areas.iter().rev()
1086 {
1087 if in_rect(col, row, *inner_rect) && *num_items > 0 {
1088 let relative_row = (row - inner_rect.y) as usize;
1090 let item_idx = scroll_offset + relative_row;
1091
1092 if item_idx < *num_items {
1093 return Some(HoverTarget::PopupListItem(*popup_idx, item_idx));
1094 }
1095 }
1096 }
1097
1098 if self.is_file_open_active() {
1100 if let Some(hover) = self.compute_file_browser_hover(col, row) {
1101 return Some(hover);
1102 }
1103 }
1104
1105 None
1106 }
1107
1108 fn hover_target_in_chrome(&self, col: u16, row: u16) -> Option<HoverTarget> {
1112 if self.active_window().menu_bar_visible {
1115 if let Some(ref menu_layout) = self.active_chrome().menu_layout {
1116 if let Some(menu_idx) = menu_layout.menu_at(col, row) {
1117 return Some(HoverTarget::MenuBarItem(menu_idx));
1118 }
1119 }
1120 }
1121
1122 if let Some(active_idx) = self.menu_state.active_menu {
1124 if let Some(hover) = self.compute_menu_dropdown_hover(col, row, active_idx) {
1125 return Some(hover);
1126 }
1127 }
1128
1129 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1131 let close_button_x = explorer_area.x + explorer_area.width.saturating_sub(3);
1133 if row == explorer_area.y
1134 && col >= close_button_x
1135 && col < explorer_area.x + explorer_area.width
1136 {
1137 return Some(HoverTarget::FileExplorerCloseButton);
1138 }
1139
1140 let content_start_y = explorer_area.y + 1; let content_end_y = explorer_area.y + explorer_area.height.saturating_sub(1); let content_width = explorer_area.width.saturating_sub(3) as usize;
1144
1145 if row >= content_start_y && row < content_end_y {
1146 if let Some(explorer) = self.file_explorer().as_ref() {
1148 let relative_row = row.saturating_sub(content_start_y) as usize;
1149 let scroll_offset = explorer.get_scroll_offset();
1150 let item_index = relative_row + scroll_offset;
1151 let display_nodes = explorer.get_display_nodes();
1152
1153 if item_index < display_nodes.len() {
1154 let (node_id, indent) = display_nodes[item_index];
1155 if let Some(node) = explorer.tree().get_node(node_id) {
1156 let theme = self.theme.read().unwrap();
1157 let neutral_fg = if node
1158 .entry
1159 .metadata
1160 .as_ref()
1161 .map(|m| m.is_hidden)
1162 .unwrap_or(false)
1163 {
1164 theme.line_number_fg
1165 } else if node.entry.is_symlink() {
1166 theme.syntax_type
1167 } else if node.is_dir() {
1168 theme.syntax_keyword
1169 } else {
1170 theme.editor_fg
1171 };
1172 let slot_resolver = self.file_explorer_slot_resolver();
1173 let slot_context = crate::view::file_tree::ExplorerSlotContext {
1174 path: &node.entry.path,
1175 is_dir: node.is_dir(),
1176 has_unsaved: self.file_explorer_node_has_unsaved_changes(
1177 &node.entry.path,
1178 node.is_dir(),
1179 ),
1180 is_symlink: node.entry.is_symlink(),
1181 is_hidden: node
1182 .entry
1183 .metadata
1184 .as_ref()
1185 .map(|m| m.is_hidden)
1186 .unwrap_or(false),
1187 decorations: &self.active_window().file_explorer_decoration_cache,
1188 slot_overrides: &self
1189 .active_window()
1190 .file_explorer_slot_override_cache,
1191 theme: &theme,
1192 neutral_fg,
1193 };
1194 let slot_resolution = slot_resolver.resolve(&slot_context);
1195 if let Some((slot_start, slot_end)) = crate::view::ui::file_explorer::FileExplorerRenderer::trailing_slot_screen_bounds(
1196 explorer,
1197 node_id,
1198 indent,
1199 content_width,
1200 &slot_resolution,
1201 &self.config.file_explorer.tree_indicator_collapsed,
1202 &self.config.file_explorer.tree_indicator_expanded,
1203 explorer_area,
1204 ) {
1205 if col >= slot_start && col < slot_end {
1206 return Some(HoverTarget::FileExplorerStatusIndicator(
1207 node.entry.path.clone(),
1208 ));
1209 }
1210 }
1211 }
1212 }
1213 }
1214 }
1215
1216 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1219 if col == border_x
1220 && row >= explorer_area.y
1221 && row < explorer_area.y + explorer_area.height
1222 {
1223 return Some(HoverTarget::FileExplorerBorder);
1224 }
1225 }
1226
1227 for (split_id, direction, sep_x, sep_y, sep_length) in &self.active_layout().separator_areas
1229 {
1230 let is_on_separator = match direction {
1231 SplitDirection::Horizontal => {
1232 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
1233 }
1234 SplitDirection::Vertical => {
1235 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
1236 }
1237 };
1238
1239 if is_on_separator {
1240 return Some(HoverTarget::SplitSeparator(*split_id, *direction));
1241 }
1242 }
1243
1244 for (split_id, btn_row, start_col, end_col) in &self.active_layout().close_split_areas {
1247 if row == *btn_row && col >= *start_col && col < *end_col {
1248 return Some(HoverTarget::CloseSplitButton(*split_id));
1249 }
1250 }
1251
1252 for (split_id, btn_row, start_col, end_col) in &self.active_layout().maximize_split_areas {
1253 if row == *btn_row && col >= *start_col && col < *end_col {
1254 return Some(HoverTarget::MaximizeSplitButton(*split_id));
1255 }
1256 }
1257
1258 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
1259 match tab_layout.hit_test(col, row) {
1260 Some(TabHit::CloseButton(target)) => {
1261 return Some(HoverTarget::TabCloseButton(target, *split_id));
1262 }
1263 Some(TabHit::TabName(target)) => {
1264 return Some(HoverTarget::TabName(target, *split_id));
1265 }
1266 Some(TabHit::ScrollLeft)
1267 | Some(TabHit::ScrollRight)
1268 | Some(TabHit::BarBackground)
1269 | Some(TabHit::NewTabButton)
1270 | None => {}
1271 }
1272 }
1273
1274 for (split_id, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1276 &self.active_layout().split_areas
1277 {
1278 if in_rect(col, row, *scrollbar_rect) {
1279 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
1280 let is_on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
1281
1282 if is_on_thumb {
1283 return Some(HoverTarget::ScrollbarThumb(*split_id));
1284 } else {
1285 return Some(HoverTarget::ScrollbarTrack(*split_id, relative_row as u16));
1286 }
1287 }
1288 }
1289
1290 if let Some((status_row, _status_x, _status_width)) = self.active_chrome().status_bar.area {
1293 if row == status_row {
1294 for (id, indicator_row, start, end) in &self.active_chrome().status_bar.clickable {
1295 if row == *indicator_row && col >= *start && col < *end {
1296 return Some(HoverTarget::StatusBarClickable(*id));
1297 }
1298 }
1299 }
1300 }
1301
1302 if let Some(ref layout) = self.active_chrome().search_options_layout {
1304 use crate::view::ui::status_bar::SearchOptionsHover;
1305 if let Some(hover) = layout.checkbox_at(col, row) {
1306 return Some(match hover {
1307 SearchOptionsHover::CaseSensitive => HoverTarget::SearchOptionCaseSensitive,
1308 SearchOptionsHover::WholeWord => HoverTarget::SearchOptionWholeWord,
1309 SearchOptionsHover::Regex => HoverTarget::SearchOptionRegex,
1310 SearchOptionsHover::ConfirmEach => HoverTarget::SearchOptionConfirmEach,
1311 SearchOptionsHover::None => return None,
1312 });
1313 }
1314 }
1315
1316 None
1317 }
1318
1319 pub(super) fn handle_mouse_double_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1322 tracing::debug!("handle_mouse_double_click at col={}, row={}", col, row);
1323
1324 if let Some(r) = self.handle_click_suggestions_confirm(col, row) {
1328 return r;
1329 }
1330
1331 if self.overlay_prompt_active() {
1334 return Ok(());
1335 }
1336
1337 if self.is_mouse_over_any_popup(col, row) {
1339 return Ok(());
1341 } else {
1342 self.dismiss_transient_popups();
1344 }
1345
1346 if self.handle_file_open_double_click(col, row) {
1348 return Ok(());
1349 }
1350
1351 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1353 if col >= explorer_area.x
1354 && col < explorer_area.x + explorer_area.width
1355 && row > explorer_area.y && row < explorer_area.y + explorer_area.height
1357 {
1358 self.file_explorer_open_file()?;
1360 return Ok(());
1361 }
1362 }
1363
1364 let split_areas = self.active_layout().split_areas.clone();
1366 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1367 &split_areas
1368 {
1369 if in_rect(col, row, *content_rect) {
1370 if self.active_window().is_terminal_buffer(*buffer_id) {
1372 self.active_window_mut().key_context =
1373 crate::input::keybindings::KeyContext::Terminal;
1374 return Ok(());
1376 }
1377
1378 self.active_window_mut().key_context =
1379 crate::input::keybindings::KeyContext::Normal;
1380
1381 self.handle_editor_double_click(col, row, *split_id, *buffer_id, *content_rect)?;
1383 return Ok(());
1384 }
1385 }
1386
1387 Ok(())
1388 }
1389
1390 fn handle_editor_double_click(
1392 &mut self,
1393 col: u16,
1394 row: u16,
1395 split_id: LeafId,
1396 buffer_id: BufferId,
1397 content_rect: ratatui::layout::Rect,
1398 ) -> AnyhowResult<()> {
1399 use crate::model::event::Event;
1400
1401 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1405 return Ok(());
1406 }
1407
1408 self.focus_split(split_id, buffer_id);
1410
1411 let cached_mappings = self
1413 .active_layout()
1414 .view_line_mappings
1415 .get(&split_id)
1416 .cloned();
1417
1418 let leaf_id = split_id;
1420 let fallback = self
1421 .windows
1422 .get(&self.active_window)
1423 .and_then(|w| w.buffers.splits())
1424 .map(|(_, vs)| vs)
1425 .expect("active window must have a populated split layout")
1426 .get(&leaf_id)
1427 .map(|vs| vs.viewport.top_byte)
1428 .unwrap_or(0);
1429
1430 let compose_width = self
1432 .windows
1433 .get(&self.active_window)
1434 .and_then(|w| w.buffers.splits())
1435 .map(|(_, vs)| vs)
1436 .expect("active window must have a populated split layout")
1437 .get(&leaf_id)
1438 .and_then(|vs| vs.compose_width);
1439
1440 let gutter_width = self
1444 .active_window()
1445 .buffers
1446 .get(&buffer_id)
1447 .map(|s| s.margins.left_total_width() as u16)
1448 .unwrap_or(0);
1449
1450 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1451 col,
1452 row,
1453 content_rect,
1454 gutter_width,
1455 &cached_mappings,
1456 fallback,
1457 true, compose_width,
1459 ) else {
1460 return Ok(());
1461 };
1462
1463 let primary_cursor_id = self
1464 .active_window()
1465 .buffers
1466 .splits()
1467 .and_then(|(_, vs)| vs.get(&leaf_id))
1468 .map(|vs| vs.cursors.primary_id())
1469 .unwrap_or(CursorId(0));
1470 let event = Event::MoveCursor {
1471 cursor_id: primary_cursor_id,
1472 old_position: 0,
1473 new_position: target_position,
1474 old_anchor: None,
1475 new_anchor: None,
1476 old_sticky_column: 0,
1477 new_sticky_column: 0,
1478 };
1479
1480 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1481 event_log.append(event.clone());
1482 }
1483 self.active_window_mut()
1484 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1485
1486 self.handle_action(Action::SelectWord)?;
1488
1489 if let Some(cursor) = self
1491 .windows
1492 .get(&self.active_window)
1493 .and_then(|w| w.buffers.splits())
1494 .map(|(_, vs)| vs)
1495 .expect("active window must have a populated split layout")
1496 .get(&leaf_id)
1497 .map(|vs| vs.cursors.primary())
1498 {
1499 let sel_start = cursor.selection_start();
1502 let sel_end = cursor.selection_end();
1503 self.active_window_mut().mouse_state.dragging_text_selection = true;
1504 self.active_window_mut().mouse_state.drag_selection_split = Some(split_id);
1505 self.active_window_mut().mouse_state.drag_selection_anchor = Some(sel_start);
1506 self.active_window_mut().mouse_state.drag_selection_by_words = true;
1507 self.active_window_mut().mouse_state.drag_selection_word_end = Some(sel_end);
1508 }
1509
1510 Ok(())
1511 }
1512 pub(super) fn handle_mouse_triple_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1515 tracing::debug!("handle_mouse_triple_click at col={}, row={}", col, row);
1516
1517 if self.overlay_prompt_active() {
1520 return Ok(());
1521 }
1522
1523 if self.is_mouse_over_any_popup(col, row) {
1525 return Ok(());
1526 } else {
1527 self.dismiss_transient_popups();
1528 }
1529
1530 let split_areas = self.active_layout().split_areas.clone();
1532 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1533 &split_areas
1534 {
1535 if in_rect(col, row, *content_rect) {
1536 if self.active_window().is_terminal_buffer(*buffer_id) {
1537 return Ok(());
1538 }
1539
1540 self.active_window_mut().key_context =
1541 crate::input::keybindings::KeyContext::Normal;
1542
1543 self.handle_editor_triple_click(col, row, *split_id, *buffer_id, *content_rect)?;
1546 return Ok(());
1547 }
1548 }
1549
1550 Ok(())
1551 }
1552
1553 fn handle_editor_triple_click(
1555 &mut self,
1556 col: u16,
1557 row: u16,
1558 split_id: LeafId,
1559 buffer_id: BufferId,
1560 content_rect: ratatui::layout::Rect,
1561 ) -> AnyhowResult<()> {
1562 use crate::model::event::Event;
1563
1564 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1565 return Ok(());
1566 }
1567
1568 self.focus_split(split_id, buffer_id);
1570
1571 let cached_mappings = self
1573 .active_layout()
1574 .view_line_mappings
1575 .get(&split_id)
1576 .cloned();
1577
1578 let leaf_id = split_id;
1579 let fallback = self
1580 .windows
1581 .get(&self.active_window)
1582 .and_then(|w| w.buffers.splits())
1583 .map(|(_, vs)| vs)
1584 .expect("active window must have a populated split layout")
1585 .get(&leaf_id)
1586 .map(|vs| vs.viewport.top_byte)
1587 .unwrap_or(0);
1588
1589 let compose_width = self
1591 .windows
1592 .get(&self.active_window)
1593 .and_then(|w| w.buffers.splits())
1594 .map(|(_, vs)| vs)
1595 .expect("active window must have a populated split layout")
1596 .get(&leaf_id)
1597 .and_then(|vs| vs.compose_width);
1598
1599 let gutter_width = self
1603 .active_window()
1604 .buffers
1605 .get(&buffer_id)
1606 .map(|s| s.margins.left_total_width() as u16)
1607 .unwrap_or(0);
1608
1609 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1610 col,
1611 row,
1612 content_rect,
1613 gutter_width,
1614 &cached_mappings,
1615 fallback,
1616 true,
1617 compose_width,
1618 ) else {
1619 return Ok(());
1620 };
1621
1622 let primary_cursor_id = self
1623 .active_window()
1624 .buffers
1625 .splits()
1626 .and_then(|(_, vs)| vs.get(&leaf_id))
1627 .map(|vs| vs.cursors.primary_id())
1628 .unwrap_or(CursorId(0));
1629 let event = Event::MoveCursor {
1630 cursor_id: primary_cursor_id,
1631 old_position: 0,
1632 new_position: target_position,
1633 old_anchor: None,
1634 new_anchor: None,
1635 old_sticky_column: 0,
1636 new_sticky_column: 0,
1637 };
1638
1639 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1640 event_log.append(event.clone());
1641 }
1642 self.active_window_mut()
1643 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1644
1645 self.handle_action(Action::SelectLine)?;
1647
1648 Ok(())
1649 }
1650
1651 pub(super) fn overlay_prompt_active(&self) -> bool {
1659 self.active_window()
1660 .prompt
1661 .as_ref()
1662 .is_some_and(|p| p.overlay)
1663 }
1664
1665 pub(super) fn handle_mouse_click(
1666 &mut self,
1667 col: u16,
1668 row: u16,
1669 modifiers: crossterm::event::KeyModifiers,
1670 ) -> AnyhowResult<()> {
1671 if self.floating_widget_panel.is_some() {
1675 self.handle_floating_widget_click(super::PanelSlot::Floating, col, row);
1676 return Ok(());
1677 }
1678 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
1683 self.dock.as_ref().map(|f| f.placement)
1684 {
1685 if col == width_cols.saturating_sub(1) {
1686 self.dock_resizing = true;
1687 return Ok(());
1688 }
1689 }
1690 if let Some((super::PanelPlacement::LeftDock { width_cols }, focused)) =
1694 self.dock.as_ref().map(|f| (f.placement, f.focused))
1695 {
1696 if col < width_cols {
1697 tracing::debug!(
1698 target: "fresh::dock",
1699 col,
1700 row,
1701 width_cols,
1702 focused,
1703 "handle_mouse_click: click in dock column"
1704 );
1705 if !focused {
1706 self.refocus_floating_panel(super::PanelSlot::Dock);
1715 }
1716 self.handle_floating_widget_click(super::PanelSlot::Dock, col, row);
1717 return Ok(());
1718 }
1719 if focused {
1720 tracing::debug!(
1721 target: "fresh::dock",
1722 col,
1723 row,
1724 width_cols,
1725 "handle_mouse_click: click outside dock — blurring"
1726 );
1727 self.blur_floating_panel(super::PanelSlot::Dock);
1728 }
1729 }
1730 if let Some(r) = self.handle_click_context_menus(col, row) {
1731 return r;
1732 }
1733 if !self.is_mouse_over_any_popup(col, row) {
1734 self.dismiss_transient_popups();
1735 }
1736 if let Some(r) = self.handle_click_suggestions(col, row) {
1737 return r;
1738 }
1739 if let Some(r) = self.handle_click_prompt_scrollbar(col, row) {
1740 return r;
1741 }
1742 if let Some(r) = self.handle_click_popup_scrollbar(col, row) {
1743 return r;
1744 }
1745 if let Some(r) = self.handle_click_global_popups(col, row) {
1746 return r;
1747 }
1748 if let Some(r) = self.handle_click_buffer_popups(col, row) {
1749 return r;
1750 }
1751 if self.is_mouse_over_any_popup(col, row) {
1752 return Ok(());
1753 }
1754 if self.is_file_open_active() && self.handle_file_open_click(col, row) {
1755 return Ok(());
1756 }
1757 if let Some(r) = self.handle_click_menu_bar(col, row) {
1758 return r;
1759 }
1760 if let Some(r) = self.handle_click_file_explorer_area(col, row) {
1761 return r;
1762 }
1763 if let Some(r) = self.handle_click_scrollbar(col, row) {
1764 return r;
1765 }
1766 if let Some(r) = self.handle_click_horizontal_scrollbar(col, row) {
1767 return r;
1768 }
1769 if let Some(r) = self.handle_click_status_bar(col, row) {
1770 return r;
1771 }
1772 if let Some(r) = self.handle_click_search_options(col, row) {
1773 return r;
1774 }
1775 if let Some(r) = self.handle_click_split_separator(col, row) {
1776 return r;
1777 }
1778 if let Some(r) = self.handle_click_split_controls(col, row) {
1779 return r;
1780 }
1781 if let Some(r) = self.handle_click_tab_bar(col, row) {
1782 return r;
1783 }
1784
1785 if self.overlay_prompt_active() {
1792 let hit = self
1793 .active_chrome()
1794 .prompt_toolbar_hits
1795 .iter()
1796 .find(|(_, r)| in_rect(col, row, *r))
1797 .map(|(k, _)| k.clone());
1798 if let Some(widget_key) = hit {
1799 if let Some(p) = self.active_window_mut().prompt.as_mut() {
1803 p.toolbar_focus = Some(widget_key.clone());
1804 }
1805 self.toggle_overlay_toolbar_widget(&widget_key);
1806 }
1807 return Ok(());
1808 }
1809
1810 tracing::debug!(
1812 "handle_mouse_click: checking {} split_areas for click at ({}, {})",
1813 self.active_layout().split_areas.len(),
1814 col,
1815 row
1816 );
1817 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1818 &self.active_layout().split_areas
1819 {
1820 tracing::debug!(
1821 " split_id={:?}, content_rect=({}, {}, {}x{})",
1822 split_id,
1823 content_rect.x,
1824 content_rect.y,
1825 content_rect.width,
1826 content_rect.height
1827 );
1828 if in_rect(col, row, *content_rect) {
1829 tracing::debug!(" -> HIT! calling handle_editor_click");
1831 self.handle_editor_click(
1832 col,
1833 row,
1834 *split_id,
1835 *buffer_id,
1836 *content_rect,
1837 modifiers,
1838 )?;
1839 return Ok(());
1840 }
1841 }
1842 tracing::debug!(" -> No split area hit");
1843
1844 Ok(())
1845 }
1846
1847 fn handle_click_context_menus(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1851 if self
1852 .active_window_mut()
1853 .file_explorer_context_menu
1854 .is_some()
1855 {
1856 if let Some(result) = self.handle_file_explorer_context_menu_click(col, row) {
1857 return Some(result);
1858 }
1859 }
1860 if self.active_window_mut().tab_context_menu.is_some() {
1861 if let Some(result) = self.handle_tab_context_menu_click(col, row) {
1862 return Some(result);
1863 }
1864 }
1865 if self.active_window_mut().new_tab_menu.is_some() {
1866 if let Some(result) = self.handle_new_tab_menu_click(col, row) {
1867 return Some(result);
1868 }
1869 }
1870 None
1871 }
1872
1873 fn suggestion_at(&self, col: u16, row: u16) -> Option<usize> {
1877 let (inner_rect, start_idx, _visible_count, total_count) =
1878 self.active_chrome().suggestions_area?;
1879 if col < inner_rect.x
1880 || col >= inner_rect.x + inner_rect.width
1881 || row < inner_rect.y
1882 || row >= inner_rect.y + inner_rect.height
1883 {
1884 return None;
1885 }
1886 let relative_row = (row - inner_rect.y) as usize;
1887 let item_idx = start_idx + relative_row;
1888 if item_idx < total_count {
1889 Some(item_idx)
1890 } else {
1891 None
1892 }
1893 }
1894
1895 fn handle_click_suggestions(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1896 let item_idx = self.suggestion_at(col, row)?;
1897 let prompt = self.active_window_mut().prompt.as_mut()?;
1898 prompt.selected_suggestion = Some(item_idx);
1899 let confirms = prompt.prompt_type.click_confirms();
1900 if !confirms {
1901 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1905 prompt.input = suggestion.get_value().to_string();
1906 prompt.cursor_pos = prompt.input.len();
1907 }
1908 }
1909 if confirms {
1910 return Some(self.handle_action(Action::PromptConfirm));
1911 }
1912 Some(Ok(()))
1913 }
1914
1915 fn handle_click_suggestions_confirm(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1919 let item_idx = self.suggestion_at(col, row)?;
1920 let prompt = self.active_window_mut().prompt.as_mut()?;
1921 prompt.selected_suggestion = Some(item_idx);
1922 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1923 prompt.input = suggestion.get_value().to_string();
1924 prompt.cursor_pos = prompt.input.len();
1925 }
1926 Some(self.handle_action(Action::PromptConfirm))
1927 }
1928
1929 fn handle_click_prompt_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1935 use crate::view::ui::scrollbar::ScrollbarState;
1936 let sb_rect = self.active_chrome().suggestions_scrollbar_rect?;
1937 if col < sb_rect.x
1938 || col >= sb_rect.x + sb_rect.width
1939 || row < sb_rect.y
1940 || row >= sb_rect.y + sb_rect.height
1941 {
1942 return None;
1943 }
1944 let suggestions_area_visible = self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
1952 let active_window_id = self.active_window;
1953 let prompt = self
1954 .windows
1955 .get_mut(&active_window_id)
1956 .and_then(|w| w.prompt.as_mut())?;
1957 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
1958 let total = prompt.suggestions.len();
1959 let track_height = sb_rect.height as usize;
1960 let click_row = row.saturating_sub(sb_rect.y) as usize;
1961 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
1962 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
1963 self.active_window_mut()
1966 .mouse_state
1967 .dragging_prompt_scrollbar = true;
1968 Some(Ok(()))
1969 }
1970
1971 fn handle_click_popup_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1972 let scrollbar_info: Option<(usize, i32)> =
1974 self.active_chrome().popup_areas.iter().rev().find_map(
1975 |(popup_idx, _popup_rect, inner_rect, _scroll, _n, scrollbar_rect, total_lines)| {
1976 let sb_rect = scrollbar_rect.as_ref()?;
1977 if col >= sb_rect.x
1978 && col < sb_rect.x + sb_rect.width
1979 && row >= sb_rect.y
1980 && row < sb_rect.y + sb_rect.height
1981 {
1982 let relative_row = (row - sb_rect.y) as usize;
1983 let track_height = sb_rect.height as usize;
1984 let visible_lines = inner_rect.height as usize;
1985 if track_height > 0 && *total_lines > visible_lines {
1986 let max_scroll = total_lines.saturating_sub(visible_lines);
1987 let target = if track_height > 1 {
1988 (relative_row * max_scroll) / (track_height.saturating_sub(1))
1989 } else {
1990 0
1991 };
1992 Some((*popup_idx, target as i32))
1993 } else {
1994 Some((*popup_idx, 0))
1995 }
1996 } else {
1997 None
1998 }
1999 },
2000 );
2001 let (popup_idx, target_scroll) = scrollbar_info?;
2002 self.active_window_mut()
2003 .mouse_state
2004 .dragging_popup_scrollbar = Some(popup_idx);
2005 self.active_window_mut().mouse_state.drag_start_row = Some(row);
2006 let current_scroll = self
2007 .active_state()
2008 .popups
2009 .get(popup_idx)
2010 .map(|p| p.scroll_offset)
2011 .unwrap_or(0);
2012 self.active_window_mut().mouse_state.drag_start_popup_scroll = Some(current_scroll);
2013 let state = self.active_state_mut();
2014 if let Some(popup) = state.popups.get_mut(popup_idx) {
2015 popup.scroll_by(target_scroll - current_scroll as i32);
2016 }
2017 Some(Ok(()))
2018 }
2019
2020 fn handle_workspace_trust_mouse(
2026 &mut self,
2027 mouse_event: crossterm::event::MouseEvent,
2028 ) -> AnyhowResult<bool> {
2029 use crossterm::event::{MouseButton, MouseEventKind};
2030 let col = mouse_event.column;
2031 let row = mouse_event.row;
2032 let layout = self.active_chrome().workspace_trust_dialog.clone();
2033
2034 match mouse_event.kind {
2035 MouseEventKind::ScrollUp => {
2036 self.workspace_trust_scroll = self.workspace_trust_scroll.saturating_sub(2);
2037 }
2038 MouseEventKind::ScrollDown => {
2039 let max = layout.as_ref().map(|l| l.max_scroll).unwrap_or(0);
2040 self.workspace_trust_scroll = (self.workspace_trust_scroll + 2).min(max);
2041 }
2042 MouseEventKind::Down(MouseButton::Left) => {
2043 if let Some(layout) = layout {
2044 let hit = |r: ratatui::layout::Rect| in_rect(col, row, r);
2045 if hit(layout.ok) {
2046 let idx = self.current_workspace_trust_selection();
2047 self.confirm_workspace_trust(idx);
2048 } else if hit(layout.quit) {
2049 self.hide_popup();
2052 if !self.workspace_trust_prompt_cancellable {
2053 self.should_quit = true;
2054 }
2055 } else if let Some(i) = layout.radios.iter().position(|r| hit(*r)) {
2056 self.confirm_workspace_trust(i);
2057 }
2058 }
2060 }
2061 _ => {}
2063 }
2064 Ok(true)
2065 }
2066
2067 fn handle_click_global_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2068 for (popup_idx, popup_rect, inner_rect, scroll_offset, num_items) in self
2069 .active_chrome()
2070 .global_popup_areas
2071 .clone()
2072 .into_iter()
2073 .rev()
2074 {
2075 if popup_rect.width >= 5 {
2076 let cb_x = popup_rect.x + popup_rect.width - 4;
2077 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
2078 return Some(self.handle_action(Action::PopupCancel));
2079 }
2080 }
2081 if in_rect(col, row, inner_rect) && num_items > 0 {
2082 let relative_row = (row - inner_rect.y) as usize;
2083 let item_idx = scroll_offset + relative_row;
2084 if item_idx < num_items {
2085 if let Some(popup) = self.global_popups.get_mut(popup_idx) {
2086 if let crate::view::popup::PopupContent::List { items: _, selected } =
2087 &mut popup.content
2088 {
2089 *selected = item_idx;
2090 }
2091 }
2092 return Some(self.handle_action(Action::PopupConfirm));
2093 }
2094 }
2095 }
2096 None
2097 }
2098
2099 fn handle_click_buffer_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2100 let close_hit = self.active_chrome().popup_areas.iter().rev().find_map(
2102 |(_idx, popup_rect, _inner, _scroll, _n, _sb, _tl)| {
2103 if popup_rect.width < 5 {
2104 return None;
2105 }
2106 let cb_x = popup_rect.x + popup_rect.width - 4;
2107 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
2108 Some(())
2109 } else {
2110 None
2111 }
2112 },
2113 );
2114 if close_hit.is_some() {
2115 return Some(self.handle_action(Action::PopupCancel));
2116 }
2117
2118 let popup_areas = self.active_chrome().popup_areas.clone();
2120 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
2121 popup_areas.iter().rev()
2122 {
2123 if !in_rect(col, row, *inner_rect) {
2124 continue;
2125 }
2126 let relative_col = (col - inner_rect.x) as usize;
2127 let relative_row = (row - inner_rect.y) as usize;
2128
2129 let link_url = {
2130 let state = self.active_state();
2131 state
2132 .popups
2133 .top()
2134 .and_then(|p| p.link_at_position(relative_col, relative_row))
2135 };
2136 if let Some(url) = link_url {
2137 #[cfg(feature = "runtime")]
2138 if let Err(e) = open::that(&url) {
2139 self.set_status_message(format!("Failed to open URL: {}", e));
2140 } else {
2141 self.set_status_message(format!("Opening: {}", url));
2142 }
2143 return Some(Ok(()));
2144 }
2145
2146 if *num_items > 0 {
2147 let item_idx = scroll_offset + relative_row;
2148 if item_idx < *num_items {
2149 let state = self.active_state_mut();
2150 if let Some(popup) = state.popups.top_mut() {
2151 if let crate::view::popup::PopupContent::List { items: _, selected } =
2152 &mut popup.content
2153 {
2154 *selected = item_idx;
2155 }
2156 }
2157 return Some(self.handle_action(Action::PopupConfirm));
2158 }
2159 }
2160
2161 let is_text_popup = {
2162 let state = self.active_state();
2163 state.popups.top().is_some_and(|p| {
2164 matches!(
2165 p.content,
2166 crate::view::popup::PopupContent::Text(_)
2167 | crate::view::popup::PopupContent::Markdown(_)
2168 )
2169 })
2170 };
2171 if is_text_popup {
2172 let line = scroll_offset + relative_row;
2173 let popup_idx_copy = *popup_idx;
2174 let state = self.active_state_mut();
2175 if let Some(popup) = state.popups.top_mut() {
2176 popup.start_selection(line, relative_col);
2177 }
2178 self.active_window_mut().mouse_state.selecting_in_popup = Some(popup_idx_copy);
2179 return Some(Ok(()));
2180 }
2181 }
2182 None
2183 }
2184
2185 fn handle_click_menu_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2186 if self.active_window_mut().menu_bar_visible {
2187 let hit = self
2189 .active_chrome()
2190 .menu_layout
2191 .as_ref()
2192 .and_then(|ml| ml.menu_at(col, row));
2193 let layout_exists = self.active_chrome().menu_layout.is_some();
2194 if layout_exists {
2195 if let Some(menu_idx) = hit {
2196 if self.menu_state.active_menu == Some(menu_idx) {
2197 self.close_menu_with_auto_hide();
2198 } else {
2199 self.active_window_mut().on_editor_focus_lost();
2200 self.menu_state.open_menu(menu_idx);
2201 }
2202 return Some(Ok(()));
2203 } else if row == 0 {
2204 self.close_menu_with_auto_hide();
2205 return Some(Ok(()));
2206 }
2207 }
2208 }
2209
2210 if let Some(active_idx) = self.menu_state.active_menu {
2211 let all_menus: Vec<crate::config::Menu> = self
2212 .menus
2213 .menus
2214 .iter()
2215 .chain(self.menu_state.plugin_menus.iter())
2216 .cloned()
2217 .collect();
2218 if let Some(menu) = all_menus.get(active_idx) {
2219 match self.handle_menu_dropdown_click(col, row, menu) {
2220 Ok(Some(click_result)) => return Some(click_result),
2221 Ok(None) => {}
2222 Err(e) => return Some(Err(e)),
2223 }
2224 }
2225 self.close_menu_with_auto_hide();
2226 return Some(Ok(()));
2227 }
2228
2229 None
2230 }
2231
2232 fn handle_click_file_explorer_area(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2233 let explorer_area = self.active_layout().file_explorer_area?;
2234 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2235 if col == border_x && row >= explorer_area.y && row < explorer_area.y + explorer_area.height
2236 {
2237 self.active_window_mut().mouse_state.dragging_file_explorer = true;
2238 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2239 self.active_window_mut()
2240 .mouse_state
2241 .drag_start_explorer_width = Some(self.active_window().file_explorer_width);
2242 return Some(Ok(()));
2243 }
2244 if in_rect(col, row, explorer_area) {
2245 return Some(self.handle_file_explorer_click(col, row, explorer_area));
2246 }
2247 None
2248 }
2249
2250 fn handle_click_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2251 let (split_id, buffer_id, scrollbar_rect, is_on_thumb) =
2252 self.active_layout().split_areas.iter().find_map(
2253 |(split_id, buffer_id, _content, scrollbar_rect, thumb_start, thumb_end)| {
2254 if in_rect(col, row, *scrollbar_rect) {
2255 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
2256 let on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
2257 Some((*split_id, *buffer_id, *scrollbar_rect, on_thumb))
2258 } else {
2259 None
2260 }
2261 },
2262 )?;
2263
2264 self.focus_split(split_id, buffer_id);
2265 if is_on_thumb {
2266 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2267 self.active_window_mut().mouse_state.drag_start_row = Some(row);
2268 if self.active_window().is_composite_buffer(buffer_id) {
2269 if let Some(vs) = self
2270 .active_window()
2271 .composite_view_states
2272 .get(&(split_id, buffer_id))
2273 {
2274 self.active_window_mut()
2275 .mouse_state
2276 .drag_start_composite_scroll_row = Some(vs.scroll_row);
2277 }
2278 } else {
2279 let snap = self
2280 .windows
2281 .get(&self.active_window)
2282 .and_then(|w| w.buffers.splits())
2283 .map(|(_, vs)| vs)
2284 .expect("active window must have a populated split layout")
2285 .get(&split_id)
2286 .map(|vs| (vs.viewport.top_byte, vs.viewport.top_view_line_offset));
2287 if let Some((top_byte, top_view_line_offset)) = snap {
2288 let ms = &mut self.active_window_mut().mouse_state;
2289 ms.drag_start_top_byte = Some(top_byte);
2290 ms.drag_start_view_line_offset = Some(top_view_line_offset);
2291 }
2292 }
2293 } else {
2294 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2295 if let Err(e) = self.active_window_mut().handle_scrollbar_jump(
2296 col,
2297 row,
2298 split_id,
2299 buffer_id,
2300 scrollbar_rect,
2301 ) {
2302 return Some(Err(e));
2303 }
2304 self.active_window_mut().mouse_state.hover_target =
2305 Some(HoverTarget::ScrollbarThumb(split_id));
2306 }
2307 Some(Ok(()))
2308 }
2309
2310 fn handle_click_horizontal_scrollbar(
2311 &mut self,
2312 col: u16,
2313 row: u16,
2314 ) -> Option<AnyhowResult<()>> {
2315 let (split_id, buffer_id, hscrollbar_rect, max_content_width, is_on_thumb) = self
2316 .active_layout()
2317 .horizontal_scrollbar_areas
2318 .iter()
2319 .find_map(
2320 |(
2321 split_id,
2322 buffer_id,
2323 hscrollbar_rect,
2324 max_content_width,
2325 thumb_start,
2326 thumb_end,
2327 )| {
2328 if col >= hscrollbar_rect.x
2329 && col < hscrollbar_rect.x + hscrollbar_rect.width
2330 && row >= hscrollbar_rect.y
2331 && row < hscrollbar_rect.y + hscrollbar_rect.height
2332 {
2333 let relative_col = col.saturating_sub(hscrollbar_rect.x) as usize;
2334 let on_thumb = relative_col >= *thumb_start && relative_col < *thumb_end;
2335 Some((
2336 *split_id,
2337 *buffer_id,
2338 *hscrollbar_rect,
2339 *max_content_width,
2340 on_thumb,
2341 ))
2342 } else {
2343 None
2344 }
2345 },
2346 )?;
2347
2348 self.focus_split(split_id, buffer_id);
2349 self.active_window_mut()
2350 .mouse_state
2351 .dragging_horizontal_scrollbar = Some(split_id);
2352 if is_on_thumb {
2353 self.active_window_mut().mouse_state.drag_start_hcol = Some(col);
2354 if let Some(vs) = self
2355 .windows
2356 .get(&self.active_window)
2357 .and_then(|w| w.buffers.splits())
2358 .map(|(_, vs)| vs)
2359 .expect("active window must have a populated split layout")
2360 .get(&split_id)
2361 {
2362 self.active_window_mut().mouse_state.drag_start_left_column =
2363 Some(vs.viewport.left_column);
2364 }
2365 } else {
2366 self.active_window_mut().mouse_state.drag_start_hcol = None;
2367 self.active_window_mut().mouse_state.drag_start_left_column = None;
2368 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2369 let track_width = hscrollbar_rect.width as f64;
2370 let ratio = if track_width > 1.0 {
2371 (relative_col / (track_width - 1.0)).clamp(0.0, 1.0)
2372 } else {
2373 0.0
2374 };
2375 if let Some(vs) = self
2376 .windows
2377 .get_mut(&self.active_window)
2378 .and_then(|w| w.split_view_states_mut())
2379 .expect("active window must have a populated split layout")
2380 .get_mut(&split_id)
2381 {
2382 let visible_width = vs.viewport.width as usize;
2383 let max_scroll = max_content_width.saturating_sub(visible_width);
2384 let target_col = (ratio * max_scroll as f64).round() as usize;
2385 vs.viewport.left_column = target_col.min(max_scroll);
2386 vs.viewport.set_skip_ensure_visible();
2387 }
2388 }
2389 Some(Ok(()))
2390 }
2391
2392 fn dispatch_status_bar_click(
2403 &mut self,
2404 id: crate::view::ui::status_bar::StatusBarClickable,
2405 ) -> AnyhowResult<()> {
2406 use crate::view::ui::status_bar::StatusBarClickable as C;
2407 match id {
2408 C::LineEnding => {
2409 self.dismiss_menu_popups_for_prompt();
2410 self.handle_action(Action::SetLineEnding)
2411 }
2412 C::Encoding => {
2413 self.dismiss_menu_popups_for_prompt();
2414 self.handle_action(Action::SetEncoding)
2415 }
2416 C::Language => {
2417 self.dismiss_menu_popups_for_prompt();
2418 self.handle_action(Action::SetLanguage)
2419 }
2420 C::Lsp => self.handle_action(Action::ShowLspStatus),
2422 C::RemoteIndicator => self.handle_action(Action::ShowRemoteIndicatorMenu),
2424 C::WorkspaceTrust => {
2425 self.dismiss_menu_popups_for_prompt();
2427 self.handle_action(Action::WorkspaceTrustPrompt)
2428 }
2429 C::Warnings => {
2430 self.dismiss_menu_popups_for_prompt();
2431 self.handle_action(Action::ShowWarnings)
2432 }
2433 C::Messages => self.handle_action(Action::ShowStatusLog),
2434 C::ReadOnly => self.handle_action(Action::ShowReadOnlyMenu),
2436 }
2437 }
2438
2439 fn handle_click_status_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2440 let (status_row, _status_x, _status_width) = self.active_chrome().status_bar.area?;
2441 if row != status_row {
2442 return None;
2443 }
2444 let clickables = self.active_chrome().status_bar.clickable.clone();
2448 for (id, r, s, e) in clickables {
2449 if row == r && col >= s && col < e {
2450 return Some(self.dispatch_status_bar_click(id));
2451 }
2452 }
2453 let plugin_areas = self.active_chrome().status_bar.plugin_token_areas.clone();
2459 for (key, (r, s, e)) in plugin_areas {
2460 if row == r && col >= s && col < e {
2461 let (plugin_name, token_name) = match key.split_once(':') {
2462 Some((p, t)) => (p.to_string(), t.to_string()),
2463 None => (String::new(), key.clone()),
2464 };
2465 self.dismiss_menu_popups_for_prompt();
2466 self.plugin_manager.read().unwrap().run_hook(
2467 "status_bar_token_clicked",
2468 crate::services::plugins::hooks::HookArgs::StatusBarTokenClicked {
2469 plugin_name,
2470 token_name,
2471 },
2472 );
2473 return Some(Ok(()));
2474 }
2475 }
2476 None
2477 }
2478
2479 fn handle_click_search_options(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2480 use crate::view::ui::status_bar::SearchOptionsHover;
2481 let layout = self.active_chrome().search_options_layout.clone()?;
2482 match layout.checkbox_at(col, row)? {
2483 SearchOptionsHover::CaseSensitive => {
2484 Some(self.handle_action(Action::ToggleSearchCaseSensitive))
2485 }
2486 SearchOptionsHover::WholeWord => {
2487 Some(self.handle_action(Action::ToggleSearchWholeWord))
2488 }
2489 SearchOptionsHover::Regex => Some(self.handle_action(Action::ToggleSearchRegex)),
2490 SearchOptionsHover::ConfirmEach => {
2491 Some(self.handle_action(Action::ToggleSearchConfirmEach))
2492 }
2493 SearchOptionsHover::None => None,
2494 }
2495 }
2496
2497 fn handle_click_split_separator(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2498 let separator_areas = self.active_layout().separator_areas.clone();
2499 for (split_id, direction, sep_x, sep_y, sep_length) in &separator_areas {
2500 let is_on_separator = match direction {
2501 SplitDirection::Horizontal => {
2502 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
2503 }
2504 SplitDirection::Vertical => {
2505 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
2506 }
2507 };
2508 if is_on_separator {
2509 self.active_window_mut().mouse_state.dragging_separator =
2510 Some((*split_id, *direction));
2511 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2512 let ratio = self
2513 .split_manager_mut()
2514 .get_ratio((*split_id).into())
2515 .or_else(|| self.grouped_split_ratio(*split_id));
2516 if let Some(ratio) = ratio {
2517 self.active_window_mut().mouse_state.drag_start_ratio = Some(ratio);
2518 }
2519 return Some(Ok(()));
2520 }
2521 }
2522 None
2523 }
2524
2525 fn handle_click_split_controls(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2526 let close_split_id = self
2527 .active_layout()
2528 .close_split_areas
2529 .iter()
2530 .find(|(_, btn_row, start_col, end_col)| {
2531 row == *btn_row && col >= *start_col && col < *end_col
2532 })
2533 .map(|(split_id, _, _, _)| *split_id);
2534 if let Some(split_id) = close_split_id {
2535 if let Err(e) = self
2536 .windows
2537 .get_mut(&self.active_window)
2538 .and_then(|w| w.split_manager_mut())
2539 .expect("active window must have a populated split layout")
2540 .close_split(split_id)
2541 {
2542 self.set_status_message(
2543 t!("error.cannot_close_split", error = e.to_string()).to_string(),
2544 );
2545 } else {
2546 let new_active = self
2547 .windows
2548 .get(&self.active_window)
2549 .and_then(|w| w.buffers.splits())
2550 .map(|(mgr, _)| mgr)
2551 .expect("active window must have a populated split layout")
2552 .active_split();
2553 if let Some(buffer_id) = self
2554 .windows
2555 .get(&self.active_window)
2556 .and_then(|w| w.buffers.splits())
2557 .map(|(mgr, _)| mgr)
2558 .expect("active window must have a populated split layout")
2559 .buffer_for_split(new_active)
2560 {
2561 self.set_active_buffer(buffer_id);
2562 }
2563 self.set_status_message(t!("split.closed").to_string());
2564 }
2565 return Some(Ok(()));
2566 }
2567
2568 let maximize_target = self
2569 .active_layout()
2570 .maximize_split_areas
2571 .iter()
2572 .find(|(_, btn_row, start_col, end_col)| {
2573 row == *btn_row && col >= *start_col && col < *end_col
2574 })
2575 .map(|(split_id, _, _, _)| *split_id);
2576 if let Some(target) = maximize_target {
2577 let already_maximized = self
2584 .windows
2585 .get(&self.active_window)
2586 .and_then(|w| w.buffers.splits())
2587 .map(|(mgr, _)| mgr.is_maximized())
2588 .unwrap_or(false);
2589 if !already_maximized {
2590 if let Some(buffer_id) = self
2591 .windows
2592 .get(&self.active_window)
2593 .and_then(|w| w.buffers.splits())
2594 .map(|(mgr, _)| mgr)
2595 .expect("active window must have a populated split layout")
2596 .buffer_for_split(target)
2597 {
2598 self.focus_split(target, buffer_id);
2599 }
2600 }
2601 match self
2602 .windows
2603 .get_mut(&self.active_window)
2604 .and_then(|w| w.split_manager_mut())
2605 .expect("active window must have a populated split layout")
2606 .toggle_maximize_for(target)
2607 {
2608 Ok(maximized) => {
2609 let msg = if maximized {
2610 t!("split.maximized").to_string()
2611 } else {
2612 t!("split.restored").to_string()
2613 };
2614 self.set_status_message(msg);
2615 }
2616 Err(e) => self.set_status_message(e),
2617 }
2618 return Some(Ok(()));
2619 }
2620
2621 None
2622 }
2623
2624 fn handle_click_tab_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2625 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
2626 tracing::debug!(
2627 "Tab layout for split {:?}: bar_area={:?}, left_scroll={:?}, right_scroll={:?}",
2628 split_id,
2629 tab_layout.bar_area,
2630 tab_layout.left_scroll_area,
2631 tab_layout.right_scroll_area
2632 );
2633 }
2634 let tab_hit = self
2635 .active_layout()
2636 .tab_layouts
2637 .iter()
2638 .find_map(|(split_id, tab_layout)| {
2639 let hit = tab_layout.hit_test(col, row);
2640 tracing::debug!(
2641 "Tab hit_test at ({}, {}) for split {:?} returned {:?}",
2642 col,
2643 row,
2644 split_id,
2645 hit
2646 );
2647 hit.map(|h| (*split_id, h))
2648 });
2649 let (split_id, hit) = tab_hit?;
2650 match hit {
2651 TabHit::CloseButton(target) => {
2652 match target {
2653 crate::view::split::TabTarget::Buffer(buffer_id) => {
2654 self.focus_split(split_id, buffer_id);
2655 self.close_tab_in_split(buffer_id, split_id);
2656 }
2657 crate::view::split::TabTarget::Group(group_leaf) => {
2658 self.close_buffer_group_by_leaf(group_leaf);
2659 }
2660 }
2661 Some(Ok(()))
2662 }
2663 TabHit::TabName(target) => {
2664 let direction = self
2665 .windows
2666 .get(&self.active_window)
2667 .and_then(|w| w.buffers.splits())
2668 .map(|(_, vs)| vs)
2669 .expect("active window must have a populated split layout")
2670 .get(&split_id)
2671 .map(|vs| {
2672 let open = &vs.open_buffers;
2673 let cur = vs.active_target();
2674 let cur_idx = open.iter().position(|t| *t == cur);
2675 let new_idx = open.iter().position(|t| *t == target);
2676 match (cur_idx, new_idx) {
2677 (Some(c), Some(n)) if n > c => 1,
2678 (Some(c), Some(n)) if n < c => -1,
2679 _ => 0,
2680 }
2681 })
2682 .unwrap_or(0);
2683 self.active_window_mut()
2684 .animate_tab_switch(split_id, direction);
2685 match target {
2686 crate::view::split::TabTarget::Buffer(buffer_id) => {
2687 self.focus_split(split_id, buffer_id);
2688 self.active_window_mut()
2689 .promote_buffer_from_preview(buffer_id);
2690 self.active_window_mut().mouse_state.dragging_tab = Some(
2691 super::types::TabDragState::new(buffer_id, split_id, (col, row)),
2692 );
2693 }
2694 crate::view::split::TabTarget::Group(group_leaf) => {
2695 self.activate_group_tab(split_id, group_leaf);
2696 }
2697 }
2698 Some(Ok(()))
2699 }
2700 TabHit::ScrollLeft => {
2701 self.set_status_message("ScrollLeft clicked!".to_string());
2702 if let Some(vs) = self
2703 .windows
2704 .get_mut(&self.active_window)
2705 .and_then(|w| w.split_view_states_mut())
2706 .expect("active window must have a populated split layout")
2707 .get_mut(&split_id)
2708 {
2709 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_sub(10);
2710 }
2711 Some(Ok(()))
2712 }
2713 TabHit::ScrollRight => {
2714 self.set_status_message("ScrollRight clicked!".to_string());
2715 if let Some(vs) = self
2716 .windows
2717 .get_mut(&self.active_window)
2718 .and_then(|w| w.split_view_states_mut())
2719 .expect("active window must have a populated split layout")
2720 .get_mut(&split_id)
2721 {
2722 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_add(10);
2723 }
2724 Some(Ok(()))
2725 }
2726 TabHit::NewTabButton => {
2727 self.active_window_mut().tab_context_menu = None;
2730 self.active_window_mut().new_tab_menu =
2731 Some(super::types::NewTabMenu::new(split_id, col, row + 1));
2732 Some(Ok(()))
2733 }
2734 TabHit::BarBackground => None,
2735 }
2736 }
2737
2738 pub(super) fn handle_mouse_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2740 if self.dock_resizing {
2744 let max_cols = self.terminal_width.max(20).saturating_sub(20).max(10);
2745 let new_w = col.saturating_add(1).clamp(10, max_cols);
2746 let mut changed = false;
2747 if let Some(fwp) = self.dock.as_mut() {
2748 if let super::PanelPlacement::LeftDock { width_cols } = &mut fwp.placement {
2749 changed = *width_cols != new_w;
2750 *width_cols = new_w;
2751 }
2752 }
2753 if changed {
2754 self.dock_width = Some(new_w);
2763 self.relayout();
2766 }
2767 return Ok(());
2768 }
2769 if self.try_widget_scrollbar_drag(super::PanelSlot::Dock, row)
2772 || self.try_widget_scrollbar_drag(super::PanelSlot::Floating, row)
2773 {
2774 let _ = col;
2775 return Ok(());
2776 }
2777 if self.overlay_prompt_active()
2782 && !self.active_window().mouse_state.dragging_prompt_scrollbar
2783 {
2784 return Ok(());
2785 }
2786
2787 if let Some(dragging_split_id) = self.active_window_mut().mouse_state.dragging_scrollbar {
2789 let split_areas = self.active_layout().split_areas.clone();
2792 for (split_id, buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2793 &split_areas
2794 {
2795 if *split_id == dragging_split_id {
2796 if self.active_window().mouse_state.drag_start_row.is_some() {
2798 self.active_window_mut().handle_scrollbar_drag_relative(
2800 row,
2801 *split_id,
2802 *buffer_id,
2803 *scrollbar_rect,
2804 )?;
2805 } else {
2806 self.active_window_mut().handle_scrollbar_jump(
2808 col,
2809 row,
2810 *split_id,
2811 *buffer_id,
2812 *scrollbar_rect,
2813 )?;
2814 }
2815 return Ok(());
2816 }
2817 }
2818 }
2819
2820 if let Some(dragging_split_id) = self
2822 .active_window_mut()
2823 .mouse_state
2824 .dragging_horizontal_scrollbar
2825 {
2826 let hscrollbar_areas = self.active_layout().horizontal_scrollbar_areas.clone();
2831 for (
2832 split_id,
2833 _buffer_id,
2834 hscrollbar_rect,
2835 max_content_width,
2836 thumb_start,
2837 thumb_end,
2838 ) in &hscrollbar_areas
2839 {
2840 if *split_id == dragging_split_id {
2841 let track_width = hscrollbar_rect.width as f64;
2842 if track_width <= 1.0 {
2843 break;
2844 }
2845
2846 if let (Some(drag_start_hcol), Some(drag_start_left_column)) = (
2847 self.active_window_mut().mouse_state.drag_start_hcol,
2848 self.active_window_mut().mouse_state.drag_start_left_column,
2849 ) {
2850 let col_offset = (col as i32) - (drag_start_hcol as i32);
2853 if let Some(view_state) = self
2854 .windows
2855 .get_mut(&self.active_window)
2856 .and_then(|w| w.split_view_states_mut())
2857 .expect("active window must have a populated split layout")
2858 .get_mut(&dragging_split_id)
2859 {
2860 let visible_width = view_state.viewport.width as usize;
2861 let max_scroll = max_content_width.saturating_sub(visible_width);
2862 if max_scroll > 0 {
2863 let thumb_size = thumb_end.saturating_sub(*thumb_start).max(1);
2864 let track_travel = (track_width - thumb_size as f64).max(1.0);
2865 let scroll_per_pixel = max_scroll as f64 / track_travel;
2866 let scroll_offset =
2867 (col_offset as f64 * scroll_per_pixel).round() as i64;
2868 let new_left =
2869 (drag_start_left_column as i64 + scroll_offset).max(0) as usize;
2870 view_state.viewport.left_column = new_left.min(max_scroll);
2871 view_state.viewport.set_skip_ensure_visible();
2872 }
2873 }
2874 } else {
2875 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2877 let ratio = (relative_col / (track_width - 1.0)).clamp(0.0, 1.0);
2878
2879 if let Some(view_state) = self
2880 .windows
2881 .get_mut(&self.active_window)
2882 .and_then(|w| w.split_view_states_mut())
2883 .expect("active window must have a populated split layout")
2884 .get_mut(&dragging_split_id)
2885 {
2886 let visible_width = view_state.viewport.width as usize;
2887 let max_scroll = max_content_width.saturating_sub(visible_width);
2888 let target_col = (ratio * max_scroll as f64).round() as usize;
2889 view_state.viewport.left_column = target_col.min(max_scroll);
2890 view_state.viewport.set_skip_ensure_visible();
2891 }
2892 }
2893
2894 return Ok(());
2895 }
2896 }
2897 }
2898
2899 if let Some(popup_idx) = self.active_window_mut().mouse_state.selecting_in_popup {
2901 if let Some((_, _, inner_rect, scroll_offset, _, _, _)) = self
2903 .active_chrome()
2904 .popup_areas
2905 .iter()
2906 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2907 {
2908 if col >= inner_rect.x
2910 && col < inner_rect.x + inner_rect.width
2911 && row >= inner_rect.y
2912 && row < inner_rect.y + inner_rect.height
2913 {
2914 let relative_col = (col - inner_rect.x) as usize;
2915 let relative_row = (row - inner_rect.y) as usize;
2916 let line = scroll_offset + relative_row;
2917
2918 let state = self.active_state_mut();
2919 if let Some(popup) = state.popups.get_mut(popup_idx) {
2920 popup.extend_selection(line, relative_col);
2921 }
2922 }
2923 }
2924 return Ok(());
2925 }
2926
2927 if self
2932 .active_window_mut()
2933 .mouse_state
2934 .dragging_prompt_scrollbar
2935 {
2936 use crate::view::ui::scrollbar::ScrollbarState;
2937 let sb_rect = self.active_chrome().suggestions_scrollbar_rect;
2940 let suggestions_area_visible =
2941 self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
2942 let active_window_id = self.active_window;
2943 if let (Some(sb_rect), Some(prompt)) = (
2944 sb_rect,
2945 self.windows
2946 .get_mut(&active_window_id)
2947 .and_then(|w| w.prompt.as_mut()),
2948 ) {
2949 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
2950 let total = prompt.suggestions.len();
2951 let track_height = sb_rect.height as usize;
2952 let clamped_row =
2956 row.clamp(sb_rect.y, sb_rect.y + sb_rect.height.saturating_sub(1));
2957 let click_row = clamped_row.saturating_sub(sb_rect.y) as usize;
2958 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
2959 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
2960 }
2961 return Ok(());
2962 }
2963
2964 if let Some(popup_idx) = self
2966 .active_window_mut()
2967 .mouse_state
2968 .dragging_popup_scrollbar
2969 {
2970 if let Some((_, _, inner_rect, _, _, Some(sb_rect), total_lines)) = self
2972 .active_chrome()
2973 .popup_areas
2974 .iter()
2975 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2976 {
2977 let track_height = sb_rect.height as usize;
2978 let visible_lines = inner_rect.height as usize;
2979
2980 if track_height > 0 && *total_lines > visible_lines {
2981 let relative_row = row.saturating_sub(sb_rect.y) as usize;
2982 let max_scroll = total_lines.saturating_sub(visible_lines);
2983 let target_scroll = if track_height > 1 {
2984 (relative_row * max_scroll) / (track_height.saturating_sub(1))
2985 } else {
2986 0
2987 };
2988
2989 let state = self.active_state_mut();
2990 if let Some(popup) = state.popups.get_mut(popup_idx) {
2991 let current_scroll = popup.scroll_offset as i32;
2992 let delta = target_scroll as i32 - current_scroll;
2993 popup.scroll_by(delta);
2994 }
2995 }
2996 }
2997 return Ok(());
2998 }
2999
3000 if let Some((split_id, direction)) = self.active_window_mut().mouse_state.dragging_separator
3002 {
3003 self.handle_separator_drag(col, row, split_id, direction)?;
3004 return Ok(());
3005 }
3006
3007 if self.active_window_mut().mouse_state.dragging_file_explorer {
3009 self.handle_file_explorer_border_drag(col)?;
3010 return Ok(());
3011 }
3012
3013 if self.active_window_mut().mouse_state.dragging_text_selection {
3015 self.handle_text_selection_drag(col, row)?;
3016 return Ok(());
3017 }
3018
3019 if self.active_window_mut().mouse_state.dragging_tab.is_some() {
3021 self.handle_tab_drag(col, row)?;
3022 return Ok(());
3023 }
3024
3025 Ok(())
3026 }
3027
3028 fn handle_text_selection_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
3030 use crate::model::event::Event;
3031 use crate::primitives::word_navigation::{find_word_end, find_word_start};
3032
3033 let Some(split_id) = self.active_window_mut().mouse_state.drag_selection_split else {
3034 return Ok(());
3035 };
3036 let Some(anchor_position) = self.active_window_mut().mouse_state.drag_selection_anchor
3037 else {
3038 return Ok(());
3039 };
3040
3041 let Some((buffer_id, content_rect)) = self
3043 .active_layout()
3044 .split_areas
3045 .iter()
3046 .find(|(sid, _, _, _, _, _)| *sid == split_id)
3047 .map(|(_, bid, rect, _, _, _)| (*bid, *rect))
3048 else {
3049 return Ok(());
3050 };
3051
3052 let cached_mappings = self
3054 .active_layout()
3055 .view_line_mappings
3056 .get(&split_id)
3057 .cloned();
3058
3059 let leaf_id = split_id;
3060
3061 let fallback = self
3063 .windows
3064 .get(&self.active_window)
3065 .and_then(|w| w.buffers.splits())
3066 .map(|(_, vs)| vs)
3067 .expect("active window must have a populated split layout")
3068 .get(&leaf_id)
3069 .map(|vs| vs.viewport.top_byte)
3070 .unwrap_or(0);
3071
3072 let compose_width = self
3074 .windows
3075 .get(&self.active_window)
3076 .and_then(|w| w.buffers.splits())
3077 .map(|(_, vs)| vs)
3078 .expect("active window must have a populated split layout")
3079 .get(&leaf_id)
3080 .and_then(|vs| vs.compose_width);
3081
3082 let drag_by_words = self.active_window_mut().mouse_state.drag_selection_by_words;
3086 let drag_word_end = self.active_window_mut().mouse_state.drag_selection_word_end;
3087
3088 let Some((target_position, new_position, anchor_position, new_sticky_column)) = self
3089 .active_window()
3090 .buffers
3091 .get(&buffer_id)
3092 .and_then(|state| {
3093 let gutter_width = state.margins.left_total_width() as u16;
3094 let target_position = super::click_geometry::screen_to_buffer_position(
3095 col,
3096 row,
3097 content_rect,
3098 gutter_width,
3099 &cached_mappings,
3100 fallback,
3101 true, compose_width,
3103 )?;
3104 let (new_position, anchor_pos) = if drag_by_words {
3105 if target_position >= anchor_position {
3106 (
3107 find_word_end(&state.buffer, target_position),
3108 anchor_position,
3109 )
3110 } else {
3111 let word_end = drag_word_end.unwrap_or(anchor_position);
3112 (find_word_start(&state.buffer, target_position), word_end)
3113 }
3114 } else {
3115 (target_position, anchor_position)
3116 };
3117 let new_sticky_column = state
3118 .buffer
3119 .offset_to_position(new_position)
3120 .map(|pos| pos.column);
3121 Some((target_position, new_position, anchor_pos, new_sticky_column))
3122 })
3123 else {
3124 return Ok(());
3125 };
3126 let _ = target_position;
3127
3128 let (primary_cursor_id, old_position, old_anchor, old_sticky_column) = self
3129 .active_window()
3130 .buffers
3131 .splits()
3132 .and_then(|(_, vs)| vs.get(&leaf_id))
3133 .map(|vs| {
3134 let cursor = vs.cursors.primary();
3135 (
3136 vs.cursors.primary_id(),
3137 cursor.position,
3138 cursor.anchor,
3139 cursor.sticky_column,
3140 )
3141 })
3142 .unwrap_or((CursorId(0), 0, None, 0));
3143
3144 let event = Event::MoveCursor {
3145 cursor_id: primary_cursor_id,
3146 old_position,
3147 new_position,
3148 old_anchor,
3149 new_anchor: Some(anchor_position),
3150 old_sticky_column,
3151 new_sticky_column: new_sticky_column.unwrap_or(old_sticky_column),
3152 };
3153
3154 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
3155 event_log.append(event.clone());
3156 }
3157 self.active_window_mut()
3158 .apply_event_to_buffer(buffer_id, leaf_id, &event);
3159
3160 Ok(())
3161 }
3162
3163 pub(super) fn handle_file_explorer_border_drag(&mut self, col: u16) -> AnyhowResult<()> {
3165 let Some((start_col, _start_row)) =
3166 self.active_window_mut().mouse_state.drag_start_position
3167 else {
3168 return Ok(());
3169 };
3170 let Some(start_width) = self
3171 .active_window_mut()
3172 .mouse_state
3173 .drag_start_explorer_width
3174 else {
3175 return Ok(());
3176 };
3177
3178 let delta = col as i32 - start_col as i32;
3179 let total_width = self.terminal_width as i32;
3180
3181 if total_width > 0 {
3185 use crate::config::ExplorerWidth;
3186 self.active_window_mut().file_explorer_width = match start_width {
3187 ExplorerWidth::Percent(start_pct) => {
3188 let percent_delta = (delta * 100) / total_width;
3189 let new_pct = (start_pct as i32 + percent_delta).clamp(0, 100) as u8;
3190 ExplorerWidth::Percent(new_pct)
3191 }
3192 ExplorerWidth::Columns(start_cols) => {
3193 let new_cols = (start_cols as i32 + delta).clamp(0, total_width) as u16;
3194 ExplorerWidth::Columns(new_cols)
3195 }
3196 };
3197 self.relayout();
3200 }
3201
3202 Ok(())
3203 }
3204
3205 pub(super) fn handle_separator_drag(
3207 &mut self,
3208 col: u16,
3209 row: u16,
3210 split_id: ContainerId,
3211 direction: SplitDirection,
3212 ) -> AnyhowResult<()> {
3213 let Some((start_col, start_row)) = self.active_window_mut().mouse_state.drag_start_position
3214 else {
3215 return Ok(());
3216 };
3217 let Some(start_ratio) = self.active_window_mut().mouse_state.drag_start_ratio else {
3218 return Ok(());
3219 };
3220 let Some(editor_area) = self.active_layout().editor_content_area else {
3221 return Ok(());
3222 };
3223
3224 let (delta, total_size) = match direction {
3226 SplitDirection::Horizontal => {
3227 let delta = row as i32 - start_row as i32;
3229 let total = editor_area.height as i32;
3230 (delta, total)
3231 }
3232 SplitDirection::Vertical => {
3233 let delta = col as i32 - start_col as i32;
3235 let total = editor_area.width as i32;
3236 (delta, total)
3237 }
3238 };
3239
3240 if total_size > 0 {
3243 let ratio_delta = delta as f32 / total_size as f32;
3244 let new_ratio = (start_ratio + ratio_delta).clamp(0.1, 0.9);
3245
3246 if self
3251 .windows
3252 .get(&self.active_window)
3253 .and_then(|w| w.buffers.splits())
3254 .map(|(mgr, _)| mgr)
3255 .expect("active window must have a populated split layout")
3256 .get_ratio(split_id.into())
3257 .is_some()
3258 {
3259 self.windows
3260 .get_mut(&self.active_window)
3261 .and_then(|w| w.split_manager_mut())
3262 .expect("active window must have a populated split layout")
3263 .set_ratio(split_id, new_ratio);
3264 } else {
3265 self.set_grouped_split_ratio(split_id, new_ratio);
3266 }
3267 self.relayout();
3270 }
3271
3272 Ok(())
3273 }
3274
3275 pub(super) fn handle_right_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
3277 self.active_window_mut().new_tab_menu = None;
3280
3281 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
3287 self.dock.as_ref().map(|f| f.placement)
3288 {
3289 if col < width_cols {
3290 if self.dock.as_ref().map(|f| !f.focused).unwrap_or(false) {
3291 self.refocus_floating_panel(super::PanelSlot::Dock);
3292 }
3293 self.handle_floating_widget_context_click(super::PanelSlot::Dock, col, row);
3294 return Ok(());
3295 }
3296 }
3297
3298 let frame_w = self.active_chrome().last_frame.width;
3299 let frame_h = self.active_chrome().last_frame.height;
3300 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
3301 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3302 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3303 let menu_height = menu.height();
3304 if col >= menu_x
3305 && col < menu_x + menu_width
3306 && row >= menu_y
3307 && row < menu_y + menu_height
3308 {
3309 return Ok(());
3310 }
3311 }
3312
3313 if let Some(ref menu) = self.active_window_mut().tab_context_menu {
3315 let menu_x = menu.position.0;
3316 let menu_y = menu.position.1;
3317 let menu_width = 22u16; let menu_height = super::types::TabContextMenuItem::all().len() as u16 + 2; if col >= menu_x
3322 && col < menu_x + menu_width
3323 && row >= menu_y
3324 && row < menu_y + menu_height
3325 {
3326 return Ok(());
3328 }
3329 }
3330
3331 if let Some(explorer_area) = self.active_layout().file_explorer_area {
3332 if col >= explorer_area.x
3333 && col < explorer_area.x + explorer_area.width
3334 && row < explorer_area.y + explorer_area.height
3335 && row > explorer_area.y
3336 {
3338 let relative_row = row.saturating_sub(explorer_area.y + 1);
3339 let (is_multi, is_root_selected) =
3340 if let Some(explorer) = self.file_explorer_mut().as_mut() {
3341 let display_nodes = explorer.get_display_nodes();
3342 let scroll_offset = explorer.get_scroll_offset();
3343 let clicked_index = (relative_row as usize) + scroll_offset;
3344 let mut clicked_is_root = false;
3345 if clicked_index < display_nodes.len() {
3346 let (node_id, _) = display_nodes[clicked_index];
3347 explorer.set_selected(Some(node_id));
3348 clicked_is_root = node_id == explorer.tree().root_id();
3349 }
3350 (explorer.has_multi_selection(), clicked_is_root)
3351 } else {
3352 (false, false)
3353 };
3354 self.active_window_mut().key_context =
3355 crate::input::keybindings::KeyContext::FileExplorer;
3356 self.active_window_mut().tab_context_menu = None;
3357 self.active_window_mut().file_explorer_context_menu =
3358 Some(super::types::FileExplorerContextMenu::new(
3359 col,
3360 row + 1,
3361 is_multi,
3362 is_root_selected,
3363 ));
3364 return Ok(());
3365 }
3366 }
3367
3368 self.active_window_mut().file_explorer_context_menu = None;
3369
3370 let tab_hit = self
3372 .active_layout()
3373 .tab_layouts
3374 .iter()
3375 .find_map(
3376 |(split_id, tab_layout)| match tab_layout.hit_test(col, row) {
3377 Some(TabHit::TabName(target) | TabHit::CloseButton(target)) => {
3378 target.as_buffer().map(|bid| (*split_id, bid))
3381 }
3382 _ => None,
3383 },
3384 );
3385
3386 if let Some((split_id, buffer_id)) = tab_hit {
3387 self.active_window_mut().tab_context_menu =
3389 Some(TabContextMenu::new(buffer_id, split_id, col, row + 1));
3390 } else {
3391 self.active_window_mut().tab_context_menu = None;
3393 }
3394
3395 Ok(())
3396 }
3397
3398 pub(super) fn handle_tab_context_menu_click(
3400 &mut self,
3401 col: u16,
3402 row: u16,
3403 ) -> Option<AnyhowResult<()>> {
3404 let menu = self.active_window_mut().tab_context_menu.as_ref()?;
3405 let menu_x = menu.position.0;
3406 let menu_y = menu.position.1;
3407 let menu_width = 22u16;
3408 let items = super::types::TabContextMenuItem::all();
3409 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
3413 {
3414 self.active_window_mut().tab_context_menu = None;
3416 return Some(Ok(()));
3417 }
3418
3419 if row == menu_y || row == menu_y + menu_height - 1 {
3421 return Some(Ok(()));
3422 }
3423
3424 let item_idx = (row - menu_y - 1) as usize;
3426 if item_idx >= items.len() {
3427 return Some(Ok(()));
3428 }
3429
3430 let buffer_id = menu.buffer_id;
3432 let split_id = menu.split_id;
3433 let item = items[item_idx];
3434
3435 self.active_window_mut().tab_context_menu = None;
3437
3438 Some(self.execute_tab_context_menu_action(item, buffer_id, split_id))
3440 }
3441
3442 pub(super) fn handle_new_tab_menu_click(
3444 &mut self,
3445 col: u16,
3446 row: u16,
3447 ) -> Option<AnyhowResult<()>> {
3448 let menu = self.active_window_mut().new_tab_menu.as_ref()?;
3449 let (menu_x, menu_y) = menu.position;
3450 let items = super::types::NewTabMenuItem::all();
3451 let menu_width = super::types::NEW_TAB_MENU_WIDTH;
3452 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
3456 {
3457 self.active_window_mut().new_tab_menu = None;
3458 return Some(Ok(()));
3459 }
3460
3461 if row == menu_y || row == menu_y + menu_height - 1 {
3463 return Some(Ok(()));
3464 }
3465
3466 let item_idx = (row - menu_y - 1) as usize;
3467 if item_idx >= items.len() {
3468 return Some(Ok(()));
3469 }
3470
3471 let split_id = menu.split_id;
3472 let item = items[item_idx];
3473
3474 self.active_window_mut().new_tab_menu = None;
3476
3477 Some(self.execute_new_tab_menu_action(item, split_id))
3478 }
3479
3480 fn execute_new_tab_menu_action(
3482 &mut self,
3483 item: super::types::NewTabMenuItem,
3484 split_id: LeafId,
3485 ) -> AnyhowResult<()> {
3486 use super::types::NewTabMenuItem;
3487 if let Some(buffer_id) = self
3491 .windows
3492 .get(&self.active_window)
3493 .and_then(|w| w.buffers.splits())
3494 .and_then(|(mgr, _)| mgr.buffer_for_split(split_id))
3495 {
3496 self.focus_split(split_id, buffer_id);
3497 }
3498 match item {
3499 NewTabMenuItem::NewTerminal => {
3500 self.open_terminal();
3501 }
3502 NewTabMenuItem::NewFile => {
3503 self.new_buffer();
3504 }
3505 }
3506 Ok(())
3507 }
3508
3509 fn execute_tab_context_menu_action(
3511 &mut self,
3512 item: super::types::TabContextMenuItem,
3513 buffer_id: BufferId,
3514 leaf_id: LeafId,
3515 ) -> AnyhowResult<()> {
3516 use super::types::TabContextMenuItem;
3517 match item {
3518 TabContextMenuItem::Close => {
3519 self.close_tab_in_split(buffer_id, leaf_id);
3520 }
3521 TabContextMenuItem::CloseOthers => {
3522 self.close_other_tabs_in_split(buffer_id, leaf_id);
3523 }
3524 TabContextMenuItem::CloseToRight => {
3525 self.close_tabs_to_right_in_split(buffer_id, leaf_id);
3526 }
3527 TabContextMenuItem::CloseToLeft => {
3528 self.close_tabs_to_left_in_split(buffer_id, leaf_id);
3529 }
3530 TabContextMenuItem::CloseAll => {
3531 self.close_all_tabs_in_split(leaf_id);
3532 }
3533 TabContextMenuItem::CopyRelativePath => {
3534 self.copy_buffer_path(buffer_id, true);
3535 }
3536 TabContextMenuItem::CopyFullPath => {
3537 self.copy_buffer_path(buffer_id, false);
3538 }
3539 }
3540
3541 Ok(())
3542 }
3543
3544 pub(super) fn handle_new_tab_menu_key(
3552 &mut self,
3553 code: crossterm::event::KeyCode,
3554 ) -> Option<AnyhowResult<()>> {
3555 use crossterm::event::KeyCode;
3556
3557 self.active_window().new_tab_menu.as_ref()?;
3558
3559 match code {
3560 KeyCode::Up => {
3561 if let Some(ref mut menu) = self.active_window_mut().new_tab_menu {
3562 menu.prev_item();
3563 }
3564 }
3565 KeyCode::Down => {
3566 if let Some(ref mut menu) = self.active_window_mut().new_tab_menu {
3567 menu.next_item();
3568 }
3569 }
3570 KeyCode::Enter => {
3571 let selected = self.active_window().new_tab_menu.as_ref().map(|menu| {
3572 (
3573 super::types::NewTabMenuItem::all()[menu.highlighted],
3574 menu.split_id,
3575 )
3576 });
3577 self.active_window_mut().new_tab_menu = None;
3578 if let Some((item, split_id)) = selected {
3579 return Some(self.execute_new_tab_menu_action(item, split_id));
3580 }
3581 }
3582 KeyCode::Esc => {
3583 self.active_window_mut().new_tab_menu = None;
3584 }
3585 _ => {}
3587 }
3588 Some(Ok(()))
3589 }
3590
3591 pub(super) fn handle_tab_context_menu_key(
3597 &mut self,
3598 code: crossterm::event::KeyCode,
3599 ) -> Option<AnyhowResult<()>> {
3600 use crossterm::event::KeyCode;
3601
3602 self.active_window().tab_context_menu.as_ref()?;
3603
3604 match code {
3605 KeyCode::Up => {
3606 if let Some(ref mut menu) = self.active_window_mut().tab_context_menu {
3607 menu.prev_item();
3608 }
3609 }
3610 KeyCode::Down => {
3611 if let Some(ref mut menu) = self.active_window_mut().tab_context_menu {
3612 menu.next_item();
3613 }
3614 }
3615 KeyCode::Enter => {
3616 let selected = self
3617 .active_window()
3618 .tab_context_menu
3619 .as_ref()
3620 .map(|menu| (menu.highlighted_item(), menu.buffer_id, menu.split_id));
3621 self.active_window_mut().tab_context_menu = None;
3622 if let Some((item, buffer_id, split_id)) = selected {
3623 return Some(self.execute_tab_context_menu_action(item, buffer_id, split_id));
3624 }
3625 }
3626 KeyCode::Esc => {
3627 self.active_window_mut().tab_context_menu = None;
3628 }
3629 _ => {}
3631 }
3632 Some(Ok(()))
3633 }
3634
3635 pub(super) fn handle_file_explorer_context_menu_key(
3638 &mut self,
3639 code: crossterm::event::KeyCode,
3640 modifiers: crossterm::event::KeyModifiers,
3641 ) -> Option<AnyhowResult<()>> {
3642 use crossterm::event::KeyCode;
3643 use crossterm::event::KeyModifiers;
3644
3645 if modifiers != KeyModifiers::NONE {
3646 return None;
3647 }
3648
3649 match code {
3650 KeyCode::Up => {
3651 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3652 menu.prev_item();
3653 }
3654 Some(Ok(()))
3655 }
3656 KeyCode::Down => {
3657 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3658 menu.next_item();
3659 }
3660 Some(Ok(()))
3661 }
3662 KeyCode::Enter => {
3663 let item = {
3664 let menu = self
3665 .active_window_mut()
3666 .file_explorer_context_menu
3667 .as_ref()?;
3668 menu.items()[menu.highlighted]
3669 };
3670 self.active_window_mut().file_explorer_context_menu = None;
3671 self.execute_file_explorer_context_menu_action(item);
3672 Some(Ok(()))
3673 }
3674 KeyCode::Esc => {
3675 self.active_window_mut().file_explorer_context_menu = None;
3676 Some(Ok(()))
3677 }
3678 _ => None,
3679 }
3680 }
3681
3682 pub(super) fn handle_file_explorer_context_menu_click(
3684 &mut self,
3685 col: u16,
3686 row: u16,
3687 ) -> Option<AnyhowResult<()>> {
3688 let frame_w = self.active_chrome().last_frame.width;
3690 let frame_h = self.active_chrome().last_frame.height;
3691 let clicked_item: Option<super::types::FileExplorerContextMenuItem> = {
3692 let menu = self.active_window().file_explorer_context_menu.as_ref()?;
3693 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3694 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3695 let menu_height = menu.height();
3696
3697 if col < menu_x
3698 || col >= menu_x + menu_width
3699 || row < menu_y
3700 || row >= menu_y + menu_height
3701 {
3702 self.active_window_mut().file_explorer_context_menu = None;
3703 return Some(Ok(()));
3704 }
3705
3706 if row == menu_y || row == menu_y + menu_height - 1 {
3707 return Some(Ok(()));
3708 }
3709
3710 let item_idx = (row - menu_y - 1) as usize;
3711 menu.items().get(item_idx).copied()
3712 };
3713
3714 self.active_window_mut().file_explorer_context_menu = None;
3715 if let Some(item) = clicked_item {
3716 self.execute_file_explorer_context_menu_action(item);
3717 }
3718 Some(Ok(()))
3719 }
3720
3721 fn execute_file_explorer_context_menu_action(
3722 &mut self,
3723 item: super::types::FileExplorerContextMenuItem,
3724 ) {
3725 use super::types::FileExplorerContextMenuItem;
3726 match item {
3727 FileExplorerContextMenuItem::NewFile => self.file_explorer_new_file(),
3728 FileExplorerContextMenuItem::NewDirectory => self.file_explorer_new_directory(),
3729 FileExplorerContextMenuItem::Rename => self.file_explorer_rename(),
3730 FileExplorerContextMenuItem::Cut => self.active_window_mut().file_explorer_cut(),
3731 FileExplorerContextMenuItem::Copy => self.active_window_mut().file_explorer_copy(),
3732 FileExplorerContextMenuItem::Paste => self.file_explorer_paste(),
3733 FileExplorerContextMenuItem::Duplicate => self.file_explorer_duplicate(),
3734 FileExplorerContextMenuItem::Delete => self.file_explorer_delete(),
3735 FileExplorerContextMenuItem::CopyFullPath => self.file_explorer_copy_path(false),
3736 FileExplorerContextMenuItem::CopyRelativePath => self.file_explorer_copy_path(true),
3737 }
3738 }
3739
3740 fn show_file_explorer_status_tooltip(&mut self, path: std::path::PathBuf, col: u16, row: u16) {
3742 use crate::view::popup::{Popup, PopupPosition};
3743 use ratatui::style::Style;
3744
3745 let is_directory = path.is_dir();
3746 let has_unsaved_changes = self.file_explorer_node_has_unsaved_changes(&path, is_directory);
3747
3748 let node_metadata = self
3749 .file_explorer()
3750 .and_then(|explorer| explorer.tree().get_node_by_path(&path))
3751 .and_then(|node| node.entry.metadata.as_ref());
3752 let is_hidden = node_metadata.map(|m| m.is_hidden).unwrap_or(false);
3753 let is_symlink = path.is_symlink();
3754 let theme = self.theme.read().unwrap();
3755 let neutral_fg = if is_hidden {
3756 theme.line_number_fg
3757 } else if is_symlink {
3758 theme.syntax_type
3759 } else if is_directory {
3760 theme.syntax_keyword
3761 } else {
3762 theme.editor_fg
3763 };
3764 let slot_resolver = self.file_explorer_slot_resolver();
3765 let slot_context = crate::view::file_tree::ExplorerSlotContext {
3766 path: &path,
3767 is_dir: is_directory,
3768 has_unsaved: has_unsaved_changes,
3769 is_symlink,
3770 is_hidden,
3771 decorations: &self.active_window().file_explorer_decoration_cache,
3772 slot_overrides: &self.active_window().file_explorer_slot_override_cache,
3773 theme: &theme,
3774 neutral_fg,
3775 };
3776 let slot_resolution = slot_resolver.resolve(&slot_context);
3777
3778 let Some(summary) = slot_resolution.trailing.and_then(|slot| slot.tooltip) else {
3780 return; };
3782 let mut lines = summary.lines;
3783 let has_custom_trailing_override = self
3784 .active_window()
3785 .file_explorer_slot_override_cache
3786 .has_trailing_override_for_path(&path);
3787
3788 if !has_custom_trailing_override {
3789 if is_directory {
3793 if let Some(modified_files) = self.get_modified_files_in_directory(&path) {
3794 lines.push(String::new()); lines.push("Modified files:".to_string());
3796 const MAX_FILES: usize = 8;
3797 for (i, file) in modified_files.iter().take(MAX_FILES).enumerate() {
3798 let display_name = file
3800 .strip_prefix(&path)
3801 .unwrap_or(file)
3802 .to_string_lossy()
3803 .to_string();
3804 lines.push(format!(" {}", display_name));
3805 if i == MAX_FILES - 1 && modified_files.len() > MAX_FILES {
3806 lines.push(format!(
3807 " ... and {} more",
3808 modified_files.len() - MAX_FILES
3809 ));
3810 break;
3811 }
3812 }
3813 }
3814 } else if let Some(stats) = self.get_git_diff_stats(&path) {
3815 lines.push(String::new()); lines.push(stats);
3818 }
3819 }
3820
3821 if lines.is_empty() {
3822 return;
3823 }
3824
3825 let mut popup = Popup::text(lines, &*self.theme.read().unwrap());
3827 popup.title = Some(summary.title);
3828 popup.transient = true;
3829 popup.position = PopupPosition::Fixed { x: col, y: row + 1 };
3830 popup.width = 50;
3831 popup.max_height = 15;
3832 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
3833 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
3834
3835 let __buffer_id = self.active_buffer();
3837 if let Some(state) = self
3838 .windows
3839 .get_mut(&self.active_window)
3840 .map(|w| &mut w.buffers)
3841 .expect("active window present")
3842 .get_mut(&__buffer_id)
3843 {
3844 state.popups.show(popup);
3845 }
3846 }
3847
3848 fn file_explorer_node_has_unsaved_changes(
3849 &self,
3850 path: &std::path::Path,
3851 is_directory: bool,
3852 ) -> bool {
3853 if is_directory {
3854 self.windows
3855 .get(&self.active_window)
3856 .map(|w| &w.buffers)
3857 .expect("active window present")
3858 .iter()
3859 .any(|(buffer_id, state)| {
3860 if state.buffer.is_modified() {
3861 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3862 {
3863 if let Some(file_path) = metadata.file_path() {
3864 return file_path.starts_with(path);
3865 }
3866 }
3867 }
3868 false
3869 })
3870 } else {
3871 self.windows
3872 .get(&self.active_window)
3873 .map(|w| &w.buffers)
3874 .expect("active window present")
3875 .iter()
3876 .any(|(buffer_id, state)| {
3877 if state.buffer.is_modified() {
3878 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3879 {
3880 return metadata.file_path().map(|p| p.as_path()) == Some(path);
3881 }
3882 }
3883 false
3884 })
3885 }
3886 }
3887
3888 fn dismiss_file_explorer_status_tooltip(&mut self) {
3890 let __buffer_id = self.active_buffer();
3892 if let Some(state) = self
3893 .windows
3894 .get_mut(&self.active_window)
3895 .map(|w| &mut w.buffers)
3896 .expect("active window present")
3897 .get_mut(&__buffer_id)
3898 {
3899 state.popups.dismiss_transient();
3900 }
3901 }
3902
3903 fn get_git_diff_stats(&self, path: &std::path::Path) -> Option<String> {
3905 use crate::services::process_hidden::HideWindow;
3906 use std::process::Command;
3907
3908 let output = Command::new("git")
3910 .args(["diff", "--numstat", "--"])
3911 .arg(path)
3912 .current_dir(self.working_dir())
3913 .hide_window()
3914 .output()
3915 .ok()?;
3916
3917 if !output.status.success() {
3918 return None;
3919 }
3920
3921 let stdout = String::from_utf8_lossy(&output.stdout);
3922 let line = stdout.lines().next()?;
3923 let parts: Vec<&str> = line.split('\t').collect();
3924
3925 if parts.len() >= 2 {
3926 let insertions = parts[0];
3927 let deletions = parts[1];
3928
3929 if insertions == "-" && deletions == "-" {
3931 return Some("Binary file changed".to_string());
3932 }
3933
3934 let ins: i32 = insertions.parse().unwrap_or(0);
3935 let del: i32 = deletions.parse().unwrap_or(0);
3936
3937 if ins > 0 || del > 0 {
3938 return Some(format!("+{} -{} lines", ins, del));
3939 }
3940 }
3941
3942 let staged_output = Command::new("git")
3944 .args(["diff", "--numstat", "--cached", "--"])
3945 .arg(path)
3946 .current_dir(self.working_dir())
3947 .hide_window()
3948 .output()
3949 .ok()?;
3950
3951 if staged_output.status.success() {
3952 let staged_stdout = String::from_utf8_lossy(&staged_output.stdout);
3953 if let Some(line) = staged_stdout.lines().next() {
3954 let parts: Vec<&str> = line.split('\t').collect();
3955 if parts.len() >= 2 {
3956 let insertions = parts[0];
3957 let deletions = parts[1];
3958
3959 if insertions == "-" && deletions == "-" {
3960 return Some("Binary file staged".to_string());
3961 }
3962
3963 let ins: i32 = insertions.parse().unwrap_or(0);
3964 let del: i32 = deletions.parse().unwrap_or(0);
3965
3966 if ins > 0 || del > 0 {
3967 return Some(format!("+{} -{} lines (staged)", ins, del));
3968 }
3969 }
3970 }
3971 }
3972
3973 None
3974 }
3975
3976 fn get_modified_files_in_directory(
3978 &self,
3979 dir_path: &std::path::Path,
3980 ) -> Option<Vec<std::path::PathBuf>> {
3981 let modified_files = self
3982 .active_window()
3983 .file_explorer_decoration_cache
3984 .direct_paths_under(dir_path);
3985
3986 (!modified_files.is_empty()).then_some(modified_files)
3987 }
3988
3989 fn handle_floating_widget_panel_wheel(
4001 &mut self,
4002 slot: super::PanelSlot,
4003 col: u16,
4004 row: u16,
4005 delta: i32,
4006 ) -> bool {
4007 let inner = match self.panel(slot) {
4008 Some(fwp) => match fwp.last_inner_rect {
4009 Some(rect) => rect,
4010 None => return false,
4011 },
4012 None => return false,
4013 };
4014 if col < inner.x || col >= inner.x + inner.width {
4015 return false;
4016 }
4017 if row < inner.y || row >= inner.y + inner.height {
4018 return false;
4019 }
4020 let scrolled = self.handle_widget_panel_wheel(slot.buffer_id(), delta);
4021 let is_dock = matches!(
4025 self.panel(slot).map(|f| f.placement),
4026 Some(super::PanelPlacement::LeftDock { .. })
4027 );
4028 scrolled || is_dock
4029 }
4030
4031 fn try_widget_scrollbar_press(&mut self, slot: super::PanelSlot, col: u16, row: u16) -> bool {
4036 use crate::view::ui::scrollbar::ScrollbarState;
4037 let (panel_key, tracks) = match self.panel(slot) {
4038 Some(fwp) => (fwp.panel_key.clone(), fwp.scrollbar_tracks.clone()),
4039 None => return false,
4040 };
4041 for t in &tracks {
4042 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
4043 let pressed = self
4044 .panel_mut(slot)
4045 .and_then(|fwp| fwp.scrollbar_mouse.press(state, t.rect, col, row));
4046 if let Some(new_offset) = pressed {
4047 if let Some(fwp) = self.panel_mut(slot) {
4048 fwp.scrollbar_drag_key = Some(t.list_key.clone());
4049 }
4050 self.apply_widget_scroll(&panel_key, &t.list_key, new_offset, t.visible);
4051 return true;
4052 }
4053 }
4054 false
4055 }
4056
4057 fn try_widget_scrollbar_drag(&mut self, slot: super::PanelSlot, row: u16) -> bool {
4060 use crate::view::ui::scrollbar::ScrollbarState;
4061 let (panel_key, key) = match self.panel(slot) {
4062 Some(fwp) => match &fwp.scrollbar_drag_key {
4063 Some(k) => (fwp.panel_key.clone(), k.clone()),
4064 None => return false,
4065 },
4066 None => return false,
4067 };
4068 let track = self.panel(slot).and_then(|fwp| {
4071 fwp.scrollbar_tracks
4072 .iter()
4073 .find(|t| t.list_key == key)
4074 .cloned()
4075 });
4076 let Some(t) = track else {
4077 return false;
4078 };
4079 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
4080 let new_offset = self
4081 .panel_mut(slot)
4082 .and_then(|fwp| fwp.scrollbar_mouse.drag(state, t.rect, row));
4083 if let Some(off) = new_offset {
4084 self.apply_widget_scroll(&panel_key, &key, off, t.visible);
4085 }
4086 true
4087 }
4088
4089 pub(super) fn release_widget_scrollbar(&mut self) {
4091 for fwp in [self.dock.as_mut(), self.floating_widget_panel.as_mut()]
4092 .into_iter()
4093 .flatten()
4094 {
4095 fwp.scrollbar_mouse.release();
4096 fwp.scrollbar_drag_key = None;
4097 }
4098 }
4099
4100 fn apply_widget_scroll(
4106 &mut self,
4107 panel_key: &crate::widgets::PanelKey,
4108 list_key: &str,
4109 new_offset: usize,
4110 visible: usize,
4111 ) {
4112 let moved_sel = self.widget_registry.set_list_scroll(
4113 panel_key,
4114 list_key,
4115 new_offset as u32,
4116 visible as u32,
4117 );
4118 self.rerender_widget_panel(panel_key);
4119 if let Some(sel) = moved_sel {
4120 self.fire_widget_event(
4121 panel_key,
4122 list_key.to_string(),
4123 "select".to_string(),
4124 serde_json::json!({ "index": sel as i64 }),
4125 );
4126 }
4127 }
4128
4129 fn handle_floating_widget_context_click(
4138 &mut self,
4139 slot: super::PanelSlot,
4140 col: u16,
4141 row: u16,
4142 ) -> bool {
4143 let (panel_key, inner) = match self.panel(slot) {
4144 Some(fwp) => match fwp.last_inner_rect {
4145 Some(rect) => (fwp.panel_key.clone(), rect),
4146 None => return false,
4147 },
4148 None => return false,
4149 };
4150 if col < inner.x || col >= inner.x + inner.width {
4151 return false;
4152 }
4153 if row < inner.y || row >= inner.y + inner.height {
4154 return false;
4155 }
4156 let brow = (row - inner.y) as u32;
4157 let entries = self
4158 .panel(slot)
4159 .map(|f| f.entries.clone())
4160 .unwrap_or_default();
4161 let local_screen_col = (col - inner.x) as usize;
4162 let bcol = match entries.get(brow as usize) {
4163 Some(entry) => screen_col_to_byte(&entry.text, local_screen_col),
4164 None => return false,
4165 };
4166 let (mut payload, key, kind) =
4167 match self
4168 .widget_registry
4169 .hit_test(slot.buffer_id(), brow, bcol as u32)
4170 {
4171 Some((_, hit)) => (hit.payload.clone(), hit.widget_key.clone(), hit.widget_kind),
4172 None => return false,
4173 };
4174 if kind != "list" {
4176 return false;
4177 }
4178 if let Some(obj) = payload.as_object_mut() {
4181 obj.insert("col".to_string(), serde_json::json!(col));
4182 obj.insert("row".to_string(), serde_json::json!(row));
4183 }
4184 if !self
4185 .plugin_manager
4186 .read()
4187 .unwrap()
4188 .has_hook_handlers("widget_event")
4189 {
4190 return false;
4191 }
4192 self.fire_widget_event(&panel_key, key, "context".to_string(), payload);
4193 true
4194 }
4195
4196 fn floating_panel_is_anchored(&self) -> bool {
4199 matches!(
4200 self.floating_widget_panel.as_ref().map(|f| f.placement),
4201 Some(super::PanelPlacement::Anchored { .. })
4202 )
4203 }
4204
4205 fn point_in_floating_panel(&self, slot: super::PanelSlot, col: u16, row: u16) -> bool {
4209 let Some(inner) = self.panel(slot).and_then(|f| f.last_inner_rect) else {
4210 return false;
4211 };
4212 let x0 = inner.x.saturating_sub(1);
4213 let y0 = inner.y.saturating_sub(1);
4214 col >= x0 && col <= inner.x + inner.width && row >= y0 && row <= inner.y + inner.height
4216 }
4217
4218 fn dismiss_floating_panel_with_cancel(&mut self, slot: super::PanelSlot) {
4222 let panel_key = match self.panel(slot) {
4223 Some(f) => f.panel_key.clone(),
4224 None => return,
4225 };
4226 let widget_key = self
4227 .widget_registry
4228 .get(&panel_key)
4229 .map(|p| p.focus_key.clone())
4230 .unwrap_or_default();
4231 self.fire_widget_event(
4232 &panel_key,
4233 widget_key,
4234 "cancel".to_string(),
4235 serde_json::json!({}),
4236 );
4237 *self.panel_opt_mut(slot) = None;
4238 let _ = self.widget_registry.unmount(&panel_key);
4239 }
4240
4241 fn handle_floating_widget_click(&mut self, slot: super::PanelSlot, col: u16, row: u16) {
4244 if self.try_widget_scrollbar_press(slot, col, row) {
4247 return;
4248 }
4249 let (panel_key, inner) = match self.panel(slot) {
4250 Some(fwp) => match fwp.last_inner_rect {
4251 Some(rect) => (fwp.panel_key.clone(), rect),
4252 None => return,
4253 },
4254 None => return,
4255 };
4256 if col < inner.x || col >= inner.x + inner.width {
4257 return;
4258 }
4259 if row < inner.y || row >= inner.y + inner.height {
4260 return;
4261 }
4262 let brow = (row - inner.y) as u32;
4263 let entries = self
4264 .panel(slot)
4265 .map(|f| f.entries.clone())
4266 .unwrap_or_default();
4267 let local_screen_col = (col - inner.x) as usize;
4268 let bcol = match entries.get(brow as usize) {
4269 Some(entry) => screen_col_to_byte(&entry.text, local_screen_col),
4270 None => return,
4271 };
4272 let (mut hit_payload, hit_event, hit_key, hit_kind) =
4273 match self
4274 .widget_registry
4275 .hit_test(slot.buffer_id(), brow, bcol as u32)
4276 {
4277 Some((_, hit)) => (
4278 hit.payload.clone(),
4279 hit.event_type.to_string(),
4280 hit.widget_key.clone(),
4281 hit.widget_kind,
4282 ),
4283 None => {
4284 tracing::debug!(
4285 target: "fresh::dock",
4286 ?slot, col, row, brow, bcol,
4287 "handle_floating_widget_click: hit_test found no widget"
4288 );
4289 return;
4290 }
4291 };
4292 if !hit_key.is_empty() {
4293 let tabbable = self
4294 .widget_registry
4295 .get(&panel_key)
4296 .map(|p| p.tabbable.iter().any(|k| k == &hit_key))
4297 .unwrap_or(false);
4298 tracing::debug!(
4299 target: "fresh::dock",
4300 hit_key = %hit_key,
4301 hit_kind,
4302 hit_event = %hit_event,
4303 tabbable,
4304 "handle_floating_widget_click: hit"
4305 );
4306 if tabbable {
4307 self.set_panel_focus_and_notify(&panel_key, hit_key.clone());
4308 }
4309 self.rerender_widget_panel(&panel_key);
4310 } else {
4311 tracing::debug!(
4312 target: "fresh::dock",
4313 hit_kind,
4314 hit_event = %hit_event,
4315 "handle_floating_widget_click: hit with empty key (not focusable)"
4316 );
4317 }
4318 let handled_specially = if hit_kind == "tree" && hit_event == "expand" {
4319 if let Some(item_key) = hit_payload.get("key").and_then(|v| v.as_str()) {
4320 self.handle_widget_tree_expand_toggle(&panel_key, &hit_key, item_key);
4321 true
4322 } else {
4323 false
4324 }
4325 } else {
4326 false
4327 };
4328 if !handled_specially {
4329 if let Some(obj) = hit_payload.as_object_mut() {
4336 obj.insert("via".to_string(), serde_json::json!("click"));
4337 }
4338 self.fire_widget_event(&panel_key, hit_key, hit_event, hit_payload);
4339 }
4340 }
4341
4342 fn clear_active_window_drag_state(&mut self) {
4346 let ms = &mut self.active_window_mut().mouse_state;
4347 ms.dragging_scrollbar = None;
4348 ms.drag_start_row = None;
4349 ms.drag_start_top_byte = None;
4350 ms.dragging_horizontal_scrollbar = None;
4351 ms.drag_start_hcol = None;
4352 ms.drag_start_left_column = None;
4353 ms.dragging_separator = None;
4354 ms.drag_start_position = None;
4355 ms.drag_start_ratio = None;
4356 ms.dragging_file_explorer = false;
4357 ms.drag_start_explorer_width = None;
4358 ms.dragging_text_selection = false;
4359 ms.drag_selection_split = None;
4360 ms.drag_selection_anchor = None;
4361 ms.drag_selection_by_words = false;
4362 ms.drag_selection_word_end = None;
4363 ms.dragging_popup_scrollbar = None;
4364 ms.drag_start_popup_scroll = None;
4365 ms.dragging_prompt_scrollbar = false;
4366 ms.selecting_in_popup = None;
4367 }
4368}
4369
4370fn screen_col_to_byte(text: &str, target_col: usize) -> usize {
4375 use unicode_width::UnicodeWidthChar;
4376 let mut byte = 0;
4377 let mut col = 0usize;
4378 for ch in text.chars() {
4379 let w = UnicodeWidthChar::width(ch).unwrap_or(0);
4380 if col + w > target_col {
4381 return byte;
4382 }
4383 col += w;
4384 byte += ch.len_utf8();
4385 }
4386 byte
4387}