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