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 let actual_button_row = popup_rect.y + button_row_offset;
207 if row == actual_button_row {
208 let fg_key = self
209 .active_window_mut()
210 .theme_info_popup
211 .as_ref()
212 .and_then(|p| p.info.fg_key.clone());
213 self.active_window_mut().theme_info_popup = None;
214 if let Some(key) = fg_key {
215 self.fire_theme_inspect_hook(key);
216 }
217 return Ok(true);
218 }
219 return Ok(true);
221 }
222 }
223 self.active_window_mut().theme_info_popup = None;
225 needs_render = true;
226 }
227 }
228
229 match mouse_event.kind {
230 MouseEventKind::Down(MouseButton::Left) => {
231 if is_double_click || is_triple_click {
232 if let Some((buffer_id, byte_pos)) =
233 self.fold_toggle_line_at_screen_position(col, row)
234 {
235 self.active_window_mut()
236 .toggle_fold_at_byte(buffer_id, byte_pos);
237 needs_render = true;
238 return Ok(needs_render);
239 }
240 }
241 if is_triple_click {
242 self.handle_mouse_triple_click(col, row)?;
244 needs_render = true;
245 return Ok(needs_render);
246 }
247 if is_double_click {
248 self.handle_mouse_double_click(col, row)?;
250 needs_render = true;
251 return Ok(needs_render);
252 }
253 self.handle_mouse_click(col, row, mouse_event.modifiers)?;
254 needs_render = true;
255 }
256 MouseEventKind::Drag(MouseButton::Left) => {
257 self.handle_mouse_drag(col, row)?;
258 needs_render = true;
259 }
260 MouseEventKind::Up(MouseButton::Left) => {
261 if self.dock_resizing {
264 self.dock_resizing = false;
265 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
266 self.dock.as_ref().map(|f| f.placement)
267 {
268 self.dock_width = Some(width_cols);
269 }
270 return Ok(true);
271 }
272 let was_dragging_separator = self
274 .active_window_mut()
275 .mouse_state
276 .dragging_separator
277 .is_some();
278
279 if let Some(drag_state) = self.active_window_mut().mouse_state.dragging_tab.take() {
281 if drag_state.is_dragging() {
282 if let Some(drop_zone) = drag_state.drop_zone {
283 self.execute_tab_drop(
284 drag_state.buffer_id,
285 drag_state.source_split_id,
286 drop_zone,
287 );
288 }
289 }
290 }
291
292 self.release_widget_scrollbar();
294 self.clear_active_window_drag_state();
295
296 if was_dragging_separator {
299 self.relayout();
300 }
301
302 needs_render = true;
303 }
304 MouseEventKind::Moved => {
305 {
307 let content_rect = self
309 .active_layout()
310 .split_areas
311 .iter()
312 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
313 .map(|(_, _, rect, _, _, _)| *rect);
314
315 let (content_x, content_y) = content_rect.map(|r| (r.x, r.y)).unwrap_or((0, 0));
316
317 self.plugin_manager.read().unwrap().run_hook(
318 "mouse_move",
319 HookArgs::MouseMove {
320 column: col,
321 row,
322 content_x,
323 content_y,
324 },
325 );
326 }
327
328 let hover_changed = self.update_hover_target(col, row);
331 needs_render = needs_render || hover_changed;
332
333 let term_link_changed =
336 self.update_terminal_link_hover(col, row, mouse_event.modifiers);
337 needs_render = needs_render || term_link_changed;
338
339 if let Some((popup_rect, button_row_offset)) = self.theme_info_popup_rect() {
341 let button_row = popup_rect.y + button_row_offset;
342 let new_highlighted = row == button_row
343 && col >= popup_rect.x
344 && col < popup_rect.x + popup_rect.width;
345 if let Some(ref mut popup) = self.active_window_mut().theme_info_popup {
346 if popup.button_highlighted != new_highlighted {
347 popup.button_highlighted = new_highlighted;
348 needs_render = true;
349 }
350 }
351 }
352
353 self.update_lsp_hover_state(col, row);
355
356 let now_over = self
364 .dock
365 .as_ref()
366 .map(|d| {
367 d.scrollbar_hover_zones.iter().any(|z| {
368 col >= z.x && col < z.x + z.width && row >= z.y && row < z.y + z.height
369 })
370 })
371 .unwrap_or(false);
372 if let Some(d) = self.dock.as_mut() {
373 if d.scrollbar_zone_hovered != now_over {
374 d.scrollbar_zone_hovered = now_over;
375 needs_render = true;
376 }
377 }
378 }
379 MouseEventKind::ScrollUp => {
380 self.handle_vertical_scroll(col, row, mouse_event.modifiers, -3)?;
381 needs_render = true;
382 }
383 MouseEventKind::ScrollDown => {
384 self.handle_vertical_scroll(col, row, mouse_event.modifiers, 3)?;
385 needs_render = true;
386 }
387 MouseEventKind::ScrollLeft => {
388 self.active_window_mut()
390 .handle_horizontal_scroll(col, row, -3)?;
391 needs_render = true;
392 }
393 MouseEventKind::ScrollRight => {
394 self.active_window_mut()
396 .handle_horizontal_scroll(col, row, 3)?;
397 needs_render = true;
398 }
399 MouseEventKind::Down(MouseButton::Right) => {
400 if self.overlay_prompt_active() {
404 needs_render = true;
405 } else if mouse_event
406 .modifiers
407 .contains(crossterm::event::KeyModifiers::CONTROL)
408 {
409 self.show_theme_info_popup(col, row)?;
411 needs_render = true;
412 } else {
413 self.handle_right_click(col, row)?;
415 needs_render = true;
416 }
417 }
418 _ => {
419 }
421 }
422
423 self.active_window_mut().mouse_state.last_position = Some((col, row));
424 Ok(needs_render)
425 }
426
427 fn detect_multi_click(
429 &mut self,
430 mouse_event: &crossterm::event::MouseEvent,
431 col: u16,
432 row: u16,
433 ) -> (bool, bool) {
434 use crossterm::event::{MouseButton, MouseEventKind};
435 if !matches!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left)) {
436 return (false, false);
437 }
438 let now = self.time_source.now();
439 let threshold = std::time::Duration::from_millis(self.config.editor.double_click_time_ms);
440 let is_consecutive = if let (Some(prev_time), Some(prev_pos)) = (
441 self.active_window_mut().previous_click_time,
442 self.active_window_mut().previous_click_position,
443 ) {
444 now.duration_since(prev_time) < threshold && prev_pos == (col, row)
445 } else {
446 false
447 };
448 if is_consecutive {
449 self.active_window_mut().click_count += 1;
450 } else {
451 self.active_window_mut().click_count = 1;
452 }
453 self.active_window_mut().previous_click_time = Some(now);
454 self.active_window_mut().previous_click_position = Some((col, row));
455 let is_triple = self.active_window_mut().click_count >= 3;
456 let is_double = self.active_window_mut().click_count == 2;
457 if is_triple {
458 self.active_window_mut().click_count = 0;
459 self.active_window_mut().previous_click_time = None;
460 self.active_window_mut().previous_click_position = None;
461 }
462 (is_double, is_triple)
463 }
464
465 fn handle_vertical_scroll(
468 &mut self,
469 col: u16,
470 row: u16,
471 modifiers: crossterm::event::KeyModifiers,
472 delta: i32,
473 ) -> AnyhowResult<()> {
474 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
475 self.active_window_mut()
476 .handle_horizontal_scroll(col, row, delta)?;
477 } else if self.handle_overlay_prompt_scroll(col, row, delta) {
478 } else if self.handle_prompt_scroll(delta) {
482 } else if self.is_file_open_active()
484 && self.is_mouse_over_file_browser(col, row)
485 && self.handle_file_open_scroll(delta)
486 {
487 } else if self.is_mouse_over_any_popup(col, row) {
489 self.scroll_popup(delta);
490 } else if self.floating_widget_panel.is_some() {
491 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, delta);
497 } else if self.dock.is_some()
498 && self.handle_floating_widget_panel_wheel(super::PanelSlot::Dock, col, row, delta)
499 {
500 } else if self
503 .active_window()
504 .split_at_position(col, row)
505 .map(|(_, buffer_id)| self.handle_widget_panel_wheel(buffer_id, delta))
506 .unwrap_or(false)
507 {
508 } else {
510 if self.active_window().terminal_mode
511 && self
512 .active_window()
513 .is_terminal_buffer(self.active_buffer())
514 {
515 {
516 let __b = self.active_buffer();
517 self.active_window_mut().sync_terminal_to_buffer(__b);
518 };
519 self.active_window_mut().terminal_mode = false;
520 self.active_window_mut().key_context =
521 crate::input::keybindings::KeyContext::Normal;
522 }
523 self.dismiss_transient_popups();
524 self.active_window_mut()
525 .handle_mouse_scroll(col, row, delta)?;
526 }
527 Ok(())
528 }
529
530 fn handle_overlay_prompt_scroll(&mut self, col: u16, row: u16, delta: i32) -> bool {
541 if !self.overlay_prompt_active() {
542 return false;
543 }
544 let preview_area = self.active_chrome().prompt_preview_area;
545 let results_visible = self
546 .active_chrome()
547 .prompt_results_area
548 .map(|r| r.height as usize)
549 .unwrap_or(0);
550 if let Some(preview) = preview_area {
551 if in_rect(col, row, preview) {
552 self.active_window_mut()
553 .scroll_overlay_preview_by_lines(delta);
554 return true;
555 }
556 }
557 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
558 prompt.scroll_results(delta, results_visible);
559 }
560 true
561 }
562
563 pub(super) fn update_hover_target(&mut self, col: u16, row: u16) -> bool {
566 let old_target = self.active_window_mut().mouse_state.hover_target.clone();
567 let new_target = self.compute_hover_target(col, row);
568 let changed = old_target != new_target;
569 self.active_window_mut().mouse_state.hover_target = new_target.clone();
570
571 if let Some(active_menu_idx) = self.menu_state.active_menu {
574 let all_menus: Vec<crate::config::Menu> = self
575 .menus
576 .menus
577 .iter()
578 .chain(self.menu_state.plugin_menus.iter())
579 .cloned()
580 .collect();
581 if let Some(HoverTarget::MenuBarItem(hovered_menu_idx)) = new_target.clone() {
582 if hovered_menu_idx != active_menu_idx {
583 self.menu_state.open_menu(hovered_menu_idx);
584 return true; }
586 }
587
588 if let Some(HoverTarget::MenuDropdownItem(_, item_idx)) = new_target.clone() {
590 if self.menu_state.submenu_path.first() == Some(&item_idx) {
593 tracing::trace!(
594 "menu hover: staying on submenu parent item_idx={}, submenu_path={:?}",
595 item_idx,
596 self.menu_state.submenu_path
597 );
598 return changed;
599 }
600
601 if !self.menu_state.submenu_path.is_empty() {
603 tracing::trace!(
604 "menu hover: clearing submenu_path={:?} for different item_idx={}",
605 self.menu_state.submenu_path,
606 item_idx
607 );
608 self.menu_state.submenu_path.clear();
609 self.menu_state.highlighted_item = Some(item_idx);
610 return true;
611 }
612
613 if let Some(menu) = all_menus.get(active_menu_idx) {
615 if let Some(crate::config::MenuItem::Submenu { items, .. }) =
616 menu.items.get(item_idx)
617 {
618 if !items.is_empty() {
619 tracing::trace!("menu hover: opening submenu at item_idx={}", item_idx);
620 self.menu_state.submenu_path.push(item_idx);
621 self.menu_state.highlighted_item = Some(0);
622 return true;
623 }
624 }
625 }
626 if self.menu_state.highlighted_item != Some(item_idx) {
628 self.menu_state.highlighted_item = Some(item_idx);
629 return true;
630 }
631 }
632
633 if let Some(HoverTarget::SubmenuItem(depth, item_idx)) = new_target {
635 if self.menu_state.submenu_path.len() > depth
639 && self.menu_state.submenu_path.get(depth) == Some(&item_idx)
640 {
641 tracing::trace!(
642 "menu hover: staying on nested submenu parent depth={}, item_idx={}, submenu_path={:?}",
643 depth,
644 item_idx,
645 self.menu_state.submenu_path
646 );
647 return changed;
648 }
649
650 if self.menu_state.submenu_path.len() > depth {
652 tracing::trace!(
653 "menu hover: truncating submenu_path={:?} to depth={} for item_idx={}",
654 self.menu_state.submenu_path,
655 depth,
656 item_idx
657 );
658 self.menu_state.submenu_path.truncate(depth);
659 }
660
661 if let Some(items) = self
663 .menu_state
664 .get_current_items(&all_menus, active_menu_idx)
665 {
666 if let Some(crate::config::MenuItem::Submenu {
668 items: sub_items, ..
669 }) = items.get(item_idx)
670 {
671 if !sub_items.is_empty()
672 && !self.menu_state.submenu_path.contains(&item_idx)
673 {
674 tracing::trace!(
675 "menu hover: opening nested submenu at depth={}, item_idx={}",
676 depth,
677 item_idx
678 );
679 self.menu_state.submenu_path.push(item_idx);
680 self.menu_state.highlighted_item = Some(0);
681 return true;
682 }
683 }
684 if self.menu_state.highlighted_item != Some(item_idx) {
686 self.menu_state.highlighted_item = Some(item_idx);
687 return true;
688 }
689 }
690 }
691 }
692
693 if let Some(HoverTarget::TabContextMenuItem(item_idx)) = new_target.clone() {
695 if let Some(ref mut menu) = self.active_window_mut().tab_context_menu {
696 if menu.highlighted != item_idx {
697 menu.highlighted = item_idx;
698 return true;
699 }
700 }
701 }
702
703 if let Some(&HoverTarget::FileExplorerContextMenuItem(item_idx)) = new_target.as_ref() {
704 if let Some(ref mut menu) = self.active_window_mut().file_explorer_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::NewTabMenuItem(item_idx)) = new_target.clone() {
714 if let Some(ref mut menu) = self.active_window_mut().new_tab_menu {
715 if menu.highlighted != item_idx {
716 menu.highlighted = item_idx;
717 return true;
718 }
719 }
720 }
721
722 if old_target != new_target
725 && matches!(
726 old_target,
727 Some(HoverTarget::FileExplorerStatusIndicator(_))
728 )
729 {
730 self.dismiss_file_explorer_status_tooltip();
731 }
732
733 if let Some(HoverTarget::FileExplorerStatusIndicator(ref path)) = new_target {
734 if old_target != new_target {
736 self.show_file_explorer_status_tooltip(path.clone(), col, row);
737 return true;
738 }
739 }
740
741 changed
742 }
743
744 fn update_lsp_hover_state(&mut self, col: u16, row: u16) {
753 tracing::trace!(col, row, "update_lsp_hover_state: raw mouse position");
754
755 if self.active_window_mut().theme_info_popup.is_some()
759 || self.active_window_mut().tab_context_menu.is_some()
760 || self.active_window_mut().new_tab_menu.is_some()
761 || self
762 .active_window_mut()
763 .file_explorer_context_menu
764 .is_some()
765 || self.is_lsp_status_popup_open()
766 {
767 if self
768 .active_window_mut()
769 .mouse_state
770 .lsp_hover_state
771 .is_some()
772 {
773 self.active_window_mut().mouse_state.lsp_hover_state = None;
774 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
775 self.dismiss_transient_popups();
776 }
777 return;
778 }
779
780 if self.is_mouse_over_transient_popup(col, row) {
782 return;
783 }
784
785 let split_info = self
787 .active_layout()
788 .split_areas
789 .iter()
790 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
791 .map(|(split_id, buffer_id, content_rect, _, _, _)| {
792 (*split_id, *buffer_id, *content_rect)
793 });
794
795 let Some((split_id, buffer_id, content_rect)) = split_info else {
796 if self
798 .active_window_mut()
799 .mouse_state
800 .lsp_hover_state
801 .is_some()
802 {
803 self.active_window_mut().mouse_state.lsp_hover_state = None;
804 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
805 self.dismiss_transient_popups();
806 }
807 return;
808 };
809
810 let cached_mappings = self
812 .active_layout()
813 .view_line_mappings
814 .get(&split_id)
815 .cloned();
816 let gutter_width = self
817 .buffers()
818 .get(&buffer_id)
819 .map(|s| s.margins.left_total_width() as u16)
820 .unwrap_or(0);
821 let fallback = self
822 .buffers()
823 .get(&buffer_id)
824 .map(|s| s.buffer.len())
825 .unwrap_or(0);
826
827 let compose_width = self
829 .windows
830 .get(&self.active_window)
831 .and_then(|w| w.buffers.splits())
832 .map(|(_, vs)| vs)
833 .expect("active window must have a populated split layout")
834 .get(&split_id)
835 .and_then(|vs| vs.compose_width);
836
837 let Some(byte_pos) = super::click_geometry::screen_to_buffer_position(
839 col,
840 row,
841 content_rect,
842 gutter_width,
843 &cached_mappings,
844 fallback,
845 false, compose_width,
847 ) else {
848 if self
852 .active_window_mut()
853 .mouse_state
854 .lsp_hover_state
855 .is_some()
856 {
857 self.active_window_mut().mouse_state.lsp_hover_state = None;
858 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
859 }
860 return;
861 };
862
863 let content_col = col.saturating_sub(content_rect.x);
865 let text_col = content_col.saturating_sub(gutter_width) as usize;
866 let visual_row = row.saturating_sub(content_rect.y) as usize;
867
868 let line_info = cached_mappings
869 .as_ref()
870 .and_then(|mappings| mappings.get(visual_row))
871 .map(|line_mapping| {
872 (
873 line_mapping.visual_to_char.len(),
874 line_mapping.line_end_byte,
875 )
876 });
877
878 let is_past_line_end_or_empty = line_info
879 .map(|(line_len, _)| {
880 if line_len <= 1 {
882 return true;
883 }
884 text_col >= line_len
885 })
886 .unwrap_or(true);
888
889 tracing::trace!(
890 col,
891 row,
892 content_col,
893 text_col,
894 visual_row,
895 gutter_width,
896 byte_pos,
897 ?line_info,
898 is_past_line_end_or_empty,
899 "update_lsp_hover_state: position check"
900 );
901
902 if is_past_line_end_or_empty {
903 tracing::trace!(
904 "update_lsp_hover_state: mouse past line end or empty line, clearing hover"
905 );
906 if self
911 .active_window_mut()
912 .mouse_state
913 .lsp_hover_state
914 .is_some()
915 {
916 self.active_window_mut().mouse_state.lsp_hover_state = None;
917 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
918 }
919 return;
920 }
921
922 if let Some((start, end)) = self.active_window_mut().hover.symbol_range() {
924 if byte_pos >= start && byte_pos < end {
925 return;
927 }
928 }
929
930 if let Some((old_pos, _, _, _)) = self.active_window_mut().mouse_state.lsp_hover_state {
932 if old_pos == byte_pos {
933 return;
935 }
936 }
942
943 self.active_window_mut().mouse_state.lsp_hover_state =
945 Some((byte_pos, std::time::Instant::now(), col, row));
946 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
947 }
948
949 fn is_mouse_over_transient_popup(&self, col: u16, row: u16) -> bool {
951 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
952 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
953 hit_tester.is_over_transient_popup(col, row)
954 }
955
956 fn is_mouse_over_any_popup(&self, col: u16, row: u16) -> bool {
958 for (_, popup_area, _, _, _) in &self.active_chrome().global_popup_areas {
961 if in_rect(col, row, *popup_area) {
962 return true;
963 }
964 }
965 if let Some(outer) = self.active_chrome().suggestions_outer_area {
969 if in_rect(col, row, outer) {
970 return true;
971 }
972 }
973 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
974 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
975 hit_tester.is_over_popup(col, row)
976 }
977
978 fn is_mouse_over_file_browser(&self, col: u16, row: u16) -> bool {
980 self.active_window()
981 .file_browser_layout
982 .as_ref()
983 .is_some_and(|layout| layout.contains(col, row))
984 }
985
986 fn compute_hover_target(&self, col: u16, row: u16) -> Option<HoverTarget> {
991 self.hover_target_in_floating_overlays(col, row)
992 .or_else(|| self.hover_target_in_chrome(col, row))
993 }
994
995 fn hover_target_in_floating_overlays(&self, col: u16, row: u16) -> Option<HoverTarget> {
999 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
1000 let (menu_x, menu_y) = menu.clamped_position(
1001 self.active_chrome().last_frame_width,
1002 self.active_chrome().last_frame_height,
1003 );
1004 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
1005 let menu_height = menu.height();
1006
1007 if col >= menu_x
1008 && col < menu_x + menu_width
1009 && row > menu_y
1010 && row < menu_y + menu_height - 1
1011 {
1012 let item_idx = (row - menu_y - 1) as usize;
1013 if item_idx < menu.items().len() {
1014 return Some(HoverTarget::FileExplorerContextMenuItem(item_idx));
1015 }
1016 }
1017 }
1018
1019 if let Some(ref menu) = self.active_window().new_tab_menu {
1021 let menu_x = menu.position.0;
1022 let menu_y = menu.position.1;
1023 let menu_width = super::types::NEW_TAB_MENU_WIDTH;
1024 let items = super::types::NewTabMenuItem::all();
1025 let menu_height = items.len() as u16 + 2;
1026
1027 if col >= menu_x
1028 && col < menu_x + menu_width
1029 && row > menu_y
1030 && row < menu_y + menu_height - 1
1031 {
1032 let item_idx = (row - menu_y - 1) as usize;
1033 if item_idx < items.len() {
1034 return Some(HoverTarget::NewTabMenuItem(item_idx));
1035 }
1036 }
1037 }
1038
1039 if let Some(ref menu) = self.active_window().tab_context_menu {
1041 let menu_x = menu.position.0;
1042 let menu_y = menu.position.1;
1043 let menu_width = 22u16;
1044 let items = super::types::TabContextMenuItem::all();
1045 let menu_height = items.len() as u16 + 2;
1046
1047 if col >= menu_x
1048 && col < menu_x + menu_width
1049 && row > menu_y
1050 && row < menu_y + menu_height - 1
1051 {
1052 let item_idx = (row - menu_y - 1) as usize;
1053 if item_idx < items.len() {
1054 return Some(HoverTarget::TabContextMenuItem(item_idx));
1055 }
1056 }
1057 }
1058
1059 if let Some((inner_rect, start_idx, _visible_count, total_count)) =
1061 &self.active_chrome().suggestions_area
1062 {
1063 if in_rect(col, row, *inner_rect) {
1064 let relative_row = (row - inner_rect.y) as usize;
1065 let item_idx = start_idx + relative_row;
1066
1067 if item_idx < *total_count {
1068 return Some(HoverTarget::SuggestionItem(item_idx));
1069 }
1070 }
1071 }
1072
1073 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
1076 self.active_chrome().popup_areas.iter().rev()
1077 {
1078 if in_rect(col, row, *inner_rect) && *num_items > 0 {
1079 let relative_row = (row - inner_rect.y) as usize;
1081 let item_idx = scroll_offset + relative_row;
1082
1083 if item_idx < *num_items {
1084 return Some(HoverTarget::PopupListItem(*popup_idx, item_idx));
1085 }
1086 }
1087 }
1088
1089 if self.is_file_open_active() {
1091 if let Some(hover) = self.compute_file_browser_hover(col, row) {
1092 return Some(hover);
1093 }
1094 }
1095
1096 None
1097 }
1098
1099 fn hover_target_in_chrome(&self, col: u16, row: u16) -> Option<HoverTarget> {
1103 if self.active_window().menu_bar_visible {
1106 if let Some(ref menu_layout) = self.active_chrome().menu_layout {
1107 if let Some(menu_idx) = menu_layout.menu_at(col, row) {
1108 return Some(HoverTarget::MenuBarItem(menu_idx));
1109 }
1110 }
1111 }
1112
1113 if let Some(active_idx) = self.menu_state.active_menu {
1115 if let Some(hover) = self.compute_menu_dropdown_hover(col, row, active_idx) {
1116 return Some(hover);
1117 }
1118 }
1119
1120 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1122 let close_button_x = explorer_area.x + explorer_area.width.saturating_sub(3);
1124 if row == explorer_area.y
1125 && col >= close_button_x
1126 && col < explorer_area.x + explorer_area.width
1127 {
1128 return Some(HoverTarget::FileExplorerCloseButton);
1129 }
1130
1131 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;
1135
1136 if row >= content_start_y && row < content_end_y {
1137 if let Some(explorer) = self.file_explorer().as_ref() {
1139 let relative_row = row.saturating_sub(content_start_y) as usize;
1140 let scroll_offset = explorer.get_scroll_offset();
1141 let item_index = relative_row + scroll_offset;
1142 let display_nodes = explorer.get_display_nodes();
1143
1144 if item_index < display_nodes.len() {
1145 let (node_id, indent) = display_nodes[item_index];
1146 if let Some(node) = explorer.tree().get_node(node_id) {
1147 let theme = self.theme.read().unwrap();
1148 let neutral_fg = if node
1149 .entry
1150 .metadata
1151 .as_ref()
1152 .map(|m| m.is_hidden)
1153 .unwrap_or(false)
1154 {
1155 theme.line_number_fg
1156 } else if node.entry.is_symlink() {
1157 theme.syntax_type
1158 } else if node.is_dir() {
1159 theme.syntax_keyword
1160 } else {
1161 theme.editor_fg
1162 };
1163 let slot_resolver = self.file_explorer_slot_resolver();
1164 let slot_context = crate::view::file_tree::ExplorerSlotContext {
1165 path: &node.entry.path,
1166 is_dir: node.is_dir(),
1167 has_unsaved: self.file_explorer_node_has_unsaved_changes(
1168 &node.entry.path,
1169 node.is_dir(),
1170 ),
1171 is_symlink: node.entry.is_symlink(),
1172 is_hidden: node
1173 .entry
1174 .metadata
1175 .as_ref()
1176 .map(|m| m.is_hidden)
1177 .unwrap_or(false),
1178 decorations: &self.active_window().file_explorer_decoration_cache,
1179 slot_overrides: &self
1180 .active_window()
1181 .file_explorer_slot_override_cache,
1182 theme: &theme,
1183 neutral_fg,
1184 };
1185 let slot_resolution = slot_resolver.resolve(&slot_context);
1186 if let Some((slot_start, slot_end)) = crate::view::ui::file_explorer::FileExplorerRenderer::trailing_slot_screen_bounds(
1187 explorer,
1188 node_id,
1189 indent,
1190 content_width,
1191 &slot_resolution,
1192 &self.config.file_explorer.tree_indicator_collapsed,
1193 &self.config.file_explorer.tree_indicator_expanded,
1194 explorer_area,
1195 ) {
1196 if col >= slot_start && col < slot_end {
1197 return Some(HoverTarget::FileExplorerStatusIndicator(
1198 node.entry.path.clone(),
1199 ));
1200 }
1201 }
1202 }
1203 }
1204 }
1205 }
1206
1207 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1210 if col == border_x
1211 && row >= explorer_area.y
1212 && row < explorer_area.y + explorer_area.height
1213 {
1214 return Some(HoverTarget::FileExplorerBorder);
1215 }
1216 }
1217
1218 for (split_id, direction, sep_x, sep_y, sep_length) in &self.active_layout().separator_areas
1220 {
1221 let is_on_separator = match direction {
1222 SplitDirection::Horizontal => {
1223 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
1224 }
1225 SplitDirection::Vertical => {
1226 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
1227 }
1228 };
1229
1230 if is_on_separator {
1231 return Some(HoverTarget::SplitSeparator(*split_id, *direction));
1232 }
1233 }
1234
1235 for (split_id, btn_row, start_col, end_col) in &self.active_layout().close_split_areas {
1238 if row == *btn_row && col >= *start_col && col < *end_col {
1239 return Some(HoverTarget::CloseSplitButton(*split_id));
1240 }
1241 }
1242
1243 for (split_id, btn_row, start_col, end_col) in &self.active_layout().maximize_split_areas {
1244 if row == *btn_row && col >= *start_col && col < *end_col {
1245 return Some(HoverTarget::MaximizeSplitButton(*split_id));
1246 }
1247 }
1248
1249 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
1250 match tab_layout.hit_test(col, row) {
1251 Some(TabHit::CloseButton(target)) => {
1252 return Some(HoverTarget::TabCloseButton(target, *split_id));
1253 }
1254 Some(TabHit::TabName(target)) => {
1255 return Some(HoverTarget::TabName(target, *split_id));
1256 }
1257 Some(TabHit::ScrollLeft)
1258 | Some(TabHit::ScrollRight)
1259 | Some(TabHit::BarBackground)
1260 | Some(TabHit::NewTabButton)
1261 | None => {}
1262 }
1263 }
1264
1265 for (split_id, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1267 &self.active_layout().split_areas
1268 {
1269 if in_rect(col, row, *scrollbar_rect) {
1270 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
1271 let is_on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
1272
1273 if is_on_thumb {
1274 return Some(HoverTarget::ScrollbarThumb(*split_id));
1275 } else {
1276 return Some(HoverTarget::ScrollbarTrack(*split_id, relative_row as u16));
1277 }
1278 }
1279 }
1280
1281 if let Some((status_row, _status_x, _status_width)) = self.active_chrome().status_bar_area {
1283 if row == status_row {
1284 let indicators = [
1285 (
1286 self.active_chrome().status_bar_line_ending_area,
1287 HoverTarget::StatusBarLineEndingIndicator,
1288 ),
1289 (
1290 self.active_chrome().status_bar_encoding_area,
1291 HoverTarget::StatusBarEncodingIndicator,
1292 ),
1293 (
1294 self.active_chrome().status_bar_language_area,
1295 HoverTarget::StatusBarLanguageIndicator,
1296 ),
1297 (
1298 self.active_chrome().status_bar_lsp_area,
1299 HoverTarget::StatusBarLspIndicator,
1300 ),
1301 (
1302 self.active_chrome().status_bar_remote_area,
1303 HoverTarget::StatusBarRemoteIndicator,
1304 ),
1305 (
1306 self.active_chrome().status_bar_trust_area,
1307 HoverTarget::StatusBarTrustIndicator,
1308 ),
1309 (
1310 self.active_chrome().status_bar_warning_area,
1311 HoverTarget::StatusBarWarningBadge,
1312 ),
1313 ];
1314 for (area, target) in indicators {
1315 if let Some((indicator_row, start, end)) = area {
1316 if row == indicator_row && col >= start && col < end {
1317 return Some(target);
1318 }
1319 }
1320 }
1321 }
1322 }
1323
1324 if let Some(ref layout) = self.active_chrome().search_options_layout {
1326 use crate::view::ui::status_bar::SearchOptionsHover;
1327 if let Some(hover) = layout.checkbox_at(col, row) {
1328 return Some(match hover {
1329 SearchOptionsHover::CaseSensitive => HoverTarget::SearchOptionCaseSensitive,
1330 SearchOptionsHover::WholeWord => HoverTarget::SearchOptionWholeWord,
1331 SearchOptionsHover::Regex => HoverTarget::SearchOptionRegex,
1332 SearchOptionsHover::ConfirmEach => HoverTarget::SearchOptionConfirmEach,
1333 SearchOptionsHover::None => return None,
1334 });
1335 }
1336 }
1337
1338 None
1339 }
1340
1341 pub(super) fn handle_mouse_double_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1344 tracing::debug!("handle_mouse_double_click at col={}, row={}", col, row);
1345
1346 if let Some(r) = self.handle_click_suggestions_confirm(col, row) {
1350 return r;
1351 }
1352
1353 if self.overlay_prompt_active() {
1356 return Ok(());
1357 }
1358
1359 if self.is_mouse_over_any_popup(col, row) {
1361 return Ok(());
1363 } else {
1364 self.dismiss_transient_popups();
1366 }
1367
1368 if self.handle_file_open_double_click(col, row) {
1370 return Ok(());
1371 }
1372
1373 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1375 if col >= explorer_area.x
1376 && col < explorer_area.x + explorer_area.width
1377 && row > explorer_area.y && row < explorer_area.y + explorer_area.height
1379 {
1380 self.file_explorer_open_file()?;
1382 return Ok(());
1383 }
1384 }
1385
1386 let split_areas = self.active_layout().split_areas.clone();
1388 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1389 &split_areas
1390 {
1391 if in_rect(col, row, *content_rect) {
1392 if self.active_window().is_terminal_buffer(*buffer_id) {
1394 self.active_window_mut().key_context =
1395 crate::input::keybindings::KeyContext::Terminal;
1396 return Ok(());
1398 }
1399
1400 self.active_window_mut().key_context =
1401 crate::input::keybindings::KeyContext::Normal;
1402
1403 self.handle_editor_double_click(col, row, *split_id, *buffer_id, *content_rect)?;
1405 return Ok(());
1406 }
1407 }
1408
1409 Ok(())
1410 }
1411
1412 fn handle_editor_double_click(
1414 &mut self,
1415 col: u16,
1416 row: u16,
1417 split_id: LeafId,
1418 buffer_id: BufferId,
1419 content_rect: ratatui::layout::Rect,
1420 ) -> AnyhowResult<()> {
1421 use crate::model::event::Event;
1422
1423 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1427 return Ok(());
1428 }
1429
1430 self.focus_split(split_id, buffer_id);
1432
1433 let cached_mappings = self
1435 .active_layout()
1436 .view_line_mappings
1437 .get(&split_id)
1438 .cloned();
1439
1440 let leaf_id = split_id;
1442 let fallback = self
1443 .windows
1444 .get(&self.active_window)
1445 .and_then(|w| w.buffers.splits())
1446 .map(|(_, vs)| vs)
1447 .expect("active window must have a populated split layout")
1448 .get(&leaf_id)
1449 .map(|vs| vs.viewport.top_byte)
1450 .unwrap_or(0);
1451
1452 let compose_width = self
1454 .windows
1455 .get(&self.active_window)
1456 .and_then(|w| w.buffers.splits())
1457 .map(|(_, vs)| vs)
1458 .expect("active window must have a populated split layout")
1459 .get(&leaf_id)
1460 .and_then(|vs| vs.compose_width);
1461
1462 let gutter_width = self
1466 .active_window()
1467 .buffers
1468 .get(&buffer_id)
1469 .map(|s| s.margins.left_total_width() as u16)
1470 .unwrap_or(0);
1471
1472 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1473 col,
1474 row,
1475 content_rect,
1476 gutter_width,
1477 &cached_mappings,
1478 fallback,
1479 true, compose_width,
1481 ) else {
1482 return Ok(());
1483 };
1484
1485 let primary_cursor_id = self
1486 .active_window()
1487 .buffers
1488 .splits()
1489 .and_then(|(_, vs)| vs.get(&leaf_id))
1490 .map(|vs| vs.cursors.primary_id())
1491 .unwrap_or(CursorId(0));
1492 let event = Event::MoveCursor {
1493 cursor_id: primary_cursor_id,
1494 old_position: 0,
1495 new_position: target_position,
1496 old_anchor: None,
1497 new_anchor: None,
1498 old_sticky_column: 0,
1499 new_sticky_column: 0,
1500 };
1501
1502 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1503 event_log.append(event.clone());
1504 }
1505 self.active_window_mut()
1506 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1507
1508 self.handle_action(Action::SelectWord)?;
1510
1511 if let Some(cursor) = self
1513 .windows
1514 .get(&self.active_window)
1515 .and_then(|w| w.buffers.splits())
1516 .map(|(_, vs)| vs)
1517 .expect("active window must have a populated split layout")
1518 .get(&leaf_id)
1519 .map(|vs| vs.cursors.primary())
1520 {
1521 let sel_start = cursor.selection_start();
1524 let sel_end = cursor.selection_end();
1525 self.active_window_mut().mouse_state.dragging_text_selection = true;
1526 self.active_window_mut().mouse_state.drag_selection_split = Some(split_id);
1527 self.active_window_mut().mouse_state.drag_selection_anchor = Some(sel_start);
1528 self.active_window_mut().mouse_state.drag_selection_by_words = true;
1529 self.active_window_mut().mouse_state.drag_selection_word_end = Some(sel_end);
1530 }
1531
1532 Ok(())
1533 }
1534 pub(super) fn handle_mouse_triple_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1537 tracing::debug!("handle_mouse_triple_click at col={}, row={}", col, row);
1538
1539 if self.overlay_prompt_active() {
1542 return Ok(());
1543 }
1544
1545 if self.is_mouse_over_any_popup(col, row) {
1547 return Ok(());
1548 } else {
1549 self.dismiss_transient_popups();
1550 }
1551
1552 let split_areas = self.active_layout().split_areas.clone();
1554 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1555 &split_areas
1556 {
1557 if in_rect(col, row, *content_rect) {
1558 if self.active_window().is_terminal_buffer(*buffer_id) {
1559 return Ok(());
1560 }
1561
1562 self.active_window_mut().key_context =
1563 crate::input::keybindings::KeyContext::Normal;
1564
1565 self.handle_editor_triple_click(col, row, *split_id, *buffer_id, *content_rect)?;
1568 return Ok(());
1569 }
1570 }
1571
1572 Ok(())
1573 }
1574
1575 fn handle_editor_triple_click(
1577 &mut self,
1578 col: u16,
1579 row: u16,
1580 split_id: LeafId,
1581 buffer_id: BufferId,
1582 content_rect: ratatui::layout::Rect,
1583 ) -> AnyhowResult<()> {
1584 use crate::model::event::Event;
1585
1586 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1587 return Ok(());
1588 }
1589
1590 self.focus_split(split_id, buffer_id);
1592
1593 let cached_mappings = self
1595 .active_layout()
1596 .view_line_mappings
1597 .get(&split_id)
1598 .cloned();
1599
1600 let leaf_id = split_id;
1601 let fallback = self
1602 .windows
1603 .get(&self.active_window)
1604 .and_then(|w| w.buffers.splits())
1605 .map(|(_, vs)| vs)
1606 .expect("active window must have a populated split layout")
1607 .get(&leaf_id)
1608 .map(|vs| vs.viewport.top_byte)
1609 .unwrap_or(0);
1610
1611 let compose_width = self
1613 .windows
1614 .get(&self.active_window)
1615 .and_then(|w| w.buffers.splits())
1616 .map(|(_, vs)| vs)
1617 .expect("active window must have a populated split layout")
1618 .get(&leaf_id)
1619 .and_then(|vs| vs.compose_width);
1620
1621 let gutter_width = self
1625 .active_window()
1626 .buffers
1627 .get(&buffer_id)
1628 .map(|s| s.margins.left_total_width() as u16)
1629 .unwrap_or(0);
1630
1631 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1632 col,
1633 row,
1634 content_rect,
1635 gutter_width,
1636 &cached_mappings,
1637 fallback,
1638 true,
1639 compose_width,
1640 ) else {
1641 return Ok(());
1642 };
1643
1644 let primary_cursor_id = self
1645 .active_window()
1646 .buffers
1647 .splits()
1648 .and_then(|(_, vs)| vs.get(&leaf_id))
1649 .map(|vs| vs.cursors.primary_id())
1650 .unwrap_or(CursorId(0));
1651 let event = Event::MoveCursor {
1652 cursor_id: primary_cursor_id,
1653 old_position: 0,
1654 new_position: target_position,
1655 old_anchor: None,
1656 new_anchor: None,
1657 old_sticky_column: 0,
1658 new_sticky_column: 0,
1659 };
1660
1661 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1662 event_log.append(event.clone());
1663 }
1664 self.active_window_mut()
1665 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1666
1667 self.handle_action(Action::SelectLine)?;
1669
1670 Ok(())
1671 }
1672
1673 pub(super) fn overlay_prompt_active(&self) -> bool {
1681 self.active_window()
1682 .prompt
1683 .as_ref()
1684 .is_some_and(|p| p.overlay)
1685 }
1686
1687 pub(super) fn handle_mouse_click(
1688 &mut self,
1689 col: u16,
1690 row: u16,
1691 modifiers: crossterm::event::KeyModifiers,
1692 ) -> AnyhowResult<()> {
1693 if self.floating_widget_panel.is_some() {
1697 self.handle_floating_widget_click(super::PanelSlot::Floating, col, row);
1698 return Ok(());
1699 }
1700 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
1705 self.dock.as_ref().map(|f| f.placement)
1706 {
1707 if col == width_cols.saturating_sub(1) {
1708 self.dock_resizing = true;
1709 return Ok(());
1710 }
1711 }
1712 if let Some((super::PanelPlacement::LeftDock { width_cols }, focused)) =
1716 self.dock.as_ref().map(|f| (f.placement, f.focused))
1717 {
1718 if col < width_cols {
1719 tracing::debug!(
1720 target: "fresh::dock",
1721 col,
1722 row,
1723 width_cols,
1724 focused,
1725 "handle_mouse_click: click in dock column"
1726 );
1727 if !focused {
1728 self.refocus_floating_panel(super::PanelSlot::Dock);
1737 }
1738 self.handle_floating_widget_click(super::PanelSlot::Dock, col, row);
1739 return Ok(());
1740 }
1741 if focused {
1742 tracing::debug!(
1743 target: "fresh::dock",
1744 col,
1745 row,
1746 width_cols,
1747 "handle_mouse_click: click outside dock — blurring"
1748 );
1749 self.blur_floating_panel(super::PanelSlot::Dock);
1750 }
1751 }
1752 if let Some(r) = self.handle_click_context_menus(col, row) {
1753 return r;
1754 }
1755 if !self.is_mouse_over_any_popup(col, row) {
1756 self.dismiss_transient_popups();
1757 }
1758 if let Some(r) = self.handle_click_suggestions(col, row) {
1759 return r;
1760 }
1761 if let Some(r) = self.handle_click_prompt_scrollbar(col, row) {
1762 return r;
1763 }
1764 if let Some(r) = self.handle_click_popup_scrollbar(col, row) {
1765 return r;
1766 }
1767 if let Some(r) = self.handle_click_global_popups(col, row) {
1768 return r;
1769 }
1770 if let Some(r) = self.handle_click_buffer_popups(col, row) {
1771 return r;
1772 }
1773 if self.is_mouse_over_any_popup(col, row) {
1774 return Ok(());
1775 }
1776 if self.is_file_open_active() && self.handle_file_open_click(col, row) {
1777 return Ok(());
1778 }
1779 if let Some(r) = self.handle_click_menu_bar(col, row) {
1780 return r;
1781 }
1782 if let Some(r) = self.handle_click_file_explorer_area(col, row) {
1783 return r;
1784 }
1785 if let Some(r) = self.handle_click_scrollbar(col, row) {
1786 return r;
1787 }
1788 if let Some(r) = self.handle_click_horizontal_scrollbar(col, row) {
1789 return r;
1790 }
1791 if let Some(r) = self.handle_click_status_bar(col, row) {
1792 return r;
1793 }
1794 if let Some(r) = self.handle_click_search_options(col, row) {
1795 return r;
1796 }
1797 if let Some(r) = self.handle_click_split_separator(col, row) {
1798 return r;
1799 }
1800 if let Some(r) = self.handle_click_split_controls(col, row) {
1801 return r;
1802 }
1803 if let Some(r) = self.handle_click_tab_bar(col, row) {
1804 return r;
1805 }
1806
1807 if self.overlay_prompt_active() {
1814 let hit = self
1815 .active_chrome()
1816 .prompt_toolbar_hits
1817 .iter()
1818 .find(|(_, r)| in_rect(col, row, *r))
1819 .map(|(k, _)| k.clone());
1820 if let Some(widget_key) = hit {
1821 if let Some(p) = self.active_window_mut().prompt.as_mut() {
1825 p.toolbar_focus = Some(widget_key.clone());
1826 }
1827 self.toggle_overlay_toolbar_widget(&widget_key);
1828 }
1829 return Ok(());
1830 }
1831
1832 tracing::debug!(
1834 "handle_mouse_click: checking {} split_areas for click at ({}, {})",
1835 self.active_layout().split_areas.len(),
1836 col,
1837 row
1838 );
1839 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1840 &self.active_layout().split_areas
1841 {
1842 tracing::debug!(
1843 " split_id={:?}, content_rect=({}, {}, {}x{})",
1844 split_id,
1845 content_rect.x,
1846 content_rect.y,
1847 content_rect.width,
1848 content_rect.height
1849 );
1850 if in_rect(col, row, *content_rect) {
1851 tracing::debug!(" -> HIT! calling handle_editor_click");
1853 self.handle_editor_click(
1854 col,
1855 row,
1856 *split_id,
1857 *buffer_id,
1858 *content_rect,
1859 modifiers,
1860 )?;
1861 return Ok(());
1862 }
1863 }
1864 tracing::debug!(" -> No split area hit");
1865
1866 Ok(())
1867 }
1868
1869 fn handle_click_context_menus(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1873 if self
1874 .active_window_mut()
1875 .file_explorer_context_menu
1876 .is_some()
1877 {
1878 if let Some(result) = self.handle_file_explorer_context_menu_click(col, row) {
1879 return Some(result);
1880 }
1881 }
1882 if self.active_window_mut().tab_context_menu.is_some() {
1883 if let Some(result) = self.handle_tab_context_menu_click(col, row) {
1884 return Some(result);
1885 }
1886 }
1887 if self.active_window_mut().new_tab_menu.is_some() {
1888 if let Some(result) = self.handle_new_tab_menu_click(col, row) {
1889 return Some(result);
1890 }
1891 }
1892 None
1893 }
1894
1895 fn suggestion_at(&self, col: u16, row: u16) -> Option<usize> {
1899 let (inner_rect, start_idx, _visible_count, total_count) =
1900 self.active_chrome().suggestions_area?;
1901 if col < inner_rect.x
1902 || col >= inner_rect.x + inner_rect.width
1903 || row < inner_rect.y
1904 || row >= inner_rect.y + inner_rect.height
1905 {
1906 return None;
1907 }
1908 let relative_row = (row - inner_rect.y) as usize;
1909 let item_idx = start_idx + relative_row;
1910 if item_idx < total_count {
1911 Some(item_idx)
1912 } else {
1913 None
1914 }
1915 }
1916
1917 fn handle_click_suggestions(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1918 let item_idx = self.suggestion_at(col, row)?;
1919 let prompt = self.active_window_mut().prompt.as_mut()?;
1920 prompt.selected_suggestion = Some(item_idx);
1921 let confirms = prompt.prompt_type.click_confirms();
1922 if !confirms {
1923 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1927 prompt.input = suggestion.get_value().to_string();
1928 prompt.cursor_pos = prompt.input.len();
1929 }
1930 }
1931 if confirms {
1932 return Some(self.handle_action(Action::PromptConfirm));
1933 }
1934 Some(Ok(()))
1935 }
1936
1937 fn handle_click_suggestions_confirm(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1941 let item_idx = self.suggestion_at(col, row)?;
1942 let prompt = self.active_window_mut().prompt.as_mut()?;
1943 prompt.selected_suggestion = Some(item_idx);
1944 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1945 prompt.input = suggestion.get_value().to_string();
1946 prompt.cursor_pos = prompt.input.len();
1947 }
1948 Some(self.handle_action(Action::PromptConfirm))
1949 }
1950
1951 fn handle_click_prompt_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1957 use crate::view::ui::scrollbar::ScrollbarState;
1958 let sb_rect = self.active_chrome().suggestions_scrollbar_rect?;
1959 if col < sb_rect.x
1960 || col >= sb_rect.x + sb_rect.width
1961 || row < sb_rect.y
1962 || row >= sb_rect.y + sb_rect.height
1963 {
1964 return None;
1965 }
1966 let suggestions_area_visible = self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
1974 let active_window_id = self.active_window;
1975 let prompt = self
1976 .windows
1977 .get_mut(&active_window_id)
1978 .and_then(|w| w.prompt.as_mut())?;
1979 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
1980 let total = prompt.suggestions.len();
1981 let track_height = sb_rect.height as usize;
1982 let click_row = row.saturating_sub(sb_rect.y) as usize;
1983 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
1984 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
1985 self.active_window_mut()
1988 .mouse_state
1989 .dragging_prompt_scrollbar = true;
1990 Some(Ok(()))
1991 }
1992
1993 fn handle_click_popup_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1994 let scrollbar_info: Option<(usize, i32)> =
1996 self.active_chrome().popup_areas.iter().rev().find_map(
1997 |(popup_idx, _popup_rect, inner_rect, _scroll, _n, scrollbar_rect, total_lines)| {
1998 let sb_rect = scrollbar_rect.as_ref()?;
1999 if col >= sb_rect.x
2000 && col < sb_rect.x + sb_rect.width
2001 && row >= sb_rect.y
2002 && row < sb_rect.y + sb_rect.height
2003 {
2004 let relative_row = (row - sb_rect.y) as usize;
2005 let track_height = sb_rect.height as usize;
2006 let visible_lines = inner_rect.height as usize;
2007 if track_height > 0 && *total_lines > visible_lines {
2008 let max_scroll = total_lines.saturating_sub(visible_lines);
2009 let target = if track_height > 1 {
2010 (relative_row * max_scroll) / (track_height.saturating_sub(1))
2011 } else {
2012 0
2013 };
2014 Some((*popup_idx, target as i32))
2015 } else {
2016 Some((*popup_idx, 0))
2017 }
2018 } else {
2019 None
2020 }
2021 },
2022 );
2023 let (popup_idx, target_scroll) = scrollbar_info?;
2024 self.active_window_mut()
2025 .mouse_state
2026 .dragging_popup_scrollbar = Some(popup_idx);
2027 self.active_window_mut().mouse_state.drag_start_row = Some(row);
2028 let current_scroll = self
2029 .active_state()
2030 .popups
2031 .get(popup_idx)
2032 .map(|p| p.scroll_offset)
2033 .unwrap_or(0);
2034 self.active_window_mut().mouse_state.drag_start_popup_scroll = Some(current_scroll);
2035 let state = self.active_state_mut();
2036 if let Some(popup) = state.popups.get_mut(popup_idx) {
2037 popup.scroll_by(target_scroll - current_scroll as i32);
2038 }
2039 Some(Ok(()))
2040 }
2041
2042 fn handle_workspace_trust_mouse(
2048 &mut self,
2049 mouse_event: crossterm::event::MouseEvent,
2050 ) -> AnyhowResult<bool> {
2051 use crossterm::event::{MouseButton, MouseEventKind};
2052 let col = mouse_event.column;
2053 let row = mouse_event.row;
2054 let layout = self.active_chrome().workspace_trust_dialog.clone();
2055
2056 match mouse_event.kind {
2057 MouseEventKind::ScrollUp => {
2058 self.workspace_trust_scroll = self.workspace_trust_scroll.saturating_sub(2);
2059 }
2060 MouseEventKind::ScrollDown => {
2061 let max = layout.as_ref().map(|l| l.max_scroll).unwrap_or(0);
2062 self.workspace_trust_scroll = (self.workspace_trust_scroll + 2).min(max);
2063 }
2064 MouseEventKind::Down(MouseButton::Left) => {
2065 if let Some(layout) = layout {
2066 let hit = |r: ratatui::layout::Rect| in_rect(col, row, r);
2067 if hit(layout.ok) {
2068 let idx = self.current_workspace_trust_selection();
2069 self.confirm_workspace_trust(idx);
2070 } else if hit(layout.quit) {
2071 self.hide_popup();
2074 if !self.workspace_trust_prompt_cancellable {
2075 self.should_quit = true;
2076 }
2077 } else if let Some(i) = layout.radios.iter().position(|r| hit(*r)) {
2078 self.confirm_workspace_trust(i);
2079 }
2080 }
2082 }
2083 _ => {}
2085 }
2086 Ok(true)
2087 }
2088
2089 fn handle_click_global_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2090 for (popup_idx, popup_rect, inner_rect, scroll_offset, num_items) in self
2091 .active_chrome()
2092 .global_popup_areas
2093 .clone()
2094 .into_iter()
2095 .rev()
2096 {
2097 if popup_rect.width >= 5 {
2098 let cb_x = popup_rect.x + popup_rect.width - 4;
2099 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
2100 return Some(self.handle_action(Action::PopupCancel));
2101 }
2102 }
2103 if in_rect(col, row, inner_rect) && num_items > 0 {
2104 let relative_row = (row - inner_rect.y) as usize;
2105 let item_idx = scroll_offset + relative_row;
2106 if item_idx < num_items {
2107 if let Some(popup) = self.global_popups.get_mut(popup_idx) {
2108 if let crate::view::popup::PopupContent::List { items: _, selected } =
2109 &mut popup.content
2110 {
2111 *selected = item_idx;
2112 }
2113 }
2114 return Some(self.handle_action(Action::PopupConfirm));
2115 }
2116 }
2117 }
2118 None
2119 }
2120
2121 fn handle_click_buffer_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2122 let close_hit = self.active_chrome().popup_areas.iter().rev().find_map(
2124 |(_idx, popup_rect, _inner, _scroll, _n, _sb, _tl)| {
2125 if popup_rect.width < 5 {
2126 return None;
2127 }
2128 let cb_x = popup_rect.x + popup_rect.width - 4;
2129 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
2130 Some(())
2131 } else {
2132 None
2133 }
2134 },
2135 );
2136 if close_hit.is_some() {
2137 return Some(self.handle_action(Action::PopupCancel));
2138 }
2139
2140 let popup_areas = self.active_chrome().popup_areas.clone();
2142 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
2143 popup_areas.iter().rev()
2144 {
2145 if !in_rect(col, row, *inner_rect) {
2146 continue;
2147 }
2148 let relative_col = (col - inner_rect.x) as usize;
2149 let relative_row = (row - inner_rect.y) as usize;
2150
2151 let link_url = {
2152 let state = self.active_state();
2153 state
2154 .popups
2155 .top()
2156 .and_then(|p| p.link_at_position(relative_col, relative_row))
2157 };
2158 if let Some(url) = link_url {
2159 #[cfg(feature = "runtime")]
2160 if let Err(e) = open::that(&url) {
2161 self.set_status_message(format!("Failed to open URL: {}", e));
2162 } else {
2163 self.set_status_message(format!("Opening: {}", url));
2164 }
2165 return Some(Ok(()));
2166 }
2167
2168 if *num_items > 0 {
2169 let item_idx = scroll_offset + relative_row;
2170 if item_idx < *num_items {
2171 let state = self.active_state_mut();
2172 if let Some(popup) = state.popups.top_mut() {
2173 if let crate::view::popup::PopupContent::List { items: _, selected } =
2174 &mut popup.content
2175 {
2176 *selected = item_idx;
2177 }
2178 }
2179 return Some(self.handle_action(Action::PopupConfirm));
2180 }
2181 }
2182
2183 let is_text_popup = {
2184 let state = self.active_state();
2185 state.popups.top().is_some_and(|p| {
2186 matches!(
2187 p.content,
2188 crate::view::popup::PopupContent::Text(_)
2189 | crate::view::popup::PopupContent::Markdown(_)
2190 )
2191 })
2192 };
2193 if is_text_popup {
2194 let line = scroll_offset + relative_row;
2195 let popup_idx_copy = *popup_idx;
2196 let state = self.active_state_mut();
2197 if let Some(popup) = state.popups.top_mut() {
2198 popup.start_selection(line, relative_col);
2199 }
2200 self.active_window_mut().mouse_state.selecting_in_popup = Some(popup_idx_copy);
2201 return Some(Ok(()));
2202 }
2203 }
2204 None
2205 }
2206
2207 fn handle_click_menu_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2208 if self.active_window_mut().menu_bar_visible {
2209 let hit = self
2211 .active_chrome()
2212 .menu_layout
2213 .as_ref()
2214 .and_then(|ml| ml.menu_at(col, row));
2215 let layout_exists = self.active_chrome().menu_layout.is_some();
2216 if layout_exists {
2217 if let Some(menu_idx) = hit {
2218 if self.menu_state.active_menu == Some(menu_idx) {
2219 self.close_menu_with_auto_hide();
2220 } else {
2221 self.active_window_mut().on_editor_focus_lost();
2222 self.menu_state.open_menu(menu_idx);
2223 }
2224 return Some(Ok(()));
2225 } else if row == 0 {
2226 self.close_menu_with_auto_hide();
2227 return Some(Ok(()));
2228 }
2229 }
2230 }
2231
2232 if let Some(active_idx) = self.menu_state.active_menu {
2233 let all_menus: Vec<crate::config::Menu> = self
2234 .menus
2235 .menus
2236 .iter()
2237 .chain(self.menu_state.plugin_menus.iter())
2238 .cloned()
2239 .collect();
2240 if let Some(menu) = all_menus.get(active_idx) {
2241 match self.handle_menu_dropdown_click(col, row, menu) {
2242 Ok(Some(click_result)) => return Some(click_result),
2243 Ok(None) => {}
2244 Err(e) => return Some(Err(e)),
2245 }
2246 }
2247 self.close_menu_with_auto_hide();
2248 return Some(Ok(()));
2249 }
2250
2251 None
2252 }
2253
2254 fn handle_click_file_explorer_area(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2255 let explorer_area = self.active_layout().file_explorer_area?;
2256 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2257 if col == border_x && row >= explorer_area.y && row < explorer_area.y + explorer_area.height
2258 {
2259 self.active_window_mut().mouse_state.dragging_file_explorer = true;
2260 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2261 self.active_window_mut()
2262 .mouse_state
2263 .drag_start_explorer_width = Some(self.active_window().file_explorer_width);
2264 return Some(Ok(()));
2265 }
2266 if in_rect(col, row, explorer_area) {
2267 return Some(self.handle_file_explorer_click(col, row, explorer_area));
2268 }
2269 None
2270 }
2271
2272 fn handle_click_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2273 let (split_id, buffer_id, scrollbar_rect, is_on_thumb) =
2274 self.active_layout().split_areas.iter().find_map(
2275 |(split_id, buffer_id, _content, scrollbar_rect, thumb_start, thumb_end)| {
2276 if in_rect(col, row, *scrollbar_rect) {
2277 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
2278 let on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
2279 Some((*split_id, *buffer_id, *scrollbar_rect, on_thumb))
2280 } else {
2281 None
2282 }
2283 },
2284 )?;
2285
2286 self.focus_split(split_id, buffer_id);
2287 if is_on_thumb {
2288 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2289 self.active_window_mut().mouse_state.drag_start_row = Some(row);
2290 if self.active_window().is_composite_buffer(buffer_id) {
2291 if let Some(vs) = self
2292 .active_window()
2293 .composite_view_states
2294 .get(&(split_id, buffer_id))
2295 {
2296 self.active_window_mut()
2297 .mouse_state
2298 .drag_start_composite_scroll_row = Some(vs.scroll_row);
2299 }
2300 } else {
2301 let snap = self
2302 .windows
2303 .get(&self.active_window)
2304 .and_then(|w| w.buffers.splits())
2305 .map(|(_, vs)| vs)
2306 .expect("active window must have a populated split layout")
2307 .get(&split_id)
2308 .map(|vs| (vs.viewport.top_byte, vs.viewport.top_view_line_offset));
2309 if let Some((top_byte, top_view_line_offset)) = snap {
2310 let ms = &mut self.active_window_mut().mouse_state;
2311 ms.drag_start_top_byte = Some(top_byte);
2312 ms.drag_start_view_line_offset = Some(top_view_line_offset);
2313 }
2314 }
2315 } else {
2316 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2317 if let Err(e) = self.active_window_mut().handle_scrollbar_jump(
2318 col,
2319 row,
2320 split_id,
2321 buffer_id,
2322 scrollbar_rect,
2323 ) {
2324 return Some(Err(e));
2325 }
2326 self.active_window_mut().mouse_state.hover_target =
2327 Some(HoverTarget::ScrollbarThumb(split_id));
2328 }
2329 Some(Ok(()))
2330 }
2331
2332 fn handle_click_horizontal_scrollbar(
2333 &mut self,
2334 col: u16,
2335 row: u16,
2336 ) -> Option<AnyhowResult<()>> {
2337 let (split_id, buffer_id, hscrollbar_rect, max_content_width, is_on_thumb) = self
2338 .active_layout()
2339 .horizontal_scrollbar_areas
2340 .iter()
2341 .find_map(
2342 |(
2343 split_id,
2344 buffer_id,
2345 hscrollbar_rect,
2346 max_content_width,
2347 thumb_start,
2348 thumb_end,
2349 )| {
2350 if col >= hscrollbar_rect.x
2351 && col < hscrollbar_rect.x + hscrollbar_rect.width
2352 && row >= hscrollbar_rect.y
2353 && row < hscrollbar_rect.y + hscrollbar_rect.height
2354 {
2355 let relative_col = col.saturating_sub(hscrollbar_rect.x) as usize;
2356 let on_thumb = relative_col >= *thumb_start && relative_col < *thumb_end;
2357 Some((
2358 *split_id,
2359 *buffer_id,
2360 *hscrollbar_rect,
2361 *max_content_width,
2362 on_thumb,
2363 ))
2364 } else {
2365 None
2366 }
2367 },
2368 )?;
2369
2370 self.focus_split(split_id, buffer_id);
2371 self.active_window_mut()
2372 .mouse_state
2373 .dragging_horizontal_scrollbar = Some(split_id);
2374 if is_on_thumb {
2375 self.active_window_mut().mouse_state.drag_start_hcol = Some(col);
2376 if let Some(vs) = self
2377 .windows
2378 .get(&self.active_window)
2379 .and_then(|w| w.buffers.splits())
2380 .map(|(_, vs)| vs)
2381 .expect("active window must have a populated split layout")
2382 .get(&split_id)
2383 {
2384 self.active_window_mut().mouse_state.drag_start_left_column =
2385 Some(vs.viewport.left_column);
2386 }
2387 } else {
2388 self.active_window_mut().mouse_state.drag_start_hcol = None;
2389 self.active_window_mut().mouse_state.drag_start_left_column = None;
2390 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2391 let track_width = hscrollbar_rect.width as f64;
2392 let ratio = if track_width > 1.0 {
2393 (relative_col / (track_width - 1.0)).clamp(0.0, 1.0)
2394 } else {
2395 0.0
2396 };
2397 if let Some(vs) = self
2398 .windows
2399 .get_mut(&self.active_window)
2400 .and_then(|w| w.split_view_states_mut())
2401 .expect("active window must have a populated split layout")
2402 .get_mut(&split_id)
2403 {
2404 let visible_width = vs.viewport.width as usize;
2405 let max_scroll = max_content_width.saturating_sub(visible_width);
2406 let target_col = (ratio * max_scroll as f64).round() as usize;
2407 vs.viewport.left_column = target_col.min(max_scroll);
2408 vs.viewport.set_skip_ensure_visible();
2409 }
2410 }
2411 Some(Ok(()))
2412 }
2413
2414 fn handle_click_status_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2415 let (status_row, _status_x, _status_width) = self.active_chrome().status_bar_area?;
2416 if row != status_row {
2417 return None;
2418 }
2419 if let Some((r, s, e)) = self.active_chrome().status_bar_line_ending_area {
2429 if row == r && col >= s && col < e {
2430 self.dismiss_menu_popups_for_prompt();
2431 return Some(self.handle_action(Action::SetLineEnding));
2432 }
2433 }
2434 if let Some((r, s, e)) = self.active_chrome().status_bar_encoding_area {
2435 if row == r && col >= s && col < e {
2436 self.dismiss_menu_popups_for_prompt();
2437 return Some(self.handle_action(Action::SetEncoding));
2438 }
2439 }
2440 if let Some((r, s, e)) = self.active_chrome().status_bar_language_area {
2441 if row == r && col >= s && col < e {
2442 self.dismiss_menu_popups_for_prompt();
2443 return Some(self.handle_action(Action::SetLanguage));
2444 }
2445 }
2446 if let Some((r, s, e)) = self.active_chrome().status_bar_lsp_area {
2447 if row == r && col >= s && col < e {
2448 return Some(self.handle_action(Action::ShowLspStatus));
2451 }
2452 }
2453 if let Some((r, s, e)) = self.active_chrome().status_bar_remote_area {
2454 if row == r && col >= s && col < e {
2455 return Some(self.handle_action(Action::ShowRemoteIndicatorMenu));
2462 }
2463 }
2464 if let Some((r, s, e)) = self.active_chrome().status_bar_trust_area {
2465 if row == r && col >= s && col < e {
2466 self.dismiss_menu_popups_for_prompt();
2469 return Some(self.handle_action(Action::WorkspaceTrustPrompt));
2470 }
2471 }
2472 if let Some((r, s, e)) = self.active_chrome().status_bar_warning_area {
2473 if row == r && col >= s && col < e {
2474 self.dismiss_menu_popups_for_prompt();
2475 return Some(self.handle_action(Action::ShowWarnings));
2476 }
2477 }
2478 if let Some((r, s, e)) = self.active_chrome().status_bar_message_area {
2479 if row == r && col >= s && col < e {
2480 return Some(self.handle_action(Action::ShowStatusLog));
2481 }
2482 }
2483 let plugin_areas = self.active_chrome().status_bar_plugin_token_areas.clone();
2489 for (key, (r, s, e)) in plugin_areas {
2490 if row == r && col >= s && col < e {
2491 let (plugin_name, token_name) = match key.split_once(':') {
2492 Some((p, t)) => (p.to_string(), t.to_string()),
2493 None => (String::new(), key.clone()),
2494 };
2495 self.dismiss_menu_popups_for_prompt();
2496 self.plugin_manager.read().unwrap().run_hook(
2497 "status_bar_token_clicked",
2498 crate::services::plugins::hooks::HookArgs::StatusBarTokenClicked {
2499 plugin_name,
2500 token_name,
2501 },
2502 );
2503 return Some(Ok(()));
2504 }
2505 }
2506 None
2507 }
2508
2509 fn handle_click_search_options(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2510 use crate::view::ui::status_bar::SearchOptionsHover;
2511 let layout = self.active_chrome().search_options_layout.clone()?;
2512 match layout.checkbox_at(col, row)? {
2513 SearchOptionsHover::CaseSensitive => {
2514 Some(self.handle_action(Action::ToggleSearchCaseSensitive))
2515 }
2516 SearchOptionsHover::WholeWord => {
2517 Some(self.handle_action(Action::ToggleSearchWholeWord))
2518 }
2519 SearchOptionsHover::Regex => Some(self.handle_action(Action::ToggleSearchRegex)),
2520 SearchOptionsHover::ConfirmEach => {
2521 Some(self.handle_action(Action::ToggleSearchConfirmEach))
2522 }
2523 SearchOptionsHover::None => None,
2524 }
2525 }
2526
2527 fn handle_click_split_separator(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2528 let separator_areas = self.active_layout().separator_areas.clone();
2529 for (split_id, direction, sep_x, sep_y, sep_length) in &separator_areas {
2530 let is_on_separator = match direction {
2531 SplitDirection::Horizontal => {
2532 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
2533 }
2534 SplitDirection::Vertical => {
2535 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
2536 }
2537 };
2538 if is_on_separator {
2539 self.active_window_mut().mouse_state.dragging_separator =
2540 Some((*split_id, *direction));
2541 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2542 let ratio = self
2543 .split_manager_mut()
2544 .get_ratio((*split_id).into())
2545 .or_else(|| self.grouped_split_ratio(*split_id));
2546 if let Some(ratio) = ratio {
2547 self.active_window_mut().mouse_state.drag_start_ratio = Some(ratio);
2548 }
2549 return Some(Ok(()));
2550 }
2551 }
2552 None
2553 }
2554
2555 fn handle_click_split_controls(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2556 let close_split_id = self
2557 .active_layout()
2558 .close_split_areas
2559 .iter()
2560 .find(|(_, btn_row, start_col, end_col)| {
2561 row == *btn_row && col >= *start_col && col < *end_col
2562 })
2563 .map(|(split_id, _, _, _)| *split_id);
2564 if let Some(split_id) = close_split_id {
2565 if let Err(e) = self
2566 .windows
2567 .get_mut(&self.active_window)
2568 .and_then(|w| w.split_manager_mut())
2569 .expect("active window must have a populated split layout")
2570 .close_split(split_id)
2571 {
2572 self.set_status_message(
2573 t!("error.cannot_close_split", error = e.to_string()).to_string(),
2574 );
2575 } else {
2576 let new_active = self
2577 .windows
2578 .get(&self.active_window)
2579 .and_then(|w| w.buffers.splits())
2580 .map(|(mgr, _)| mgr)
2581 .expect("active window must have a populated split layout")
2582 .active_split();
2583 if let Some(buffer_id) = self
2584 .windows
2585 .get(&self.active_window)
2586 .and_then(|w| w.buffers.splits())
2587 .map(|(mgr, _)| mgr)
2588 .expect("active window must have a populated split layout")
2589 .buffer_for_split(new_active)
2590 {
2591 self.set_active_buffer(buffer_id);
2592 }
2593 self.set_status_message(t!("split.closed").to_string());
2594 }
2595 return Some(Ok(()));
2596 }
2597
2598 let maximize_target = self
2599 .active_layout()
2600 .maximize_split_areas
2601 .iter()
2602 .find(|(_, btn_row, start_col, end_col)| {
2603 row == *btn_row && col >= *start_col && col < *end_col
2604 })
2605 .map(|(split_id, _, _, _)| *split_id);
2606 if let Some(target) = maximize_target {
2607 let already_maximized = self
2614 .windows
2615 .get(&self.active_window)
2616 .and_then(|w| w.buffers.splits())
2617 .map(|(mgr, _)| mgr.is_maximized())
2618 .unwrap_or(false);
2619 if !already_maximized {
2620 if let Some(buffer_id) = self
2621 .windows
2622 .get(&self.active_window)
2623 .and_then(|w| w.buffers.splits())
2624 .map(|(mgr, _)| mgr)
2625 .expect("active window must have a populated split layout")
2626 .buffer_for_split(target)
2627 {
2628 self.focus_split(target, buffer_id);
2629 }
2630 }
2631 match self
2632 .windows
2633 .get_mut(&self.active_window)
2634 .and_then(|w| w.split_manager_mut())
2635 .expect("active window must have a populated split layout")
2636 .toggle_maximize_for(target)
2637 {
2638 Ok(maximized) => {
2639 let msg = if maximized {
2640 t!("split.maximized").to_string()
2641 } else {
2642 t!("split.restored").to_string()
2643 };
2644 self.set_status_message(msg);
2645 }
2646 Err(e) => self.set_status_message(e),
2647 }
2648 return Some(Ok(()));
2649 }
2650
2651 None
2652 }
2653
2654 fn handle_click_tab_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2655 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
2656 tracing::debug!(
2657 "Tab layout for split {:?}: bar_area={:?}, left_scroll={:?}, right_scroll={:?}",
2658 split_id,
2659 tab_layout.bar_area,
2660 tab_layout.left_scroll_area,
2661 tab_layout.right_scroll_area
2662 );
2663 }
2664 let tab_hit = self
2665 .active_layout()
2666 .tab_layouts
2667 .iter()
2668 .find_map(|(split_id, tab_layout)| {
2669 let hit = tab_layout.hit_test(col, row);
2670 tracing::debug!(
2671 "Tab hit_test at ({}, {}) for split {:?} returned {:?}",
2672 col,
2673 row,
2674 split_id,
2675 hit
2676 );
2677 hit.map(|h| (*split_id, h))
2678 });
2679 let (split_id, hit) = tab_hit?;
2680 match hit {
2681 TabHit::CloseButton(target) => {
2682 match target {
2683 crate::view::split::TabTarget::Buffer(buffer_id) => {
2684 self.focus_split(split_id, buffer_id);
2685 self.close_tab_in_split(buffer_id, split_id);
2686 }
2687 crate::view::split::TabTarget::Group(group_leaf) => {
2688 self.close_buffer_group_by_leaf(group_leaf);
2689 }
2690 }
2691 Some(Ok(()))
2692 }
2693 TabHit::TabName(target) => {
2694 let direction = self
2695 .windows
2696 .get(&self.active_window)
2697 .and_then(|w| w.buffers.splits())
2698 .map(|(_, vs)| vs)
2699 .expect("active window must have a populated split layout")
2700 .get(&split_id)
2701 .map(|vs| {
2702 let open = &vs.open_buffers;
2703 let cur = vs.active_target();
2704 let cur_idx = open.iter().position(|t| *t == cur);
2705 let new_idx = open.iter().position(|t| *t == target);
2706 match (cur_idx, new_idx) {
2707 (Some(c), Some(n)) if n > c => 1,
2708 (Some(c), Some(n)) if n < c => -1,
2709 _ => 0,
2710 }
2711 })
2712 .unwrap_or(0);
2713 self.active_window_mut()
2714 .animate_tab_switch(split_id, direction);
2715 match target {
2716 crate::view::split::TabTarget::Buffer(buffer_id) => {
2717 self.focus_split(split_id, buffer_id);
2718 self.active_window_mut()
2719 .promote_buffer_from_preview(buffer_id);
2720 self.active_window_mut().mouse_state.dragging_tab = Some(
2721 super::types::TabDragState::new(buffer_id, split_id, (col, row)),
2722 );
2723 }
2724 crate::view::split::TabTarget::Group(group_leaf) => {
2725 self.activate_group_tab(split_id, group_leaf);
2726 }
2727 }
2728 Some(Ok(()))
2729 }
2730 TabHit::ScrollLeft => {
2731 self.set_status_message("ScrollLeft clicked!".to_string());
2732 if let Some(vs) = self
2733 .windows
2734 .get_mut(&self.active_window)
2735 .and_then(|w| w.split_view_states_mut())
2736 .expect("active window must have a populated split layout")
2737 .get_mut(&split_id)
2738 {
2739 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_sub(10);
2740 }
2741 Some(Ok(()))
2742 }
2743 TabHit::ScrollRight => {
2744 self.set_status_message("ScrollRight clicked!".to_string());
2745 if let Some(vs) = self
2746 .windows
2747 .get_mut(&self.active_window)
2748 .and_then(|w| w.split_view_states_mut())
2749 .expect("active window must have a populated split layout")
2750 .get_mut(&split_id)
2751 {
2752 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_add(10);
2753 }
2754 Some(Ok(()))
2755 }
2756 TabHit::NewTabButton => {
2757 self.active_window_mut().tab_context_menu = None;
2760 self.active_window_mut().new_tab_menu =
2761 Some(super::types::NewTabMenu::new(split_id, col, row + 1));
2762 Some(Ok(()))
2763 }
2764 TabHit::BarBackground => None,
2765 }
2766 }
2767
2768 pub(super) fn handle_mouse_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2770 if self.dock_resizing {
2774 let max_cols = self.terminal_width.max(20).saturating_sub(20).max(10);
2775 let new_w = col.saturating_add(1).clamp(10, max_cols);
2776 let mut changed = false;
2777 if let Some(fwp) = self.dock.as_mut() {
2778 if let super::PanelPlacement::LeftDock { width_cols } = &mut fwp.placement {
2779 changed = *width_cols != new_w;
2780 *width_cols = new_w;
2781 }
2782 }
2783 if changed {
2784 self.dock_width = Some(new_w);
2793 self.relayout();
2796 }
2797 return Ok(());
2798 }
2799 if self.try_widget_scrollbar_drag(super::PanelSlot::Dock, row)
2802 || self.try_widget_scrollbar_drag(super::PanelSlot::Floating, row)
2803 {
2804 let _ = col;
2805 return Ok(());
2806 }
2807 if self.overlay_prompt_active()
2812 && !self.active_window().mouse_state.dragging_prompt_scrollbar
2813 {
2814 return Ok(());
2815 }
2816
2817 if let Some(dragging_split_id) = self.active_window_mut().mouse_state.dragging_scrollbar {
2819 let split_areas = self.active_layout().split_areas.clone();
2822 for (split_id, buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2823 &split_areas
2824 {
2825 if *split_id == dragging_split_id {
2826 if self.active_window().mouse_state.drag_start_row.is_some() {
2828 self.active_window_mut().handle_scrollbar_drag_relative(
2830 row,
2831 *split_id,
2832 *buffer_id,
2833 *scrollbar_rect,
2834 )?;
2835 } else {
2836 self.active_window_mut().handle_scrollbar_jump(
2838 col,
2839 row,
2840 *split_id,
2841 *buffer_id,
2842 *scrollbar_rect,
2843 )?;
2844 }
2845 return Ok(());
2846 }
2847 }
2848 }
2849
2850 if let Some(dragging_split_id) = self
2852 .active_window_mut()
2853 .mouse_state
2854 .dragging_horizontal_scrollbar
2855 {
2856 let hscrollbar_areas = self.active_layout().horizontal_scrollbar_areas.clone();
2861 for (
2862 split_id,
2863 _buffer_id,
2864 hscrollbar_rect,
2865 max_content_width,
2866 thumb_start,
2867 thumb_end,
2868 ) in &hscrollbar_areas
2869 {
2870 if *split_id == dragging_split_id {
2871 let track_width = hscrollbar_rect.width as f64;
2872 if track_width <= 1.0 {
2873 break;
2874 }
2875
2876 if let (Some(drag_start_hcol), Some(drag_start_left_column)) = (
2877 self.active_window_mut().mouse_state.drag_start_hcol,
2878 self.active_window_mut().mouse_state.drag_start_left_column,
2879 ) {
2880 let col_offset = (col as i32) - (drag_start_hcol as i32);
2883 if let Some(view_state) = self
2884 .windows
2885 .get_mut(&self.active_window)
2886 .and_then(|w| w.split_view_states_mut())
2887 .expect("active window must have a populated split layout")
2888 .get_mut(&dragging_split_id)
2889 {
2890 let visible_width = view_state.viewport.width as usize;
2891 let max_scroll = max_content_width.saturating_sub(visible_width);
2892 if max_scroll > 0 {
2893 let thumb_size = thumb_end.saturating_sub(*thumb_start).max(1);
2894 let track_travel = (track_width - thumb_size as f64).max(1.0);
2895 let scroll_per_pixel = max_scroll as f64 / track_travel;
2896 let scroll_offset =
2897 (col_offset as f64 * scroll_per_pixel).round() as i64;
2898 let new_left =
2899 (drag_start_left_column as i64 + scroll_offset).max(0) as usize;
2900 view_state.viewport.left_column = new_left.min(max_scroll);
2901 view_state.viewport.set_skip_ensure_visible();
2902 }
2903 }
2904 } else {
2905 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2907 let ratio = (relative_col / (track_width - 1.0)).clamp(0.0, 1.0);
2908
2909 if let Some(view_state) = self
2910 .windows
2911 .get_mut(&self.active_window)
2912 .and_then(|w| w.split_view_states_mut())
2913 .expect("active window must have a populated split layout")
2914 .get_mut(&dragging_split_id)
2915 {
2916 let visible_width = view_state.viewport.width as usize;
2917 let max_scroll = max_content_width.saturating_sub(visible_width);
2918 let target_col = (ratio * max_scroll as f64).round() as usize;
2919 view_state.viewport.left_column = target_col.min(max_scroll);
2920 view_state.viewport.set_skip_ensure_visible();
2921 }
2922 }
2923
2924 return Ok(());
2925 }
2926 }
2927 }
2928
2929 if let Some(popup_idx) = self.active_window_mut().mouse_state.selecting_in_popup {
2931 if let Some((_, _, inner_rect, scroll_offset, _, _, _)) = self
2933 .active_chrome()
2934 .popup_areas
2935 .iter()
2936 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2937 {
2938 if col >= inner_rect.x
2940 && col < inner_rect.x + inner_rect.width
2941 && row >= inner_rect.y
2942 && row < inner_rect.y + inner_rect.height
2943 {
2944 let relative_col = (col - inner_rect.x) as usize;
2945 let relative_row = (row - inner_rect.y) as usize;
2946 let line = scroll_offset + relative_row;
2947
2948 let state = self.active_state_mut();
2949 if let Some(popup) = state.popups.get_mut(popup_idx) {
2950 popup.extend_selection(line, relative_col);
2951 }
2952 }
2953 }
2954 return Ok(());
2955 }
2956
2957 if self
2962 .active_window_mut()
2963 .mouse_state
2964 .dragging_prompt_scrollbar
2965 {
2966 use crate::view::ui::scrollbar::ScrollbarState;
2967 let sb_rect = self.active_chrome().suggestions_scrollbar_rect;
2970 let suggestions_area_visible =
2971 self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
2972 let active_window_id = self.active_window;
2973 if let (Some(sb_rect), Some(prompt)) = (
2974 sb_rect,
2975 self.windows
2976 .get_mut(&active_window_id)
2977 .and_then(|w| w.prompt.as_mut()),
2978 ) {
2979 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
2980 let total = prompt.suggestions.len();
2981 let track_height = sb_rect.height as usize;
2982 let clamped_row =
2986 row.clamp(sb_rect.y, sb_rect.y + sb_rect.height.saturating_sub(1));
2987 let click_row = clamped_row.saturating_sub(sb_rect.y) as usize;
2988 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
2989 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
2990 }
2991 return Ok(());
2992 }
2993
2994 if let Some(popup_idx) = self
2996 .active_window_mut()
2997 .mouse_state
2998 .dragging_popup_scrollbar
2999 {
3000 if let Some((_, _, inner_rect, _, _, Some(sb_rect), total_lines)) = self
3002 .active_chrome()
3003 .popup_areas
3004 .iter()
3005 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
3006 {
3007 let track_height = sb_rect.height as usize;
3008 let visible_lines = inner_rect.height as usize;
3009
3010 if track_height > 0 && *total_lines > visible_lines {
3011 let relative_row = row.saturating_sub(sb_rect.y) as usize;
3012 let max_scroll = total_lines.saturating_sub(visible_lines);
3013 let target_scroll = if track_height > 1 {
3014 (relative_row * max_scroll) / (track_height.saturating_sub(1))
3015 } else {
3016 0
3017 };
3018
3019 let state = self.active_state_mut();
3020 if let Some(popup) = state.popups.get_mut(popup_idx) {
3021 let current_scroll = popup.scroll_offset as i32;
3022 let delta = target_scroll as i32 - current_scroll;
3023 popup.scroll_by(delta);
3024 }
3025 }
3026 }
3027 return Ok(());
3028 }
3029
3030 if let Some((split_id, direction)) = self.active_window_mut().mouse_state.dragging_separator
3032 {
3033 self.handle_separator_drag(col, row, split_id, direction)?;
3034 return Ok(());
3035 }
3036
3037 if self.active_window_mut().mouse_state.dragging_file_explorer {
3039 self.handle_file_explorer_border_drag(col)?;
3040 return Ok(());
3041 }
3042
3043 if self.active_window_mut().mouse_state.dragging_text_selection {
3045 self.handle_text_selection_drag(col, row)?;
3046 return Ok(());
3047 }
3048
3049 if self.active_window_mut().mouse_state.dragging_tab.is_some() {
3051 self.handle_tab_drag(col, row)?;
3052 return Ok(());
3053 }
3054
3055 Ok(())
3056 }
3057
3058 fn handle_text_selection_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
3060 use crate::model::event::Event;
3061 use crate::primitives::word_navigation::{find_word_end, find_word_start};
3062
3063 let Some(split_id) = self.active_window_mut().mouse_state.drag_selection_split else {
3064 return Ok(());
3065 };
3066 let Some(anchor_position) = self.active_window_mut().mouse_state.drag_selection_anchor
3067 else {
3068 return Ok(());
3069 };
3070
3071 let Some((buffer_id, content_rect)) = self
3073 .active_layout()
3074 .split_areas
3075 .iter()
3076 .find(|(sid, _, _, _, _, _)| *sid == split_id)
3077 .map(|(_, bid, rect, _, _, _)| (*bid, *rect))
3078 else {
3079 return Ok(());
3080 };
3081
3082 let cached_mappings = self
3084 .active_layout()
3085 .view_line_mappings
3086 .get(&split_id)
3087 .cloned();
3088
3089 let leaf_id = split_id;
3090
3091 let fallback = self
3093 .windows
3094 .get(&self.active_window)
3095 .and_then(|w| w.buffers.splits())
3096 .map(|(_, vs)| vs)
3097 .expect("active window must have a populated split layout")
3098 .get(&leaf_id)
3099 .map(|vs| vs.viewport.top_byte)
3100 .unwrap_or(0);
3101
3102 let compose_width = self
3104 .windows
3105 .get(&self.active_window)
3106 .and_then(|w| w.buffers.splits())
3107 .map(|(_, vs)| vs)
3108 .expect("active window must have a populated split layout")
3109 .get(&leaf_id)
3110 .and_then(|vs| vs.compose_width);
3111
3112 let drag_by_words = self.active_window_mut().mouse_state.drag_selection_by_words;
3116 let drag_word_end = self.active_window_mut().mouse_state.drag_selection_word_end;
3117
3118 let Some((target_position, new_position, anchor_position, new_sticky_column)) = self
3119 .active_window()
3120 .buffers
3121 .get(&buffer_id)
3122 .and_then(|state| {
3123 let gutter_width = state.margins.left_total_width() as u16;
3124 let target_position = super::click_geometry::screen_to_buffer_position(
3125 col,
3126 row,
3127 content_rect,
3128 gutter_width,
3129 &cached_mappings,
3130 fallback,
3131 true, compose_width,
3133 )?;
3134 let (new_position, anchor_pos) = if drag_by_words {
3135 if target_position >= anchor_position {
3136 (
3137 find_word_end(&state.buffer, target_position),
3138 anchor_position,
3139 )
3140 } else {
3141 let word_end = drag_word_end.unwrap_or(anchor_position);
3142 (find_word_start(&state.buffer, target_position), word_end)
3143 }
3144 } else {
3145 (target_position, anchor_position)
3146 };
3147 let new_sticky_column = state
3148 .buffer
3149 .offset_to_position(new_position)
3150 .map(|pos| pos.column);
3151 Some((target_position, new_position, anchor_pos, new_sticky_column))
3152 })
3153 else {
3154 return Ok(());
3155 };
3156 let _ = target_position;
3157
3158 let (primary_cursor_id, old_position, old_anchor, old_sticky_column) = self
3159 .active_window()
3160 .buffers
3161 .splits()
3162 .and_then(|(_, vs)| vs.get(&leaf_id))
3163 .map(|vs| {
3164 let cursor = vs.cursors.primary();
3165 (
3166 vs.cursors.primary_id(),
3167 cursor.position,
3168 cursor.anchor,
3169 cursor.sticky_column,
3170 )
3171 })
3172 .unwrap_or((CursorId(0), 0, None, 0));
3173
3174 let event = Event::MoveCursor {
3175 cursor_id: primary_cursor_id,
3176 old_position,
3177 new_position,
3178 old_anchor,
3179 new_anchor: Some(anchor_position),
3180 old_sticky_column,
3181 new_sticky_column: new_sticky_column.unwrap_or(old_sticky_column),
3182 };
3183
3184 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
3185 event_log.append(event.clone());
3186 }
3187 self.active_window_mut()
3188 .apply_event_to_buffer(buffer_id, leaf_id, &event);
3189
3190 Ok(())
3191 }
3192
3193 pub(super) fn handle_file_explorer_border_drag(&mut self, col: u16) -> AnyhowResult<()> {
3195 let Some((start_col, _start_row)) =
3196 self.active_window_mut().mouse_state.drag_start_position
3197 else {
3198 return Ok(());
3199 };
3200 let Some(start_width) = self
3201 .active_window_mut()
3202 .mouse_state
3203 .drag_start_explorer_width
3204 else {
3205 return Ok(());
3206 };
3207
3208 let delta = col as i32 - start_col as i32;
3209 let total_width = self.terminal_width as i32;
3210
3211 if total_width > 0 {
3215 use crate::config::ExplorerWidth;
3216 self.active_window_mut().file_explorer_width = match start_width {
3217 ExplorerWidth::Percent(start_pct) => {
3218 let percent_delta = (delta * 100) / total_width;
3219 let new_pct = (start_pct as i32 + percent_delta).clamp(0, 100) as u8;
3220 ExplorerWidth::Percent(new_pct)
3221 }
3222 ExplorerWidth::Columns(start_cols) => {
3223 let new_cols = (start_cols as i32 + delta).clamp(0, total_width) as u16;
3224 ExplorerWidth::Columns(new_cols)
3225 }
3226 };
3227 self.relayout();
3230 }
3231
3232 Ok(())
3233 }
3234
3235 pub(super) fn handle_separator_drag(
3237 &mut self,
3238 col: u16,
3239 row: u16,
3240 split_id: ContainerId,
3241 direction: SplitDirection,
3242 ) -> AnyhowResult<()> {
3243 let Some((start_col, start_row)) = self.active_window_mut().mouse_state.drag_start_position
3244 else {
3245 return Ok(());
3246 };
3247 let Some(start_ratio) = self.active_window_mut().mouse_state.drag_start_ratio else {
3248 return Ok(());
3249 };
3250 let Some(editor_area) = self.active_layout().editor_content_area else {
3251 return Ok(());
3252 };
3253
3254 let (delta, total_size) = match direction {
3256 SplitDirection::Horizontal => {
3257 let delta = row as i32 - start_row as i32;
3259 let total = editor_area.height as i32;
3260 (delta, total)
3261 }
3262 SplitDirection::Vertical => {
3263 let delta = col as i32 - start_col as i32;
3265 let total = editor_area.width as i32;
3266 (delta, total)
3267 }
3268 };
3269
3270 if total_size > 0 {
3273 let ratio_delta = delta as f32 / total_size as f32;
3274 let new_ratio = (start_ratio + ratio_delta).clamp(0.1, 0.9);
3275
3276 if self
3281 .windows
3282 .get(&self.active_window)
3283 .and_then(|w| w.buffers.splits())
3284 .map(|(mgr, _)| mgr)
3285 .expect("active window must have a populated split layout")
3286 .get_ratio(split_id.into())
3287 .is_some()
3288 {
3289 self.windows
3290 .get_mut(&self.active_window)
3291 .and_then(|w| w.split_manager_mut())
3292 .expect("active window must have a populated split layout")
3293 .set_ratio(split_id, new_ratio);
3294 } else {
3295 self.set_grouped_split_ratio(split_id, new_ratio);
3296 }
3297 self.relayout();
3300 }
3301
3302 Ok(())
3303 }
3304
3305 pub(super) fn handle_right_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
3307 self.active_window_mut().new_tab_menu = None;
3310
3311 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
3317 self.dock.as_ref().map(|f| f.placement)
3318 {
3319 if col < width_cols {
3320 if self.dock.as_ref().map(|f| !f.focused).unwrap_or(false) {
3321 self.refocus_floating_panel(super::PanelSlot::Dock);
3322 }
3323 self.handle_floating_widget_context_click(super::PanelSlot::Dock, col, row);
3324 return Ok(());
3325 }
3326 }
3327
3328 let frame_w = self.active_chrome().last_frame_width;
3329 let frame_h = self.active_chrome().last_frame_height;
3330 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
3331 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3332 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3333 let menu_height = menu.height();
3334 if col >= menu_x
3335 && col < menu_x + menu_width
3336 && row >= menu_y
3337 && row < menu_y + menu_height
3338 {
3339 return Ok(());
3340 }
3341 }
3342
3343 if let Some(ref menu) = self.active_window_mut().tab_context_menu {
3345 let menu_x = menu.position.0;
3346 let menu_y = menu.position.1;
3347 let menu_width = 22u16; let menu_height = super::types::TabContextMenuItem::all().len() as u16 + 2; if col >= menu_x
3352 && col < menu_x + menu_width
3353 && row >= menu_y
3354 && row < menu_y + menu_height
3355 {
3356 return Ok(());
3358 }
3359 }
3360
3361 if let Some(explorer_area) = self.active_layout().file_explorer_area {
3362 if col >= explorer_area.x
3363 && col < explorer_area.x + explorer_area.width
3364 && row < explorer_area.y + explorer_area.height
3365 && row > explorer_area.y
3366 {
3368 let relative_row = row.saturating_sub(explorer_area.y + 1);
3369 let (is_multi, is_root_selected) =
3370 if let Some(explorer) = self.file_explorer_mut().as_mut() {
3371 let display_nodes = explorer.get_display_nodes();
3372 let scroll_offset = explorer.get_scroll_offset();
3373 let clicked_index = (relative_row as usize) + scroll_offset;
3374 let mut clicked_is_root = false;
3375 if clicked_index < display_nodes.len() {
3376 let (node_id, _) = display_nodes[clicked_index];
3377 explorer.set_selected(Some(node_id));
3378 clicked_is_root = node_id == explorer.tree().root_id();
3379 }
3380 (explorer.has_multi_selection(), clicked_is_root)
3381 } else {
3382 (false, false)
3383 };
3384 self.active_window_mut().key_context =
3385 crate::input::keybindings::KeyContext::FileExplorer;
3386 self.active_window_mut().tab_context_menu = None;
3387 self.active_window_mut().file_explorer_context_menu =
3388 Some(super::types::FileExplorerContextMenu::new(
3389 col,
3390 row + 1,
3391 is_multi,
3392 is_root_selected,
3393 ));
3394 return Ok(());
3395 }
3396 }
3397
3398 self.active_window_mut().file_explorer_context_menu = None;
3399
3400 let tab_hit = self
3402 .active_layout()
3403 .tab_layouts
3404 .iter()
3405 .find_map(
3406 |(split_id, tab_layout)| match tab_layout.hit_test(col, row) {
3407 Some(TabHit::TabName(target) | TabHit::CloseButton(target)) => {
3408 target.as_buffer().map(|bid| (*split_id, bid))
3411 }
3412 _ => None,
3413 },
3414 );
3415
3416 if let Some((split_id, buffer_id)) = tab_hit {
3417 self.active_window_mut().tab_context_menu =
3419 Some(TabContextMenu::new(buffer_id, split_id, col, row + 1));
3420 } else {
3421 self.active_window_mut().tab_context_menu = None;
3423 }
3424
3425 Ok(())
3426 }
3427
3428 pub(super) fn handle_tab_context_menu_click(
3430 &mut self,
3431 col: u16,
3432 row: u16,
3433 ) -> Option<AnyhowResult<()>> {
3434 let menu = self.active_window_mut().tab_context_menu.as_ref()?;
3435 let menu_x = menu.position.0;
3436 let menu_y = menu.position.1;
3437 let menu_width = 22u16;
3438 let items = super::types::TabContextMenuItem::all();
3439 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
3443 {
3444 self.active_window_mut().tab_context_menu = None;
3446 return Some(Ok(()));
3447 }
3448
3449 if row == menu_y || row == menu_y + menu_height - 1 {
3451 return Some(Ok(()));
3452 }
3453
3454 let item_idx = (row - menu_y - 1) as usize;
3456 if item_idx >= items.len() {
3457 return Some(Ok(()));
3458 }
3459
3460 let buffer_id = menu.buffer_id;
3462 let split_id = menu.split_id;
3463 let item = items[item_idx];
3464
3465 self.active_window_mut().tab_context_menu = None;
3467
3468 Some(self.execute_tab_context_menu_action(item, buffer_id, split_id))
3470 }
3471
3472 pub(super) fn handle_new_tab_menu_click(
3474 &mut self,
3475 col: u16,
3476 row: u16,
3477 ) -> Option<AnyhowResult<()>> {
3478 let menu = self.active_window_mut().new_tab_menu.as_ref()?;
3479 let (menu_x, menu_y) = menu.position;
3480 let items = super::types::NewTabMenuItem::all();
3481 let menu_width = super::types::NEW_TAB_MENU_WIDTH;
3482 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
3486 {
3487 self.active_window_mut().new_tab_menu = None;
3488 return Some(Ok(()));
3489 }
3490
3491 if row == menu_y || row == menu_y + menu_height - 1 {
3493 return Some(Ok(()));
3494 }
3495
3496 let item_idx = (row - menu_y - 1) as usize;
3497 if item_idx >= items.len() {
3498 return Some(Ok(()));
3499 }
3500
3501 let split_id = menu.split_id;
3502 let item = items[item_idx];
3503
3504 self.active_window_mut().new_tab_menu = None;
3506
3507 Some(self.execute_new_tab_menu_action(item, split_id))
3508 }
3509
3510 fn execute_new_tab_menu_action(
3512 &mut self,
3513 item: super::types::NewTabMenuItem,
3514 split_id: LeafId,
3515 ) -> AnyhowResult<()> {
3516 use super::types::NewTabMenuItem;
3517 if let Some(buffer_id) = self
3521 .windows
3522 .get(&self.active_window)
3523 .and_then(|w| w.buffers.splits())
3524 .and_then(|(mgr, _)| mgr.buffer_for_split(split_id))
3525 {
3526 self.focus_split(split_id, buffer_id);
3527 }
3528 match item {
3529 NewTabMenuItem::NewTerminal => {
3530 self.open_terminal();
3531 }
3532 NewTabMenuItem::NewFile => {
3533 self.new_buffer();
3534 }
3535 }
3536 Ok(())
3537 }
3538
3539 fn execute_tab_context_menu_action(
3541 &mut self,
3542 item: super::types::TabContextMenuItem,
3543 buffer_id: BufferId,
3544 leaf_id: LeafId,
3545 ) -> AnyhowResult<()> {
3546 use super::types::TabContextMenuItem;
3547 match item {
3548 TabContextMenuItem::Close => {
3549 self.close_tab_in_split(buffer_id, leaf_id);
3550 }
3551 TabContextMenuItem::CloseOthers => {
3552 self.close_other_tabs_in_split(buffer_id, leaf_id);
3553 }
3554 TabContextMenuItem::CloseToRight => {
3555 self.close_tabs_to_right_in_split(buffer_id, leaf_id);
3556 }
3557 TabContextMenuItem::CloseToLeft => {
3558 self.close_tabs_to_left_in_split(buffer_id, leaf_id);
3559 }
3560 TabContextMenuItem::CloseAll => {
3561 self.close_all_tabs_in_split(leaf_id);
3562 }
3563 TabContextMenuItem::CopyRelativePath => {
3564 self.copy_buffer_path(buffer_id, true);
3565 }
3566 TabContextMenuItem::CopyFullPath => {
3567 self.copy_buffer_path(buffer_id, false);
3568 }
3569 }
3570
3571 Ok(())
3572 }
3573
3574 pub(super) fn handle_file_explorer_context_menu_key(
3577 &mut self,
3578 code: crossterm::event::KeyCode,
3579 modifiers: crossterm::event::KeyModifiers,
3580 ) -> Option<AnyhowResult<()>> {
3581 use crossterm::event::KeyCode;
3582 use crossterm::event::KeyModifiers;
3583
3584 if modifiers != KeyModifiers::NONE {
3585 return None;
3586 }
3587
3588 match code {
3589 KeyCode::Up => {
3590 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3591 menu.prev_item();
3592 }
3593 Some(Ok(()))
3594 }
3595 KeyCode::Down => {
3596 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3597 menu.next_item();
3598 }
3599 Some(Ok(()))
3600 }
3601 KeyCode::Enter => {
3602 let item = {
3603 let menu = self
3604 .active_window_mut()
3605 .file_explorer_context_menu
3606 .as_ref()?;
3607 menu.items()[menu.highlighted]
3608 };
3609 self.active_window_mut().file_explorer_context_menu = None;
3610 self.execute_file_explorer_context_menu_action(item);
3611 Some(Ok(()))
3612 }
3613 KeyCode::Esc => {
3614 self.active_window_mut().file_explorer_context_menu = None;
3615 Some(Ok(()))
3616 }
3617 _ => None,
3618 }
3619 }
3620
3621 pub(super) fn handle_file_explorer_context_menu_click(
3623 &mut self,
3624 col: u16,
3625 row: u16,
3626 ) -> Option<AnyhowResult<()>> {
3627 let frame_w = self.active_chrome().last_frame_width;
3629 let frame_h = self.active_chrome().last_frame_height;
3630 let clicked_item: Option<super::types::FileExplorerContextMenuItem> = {
3631 let menu = self.active_window().file_explorer_context_menu.as_ref()?;
3632 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3633 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3634 let menu_height = menu.height();
3635
3636 if col < menu_x
3637 || col >= menu_x + menu_width
3638 || row < menu_y
3639 || row >= menu_y + menu_height
3640 {
3641 self.active_window_mut().file_explorer_context_menu = None;
3642 return Some(Ok(()));
3643 }
3644
3645 if row == menu_y || row == menu_y + menu_height - 1 {
3646 return Some(Ok(()));
3647 }
3648
3649 let item_idx = (row - menu_y - 1) as usize;
3650 menu.items().get(item_idx).copied()
3651 };
3652
3653 self.active_window_mut().file_explorer_context_menu = None;
3654 if let Some(item) = clicked_item {
3655 self.execute_file_explorer_context_menu_action(item);
3656 }
3657 Some(Ok(()))
3658 }
3659
3660 fn execute_file_explorer_context_menu_action(
3661 &mut self,
3662 item: super::types::FileExplorerContextMenuItem,
3663 ) {
3664 use super::types::FileExplorerContextMenuItem;
3665 match item {
3666 FileExplorerContextMenuItem::NewFile => self.file_explorer_new_file(),
3667 FileExplorerContextMenuItem::NewDirectory => self.file_explorer_new_directory(),
3668 FileExplorerContextMenuItem::Rename => self.file_explorer_rename(),
3669 FileExplorerContextMenuItem::Cut => self.active_window_mut().file_explorer_cut(),
3670 FileExplorerContextMenuItem::Copy => self.active_window_mut().file_explorer_copy(),
3671 FileExplorerContextMenuItem::Paste => self.file_explorer_paste(),
3672 FileExplorerContextMenuItem::Duplicate => self.file_explorer_duplicate(),
3673 FileExplorerContextMenuItem::Delete => self.file_explorer_delete(),
3674 FileExplorerContextMenuItem::CopyFullPath => self.file_explorer_copy_path(false),
3675 FileExplorerContextMenuItem::CopyRelativePath => self.file_explorer_copy_path(true),
3676 }
3677 }
3678
3679 fn show_file_explorer_status_tooltip(&mut self, path: std::path::PathBuf, col: u16, row: u16) {
3681 use crate::view::popup::{Popup, PopupPosition};
3682 use ratatui::style::Style;
3683
3684 let is_directory = path.is_dir();
3685 let has_unsaved_changes = self.file_explorer_node_has_unsaved_changes(&path, is_directory);
3686
3687 let node_metadata = self
3688 .file_explorer()
3689 .and_then(|explorer| explorer.tree().get_node_by_path(&path))
3690 .and_then(|node| node.entry.metadata.as_ref());
3691 let is_hidden = node_metadata.map(|m| m.is_hidden).unwrap_or(false);
3692 let is_symlink = path.is_symlink();
3693 let theme = self.theme.read().unwrap();
3694 let neutral_fg = if is_hidden {
3695 theme.line_number_fg
3696 } else if is_symlink {
3697 theme.syntax_type
3698 } else if is_directory {
3699 theme.syntax_keyword
3700 } else {
3701 theme.editor_fg
3702 };
3703 let slot_resolver = self.file_explorer_slot_resolver();
3704 let slot_context = crate::view::file_tree::ExplorerSlotContext {
3705 path: &path,
3706 is_dir: is_directory,
3707 has_unsaved: has_unsaved_changes,
3708 is_symlink,
3709 is_hidden,
3710 decorations: &self.active_window().file_explorer_decoration_cache,
3711 slot_overrides: &self.active_window().file_explorer_slot_override_cache,
3712 theme: &theme,
3713 neutral_fg,
3714 };
3715 let slot_resolution = slot_resolver.resolve(&slot_context);
3716
3717 let Some(summary) = slot_resolution.trailing.and_then(|slot| slot.tooltip) else {
3719 return; };
3721 let mut lines = summary.lines;
3722 let has_custom_trailing_override = self
3723 .active_window()
3724 .file_explorer_slot_override_cache
3725 .has_trailing_override_for_path(&path);
3726
3727 if !has_custom_trailing_override {
3728 if is_directory {
3732 if let Some(modified_files) = self.get_modified_files_in_directory(&path) {
3733 lines.push(String::new()); lines.push("Modified files:".to_string());
3735 const MAX_FILES: usize = 8;
3736 for (i, file) in modified_files.iter().take(MAX_FILES).enumerate() {
3737 let display_name = file
3739 .strip_prefix(&path)
3740 .unwrap_or(file)
3741 .to_string_lossy()
3742 .to_string();
3743 lines.push(format!(" {}", display_name));
3744 if i == MAX_FILES - 1 && modified_files.len() > MAX_FILES {
3745 lines.push(format!(
3746 " ... and {} more",
3747 modified_files.len() - MAX_FILES
3748 ));
3749 break;
3750 }
3751 }
3752 }
3753 } else if let Some(stats) = self.get_git_diff_stats(&path) {
3754 lines.push(String::new()); lines.push(stats);
3757 }
3758 }
3759
3760 if lines.is_empty() {
3761 return;
3762 }
3763
3764 let mut popup = Popup::text(lines, &*self.theme.read().unwrap());
3766 popup.title = Some(summary.title);
3767 popup.transient = true;
3768 popup.position = PopupPosition::Fixed { x: col, y: row + 1 };
3769 popup.width = 50;
3770 popup.max_height = 15;
3771 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
3772 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
3773
3774 let __buffer_id = self.active_buffer();
3776 if let Some(state) = self
3777 .windows
3778 .get_mut(&self.active_window)
3779 .map(|w| &mut w.buffers)
3780 .expect("active window present")
3781 .get_mut(&__buffer_id)
3782 {
3783 state.popups.show(popup);
3784 }
3785 }
3786
3787 fn file_explorer_node_has_unsaved_changes(
3788 &self,
3789 path: &std::path::Path,
3790 is_directory: bool,
3791 ) -> bool {
3792 if is_directory {
3793 self.windows
3794 .get(&self.active_window)
3795 .map(|w| &w.buffers)
3796 .expect("active window present")
3797 .iter()
3798 .any(|(buffer_id, state)| {
3799 if state.buffer.is_modified() {
3800 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3801 {
3802 if let Some(file_path) = metadata.file_path() {
3803 return file_path.starts_with(path);
3804 }
3805 }
3806 }
3807 false
3808 })
3809 } else {
3810 self.windows
3811 .get(&self.active_window)
3812 .map(|w| &w.buffers)
3813 .expect("active window present")
3814 .iter()
3815 .any(|(buffer_id, state)| {
3816 if state.buffer.is_modified() {
3817 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3818 {
3819 return metadata.file_path().map(|p| p.as_path()) == Some(path);
3820 }
3821 }
3822 false
3823 })
3824 }
3825 }
3826
3827 fn dismiss_file_explorer_status_tooltip(&mut self) {
3829 let __buffer_id = self.active_buffer();
3831 if let Some(state) = self
3832 .windows
3833 .get_mut(&self.active_window)
3834 .map(|w| &mut w.buffers)
3835 .expect("active window present")
3836 .get_mut(&__buffer_id)
3837 {
3838 state.popups.dismiss_transient();
3839 }
3840 }
3841
3842 fn get_git_diff_stats(&self, path: &std::path::Path) -> Option<String> {
3844 use crate::services::process_hidden::HideWindow;
3845 use std::process::Command;
3846
3847 let output = Command::new("git")
3849 .args(["diff", "--numstat", "--"])
3850 .arg(path)
3851 .current_dir(self.working_dir())
3852 .hide_window()
3853 .output()
3854 .ok()?;
3855
3856 if !output.status.success() {
3857 return None;
3858 }
3859
3860 let stdout = String::from_utf8_lossy(&output.stdout);
3861 let line = stdout.lines().next()?;
3862 let parts: Vec<&str> = line.split('\t').collect();
3863
3864 if parts.len() >= 2 {
3865 let insertions = parts[0];
3866 let deletions = parts[1];
3867
3868 if insertions == "-" && deletions == "-" {
3870 return Some("Binary file changed".to_string());
3871 }
3872
3873 let ins: i32 = insertions.parse().unwrap_or(0);
3874 let del: i32 = deletions.parse().unwrap_or(0);
3875
3876 if ins > 0 || del > 0 {
3877 return Some(format!("+{} -{} lines", ins, del));
3878 }
3879 }
3880
3881 let staged_output = Command::new("git")
3883 .args(["diff", "--numstat", "--cached", "--"])
3884 .arg(path)
3885 .current_dir(self.working_dir())
3886 .hide_window()
3887 .output()
3888 .ok()?;
3889
3890 if staged_output.status.success() {
3891 let staged_stdout = String::from_utf8_lossy(&staged_output.stdout);
3892 if let Some(line) = staged_stdout.lines().next() {
3893 let parts: Vec<&str> = line.split('\t').collect();
3894 if parts.len() >= 2 {
3895 let insertions = parts[0];
3896 let deletions = parts[1];
3897
3898 if insertions == "-" && deletions == "-" {
3899 return Some("Binary file staged".to_string());
3900 }
3901
3902 let ins: i32 = insertions.parse().unwrap_or(0);
3903 let del: i32 = deletions.parse().unwrap_or(0);
3904
3905 if ins > 0 || del > 0 {
3906 return Some(format!("+{} -{} lines (staged)", ins, del));
3907 }
3908 }
3909 }
3910 }
3911
3912 None
3913 }
3914
3915 fn get_modified_files_in_directory(
3917 &self,
3918 dir_path: &std::path::Path,
3919 ) -> Option<Vec<std::path::PathBuf>> {
3920 let modified_files = self
3921 .active_window()
3922 .file_explorer_decoration_cache
3923 .direct_paths_under(dir_path);
3924
3925 (!modified_files.is_empty()).then_some(modified_files)
3926 }
3927
3928 fn handle_floating_widget_panel_wheel(
3940 &mut self,
3941 slot: super::PanelSlot,
3942 col: u16,
3943 row: u16,
3944 delta: i32,
3945 ) -> bool {
3946 let inner = match self.panel(slot) {
3947 Some(fwp) => match fwp.last_inner_rect {
3948 Some(rect) => rect,
3949 None => return false,
3950 },
3951 None => return false,
3952 };
3953 if col < inner.x || col >= inner.x + inner.width {
3954 return false;
3955 }
3956 if row < inner.y || row >= inner.y + inner.height {
3957 return false;
3958 }
3959 let scrolled = self.handle_widget_panel_wheel(slot.buffer_id(), delta);
3960 let is_dock = matches!(
3964 self.panel(slot).map(|f| f.placement),
3965 Some(super::PanelPlacement::LeftDock { .. })
3966 );
3967 scrolled || is_dock
3968 }
3969
3970 fn try_widget_scrollbar_press(&mut self, slot: super::PanelSlot, col: u16, row: u16) -> bool {
3975 use crate::view::ui::scrollbar::ScrollbarState;
3976 let (panel_id, tracks) = match self.panel(slot) {
3977 Some(fwp) => (fwp.panel_id, fwp.scrollbar_tracks.clone()),
3978 None => return false,
3979 };
3980 for t in &tracks {
3981 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
3982 let pressed = self
3983 .panel_mut(slot)
3984 .and_then(|fwp| fwp.scrollbar_mouse.press(state, t.rect, col, row));
3985 if let Some(new_offset) = pressed {
3986 if let Some(fwp) = self.panel_mut(slot) {
3987 fwp.scrollbar_drag_key = Some(t.list_key.clone());
3988 }
3989 self.apply_widget_scroll(panel_id, &t.list_key, new_offset, t.visible);
3990 return true;
3991 }
3992 }
3993 false
3994 }
3995
3996 fn try_widget_scrollbar_drag(&mut self, slot: super::PanelSlot, row: u16) -> bool {
3999 use crate::view::ui::scrollbar::ScrollbarState;
4000 let (panel_id, key) = match self.panel(slot) {
4001 Some(fwp) => match &fwp.scrollbar_drag_key {
4002 Some(k) => (fwp.panel_id, k.clone()),
4003 None => return false,
4004 },
4005 None => return false,
4006 };
4007 let track = self.panel(slot).and_then(|fwp| {
4010 fwp.scrollbar_tracks
4011 .iter()
4012 .find(|t| t.list_key == key)
4013 .cloned()
4014 });
4015 let Some(t) = track else {
4016 return false;
4017 };
4018 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
4019 let new_offset = self
4020 .panel_mut(slot)
4021 .and_then(|fwp| fwp.scrollbar_mouse.drag(state, t.rect, row));
4022 if let Some(off) = new_offset {
4023 self.apply_widget_scroll(panel_id, &key, off, t.visible);
4024 }
4025 true
4026 }
4027
4028 pub(super) fn release_widget_scrollbar(&mut self) {
4030 for fwp in [self.dock.as_mut(), self.floating_widget_panel.as_mut()]
4031 .into_iter()
4032 .flatten()
4033 {
4034 fwp.scrollbar_mouse.release();
4035 fwp.scrollbar_drag_key = None;
4036 }
4037 }
4038
4039 fn apply_widget_scroll(
4045 &mut self,
4046 panel_id: u64,
4047 list_key: &str,
4048 new_offset: usize,
4049 visible: usize,
4050 ) {
4051 let moved_sel = self.widget_registry.set_list_scroll(
4052 panel_id,
4053 list_key,
4054 new_offset as u32,
4055 visible as u32,
4056 );
4057 self.rerender_widget_panel(panel_id);
4058 if let Some(sel) = moved_sel {
4059 if self
4060 .plugin_manager
4061 .read()
4062 .unwrap()
4063 .has_hook_handlers("widget_event")
4064 {
4065 self.plugin_manager.read().unwrap().run_hook(
4066 "widget_event",
4067 crate::services::plugins::hooks::HookArgs::WidgetEvent {
4068 panel_id,
4069 widget_key: list_key.to_string(),
4070 event_type: "select".to_string(),
4071 payload: serde_json::json!({ "index": sel as i64 }),
4072 },
4073 );
4074 }
4075 }
4076 }
4077
4078 fn handle_floating_widget_context_click(
4087 &mut self,
4088 slot: super::PanelSlot,
4089 col: u16,
4090 row: u16,
4091 ) -> bool {
4092 let (panel_id, inner) = match self.panel(slot) {
4093 Some(fwp) => match fwp.last_inner_rect {
4094 Some(rect) => (fwp.panel_id, rect),
4095 None => return false,
4096 },
4097 None => return false,
4098 };
4099 if col < inner.x || col >= inner.x + inner.width {
4100 return false;
4101 }
4102 if row < inner.y || row >= inner.y + inner.height {
4103 return false;
4104 }
4105 let brow = (row - inner.y) as u32;
4106 let entries = self
4107 .panel(slot)
4108 .map(|f| f.entries.clone())
4109 .unwrap_or_default();
4110 let local_screen_col = (col - inner.x) as usize;
4111 let bcol = match entries.get(brow as usize) {
4112 Some(entry) => screen_col_to_byte(&entry.text, local_screen_col),
4113 None => return false,
4114 };
4115 let (mut payload, key, kind) =
4116 match self
4117 .widget_registry
4118 .hit_test(slot.buffer_id(), brow, bcol as u32)
4119 {
4120 Some((_, hit)) => (hit.payload.clone(), hit.widget_key.clone(), hit.widget_kind),
4121 None => return false,
4122 };
4123 if kind != "list" {
4125 return false;
4126 }
4127 if let Some(obj) = payload.as_object_mut() {
4130 obj.insert("col".to_string(), serde_json::json!(col));
4131 obj.insert("row".to_string(), serde_json::json!(row));
4132 }
4133 if !self
4134 .plugin_manager
4135 .read()
4136 .unwrap()
4137 .has_hook_handlers("widget_event")
4138 {
4139 return false;
4140 }
4141 self.plugin_manager.read().unwrap().run_hook(
4142 "widget_event",
4143 crate::services::plugins::hooks::HookArgs::WidgetEvent {
4144 panel_id,
4145 widget_key: key,
4146 event_type: "context".to_string(),
4147 payload,
4148 },
4149 );
4150 true
4151 }
4152
4153 fn floating_panel_is_anchored(&self) -> bool {
4156 matches!(
4157 self.floating_widget_panel.as_ref().map(|f| f.placement),
4158 Some(super::PanelPlacement::Anchored { .. })
4159 )
4160 }
4161
4162 fn point_in_floating_panel(&self, slot: super::PanelSlot, col: u16, row: u16) -> bool {
4166 let Some(inner) = self.panel(slot).and_then(|f| f.last_inner_rect) else {
4167 return false;
4168 };
4169 let x0 = inner.x.saturating_sub(1);
4170 let y0 = inner.y.saturating_sub(1);
4171 col >= x0 && col <= inner.x + inner.width && row >= y0 && row <= inner.y + inner.height
4173 }
4174
4175 fn dismiss_floating_panel_with_cancel(&mut self, slot: super::PanelSlot) {
4179 let panel_id = match self.panel(slot) {
4180 Some(f) => f.panel_id,
4181 None => return,
4182 };
4183 let widget_key = self
4184 .widget_registry
4185 .get(panel_id)
4186 .map(|p| p.focus_key.clone())
4187 .unwrap_or_default();
4188 if self
4189 .plugin_manager
4190 .read()
4191 .unwrap()
4192 .has_hook_handlers("widget_event")
4193 {
4194 self.plugin_manager.read().unwrap().run_hook(
4195 "widget_event",
4196 crate::services::plugins::hooks::HookArgs::WidgetEvent {
4197 panel_id,
4198 widget_key,
4199 event_type: "cancel".to_string(),
4200 payload: serde_json::json!({}),
4201 },
4202 );
4203 }
4204 *self.panel_opt_mut(slot) = None;
4205 let _ = self.widget_registry.unmount(panel_id);
4206 }
4207
4208 fn handle_floating_widget_click(&mut self, slot: super::PanelSlot, col: u16, row: u16) {
4211 if self.try_widget_scrollbar_press(slot, col, row) {
4214 return;
4215 }
4216 let (panel_id, inner) = match self.panel(slot) {
4217 Some(fwp) => match fwp.last_inner_rect {
4218 Some(rect) => (fwp.panel_id, rect),
4219 None => return,
4220 },
4221 None => return,
4222 };
4223 if col < inner.x || col >= inner.x + inner.width {
4224 return;
4225 }
4226 if row < inner.y || row >= inner.y + inner.height {
4227 return;
4228 }
4229 let brow = (row - inner.y) as u32;
4230 let entries = self
4231 .panel(slot)
4232 .map(|f| f.entries.clone())
4233 .unwrap_or_default();
4234 let local_screen_col = (col - inner.x) as usize;
4235 let bcol = match entries.get(brow as usize) {
4236 Some(entry) => screen_col_to_byte(&entry.text, local_screen_col),
4237 None => return,
4238 };
4239 let (mut hit_payload, hit_event, hit_key, hit_kind) =
4240 match self
4241 .widget_registry
4242 .hit_test(slot.buffer_id(), brow, bcol as u32)
4243 {
4244 Some((_, hit)) => (
4245 hit.payload.clone(),
4246 hit.event_type.to_string(),
4247 hit.widget_key.clone(),
4248 hit.widget_kind,
4249 ),
4250 None => {
4251 tracing::debug!(
4252 target: "fresh::dock",
4253 ?slot, col, row, brow, bcol,
4254 "handle_floating_widget_click: hit_test found no widget"
4255 );
4256 return;
4257 }
4258 };
4259 if !hit_key.is_empty() {
4260 let tabbable = self
4261 .widget_registry
4262 .get(panel_id)
4263 .map(|p| p.tabbable.iter().any(|k| k == &hit_key))
4264 .unwrap_or(false);
4265 tracing::debug!(
4266 target: "fresh::dock",
4267 hit_key = %hit_key,
4268 hit_kind,
4269 hit_event = %hit_event,
4270 tabbable,
4271 "handle_floating_widget_click: hit"
4272 );
4273 if tabbable {
4274 self.set_panel_focus_and_notify(panel_id, hit_key.clone());
4275 }
4276 self.rerender_widget_panel(panel_id);
4277 } else {
4278 tracing::debug!(
4279 target: "fresh::dock",
4280 hit_kind,
4281 hit_event = %hit_event,
4282 "handle_floating_widget_click: hit with empty key (not focusable)"
4283 );
4284 }
4285 let handled_specially = if hit_kind == "tree" && hit_event == "expand" {
4286 if let Some(item_key) = hit_payload.get("key").and_then(|v| v.as_str()) {
4287 self.handle_widget_tree_expand_toggle(panel_id, &hit_key, item_key);
4288 true
4289 } else {
4290 false
4291 }
4292 } else {
4293 false
4294 };
4295 if !handled_specially
4296 && self
4297 .plugin_manager
4298 .read()
4299 .unwrap()
4300 .has_hook_handlers("widget_event")
4301 {
4302 if let Some(obj) = hit_payload.as_object_mut() {
4309 obj.insert("via".to_string(), serde_json::json!("click"));
4310 }
4311 self.plugin_manager.read().unwrap().run_hook(
4312 "widget_event",
4313 crate::services::plugins::hooks::HookArgs::WidgetEvent {
4314 panel_id,
4315 widget_key: hit_key,
4316 event_type: hit_event,
4317 payload: hit_payload,
4318 },
4319 );
4320 }
4321 }
4322
4323 fn clear_active_window_drag_state(&mut self) {
4327 let ms = &mut self.active_window_mut().mouse_state;
4328 ms.dragging_scrollbar = None;
4329 ms.drag_start_row = None;
4330 ms.drag_start_top_byte = None;
4331 ms.dragging_horizontal_scrollbar = None;
4332 ms.drag_start_hcol = None;
4333 ms.drag_start_left_column = None;
4334 ms.dragging_separator = None;
4335 ms.drag_start_position = None;
4336 ms.drag_start_ratio = None;
4337 ms.dragging_file_explorer = false;
4338 ms.drag_start_explorer_width = None;
4339 ms.dragging_text_selection = false;
4340 ms.drag_selection_split = None;
4341 ms.drag_selection_anchor = None;
4342 ms.drag_selection_by_words = false;
4343 ms.drag_selection_word_end = None;
4344 ms.dragging_popup_scrollbar = None;
4345 ms.drag_start_popup_scroll = None;
4346 ms.dragging_prompt_scrollbar = false;
4347 ms.selecting_in_popup = None;
4348 }
4349}
4350
4351fn screen_col_to_byte(text: &str, target_col: usize) -> usize {
4356 use unicode_width::UnicodeWidthChar;
4357 let mut byte = 0;
4358 let mut col = 0usize;
4359 for ch in text.chars() {
4360 let w = UnicodeWidthChar::width(ch).unwrap_or(0);
4361 if col + w > target_col {
4362 return byte;
4363 }
4364 col += w;
4365 byte += ch.len_utf8();
4366 }
4367 byte
4368}