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 self.handle_floating_widget_click(super::PanelSlot::Floating, col, row);
91 }
92 MouseEventKind::Drag(MouseButton::Left) => {
93 self.try_widget_scrollbar_drag(super::PanelSlot::Floating, row);
96 }
97 MouseEventKind::Up(MouseButton::Left) => {
98 self.release_widget_scrollbar();
99 }
100 MouseEventKind::ScrollUp => {
101 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, -3);
102 }
103 MouseEventKind::ScrollDown => {
104 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, 3);
105 }
106 _ => {}
109 }
110 Ok(true)
111 }
112
113 pub fn handle_mouse(
116 &mut self,
117 mouse_event: crossterm::event::MouseEvent,
118 ) -> AnyhowResult<bool> {
119 use crossterm::event::{MouseButton, MouseEventKind};
120
121 let col = mouse_event.column;
122 let row = mouse_event.row;
123
124 let (is_double_click, is_triple_click) = self.detect_multi_click(&mouse_event, col, row);
125
126 if let Some(result) = self.dispatch_modal_mouse(mouse_event, is_double_click) {
132 return result;
133 }
134
135 let mut needs_render = false;
137 if let Some(ref prompt) = self.active_window_mut().prompt {
138 if matches!(prompt.prompt_type, PromptType::LspRename { .. }) {
139 self.cancel_prompt();
140 needs_render = true;
141 }
142 }
143
144 let cursor_moved = self.active_window_mut().mouse_cursor_position != Some((col, row));
147 self.active_window_mut().mouse_cursor_position = Some((col, row));
148 if self.active_window_mut().gpm_active && cursor_moved {
149 needs_render = true;
150 }
151
152 tracing::trace!(
153 "handle_mouse: kind={:?}, col={}, row={}",
154 mouse_event.kind,
155 col,
156 row
157 );
158
159 let chrome_drag_active = self.dock_resizing || {
171 let ms = &self.active_window().mouse_state;
172 ms.dragging_separator.is_some() || ms.drag_start_explorer_width.is_some()
173 };
174 if !chrome_drag_active {
175 if let Some(result) =
176 self.active_window_mut()
177 .try_forward_mouse_to_terminal(col, row, mouse_event)
178 {
179 return result;
180 }
181 }
182
183 if self.active_window_mut().theme_info_popup.is_some() {
185 if let MouseEventKind::Down(MouseButton::Left) = mouse_event.kind {
186 if let Some((popup_rect, button_row_offset)) = self.theme_info_popup_rect() {
187 if in_rect(col, row, popup_rect) {
188 let actual_button_row = popup_rect.y + button_row_offset;
190 if row == actual_button_row {
191 let fg_key = self
192 .active_window_mut()
193 .theme_info_popup
194 .as_ref()
195 .and_then(|p| p.info.fg_key.clone());
196 self.active_window_mut().theme_info_popup = None;
197 if let Some(key) = fg_key {
198 self.fire_theme_inspect_hook(key);
199 }
200 return Ok(true);
201 }
202 return Ok(true);
204 }
205 }
206 self.active_window_mut().theme_info_popup = None;
208 needs_render = true;
209 }
210 }
211
212 match mouse_event.kind {
213 MouseEventKind::Down(MouseButton::Left) => {
214 if is_double_click || is_triple_click {
215 if let Some((buffer_id, byte_pos)) =
216 self.fold_toggle_line_at_screen_position(col, row)
217 {
218 self.active_window_mut()
219 .toggle_fold_at_byte(buffer_id, byte_pos);
220 needs_render = true;
221 return Ok(needs_render);
222 }
223 }
224 if is_triple_click {
225 self.handle_mouse_triple_click(col, row)?;
227 needs_render = true;
228 return Ok(needs_render);
229 }
230 if is_double_click {
231 self.handle_mouse_double_click(col, row)?;
233 needs_render = true;
234 return Ok(needs_render);
235 }
236 self.handle_mouse_click(col, row, mouse_event.modifiers)?;
237 needs_render = true;
238 }
239 MouseEventKind::Drag(MouseButton::Left) => {
240 self.handle_mouse_drag(col, row)?;
241 needs_render = true;
242 }
243 MouseEventKind::Up(MouseButton::Left) => {
244 if self.dock_resizing {
247 self.dock_resizing = false;
248 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
249 self.dock.as_ref().map(|f| f.placement)
250 {
251 self.dock_width = Some(width_cols);
252 }
253 return Ok(true);
254 }
255 let was_dragging_separator = self
257 .active_window_mut()
258 .mouse_state
259 .dragging_separator
260 .is_some();
261
262 if let Some(drag_state) = self.active_window_mut().mouse_state.dragging_tab.take() {
264 if drag_state.is_dragging() {
265 if let Some(drop_zone) = drag_state.drop_zone {
266 self.execute_tab_drop(
267 drag_state.buffer_id,
268 drag_state.source_split_id,
269 drop_zone,
270 );
271 }
272 }
273 }
274
275 self.release_widget_scrollbar();
277 self.clear_active_window_drag_state();
278
279 if was_dragging_separator {
282 self.relayout();
283 }
284
285 needs_render = true;
286 }
287 MouseEventKind::Moved => {
288 {
290 let content_rect = self
292 .active_layout()
293 .split_areas
294 .iter()
295 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
296 .map(|(_, _, rect, _, _, _)| *rect);
297
298 let (content_x, content_y) = content_rect.map(|r| (r.x, r.y)).unwrap_or((0, 0));
299
300 self.plugin_manager.read().unwrap().run_hook(
301 "mouse_move",
302 HookArgs::MouseMove {
303 column: col,
304 row,
305 content_x,
306 content_y,
307 },
308 );
309 }
310
311 let hover_changed = self.update_hover_target(col, row);
314 needs_render = needs_render || hover_changed;
315
316 if let Some((popup_rect, button_row_offset)) = self.theme_info_popup_rect() {
318 let button_row = popup_rect.y + button_row_offset;
319 let new_highlighted = row == button_row
320 && col >= popup_rect.x
321 && col < popup_rect.x + popup_rect.width;
322 if let Some(ref mut popup) = self.active_window_mut().theme_info_popup {
323 if popup.button_highlighted != new_highlighted {
324 popup.button_highlighted = new_highlighted;
325 needs_render = true;
326 }
327 }
328 }
329
330 self.update_lsp_hover_state(col, row);
332 }
333 MouseEventKind::ScrollUp => {
334 self.handle_vertical_scroll(col, row, mouse_event.modifiers, -3)?;
335 needs_render = true;
336 }
337 MouseEventKind::ScrollDown => {
338 self.handle_vertical_scroll(col, row, mouse_event.modifiers, 3)?;
339 needs_render = true;
340 }
341 MouseEventKind::ScrollLeft => {
342 self.active_window_mut()
344 .handle_horizontal_scroll(col, row, -3)?;
345 needs_render = true;
346 }
347 MouseEventKind::ScrollRight => {
348 self.active_window_mut()
350 .handle_horizontal_scroll(col, row, 3)?;
351 needs_render = true;
352 }
353 MouseEventKind::Down(MouseButton::Right) => {
354 if self.overlay_prompt_active() {
358 needs_render = true;
359 } else if mouse_event
360 .modifiers
361 .contains(crossterm::event::KeyModifiers::CONTROL)
362 {
363 self.show_theme_info_popup(col, row)?;
365 needs_render = true;
366 } else {
367 self.handle_right_click(col, row)?;
369 needs_render = true;
370 }
371 }
372 _ => {
373 }
375 }
376
377 self.active_window_mut().mouse_state.last_position = Some((col, row));
378 Ok(needs_render)
379 }
380
381 fn detect_multi_click(
383 &mut self,
384 mouse_event: &crossterm::event::MouseEvent,
385 col: u16,
386 row: u16,
387 ) -> (bool, bool) {
388 use crossterm::event::{MouseButton, MouseEventKind};
389 if !matches!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left)) {
390 return (false, false);
391 }
392 let now = self.time_source.now();
393 let threshold = std::time::Duration::from_millis(self.config.editor.double_click_time_ms);
394 let is_consecutive = if let (Some(prev_time), Some(prev_pos)) = (
395 self.active_window_mut().previous_click_time,
396 self.active_window_mut().previous_click_position,
397 ) {
398 now.duration_since(prev_time) < threshold && prev_pos == (col, row)
399 } else {
400 false
401 };
402 if is_consecutive {
403 self.active_window_mut().click_count += 1;
404 } else {
405 self.active_window_mut().click_count = 1;
406 }
407 self.active_window_mut().previous_click_time = Some(now);
408 self.active_window_mut().previous_click_position = Some((col, row));
409 let is_triple = self.active_window_mut().click_count >= 3;
410 let is_double = self.active_window_mut().click_count == 2;
411 if is_triple {
412 self.active_window_mut().click_count = 0;
413 self.active_window_mut().previous_click_time = None;
414 self.active_window_mut().previous_click_position = None;
415 }
416 (is_double, is_triple)
417 }
418
419 fn handle_vertical_scroll(
422 &mut self,
423 col: u16,
424 row: u16,
425 modifiers: crossterm::event::KeyModifiers,
426 delta: i32,
427 ) -> AnyhowResult<()> {
428 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
429 self.active_window_mut()
430 .handle_horizontal_scroll(col, row, delta)?;
431 } else if self.handle_overlay_prompt_scroll(col, row, delta) {
432 } else if self.handle_prompt_scroll(delta) {
436 } else if self.is_file_open_active()
438 && self.is_mouse_over_file_browser(col, row)
439 && self.handle_file_open_scroll(delta)
440 {
441 } else if self.is_mouse_over_any_popup(col, row) {
443 self.scroll_popup(delta);
444 } else if self.floating_widget_panel.is_some() {
445 self.handle_floating_widget_panel_wheel(super::PanelSlot::Floating, col, row, delta);
451 } else if self.dock.is_some()
452 && self.handle_floating_widget_panel_wheel(super::PanelSlot::Dock, col, row, delta)
453 {
454 } else if self
457 .active_window()
458 .split_at_position(col, row)
459 .map(|(_, buffer_id)| self.handle_widget_panel_wheel(buffer_id, delta))
460 .unwrap_or(false)
461 {
462 } else {
464 if self.active_window().terminal_mode
465 && self
466 .active_window()
467 .is_terminal_buffer(self.active_buffer())
468 {
469 {
470 let __b = self.active_buffer();
471 self.active_window_mut().sync_terminal_to_buffer(__b);
472 };
473 self.active_window_mut().terminal_mode = false;
474 self.active_window_mut().key_context =
475 crate::input::keybindings::KeyContext::Normal;
476 }
477 self.dismiss_transient_popups();
478 self.active_window_mut()
479 .handle_mouse_scroll(col, row, delta)?;
480 }
481 Ok(())
482 }
483
484 fn handle_overlay_prompt_scroll(&mut self, col: u16, row: u16, delta: i32) -> bool {
495 if !self.overlay_prompt_active() {
496 return false;
497 }
498 let preview_area = self.active_chrome().prompt_preview_area;
499 let results_visible = self
500 .active_chrome()
501 .prompt_results_area
502 .map(|r| r.height as usize)
503 .unwrap_or(0);
504 if let Some(preview) = preview_area {
505 if in_rect(col, row, preview) {
506 self.active_window_mut()
507 .scroll_overlay_preview_by_lines(delta);
508 return true;
509 }
510 }
511 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
512 prompt.scroll_results(delta, results_visible);
513 }
514 true
515 }
516
517 pub(super) fn update_hover_target(&mut self, col: u16, row: u16) -> bool {
520 let old_target = self.active_window_mut().mouse_state.hover_target.clone();
521 let new_target = self.compute_hover_target(col, row);
522 let changed = old_target != new_target;
523 self.active_window_mut().mouse_state.hover_target = new_target.clone();
524
525 if let Some(active_menu_idx) = self.menu_state.active_menu {
528 let all_menus: Vec<crate::config::Menu> = self
529 .menus
530 .menus
531 .iter()
532 .chain(self.menu_state.plugin_menus.iter())
533 .cloned()
534 .collect();
535 if let Some(HoverTarget::MenuBarItem(hovered_menu_idx)) = new_target.clone() {
536 if hovered_menu_idx != active_menu_idx {
537 self.menu_state.open_menu(hovered_menu_idx);
538 return true; }
540 }
541
542 if let Some(HoverTarget::MenuDropdownItem(_, item_idx)) = new_target.clone() {
544 if self.menu_state.submenu_path.first() == Some(&item_idx) {
547 tracing::trace!(
548 "menu hover: staying on submenu parent item_idx={}, submenu_path={:?}",
549 item_idx,
550 self.menu_state.submenu_path
551 );
552 return changed;
553 }
554
555 if !self.menu_state.submenu_path.is_empty() {
557 tracing::trace!(
558 "menu hover: clearing submenu_path={:?} for different item_idx={}",
559 self.menu_state.submenu_path,
560 item_idx
561 );
562 self.menu_state.submenu_path.clear();
563 self.menu_state.highlighted_item = Some(item_idx);
564 return true;
565 }
566
567 if let Some(menu) = all_menus.get(active_menu_idx) {
569 if let Some(crate::config::MenuItem::Submenu { items, .. }) =
570 menu.items.get(item_idx)
571 {
572 if !items.is_empty() {
573 tracing::trace!("menu hover: opening submenu at item_idx={}", item_idx);
574 self.menu_state.submenu_path.push(item_idx);
575 self.menu_state.highlighted_item = Some(0);
576 return true;
577 }
578 }
579 }
580 if self.menu_state.highlighted_item != Some(item_idx) {
582 self.menu_state.highlighted_item = Some(item_idx);
583 return true;
584 }
585 }
586
587 if let Some(HoverTarget::SubmenuItem(depth, item_idx)) = new_target {
589 if self.menu_state.submenu_path.len() > depth
593 && self.menu_state.submenu_path.get(depth) == Some(&item_idx)
594 {
595 tracing::trace!(
596 "menu hover: staying on nested submenu parent depth={}, item_idx={}, submenu_path={:?}",
597 depth,
598 item_idx,
599 self.menu_state.submenu_path
600 );
601 return changed;
602 }
603
604 if self.menu_state.submenu_path.len() > depth {
606 tracing::trace!(
607 "menu hover: truncating submenu_path={:?} to depth={} for item_idx={}",
608 self.menu_state.submenu_path,
609 depth,
610 item_idx
611 );
612 self.menu_state.submenu_path.truncate(depth);
613 }
614
615 if let Some(items) = self
617 .menu_state
618 .get_current_items(&all_menus, active_menu_idx)
619 {
620 if let Some(crate::config::MenuItem::Submenu {
622 items: sub_items, ..
623 }) = items.get(item_idx)
624 {
625 if !sub_items.is_empty()
626 && !self.menu_state.submenu_path.contains(&item_idx)
627 {
628 tracing::trace!(
629 "menu hover: opening nested submenu at depth={}, item_idx={}",
630 depth,
631 item_idx
632 );
633 self.menu_state.submenu_path.push(item_idx);
634 self.menu_state.highlighted_item = Some(0);
635 return true;
636 }
637 }
638 if self.menu_state.highlighted_item != Some(item_idx) {
640 self.menu_state.highlighted_item = Some(item_idx);
641 return true;
642 }
643 }
644 }
645 }
646
647 if let Some(HoverTarget::TabContextMenuItem(item_idx)) = new_target.clone() {
649 if let Some(ref mut menu) = self.active_window_mut().tab_context_menu {
650 if menu.highlighted != item_idx {
651 menu.highlighted = item_idx;
652 return true;
653 }
654 }
655 }
656
657 if let Some(&HoverTarget::FileExplorerContextMenuItem(item_idx)) = new_target.as_ref() {
658 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
659 if menu.highlighted != item_idx {
660 menu.highlighted = item_idx;
661 return true;
662 }
663 }
664 }
665
666 if old_target != new_target
669 && matches!(
670 old_target,
671 Some(HoverTarget::FileExplorerStatusIndicator(_))
672 )
673 {
674 self.dismiss_file_explorer_status_tooltip();
675 }
676
677 if let Some(HoverTarget::FileExplorerStatusIndicator(ref path)) = new_target {
678 if old_target != new_target {
680 self.show_file_explorer_status_tooltip(path.clone(), col, row);
681 return true;
682 }
683 }
684
685 changed
686 }
687
688 fn update_lsp_hover_state(&mut self, col: u16, row: u16) {
697 tracing::trace!(col, row, "update_lsp_hover_state: raw mouse position");
698
699 if self.active_window_mut().theme_info_popup.is_some()
703 || self.active_window_mut().tab_context_menu.is_some()
704 || self
705 .active_window_mut()
706 .file_explorer_context_menu
707 .is_some()
708 || self.is_lsp_status_popup_open()
709 {
710 if self
711 .active_window_mut()
712 .mouse_state
713 .lsp_hover_state
714 .is_some()
715 {
716 self.active_window_mut().mouse_state.lsp_hover_state = None;
717 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
718 self.dismiss_transient_popups();
719 }
720 return;
721 }
722
723 if self.is_mouse_over_transient_popup(col, row) {
725 return;
726 }
727
728 let split_info = self
730 .active_layout()
731 .split_areas
732 .iter()
733 .find(|(_, _, content_rect, _, _, _)| in_rect(col, row, *content_rect))
734 .map(|(split_id, buffer_id, content_rect, _, _, _)| {
735 (*split_id, *buffer_id, *content_rect)
736 });
737
738 let Some((split_id, buffer_id, content_rect)) = split_info else {
739 if self
741 .active_window_mut()
742 .mouse_state
743 .lsp_hover_state
744 .is_some()
745 {
746 self.active_window_mut().mouse_state.lsp_hover_state = None;
747 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
748 self.dismiss_transient_popups();
749 }
750 return;
751 };
752
753 let cached_mappings = self
755 .active_layout()
756 .view_line_mappings
757 .get(&split_id)
758 .cloned();
759 let gutter_width = self
760 .buffers()
761 .get(&buffer_id)
762 .map(|s| s.margins.left_total_width() as u16)
763 .unwrap_or(0);
764 let fallback = self
765 .buffers()
766 .get(&buffer_id)
767 .map(|s| s.buffer.len())
768 .unwrap_or(0);
769
770 let compose_width = self
772 .windows
773 .get(&self.active_window)
774 .and_then(|w| w.buffers.splits())
775 .map(|(_, vs)| vs)
776 .expect("active window must have a populated split layout")
777 .get(&split_id)
778 .and_then(|vs| vs.compose_width);
779
780 let Some(byte_pos) = super::click_geometry::screen_to_buffer_position(
782 col,
783 row,
784 content_rect,
785 gutter_width,
786 &cached_mappings,
787 fallback,
788 false, compose_width,
790 ) else {
791 if self
795 .active_window_mut()
796 .mouse_state
797 .lsp_hover_state
798 .is_some()
799 {
800 self.active_window_mut().mouse_state.lsp_hover_state = None;
801 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
802 }
803 return;
804 };
805
806 let content_col = col.saturating_sub(content_rect.x);
808 let text_col = content_col.saturating_sub(gutter_width) as usize;
809 let visual_row = row.saturating_sub(content_rect.y) as usize;
810
811 let line_info = cached_mappings
812 .as_ref()
813 .and_then(|mappings| mappings.get(visual_row))
814 .map(|line_mapping| {
815 (
816 line_mapping.visual_to_char.len(),
817 line_mapping.line_end_byte,
818 )
819 });
820
821 let is_past_line_end_or_empty = line_info
822 .map(|(line_len, _)| {
823 if line_len <= 1 {
825 return true;
826 }
827 text_col >= line_len
828 })
829 .unwrap_or(true);
831
832 tracing::trace!(
833 col,
834 row,
835 content_col,
836 text_col,
837 visual_row,
838 gutter_width,
839 byte_pos,
840 ?line_info,
841 is_past_line_end_or_empty,
842 "update_lsp_hover_state: position check"
843 );
844
845 if is_past_line_end_or_empty {
846 tracing::trace!(
847 "update_lsp_hover_state: mouse past line end or empty line, clearing hover"
848 );
849 if self
854 .active_window_mut()
855 .mouse_state
856 .lsp_hover_state
857 .is_some()
858 {
859 self.active_window_mut().mouse_state.lsp_hover_state = None;
860 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
861 }
862 return;
863 }
864
865 if let Some((start, end)) = self.active_window_mut().hover.symbol_range() {
867 if byte_pos >= start && byte_pos < end {
868 return;
870 }
871 }
872
873 if let Some((old_pos, _, _, _)) = self.active_window_mut().mouse_state.lsp_hover_state {
875 if old_pos == byte_pos {
876 return;
878 }
879 }
885
886 self.active_window_mut().mouse_state.lsp_hover_state =
888 Some((byte_pos, std::time::Instant::now(), col, row));
889 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
890 }
891
892 fn is_mouse_over_transient_popup(&self, col: u16, row: u16) -> bool {
894 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
895 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
896 hit_tester.is_over_transient_popup(col, row)
897 }
898
899 fn is_mouse_over_any_popup(&self, col: u16, row: u16) -> bool {
901 for (_, popup_area, _, _, _) in &self.active_chrome().global_popup_areas {
904 if in_rect(col, row, *popup_area) {
905 return true;
906 }
907 }
908 if let Some(outer) = self.active_chrome().suggestions_outer_area {
912 if in_rect(col, row, outer) {
913 return true;
914 }
915 }
916 let layouts = popup_areas_to_layout_info(&self.active_chrome().popup_areas);
917 let hit_tester = PopupHitTester::new(&layouts, &self.active_state().popups);
918 hit_tester.is_over_popup(col, row)
919 }
920
921 fn is_mouse_over_file_browser(&self, col: u16, row: u16) -> bool {
923 self.active_window()
924 .file_browser_layout
925 .as_ref()
926 .is_some_and(|layout| layout.contains(col, row))
927 }
928
929 fn compute_hover_target(&self, col: u16, row: u16) -> Option<HoverTarget> {
934 self.hover_target_in_floating_overlays(col, row)
935 .or_else(|| self.hover_target_in_chrome(col, row))
936 }
937
938 fn hover_target_in_floating_overlays(&self, col: u16, row: u16) -> Option<HoverTarget> {
942 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
943 let (menu_x, menu_y) = menu.clamped_position(
944 self.active_chrome().last_frame_width,
945 self.active_chrome().last_frame_height,
946 );
947 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
948 let menu_height = menu.height();
949
950 if col >= menu_x
951 && col < menu_x + menu_width
952 && row > menu_y
953 && row < menu_y + menu_height - 1
954 {
955 let item_idx = (row - menu_y - 1) as usize;
956 if item_idx < menu.items().len() {
957 return Some(HoverTarget::FileExplorerContextMenuItem(item_idx));
958 }
959 }
960 }
961
962 if let Some(ref menu) = self.active_window().tab_context_menu {
964 let menu_x = menu.position.0;
965 let menu_y = menu.position.1;
966 let menu_width = 22u16;
967 let items = super::types::TabContextMenuItem::all();
968 let menu_height = items.len() as u16 + 2;
969
970 if col >= menu_x
971 && col < menu_x + menu_width
972 && row > menu_y
973 && row < menu_y + menu_height - 1
974 {
975 let item_idx = (row - menu_y - 1) as usize;
976 if item_idx < items.len() {
977 return Some(HoverTarget::TabContextMenuItem(item_idx));
978 }
979 }
980 }
981
982 if let Some((inner_rect, start_idx, _visible_count, total_count)) =
984 &self.active_chrome().suggestions_area
985 {
986 if in_rect(col, row, *inner_rect) {
987 let relative_row = (row - inner_rect.y) as usize;
988 let item_idx = start_idx + relative_row;
989
990 if item_idx < *total_count {
991 return Some(HoverTarget::SuggestionItem(item_idx));
992 }
993 }
994 }
995
996 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
999 self.active_chrome().popup_areas.iter().rev()
1000 {
1001 if in_rect(col, row, *inner_rect) && *num_items > 0 {
1002 let relative_row = (row - inner_rect.y) as usize;
1004 let item_idx = scroll_offset + relative_row;
1005
1006 if item_idx < *num_items {
1007 return Some(HoverTarget::PopupListItem(*popup_idx, item_idx));
1008 }
1009 }
1010 }
1011
1012 if self.is_file_open_active() {
1014 if let Some(hover) = self.compute_file_browser_hover(col, row) {
1015 return Some(hover);
1016 }
1017 }
1018
1019 None
1020 }
1021
1022 fn hover_target_in_chrome(&self, col: u16, row: u16) -> Option<HoverTarget> {
1026 if self.active_window().menu_bar_visible {
1029 if let Some(ref menu_layout) = self.active_chrome().menu_layout {
1030 if let Some(menu_idx) = menu_layout.menu_at(col, row) {
1031 return Some(HoverTarget::MenuBarItem(menu_idx));
1032 }
1033 }
1034 }
1035
1036 if let Some(active_idx) = self.menu_state.active_menu {
1038 if let Some(hover) = self.compute_menu_dropdown_hover(col, row, active_idx) {
1039 return Some(hover);
1040 }
1041 }
1042
1043 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1045 let close_button_x = explorer_area.x + explorer_area.width.saturating_sub(3);
1047 if row == explorer_area.y
1048 && col >= close_button_x
1049 && col < explorer_area.x + explorer_area.width
1050 {
1051 return Some(HoverTarget::FileExplorerCloseButton);
1052 }
1053
1054 let content_start_y = explorer_area.y + 1; let content_end_y = explorer_area.y + explorer_area.height.saturating_sub(1); let status_indicator_x = explorer_area.x + explorer_area.width.saturating_sub(3); if row >= content_start_y
1061 && row < content_end_y
1062 && col >= status_indicator_x
1063 && col < explorer_area.x + explorer_area.width.saturating_sub(1)
1064 {
1065 if let Some(explorer) = self.file_explorer().as_ref() {
1067 let relative_row = row.saturating_sub(content_start_y) as usize;
1068 let scroll_offset = explorer.get_scroll_offset();
1069 let item_index = relative_row + scroll_offset;
1070 let display_nodes = explorer.get_display_nodes();
1071
1072 if item_index < display_nodes.len() {
1073 let (node_id, _indent) = display_nodes[item_index];
1074 if let Some(node) = explorer.tree().get_node(node_id) {
1075 return Some(HoverTarget::FileExplorerStatusIndicator(
1076 node.entry.path.clone(),
1077 ));
1078 }
1079 }
1080 }
1081 }
1082
1083 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
1086 if col == border_x
1087 && row >= explorer_area.y
1088 && row < explorer_area.y + explorer_area.height
1089 {
1090 return Some(HoverTarget::FileExplorerBorder);
1091 }
1092 }
1093
1094 for (split_id, direction, sep_x, sep_y, sep_length) in &self.active_layout().separator_areas
1096 {
1097 let is_on_separator = match direction {
1098 SplitDirection::Horizontal => {
1099 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
1100 }
1101 SplitDirection::Vertical => {
1102 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
1103 }
1104 };
1105
1106 if is_on_separator {
1107 return Some(HoverTarget::SplitSeparator(*split_id, *direction));
1108 }
1109 }
1110
1111 for (split_id, btn_row, start_col, end_col) in &self.active_layout().close_split_areas {
1114 if row == *btn_row && col >= *start_col && col < *end_col {
1115 return Some(HoverTarget::CloseSplitButton(*split_id));
1116 }
1117 }
1118
1119 for (split_id, btn_row, start_col, end_col) in &self.active_layout().maximize_split_areas {
1120 if row == *btn_row && col >= *start_col && col < *end_col {
1121 return Some(HoverTarget::MaximizeSplitButton(*split_id));
1122 }
1123 }
1124
1125 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
1126 match tab_layout.hit_test(col, row) {
1127 Some(TabHit::CloseButton(target)) => {
1128 return Some(HoverTarget::TabCloseButton(target, *split_id));
1129 }
1130 Some(TabHit::TabName(target)) => {
1131 return Some(HoverTarget::TabName(target, *split_id));
1132 }
1133 Some(TabHit::ScrollLeft)
1134 | Some(TabHit::ScrollRight)
1135 | Some(TabHit::BarBackground)
1136 | None => {}
1137 }
1138 }
1139
1140 for (split_id, _buffer_id, _content_rect, scrollbar_rect, thumb_start, thumb_end) in
1142 &self.active_layout().split_areas
1143 {
1144 if in_rect(col, row, *scrollbar_rect) {
1145 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
1146 let is_on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
1147
1148 if is_on_thumb {
1149 return Some(HoverTarget::ScrollbarThumb(*split_id));
1150 } else {
1151 return Some(HoverTarget::ScrollbarTrack(*split_id, relative_row as u16));
1152 }
1153 }
1154 }
1155
1156 if let Some((status_row, _status_x, _status_width)) = self.active_chrome().status_bar_area {
1158 if row == status_row {
1159 let indicators = [
1160 (
1161 self.active_chrome().status_bar_line_ending_area,
1162 HoverTarget::StatusBarLineEndingIndicator,
1163 ),
1164 (
1165 self.active_chrome().status_bar_encoding_area,
1166 HoverTarget::StatusBarEncodingIndicator,
1167 ),
1168 (
1169 self.active_chrome().status_bar_language_area,
1170 HoverTarget::StatusBarLanguageIndicator,
1171 ),
1172 (
1173 self.active_chrome().status_bar_lsp_area,
1174 HoverTarget::StatusBarLspIndicator,
1175 ),
1176 (
1177 self.active_chrome().status_bar_remote_area,
1178 HoverTarget::StatusBarRemoteIndicator,
1179 ),
1180 (
1181 self.active_chrome().status_bar_warning_area,
1182 HoverTarget::StatusBarWarningBadge,
1183 ),
1184 ];
1185 for (area, target) in indicators {
1186 if let Some((indicator_row, start, end)) = area {
1187 if row == indicator_row && col >= start && col < end {
1188 return Some(target);
1189 }
1190 }
1191 }
1192 }
1193 }
1194
1195 if let Some(ref layout) = self.active_chrome().search_options_layout {
1197 use crate::view::ui::status_bar::SearchOptionsHover;
1198 if let Some(hover) = layout.checkbox_at(col, row) {
1199 return Some(match hover {
1200 SearchOptionsHover::CaseSensitive => HoverTarget::SearchOptionCaseSensitive,
1201 SearchOptionsHover::WholeWord => HoverTarget::SearchOptionWholeWord,
1202 SearchOptionsHover::Regex => HoverTarget::SearchOptionRegex,
1203 SearchOptionsHover::ConfirmEach => HoverTarget::SearchOptionConfirmEach,
1204 SearchOptionsHover::None => return None,
1205 });
1206 }
1207 }
1208
1209 None
1210 }
1211
1212 pub(super) fn handle_mouse_double_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1215 tracing::debug!("handle_mouse_double_click at col={}, row={}", col, row);
1216
1217 if let Some(r) = self.handle_click_suggestions_confirm(col, row) {
1221 return r;
1222 }
1223
1224 if self.overlay_prompt_active() {
1227 return Ok(());
1228 }
1229
1230 if self.is_mouse_over_any_popup(col, row) {
1232 return Ok(());
1234 } else {
1235 self.dismiss_transient_popups();
1237 }
1238
1239 if self.handle_file_open_double_click(col, row) {
1241 return Ok(());
1242 }
1243
1244 if let Some(explorer_area) = self.active_layout().file_explorer_area {
1246 if col >= explorer_area.x
1247 && col < explorer_area.x + explorer_area.width
1248 && row > explorer_area.y && row < explorer_area.y + explorer_area.height
1250 {
1251 self.file_explorer_open_file()?;
1253 return Ok(());
1254 }
1255 }
1256
1257 let split_areas = self.active_layout().split_areas.clone();
1259 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1260 &split_areas
1261 {
1262 if in_rect(col, row, *content_rect) {
1263 if self.active_window().is_terminal_buffer(*buffer_id) {
1265 self.active_window_mut().key_context =
1266 crate::input::keybindings::KeyContext::Terminal;
1267 return Ok(());
1269 }
1270
1271 self.active_window_mut().key_context =
1272 crate::input::keybindings::KeyContext::Normal;
1273
1274 self.handle_editor_double_click(col, row, *split_id, *buffer_id, *content_rect)?;
1276 return Ok(());
1277 }
1278 }
1279
1280 Ok(())
1281 }
1282
1283 fn handle_editor_double_click(
1285 &mut self,
1286 col: u16,
1287 row: u16,
1288 split_id: LeafId,
1289 buffer_id: BufferId,
1290 content_rect: ratatui::layout::Rect,
1291 ) -> AnyhowResult<()> {
1292 use crate::model::event::Event;
1293
1294 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1298 return Ok(());
1299 }
1300
1301 self.focus_split(split_id, buffer_id);
1303
1304 let cached_mappings = self
1306 .active_layout()
1307 .view_line_mappings
1308 .get(&split_id)
1309 .cloned();
1310
1311 let leaf_id = split_id;
1313 let fallback = self
1314 .windows
1315 .get(&self.active_window)
1316 .and_then(|w| w.buffers.splits())
1317 .map(|(_, vs)| vs)
1318 .expect("active window must have a populated split layout")
1319 .get(&leaf_id)
1320 .map(|vs| vs.viewport.top_byte)
1321 .unwrap_or(0);
1322
1323 let compose_width = self
1325 .windows
1326 .get(&self.active_window)
1327 .and_then(|w| w.buffers.splits())
1328 .map(|(_, vs)| vs)
1329 .expect("active window must have a populated split layout")
1330 .get(&leaf_id)
1331 .and_then(|vs| vs.compose_width);
1332
1333 let gutter_width = self
1337 .active_window()
1338 .buffers
1339 .get(&buffer_id)
1340 .map(|s| s.margins.left_total_width() as u16)
1341 .unwrap_or(0);
1342
1343 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1344 col,
1345 row,
1346 content_rect,
1347 gutter_width,
1348 &cached_mappings,
1349 fallback,
1350 true, compose_width,
1352 ) else {
1353 return Ok(());
1354 };
1355
1356 let primary_cursor_id = self
1357 .active_window()
1358 .buffers
1359 .splits()
1360 .and_then(|(_, vs)| vs.get(&leaf_id))
1361 .map(|vs| vs.cursors.primary_id())
1362 .unwrap_or(CursorId(0));
1363 let event = Event::MoveCursor {
1364 cursor_id: primary_cursor_id,
1365 old_position: 0,
1366 new_position: target_position,
1367 old_anchor: None,
1368 new_anchor: None,
1369 old_sticky_column: 0,
1370 new_sticky_column: 0,
1371 };
1372
1373 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1374 event_log.append(event.clone());
1375 }
1376 self.active_window_mut()
1377 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1378
1379 self.handle_action(Action::SelectWord)?;
1381
1382 if let Some(cursor) = self
1384 .windows
1385 .get(&self.active_window)
1386 .and_then(|w| w.buffers.splits())
1387 .map(|(_, vs)| vs)
1388 .expect("active window must have a populated split layout")
1389 .get(&leaf_id)
1390 .map(|vs| vs.cursors.primary())
1391 {
1392 let sel_start = cursor.selection_start();
1395 let sel_end = cursor.selection_end();
1396 self.active_window_mut().mouse_state.dragging_text_selection = true;
1397 self.active_window_mut().mouse_state.drag_selection_split = Some(split_id);
1398 self.active_window_mut().mouse_state.drag_selection_anchor = Some(sel_start);
1399 self.active_window_mut().mouse_state.drag_selection_by_words = true;
1400 self.active_window_mut().mouse_state.drag_selection_word_end = Some(sel_end);
1401 }
1402
1403 Ok(())
1404 }
1405 pub(super) fn handle_mouse_triple_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
1408 tracing::debug!("handle_mouse_triple_click at col={}, row={}", col, row);
1409
1410 if self.overlay_prompt_active() {
1413 return Ok(());
1414 }
1415
1416 if self.is_mouse_over_any_popup(col, row) {
1418 return Ok(());
1419 } else {
1420 self.dismiss_transient_popups();
1421 }
1422
1423 let split_areas = self.active_layout().split_areas.clone();
1425 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1426 &split_areas
1427 {
1428 if in_rect(col, row, *content_rect) {
1429 if self.active_window().is_terminal_buffer(*buffer_id) {
1430 return Ok(());
1431 }
1432
1433 self.active_window_mut().key_context =
1434 crate::input::keybindings::KeyContext::Normal;
1435
1436 self.handle_editor_triple_click(col, row, *split_id, *buffer_id, *content_rect)?;
1439 return Ok(());
1440 }
1441 }
1442
1443 Ok(())
1444 }
1445
1446 fn handle_editor_triple_click(
1448 &mut self,
1449 col: u16,
1450 row: u16,
1451 split_id: LeafId,
1452 buffer_id: BufferId,
1453 content_rect: ratatui::layout::Rect,
1454 ) -> AnyhowResult<()> {
1455 use crate::model::event::Event;
1456
1457 if self.active_window().is_non_scrollable_buffer(buffer_id) {
1458 return Ok(());
1459 }
1460
1461 self.focus_split(split_id, buffer_id);
1463
1464 let cached_mappings = self
1466 .active_layout()
1467 .view_line_mappings
1468 .get(&split_id)
1469 .cloned();
1470
1471 let leaf_id = split_id;
1472 let fallback = self
1473 .windows
1474 .get(&self.active_window)
1475 .and_then(|w| w.buffers.splits())
1476 .map(|(_, vs)| vs)
1477 .expect("active window must have a populated split layout")
1478 .get(&leaf_id)
1479 .map(|vs| vs.viewport.top_byte)
1480 .unwrap_or(0);
1481
1482 let compose_width = self
1484 .windows
1485 .get(&self.active_window)
1486 .and_then(|w| w.buffers.splits())
1487 .map(|(_, vs)| vs)
1488 .expect("active window must have a populated split layout")
1489 .get(&leaf_id)
1490 .and_then(|vs| vs.compose_width);
1491
1492 let gutter_width = self
1496 .active_window()
1497 .buffers
1498 .get(&buffer_id)
1499 .map(|s| s.margins.left_total_width() as u16)
1500 .unwrap_or(0);
1501
1502 let Some(target_position) = super::click_geometry::screen_to_buffer_position(
1503 col,
1504 row,
1505 content_rect,
1506 gutter_width,
1507 &cached_mappings,
1508 fallback,
1509 true,
1510 compose_width,
1511 ) else {
1512 return Ok(());
1513 };
1514
1515 let primary_cursor_id = self
1516 .active_window()
1517 .buffers
1518 .splits()
1519 .and_then(|(_, vs)| vs.get(&leaf_id))
1520 .map(|vs| vs.cursors.primary_id())
1521 .unwrap_or(CursorId(0));
1522 let event = Event::MoveCursor {
1523 cursor_id: primary_cursor_id,
1524 old_position: 0,
1525 new_position: target_position,
1526 old_anchor: None,
1527 new_anchor: None,
1528 old_sticky_column: 0,
1529 new_sticky_column: 0,
1530 };
1531
1532 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1533 event_log.append(event.clone());
1534 }
1535 self.active_window_mut()
1536 .apply_event_to_buffer(buffer_id, leaf_id, &event);
1537
1538 self.handle_action(Action::SelectLine)?;
1540
1541 Ok(())
1542 }
1543
1544 pub(super) fn overlay_prompt_active(&self) -> bool {
1552 self.active_window()
1553 .prompt
1554 .as_ref()
1555 .is_some_and(|p| p.overlay)
1556 }
1557
1558 pub(super) fn handle_mouse_click(
1559 &mut self,
1560 col: u16,
1561 row: u16,
1562 modifiers: crossterm::event::KeyModifiers,
1563 ) -> AnyhowResult<()> {
1564 if self.floating_widget_panel.is_some() {
1568 self.handle_floating_widget_click(super::PanelSlot::Floating, col, row);
1569 return Ok(());
1570 }
1571 if let Some(super::PanelPlacement::LeftDock { width_cols }) =
1576 self.dock.as_ref().map(|f| f.placement)
1577 {
1578 if col == width_cols.saturating_sub(1) {
1579 self.dock_resizing = true;
1580 return Ok(());
1581 }
1582 }
1583 if let Some((super::PanelPlacement::LeftDock { width_cols }, focused)) =
1587 self.dock.as_ref().map(|f| (f.placement, f.focused))
1588 {
1589 if col < width_cols {
1590 tracing::debug!(
1591 target: "fresh::dock",
1592 col,
1593 row,
1594 width_cols,
1595 focused,
1596 "handle_mouse_click: click in dock column"
1597 );
1598 if !focused {
1599 self.refocus_floating_panel(super::PanelSlot::Dock);
1608 }
1609 self.handle_floating_widget_click(super::PanelSlot::Dock, col, row);
1610 return Ok(());
1611 }
1612 if focused {
1613 tracing::debug!(
1614 target: "fresh::dock",
1615 col,
1616 row,
1617 width_cols,
1618 "handle_mouse_click: click outside dock — blurring"
1619 );
1620 self.blur_floating_panel(super::PanelSlot::Dock);
1621 }
1622 }
1623 if let Some(r) = self.handle_click_context_menus(col, row) {
1624 return r;
1625 }
1626 if !self.is_mouse_over_any_popup(col, row) {
1627 self.dismiss_transient_popups();
1628 }
1629 if let Some(r) = self.handle_click_suggestions(col, row) {
1630 return r;
1631 }
1632 if let Some(r) = self.handle_click_prompt_scrollbar(col, row) {
1633 return r;
1634 }
1635 if let Some(r) = self.handle_click_popup_scrollbar(col, row) {
1636 return r;
1637 }
1638 if let Some(r) = self.handle_click_global_popups(col, row) {
1639 return r;
1640 }
1641 if let Some(r) = self.handle_click_buffer_popups(col, row) {
1642 return r;
1643 }
1644 if self.is_mouse_over_any_popup(col, row) {
1645 return Ok(());
1646 }
1647 if self.is_file_open_active() && self.handle_file_open_click(col, row) {
1648 return Ok(());
1649 }
1650 if let Some(r) = self.handle_click_menu_bar(col, row) {
1651 return r;
1652 }
1653 if let Some(r) = self.handle_click_file_explorer_area(col, row) {
1654 return r;
1655 }
1656 if let Some(r) = self.handle_click_scrollbar(col, row) {
1657 return r;
1658 }
1659 if let Some(r) = self.handle_click_horizontal_scrollbar(col, row) {
1660 return r;
1661 }
1662 if let Some(r) = self.handle_click_status_bar(col, row) {
1663 return r;
1664 }
1665 if let Some(r) = self.handle_click_search_options(col, row) {
1666 return r;
1667 }
1668 if let Some(r) = self.handle_click_split_separator(col, row) {
1669 return r;
1670 }
1671 if let Some(r) = self.handle_click_split_controls(col, row) {
1672 return r;
1673 }
1674 if let Some(r) = self.handle_click_tab_bar(col, row) {
1675 return r;
1676 }
1677
1678 if self.overlay_prompt_active() {
1685 let hit = self
1686 .active_chrome()
1687 .prompt_toolbar_hits
1688 .iter()
1689 .find(|(_, r)| in_rect(col, row, *r))
1690 .map(|(k, _)| k.clone());
1691 if let Some(widget_key) = hit {
1692 if let Some(p) = self.active_window_mut().prompt.as_mut() {
1696 p.toolbar_focus = Some(widget_key.clone());
1697 }
1698 self.toggle_overlay_toolbar_widget(&widget_key);
1699 }
1700 return Ok(());
1701 }
1702
1703 tracing::debug!(
1705 "handle_mouse_click: checking {} split_areas for click at ({}, {})",
1706 self.active_layout().split_areas.len(),
1707 col,
1708 row
1709 );
1710 for (split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1711 &self.active_layout().split_areas
1712 {
1713 tracing::debug!(
1714 " split_id={:?}, content_rect=({}, {}, {}x{})",
1715 split_id,
1716 content_rect.x,
1717 content_rect.y,
1718 content_rect.width,
1719 content_rect.height
1720 );
1721 if in_rect(col, row, *content_rect) {
1722 tracing::debug!(" -> HIT! calling handle_editor_click");
1724 self.handle_editor_click(
1725 col,
1726 row,
1727 *split_id,
1728 *buffer_id,
1729 *content_rect,
1730 modifiers,
1731 )?;
1732 return Ok(());
1733 }
1734 }
1735 tracing::debug!(" -> No split area hit");
1736
1737 Ok(())
1738 }
1739
1740 fn handle_click_context_menus(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1744 if self
1745 .active_window_mut()
1746 .file_explorer_context_menu
1747 .is_some()
1748 {
1749 if let Some(result) = self.handle_file_explorer_context_menu_click(col, row) {
1750 return Some(result);
1751 }
1752 }
1753 if self.active_window_mut().tab_context_menu.is_some() {
1754 if let Some(result) = self.handle_tab_context_menu_click(col, row) {
1755 return Some(result);
1756 }
1757 }
1758 None
1759 }
1760
1761 fn suggestion_at(&self, col: u16, row: u16) -> Option<usize> {
1765 let (inner_rect, start_idx, _visible_count, total_count) =
1766 self.active_chrome().suggestions_area?;
1767 if col < inner_rect.x
1768 || col >= inner_rect.x + inner_rect.width
1769 || row < inner_rect.y
1770 || row >= inner_rect.y + inner_rect.height
1771 {
1772 return None;
1773 }
1774 let relative_row = (row - inner_rect.y) as usize;
1775 let item_idx = start_idx + relative_row;
1776 if item_idx < total_count {
1777 Some(item_idx)
1778 } else {
1779 None
1780 }
1781 }
1782
1783 fn handle_click_suggestions(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1784 let item_idx = self.suggestion_at(col, row)?;
1785 let prompt = self.active_window_mut().prompt.as_mut()?;
1786 prompt.selected_suggestion = Some(item_idx);
1787 let confirms = prompt.prompt_type.click_confirms();
1788 if !confirms {
1789 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1793 prompt.input = suggestion.get_value().to_string();
1794 prompt.cursor_pos = prompt.input.len();
1795 }
1796 }
1797 if confirms {
1798 return Some(self.handle_action(Action::PromptConfirm));
1799 }
1800 Some(Ok(()))
1801 }
1802
1803 fn handle_click_suggestions_confirm(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1807 let item_idx = self.suggestion_at(col, row)?;
1808 let prompt = self.active_window_mut().prompt.as_mut()?;
1809 prompt.selected_suggestion = Some(item_idx);
1810 if let Some(suggestion) = prompt.suggestions.get(item_idx) {
1811 prompt.input = suggestion.get_value().to_string();
1812 prompt.cursor_pos = prompt.input.len();
1813 }
1814 Some(self.handle_action(Action::PromptConfirm))
1815 }
1816
1817 fn handle_click_prompt_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1823 use crate::view::ui::scrollbar::ScrollbarState;
1824 let sb_rect = self.active_chrome().suggestions_scrollbar_rect?;
1825 if col < sb_rect.x
1826 || col >= sb_rect.x + sb_rect.width
1827 || row < sb_rect.y
1828 || row >= sb_rect.y + sb_rect.height
1829 {
1830 return None;
1831 }
1832 let suggestions_area_visible = self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
1840 let active_window_id = self.active_window;
1841 let prompt = self
1842 .windows
1843 .get_mut(&active_window_id)
1844 .and_then(|w| w.prompt.as_mut())?;
1845 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
1846 let total = prompt.suggestions.len();
1847 let track_height = sb_rect.height as usize;
1848 let click_row = row.saturating_sub(sb_rect.y) as usize;
1849 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
1850 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
1851 self.active_window_mut()
1854 .mouse_state
1855 .dragging_prompt_scrollbar = true;
1856 Some(Ok(()))
1857 }
1858
1859 fn handle_click_popup_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1860 let scrollbar_info: Option<(usize, i32)> =
1862 self.active_chrome().popup_areas.iter().rev().find_map(
1863 |(popup_idx, _popup_rect, inner_rect, _scroll, _n, scrollbar_rect, total_lines)| {
1864 let sb_rect = scrollbar_rect.as_ref()?;
1865 if col >= sb_rect.x
1866 && col < sb_rect.x + sb_rect.width
1867 && row >= sb_rect.y
1868 && row < sb_rect.y + sb_rect.height
1869 {
1870 let relative_row = (row - sb_rect.y) as usize;
1871 let track_height = sb_rect.height as usize;
1872 let visible_lines = inner_rect.height as usize;
1873 if track_height > 0 && *total_lines > visible_lines {
1874 let max_scroll = total_lines.saturating_sub(visible_lines);
1875 let target = if track_height > 1 {
1876 (relative_row * max_scroll) / (track_height.saturating_sub(1))
1877 } else {
1878 0
1879 };
1880 Some((*popup_idx, target as i32))
1881 } else {
1882 Some((*popup_idx, 0))
1883 }
1884 } else {
1885 None
1886 }
1887 },
1888 );
1889 let (popup_idx, target_scroll) = scrollbar_info?;
1890 self.active_window_mut()
1891 .mouse_state
1892 .dragging_popup_scrollbar = Some(popup_idx);
1893 self.active_window_mut().mouse_state.drag_start_row = Some(row);
1894 let current_scroll = self
1895 .active_state()
1896 .popups
1897 .get(popup_idx)
1898 .map(|p| p.scroll_offset)
1899 .unwrap_or(0);
1900 self.active_window_mut().mouse_state.drag_start_popup_scroll = Some(current_scroll);
1901 let state = self.active_state_mut();
1902 if let Some(popup) = state.popups.get_mut(popup_idx) {
1903 popup.scroll_by(target_scroll - current_scroll as i32);
1904 }
1905 Some(Ok(()))
1906 }
1907
1908 fn handle_workspace_trust_mouse(
1914 &mut self,
1915 mouse_event: crossterm::event::MouseEvent,
1916 ) -> AnyhowResult<bool> {
1917 use crossterm::event::{MouseButton, MouseEventKind};
1918 let col = mouse_event.column;
1919 let row = mouse_event.row;
1920 let layout = self.active_chrome().workspace_trust_dialog.clone();
1921
1922 match mouse_event.kind {
1923 MouseEventKind::ScrollUp => {
1924 self.workspace_trust_scroll = self.workspace_trust_scroll.saturating_sub(2);
1925 }
1926 MouseEventKind::ScrollDown => {
1927 let max = layout.as_ref().map(|l| l.max_scroll).unwrap_or(0);
1928 self.workspace_trust_scroll = (self.workspace_trust_scroll + 2).min(max);
1929 }
1930 MouseEventKind::Down(MouseButton::Left) => {
1931 if let Some(layout) = layout {
1932 let hit = |r: ratatui::layout::Rect| in_rect(col, row, r);
1933 if hit(layout.ok) {
1934 let idx = self.current_workspace_trust_selection();
1935 self.confirm_workspace_trust(idx);
1936 } else if hit(layout.quit) {
1937 self.hide_popup();
1940 if !self.workspace_trust_prompt_cancellable {
1941 self.should_quit = true;
1942 }
1943 } else if let Some(i) = layout.radios.iter().position(|r| hit(*r)) {
1944 self.confirm_workspace_trust(i);
1945 }
1946 }
1948 }
1949 _ => {}
1951 }
1952 Ok(true)
1953 }
1954
1955 fn handle_click_global_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1956 for (popup_idx, popup_rect, inner_rect, scroll_offset, num_items) in self
1957 .active_chrome()
1958 .global_popup_areas
1959 .clone()
1960 .into_iter()
1961 .rev()
1962 {
1963 if popup_rect.width >= 5 {
1964 let cb_x = popup_rect.x + popup_rect.width - 4;
1965 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
1966 return Some(self.handle_action(Action::PopupCancel));
1967 }
1968 }
1969 if in_rect(col, row, inner_rect) && num_items > 0 {
1970 let relative_row = (row - inner_rect.y) as usize;
1971 let item_idx = scroll_offset + relative_row;
1972 if item_idx < num_items {
1973 if let Some(popup) = self.global_popups.get_mut(popup_idx) {
1974 if let crate::view::popup::PopupContent::List { items: _, selected } =
1975 &mut popup.content
1976 {
1977 *selected = item_idx;
1978 }
1979 }
1980 return Some(self.handle_action(Action::PopupConfirm));
1981 }
1982 }
1983 }
1984 None
1985 }
1986
1987 fn handle_click_buffer_popups(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
1988 let close_hit = self.active_chrome().popup_areas.iter().rev().find_map(
1990 |(_idx, popup_rect, _inner, _scroll, _n, _sb, _tl)| {
1991 if popup_rect.width < 5 {
1992 return None;
1993 }
1994 let cb_x = popup_rect.x + popup_rect.width - 4;
1995 if row == popup_rect.y && col >= cb_x && col < cb_x + 3 {
1996 Some(())
1997 } else {
1998 None
1999 }
2000 },
2001 );
2002 if close_hit.is_some() {
2003 return Some(self.handle_action(Action::PopupCancel));
2004 }
2005
2006 let popup_areas = self.active_chrome().popup_areas.clone();
2008 for (popup_idx, _popup_rect, inner_rect, scroll_offset, num_items, _, _) in
2009 popup_areas.iter().rev()
2010 {
2011 if !in_rect(col, row, *inner_rect) {
2012 continue;
2013 }
2014 let relative_col = (col - inner_rect.x) as usize;
2015 let relative_row = (row - inner_rect.y) as usize;
2016
2017 let link_url = {
2018 let state = self.active_state();
2019 state
2020 .popups
2021 .top()
2022 .and_then(|p| p.link_at_position(relative_col, relative_row))
2023 };
2024 if let Some(url) = link_url {
2025 #[cfg(feature = "runtime")]
2026 if let Err(e) = open::that(&url) {
2027 self.set_status_message(format!("Failed to open URL: {}", e));
2028 } else {
2029 self.set_status_message(format!("Opening: {}", url));
2030 }
2031 return Some(Ok(()));
2032 }
2033
2034 if *num_items > 0 {
2035 let item_idx = scroll_offset + relative_row;
2036 if item_idx < *num_items {
2037 let state = self.active_state_mut();
2038 if let Some(popup) = state.popups.top_mut() {
2039 if let crate::view::popup::PopupContent::List { items: _, selected } =
2040 &mut popup.content
2041 {
2042 *selected = item_idx;
2043 }
2044 }
2045 return Some(self.handle_action(Action::PopupConfirm));
2046 }
2047 }
2048
2049 let is_text_popup = {
2050 let state = self.active_state();
2051 state.popups.top().is_some_and(|p| {
2052 matches!(
2053 p.content,
2054 crate::view::popup::PopupContent::Text(_)
2055 | crate::view::popup::PopupContent::Markdown(_)
2056 )
2057 })
2058 };
2059 if is_text_popup {
2060 let line = scroll_offset + relative_row;
2061 let popup_idx_copy = *popup_idx;
2062 let state = self.active_state_mut();
2063 if let Some(popup) = state.popups.top_mut() {
2064 popup.start_selection(line, relative_col);
2065 }
2066 self.active_window_mut().mouse_state.selecting_in_popup = Some(popup_idx_copy);
2067 return Some(Ok(()));
2068 }
2069 }
2070 None
2071 }
2072
2073 fn handle_click_menu_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2074 if self.active_window_mut().menu_bar_visible {
2075 let hit = self
2077 .active_chrome()
2078 .menu_layout
2079 .as_ref()
2080 .and_then(|ml| ml.menu_at(col, row));
2081 let layout_exists = self.active_chrome().menu_layout.is_some();
2082 if layout_exists {
2083 if let Some(menu_idx) = hit {
2084 if self.menu_state.active_menu == Some(menu_idx) {
2085 self.close_menu_with_auto_hide();
2086 } else {
2087 self.active_window_mut().on_editor_focus_lost();
2088 self.menu_state.open_menu(menu_idx);
2089 }
2090 return Some(Ok(()));
2091 } else if row == 0 {
2092 self.close_menu_with_auto_hide();
2093 return Some(Ok(()));
2094 }
2095 }
2096 }
2097
2098 if let Some(active_idx) = self.menu_state.active_menu {
2099 let all_menus: Vec<crate::config::Menu> = self
2100 .menus
2101 .menus
2102 .iter()
2103 .chain(self.menu_state.plugin_menus.iter())
2104 .cloned()
2105 .collect();
2106 if let Some(menu) = all_menus.get(active_idx) {
2107 match self.handle_menu_dropdown_click(col, row, menu) {
2108 Ok(Some(click_result)) => return Some(click_result),
2109 Ok(None) => {}
2110 Err(e) => return Some(Err(e)),
2111 }
2112 }
2113 self.close_menu_with_auto_hide();
2114 return Some(Ok(()));
2115 }
2116
2117 None
2118 }
2119
2120 fn handle_click_file_explorer_area(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2121 let explorer_area = self.active_layout().file_explorer_area?;
2122 let border_x = explorer_area.x + explorer_area.width.saturating_sub(1);
2123 if col == border_x && row >= explorer_area.y && row < explorer_area.y + explorer_area.height
2124 {
2125 self.active_window_mut().mouse_state.dragging_file_explorer = true;
2126 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2127 self.active_window_mut()
2128 .mouse_state
2129 .drag_start_explorer_width = Some(self.active_window().file_explorer_width);
2130 return Some(Ok(()));
2131 }
2132 if in_rect(col, row, explorer_area) {
2133 return Some(self.handle_file_explorer_click(col, row, explorer_area));
2134 }
2135 None
2136 }
2137
2138 fn handle_click_scrollbar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2139 let (split_id, buffer_id, scrollbar_rect, is_on_thumb) =
2140 self.active_layout().split_areas.iter().find_map(
2141 |(split_id, buffer_id, _content, scrollbar_rect, thumb_start, thumb_end)| {
2142 if in_rect(col, row, *scrollbar_rect) {
2143 let relative_row = row.saturating_sub(scrollbar_rect.y) as usize;
2144 let on_thumb = relative_row >= *thumb_start && relative_row < *thumb_end;
2145 Some((*split_id, *buffer_id, *scrollbar_rect, on_thumb))
2146 } else {
2147 None
2148 }
2149 },
2150 )?;
2151
2152 self.focus_split(split_id, buffer_id);
2153 if is_on_thumb {
2154 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2155 self.active_window_mut().mouse_state.drag_start_row = Some(row);
2156 if self.active_window().is_composite_buffer(buffer_id) {
2157 if let Some(vs) = self
2158 .active_window()
2159 .composite_view_states
2160 .get(&(split_id, buffer_id))
2161 {
2162 self.active_window_mut()
2163 .mouse_state
2164 .drag_start_composite_scroll_row = Some(vs.scroll_row);
2165 }
2166 } else {
2167 let snap = self
2168 .windows
2169 .get(&self.active_window)
2170 .and_then(|w| w.buffers.splits())
2171 .map(|(_, vs)| vs)
2172 .expect("active window must have a populated split layout")
2173 .get(&split_id)
2174 .map(|vs| (vs.viewport.top_byte, vs.viewport.top_view_line_offset));
2175 if let Some((top_byte, top_view_line_offset)) = snap {
2176 let ms = &mut self.active_window_mut().mouse_state;
2177 ms.drag_start_top_byte = Some(top_byte);
2178 ms.drag_start_view_line_offset = Some(top_view_line_offset);
2179 }
2180 }
2181 } else {
2182 self.active_window_mut().mouse_state.dragging_scrollbar = Some(split_id);
2183 if let Err(e) = self.active_window_mut().handle_scrollbar_jump(
2184 col,
2185 row,
2186 split_id,
2187 buffer_id,
2188 scrollbar_rect,
2189 ) {
2190 return Some(Err(e));
2191 }
2192 self.active_window_mut().mouse_state.hover_target =
2193 Some(HoverTarget::ScrollbarThumb(split_id));
2194 }
2195 Some(Ok(()))
2196 }
2197
2198 fn handle_click_horizontal_scrollbar(
2199 &mut self,
2200 col: u16,
2201 row: u16,
2202 ) -> Option<AnyhowResult<()>> {
2203 let (split_id, buffer_id, hscrollbar_rect, max_content_width, is_on_thumb) = self
2204 .active_layout()
2205 .horizontal_scrollbar_areas
2206 .iter()
2207 .find_map(
2208 |(
2209 split_id,
2210 buffer_id,
2211 hscrollbar_rect,
2212 max_content_width,
2213 thumb_start,
2214 thumb_end,
2215 )| {
2216 if col >= hscrollbar_rect.x
2217 && col < hscrollbar_rect.x + hscrollbar_rect.width
2218 && row >= hscrollbar_rect.y
2219 && row < hscrollbar_rect.y + hscrollbar_rect.height
2220 {
2221 let relative_col = col.saturating_sub(hscrollbar_rect.x) as usize;
2222 let on_thumb = relative_col >= *thumb_start && relative_col < *thumb_end;
2223 Some((
2224 *split_id,
2225 *buffer_id,
2226 *hscrollbar_rect,
2227 *max_content_width,
2228 on_thumb,
2229 ))
2230 } else {
2231 None
2232 }
2233 },
2234 )?;
2235
2236 self.focus_split(split_id, buffer_id);
2237 self.active_window_mut()
2238 .mouse_state
2239 .dragging_horizontal_scrollbar = Some(split_id);
2240 if is_on_thumb {
2241 self.active_window_mut().mouse_state.drag_start_hcol = Some(col);
2242 if let Some(vs) = self
2243 .windows
2244 .get(&self.active_window)
2245 .and_then(|w| w.buffers.splits())
2246 .map(|(_, vs)| vs)
2247 .expect("active window must have a populated split layout")
2248 .get(&split_id)
2249 {
2250 self.active_window_mut().mouse_state.drag_start_left_column =
2251 Some(vs.viewport.left_column);
2252 }
2253 } else {
2254 self.active_window_mut().mouse_state.drag_start_hcol = None;
2255 self.active_window_mut().mouse_state.drag_start_left_column = None;
2256 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2257 let track_width = hscrollbar_rect.width as f64;
2258 let ratio = if track_width > 1.0 {
2259 (relative_col / (track_width - 1.0)).clamp(0.0, 1.0)
2260 } else {
2261 0.0
2262 };
2263 if let Some(vs) = self
2264 .windows
2265 .get_mut(&self.active_window)
2266 .and_then(|w| w.split_view_states_mut())
2267 .expect("active window must have a populated split layout")
2268 .get_mut(&split_id)
2269 {
2270 let visible_width = vs.viewport.width as usize;
2271 let max_scroll = max_content_width.saturating_sub(visible_width);
2272 let target_col = (ratio * max_scroll as f64).round() as usize;
2273 vs.viewport.left_column = target_col.min(max_scroll);
2274 vs.viewport.set_skip_ensure_visible();
2275 }
2276 }
2277 Some(Ok(()))
2278 }
2279
2280 fn handle_click_status_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2281 let (status_row, _status_x, _status_width) = self.active_chrome().status_bar_area?;
2282 if row != status_row {
2283 return None;
2284 }
2285 if let Some((r, s, e)) = self.active_chrome().status_bar_line_ending_area {
2295 if row == r && col >= s && col < e {
2296 self.dismiss_menu_popups_for_prompt();
2297 return Some(self.handle_action(Action::SetLineEnding));
2298 }
2299 }
2300 if let Some((r, s, e)) = self.active_chrome().status_bar_encoding_area {
2301 if row == r && col >= s && col < e {
2302 self.dismiss_menu_popups_for_prompt();
2303 return Some(self.handle_action(Action::SetEncoding));
2304 }
2305 }
2306 if let Some((r, s, e)) = self.active_chrome().status_bar_language_area {
2307 if row == r && col >= s && col < e {
2308 self.dismiss_menu_popups_for_prompt();
2309 return Some(self.handle_action(Action::SetLanguage));
2310 }
2311 }
2312 if let Some((r, s, e)) = self.active_chrome().status_bar_lsp_area {
2313 if row == r && col >= s && col < e {
2314 return Some(self.handle_action(Action::ShowLspStatus));
2317 }
2318 }
2319 if let Some((r, s, e)) = self.active_chrome().status_bar_remote_area {
2320 if row == r && col >= s && col < e {
2321 self.dismiss_menu_popups_for_prompt();
2322 return Some(self.handle_action(Action::ShowRemoteIndicatorMenu));
2323 }
2324 }
2325 if let Some((r, s, e)) = self.active_chrome().status_bar_warning_area {
2326 if row == r && col >= s && col < e {
2327 self.dismiss_menu_popups_for_prompt();
2328 return Some(self.handle_action(Action::ShowWarnings));
2329 }
2330 }
2331 if let Some((r, s, e)) = self.active_chrome().status_bar_message_area {
2332 if row == r && col >= s && col < e {
2333 return Some(self.handle_action(Action::ShowStatusLog));
2334 }
2335 }
2336 None
2337 }
2338
2339 fn handle_click_search_options(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2340 use crate::view::ui::status_bar::SearchOptionsHover;
2341 let layout = self.active_chrome().search_options_layout.clone()?;
2342 match layout.checkbox_at(col, row)? {
2343 SearchOptionsHover::CaseSensitive => {
2344 Some(self.handle_action(Action::ToggleSearchCaseSensitive))
2345 }
2346 SearchOptionsHover::WholeWord => {
2347 Some(self.handle_action(Action::ToggleSearchWholeWord))
2348 }
2349 SearchOptionsHover::Regex => Some(self.handle_action(Action::ToggleSearchRegex)),
2350 SearchOptionsHover::ConfirmEach => {
2351 Some(self.handle_action(Action::ToggleSearchConfirmEach))
2352 }
2353 SearchOptionsHover::None => None,
2354 }
2355 }
2356
2357 fn handle_click_split_separator(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2358 let separator_areas = self.active_layout().separator_areas.clone();
2359 for (split_id, direction, sep_x, sep_y, sep_length) in &separator_areas {
2360 let is_on_separator = match direction {
2361 SplitDirection::Horizontal => {
2362 row == *sep_y && col >= *sep_x && col < sep_x + sep_length
2363 }
2364 SplitDirection::Vertical => {
2365 col == *sep_x && row >= *sep_y && row < sep_y + sep_length
2366 }
2367 };
2368 if is_on_separator {
2369 self.active_window_mut().mouse_state.dragging_separator =
2370 Some((*split_id, *direction));
2371 self.active_window_mut().mouse_state.drag_start_position = Some((col, row));
2372 let ratio = self
2373 .split_manager_mut()
2374 .get_ratio((*split_id).into())
2375 .or_else(|| self.grouped_split_ratio(*split_id));
2376 if let Some(ratio) = ratio {
2377 self.active_window_mut().mouse_state.drag_start_ratio = Some(ratio);
2378 }
2379 return Some(Ok(()));
2380 }
2381 }
2382 None
2383 }
2384
2385 fn handle_click_split_controls(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2386 let close_split_id = self
2387 .active_layout()
2388 .close_split_areas
2389 .iter()
2390 .find(|(_, btn_row, start_col, end_col)| {
2391 row == *btn_row && col >= *start_col && col < *end_col
2392 })
2393 .map(|(split_id, _, _, _)| *split_id);
2394 if let Some(split_id) = close_split_id {
2395 if let Err(e) = self
2396 .windows
2397 .get_mut(&self.active_window)
2398 .and_then(|w| w.split_manager_mut())
2399 .expect("active window must have a populated split layout")
2400 .close_split(split_id)
2401 {
2402 self.set_status_message(
2403 t!("error.cannot_close_split", error = e.to_string()).to_string(),
2404 );
2405 } else {
2406 let new_active = self
2407 .windows
2408 .get(&self.active_window)
2409 .and_then(|w| w.buffers.splits())
2410 .map(|(mgr, _)| mgr)
2411 .expect("active window must have a populated split layout")
2412 .active_split();
2413 if let Some(buffer_id) = self
2414 .windows
2415 .get(&self.active_window)
2416 .and_then(|w| w.buffers.splits())
2417 .map(|(mgr, _)| mgr)
2418 .expect("active window must have a populated split layout")
2419 .buffer_for_split(new_active)
2420 {
2421 self.set_active_buffer(buffer_id);
2422 }
2423 self.set_status_message(t!("split.closed").to_string());
2424 }
2425 return Some(Ok(()));
2426 }
2427
2428 let maximize_target = self
2429 .active_layout()
2430 .maximize_split_areas
2431 .iter()
2432 .find(|(_, btn_row, start_col, end_col)| {
2433 row == *btn_row && col >= *start_col && col < *end_col
2434 })
2435 .map(|(split_id, _, _, _)| *split_id);
2436 if let Some(target) = maximize_target {
2437 let already_maximized = self
2444 .windows
2445 .get(&self.active_window)
2446 .and_then(|w| w.buffers.splits())
2447 .map(|(mgr, _)| mgr.is_maximized())
2448 .unwrap_or(false);
2449 if !already_maximized {
2450 if let Some(buffer_id) = self
2451 .windows
2452 .get(&self.active_window)
2453 .and_then(|w| w.buffers.splits())
2454 .map(|(mgr, _)| mgr)
2455 .expect("active window must have a populated split layout")
2456 .buffer_for_split(target)
2457 {
2458 self.focus_split(target, buffer_id);
2459 }
2460 }
2461 match self
2462 .windows
2463 .get_mut(&self.active_window)
2464 .and_then(|w| w.split_manager_mut())
2465 .expect("active window must have a populated split layout")
2466 .toggle_maximize_for(target)
2467 {
2468 Ok(maximized) => {
2469 let msg = if maximized {
2470 t!("split.maximized").to_string()
2471 } else {
2472 t!("split.restored").to_string()
2473 };
2474 self.set_status_message(msg);
2475 }
2476 Err(e) => self.set_status_message(e),
2477 }
2478 return Some(Ok(()));
2479 }
2480
2481 None
2482 }
2483
2484 fn handle_click_tab_bar(&mut self, col: u16, row: u16) -> Option<AnyhowResult<()>> {
2485 for (split_id, tab_layout) in &self.active_layout().tab_layouts {
2486 tracing::debug!(
2487 "Tab layout for split {:?}: bar_area={:?}, left_scroll={:?}, right_scroll={:?}",
2488 split_id,
2489 tab_layout.bar_area,
2490 tab_layout.left_scroll_area,
2491 tab_layout.right_scroll_area
2492 );
2493 }
2494 let tab_hit = self
2495 .active_layout()
2496 .tab_layouts
2497 .iter()
2498 .find_map(|(split_id, tab_layout)| {
2499 let hit = tab_layout.hit_test(col, row);
2500 tracing::debug!(
2501 "Tab hit_test at ({}, {}) for split {:?} returned {:?}",
2502 col,
2503 row,
2504 split_id,
2505 hit
2506 );
2507 hit.map(|h| (*split_id, h))
2508 });
2509 let (split_id, hit) = tab_hit?;
2510 match hit {
2511 TabHit::CloseButton(target) => {
2512 match target {
2513 crate::view::split::TabTarget::Buffer(buffer_id) => {
2514 self.focus_split(split_id, buffer_id);
2515 self.close_tab_in_split(buffer_id, split_id);
2516 }
2517 crate::view::split::TabTarget::Group(group_leaf) => {
2518 self.close_buffer_group_by_leaf(group_leaf);
2519 }
2520 }
2521 Some(Ok(()))
2522 }
2523 TabHit::TabName(target) => {
2524 let direction = self
2525 .windows
2526 .get(&self.active_window)
2527 .and_then(|w| w.buffers.splits())
2528 .map(|(_, vs)| vs)
2529 .expect("active window must have a populated split layout")
2530 .get(&split_id)
2531 .map(|vs| {
2532 let open = &vs.open_buffers;
2533 let cur = vs.active_target();
2534 let cur_idx = open.iter().position(|t| *t == cur);
2535 let new_idx = open.iter().position(|t| *t == target);
2536 match (cur_idx, new_idx) {
2537 (Some(c), Some(n)) if n > c => 1,
2538 (Some(c), Some(n)) if n < c => -1,
2539 _ => 0,
2540 }
2541 })
2542 .unwrap_or(0);
2543 self.active_window_mut()
2544 .animate_tab_switch(split_id, direction);
2545 match target {
2546 crate::view::split::TabTarget::Buffer(buffer_id) => {
2547 self.focus_split(split_id, buffer_id);
2548 self.active_window_mut()
2549 .promote_buffer_from_preview(buffer_id);
2550 self.active_window_mut().mouse_state.dragging_tab = Some(
2551 super::types::TabDragState::new(buffer_id, split_id, (col, row)),
2552 );
2553 }
2554 crate::view::split::TabTarget::Group(group_leaf) => {
2555 self.activate_group_tab(split_id, group_leaf);
2556 }
2557 }
2558 Some(Ok(()))
2559 }
2560 TabHit::ScrollLeft => {
2561 self.set_status_message("ScrollLeft clicked!".to_string());
2562 if let Some(vs) = self
2563 .windows
2564 .get_mut(&self.active_window)
2565 .and_then(|w| w.split_view_states_mut())
2566 .expect("active window must have a populated split layout")
2567 .get_mut(&split_id)
2568 {
2569 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_sub(10);
2570 }
2571 Some(Ok(()))
2572 }
2573 TabHit::ScrollRight => {
2574 self.set_status_message("ScrollRight clicked!".to_string());
2575 if let Some(vs) = self
2576 .windows
2577 .get_mut(&self.active_window)
2578 .and_then(|w| w.split_view_states_mut())
2579 .expect("active window must have a populated split layout")
2580 .get_mut(&split_id)
2581 {
2582 vs.tab_scroll_offset = vs.tab_scroll_offset.saturating_add(10);
2583 }
2584 Some(Ok(()))
2585 }
2586 TabHit::BarBackground => None,
2587 }
2588 }
2589
2590 pub(super) fn handle_mouse_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2592 if self.dock_resizing {
2596 let max_cols = self.terminal_width.max(20).saturating_sub(20).max(10);
2597 let new_w = col.saturating_add(1).clamp(10, max_cols);
2598 let mut changed = false;
2599 if let Some(fwp) = self.dock.as_mut() {
2600 if let super::PanelPlacement::LeftDock { width_cols } = &mut fwp.placement {
2601 changed = *width_cols != new_w;
2602 *width_cols = new_w;
2603 }
2604 }
2605 if changed {
2606 self.dock_width = Some(new_w);
2615 self.relayout();
2618 }
2619 return Ok(());
2620 }
2621 if self.try_widget_scrollbar_drag(super::PanelSlot::Dock, row)
2624 || self.try_widget_scrollbar_drag(super::PanelSlot::Floating, row)
2625 {
2626 let _ = col;
2627 return Ok(());
2628 }
2629 if self.overlay_prompt_active()
2634 && !self.active_window().mouse_state.dragging_prompt_scrollbar
2635 {
2636 return Ok(());
2637 }
2638
2639 if let Some(dragging_split_id) = self.active_window_mut().mouse_state.dragging_scrollbar {
2641 let split_areas = self.active_layout().split_areas.clone();
2644 for (split_id, buffer_id, _content_rect, scrollbar_rect, _thumb_start, _thumb_end) in
2645 &split_areas
2646 {
2647 if *split_id == dragging_split_id {
2648 if self.active_window().mouse_state.drag_start_row.is_some() {
2650 self.active_window_mut().handle_scrollbar_drag_relative(
2652 row,
2653 *split_id,
2654 *buffer_id,
2655 *scrollbar_rect,
2656 )?;
2657 } else {
2658 self.active_window_mut().handle_scrollbar_jump(
2660 col,
2661 row,
2662 *split_id,
2663 *buffer_id,
2664 *scrollbar_rect,
2665 )?;
2666 }
2667 return Ok(());
2668 }
2669 }
2670 }
2671
2672 if let Some(dragging_split_id) = self
2674 .active_window_mut()
2675 .mouse_state
2676 .dragging_horizontal_scrollbar
2677 {
2678 let hscrollbar_areas = self.active_layout().horizontal_scrollbar_areas.clone();
2683 for (
2684 split_id,
2685 _buffer_id,
2686 hscrollbar_rect,
2687 max_content_width,
2688 thumb_start,
2689 thumb_end,
2690 ) in &hscrollbar_areas
2691 {
2692 if *split_id == dragging_split_id {
2693 let track_width = hscrollbar_rect.width as f64;
2694 if track_width <= 1.0 {
2695 break;
2696 }
2697
2698 if let (Some(drag_start_hcol), Some(drag_start_left_column)) = (
2699 self.active_window_mut().mouse_state.drag_start_hcol,
2700 self.active_window_mut().mouse_state.drag_start_left_column,
2701 ) {
2702 let col_offset = (col as i32) - (drag_start_hcol as i32);
2705 if let Some(view_state) = self
2706 .windows
2707 .get_mut(&self.active_window)
2708 .and_then(|w| w.split_view_states_mut())
2709 .expect("active window must have a populated split layout")
2710 .get_mut(&dragging_split_id)
2711 {
2712 let visible_width = view_state.viewport.width as usize;
2713 let max_scroll = max_content_width.saturating_sub(visible_width);
2714 if max_scroll > 0 {
2715 let thumb_size = thumb_end.saturating_sub(*thumb_start).max(1);
2716 let track_travel = (track_width - thumb_size as f64).max(1.0);
2717 let scroll_per_pixel = max_scroll as f64 / track_travel;
2718 let scroll_offset =
2719 (col_offset as f64 * scroll_per_pixel).round() as i64;
2720 let new_left =
2721 (drag_start_left_column as i64 + scroll_offset).max(0) as usize;
2722 view_state.viewport.left_column = new_left.min(max_scroll);
2723 view_state.viewport.set_skip_ensure_visible();
2724 }
2725 }
2726 } else {
2727 let relative_col = col.saturating_sub(hscrollbar_rect.x) as f64;
2729 let ratio = (relative_col / (track_width - 1.0)).clamp(0.0, 1.0);
2730
2731 if let Some(view_state) = self
2732 .windows
2733 .get_mut(&self.active_window)
2734 .and_then(|w| w.split_view_states_mut())
2735 .expect("active window must have a populated split layout")
2736 .get_mut(&dragging_split_id)
2737 {
2738 let visible_width = view_state.viewport.width as usize;
2739 let max_scroll = max_content_width.saturating_sub(visible_width);
2740 let target_col = (ratio * max_scroll as f64).round() as usize;
2741 view_state.viewport.left_column = target_col.min(max_scroll);
2742 view_state.viewport.set_skip_ensure_visible();
2743 }
2744 }
2745
2746 return Ok(());
2747 }
2748 }
2749 }
2750
2751 if let Some(popup_idx) = self.active_window_mut().mouse_state.selecting_in_popup {
2753 if let Some((_, _, inner_rect, scroll_offset, _, _, _)) = self
2755 .active_chrome()
2756 .popup_areas
2757 .iter()
2758 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2759 {
2760 if col >= inner_rect.x
2762 && col < inner_rect.x + inner_rect.width
2763 && row >= inner_rect.y
2764 && row < inner_rect.y + inner_rect.height
2765 {
2766 let relative_col = (col - inner_rect.x) as usize;
2767 let relative_row = (row - inner_rect.y) as usize;
2768 let line = scroll_offset + relative_row;
2769
2770 let state = self.active_state_mut();
2771 if let Some(popup) = state.popups.get_mut(popup_idx) {
2772 popup.extend_selection(line, relative_col);
2773 }
2774 }
2775 }
2776 return Ok(());
2777 }
2778
2779 if self
2784 .active_window_mut()
2785 .mouse_state
2786 .dragging_prompt_scrollbar
2787 {
2788 use crate::view::ui::scrollbar::ScrollbarState;
2789 let sb_rect = self.active_chrome().suggestions_scrollbar_rect;
2792 let suggestions_area_visible =
2793 self.active_chrome().suggestions_area.map(|(_, _, v, _)| v);
2794 let active_window_id = self.active_window;
2795 if let (Some(sb_rect), Some(prompt)) = (
2796 sb_rect,
2797 self.windows
2798 .get_mut(&active_window_id)
2799 .and_then(|w| w.prompt.as_mut()),
2800 ) {
2801 let visible = suggestions_area_visible.unwrap_or(prompt.suggestions.len().min(10));
2802 let total = prompt.suggestions.len();
2803 let track_height = sb_rect.height as usize;
2804 let clamped_row =
2808 row.clamp(sb_rect.y, sb_rect.y + sb_rect.height.saturating_sub(1));
2809 let click_row = clamped_row.saturating_sub(sb_rect.y) as usize;
2810 let state = ScrollbarState::new(total, visible, prompt.scroll_offset);
2811 prompt.scroll_offset = state.click_to_offset(track_height, click_row);
2812 }
2813 return Ok(());
2814 }
2815
2816 if let Some(popup_idx) = self
2818 .active_window_mut()
2819 .mouse_state
2820 .dragging_popup_scrollbar
2821 {
2822 if let Some((_, _, inner_rect, _, _, Some(sb_rect), total_lines)) = self
2824 .active_chrome()
2825 .popup_areas
2826 .iter()
2827 .find(|(idx, _, _, _, _, _, _)| *idx == popup_idx)
2828 {
2829 let track_height = sb_rect.height as usize;
2830 let visible_lines = inner_rect.height as usize;
2831
2832 if track_height > 0 && *total_lines > visible_lines {
2833 let relative_row = row.saturating_sub(sb_rect.y) as usize;
2834 let max_scroll = total_lines.saturating_sub(visible_lines);
2835 let target_scroll = if track_height > 1 {
2836 (relative_row * max_scroll) / (track_height.saturating_sub(1))
2837 } else {
2838 0
2839 };
2840
2841 let state = self.active_state_mut();
2842 if let Some(popup) = state.popups.get_mut(popup_idx) {
2843 let current_scroll = popup.scroll_offset as i32;
2844 let delta = target_scroll as i32 - current_scroll;
2845 popup.scroll_by(delta);
2846 }
2847 }
2848 }
2849 return Ok(());
2850 }
2851
2852 if let Some((split_id, direction)) = self.active_window_mut().mouse_state.dragging_separator
2854 {
2855 self.handle_separator_drag(col, row, split_id, direction)?;
2856 return Ok(());
2857 }
2858
2859 if self.active_window_mut().mouse_state.dragging_file_explorer {
2861 self.handle_file_explorer_border_drag(col)?;
2862 return Ok(());
2863 }
2864
2865 if self.active_window_mut().mouse_state.dragging_text_selection {
2867 self.handle_text_selection_drag(col, row)?;
2868 return Ok(());
2869 }
2870
2871 if self.active_window_mut().mouse_state.dragging_tab.is_some() {
2873 self.handle_tab_drag(col, row)?;
2874 return Ok(());
2875 }
2876
2877 Ok(())
2878 }
2879
2880 fn handle_text_selection_drag(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
2882 use crate::model::event::Event;
2883 use crate::primitives::word_navigation::{find_word_end, find_word_start};
2884
2885 let Some(split_id) = self.active_window_mut().mouse_state.drag_selection_split else {
2886 return Ok(());
2887 };
2888 let Some(anchor_position) = self.active_window_mut().mouse_state.drag_selection_anchor
2889 else {
2890 return Ok(());
2891 };
2892
2893 let Some((buffer_id, content_rect)) = self
2895 .active_layout()
2896 .split_areas
2897 .iter()
2898 .find(|(sid, _, _, _, _, _)| *sid == split_id)
2899 .map(|(_, bid, rect, _, _, _)| (*bid, *rect))
2900 else {
2901 return Ok(());
2902 };
2903
2904 let cached_mappings = self
2906 .active_layout()
2907 .view_line_mappings
2908 .get(&split_id)
2909 .cloned();
2910
2911 let leaf_id = split_id;
2912
2913 let fallback = self
2915 .windows
2916 .get(&self.active_window)
2917 .and_then(|w| w.buffers.splits())
2918 .map(|(_, vs)| vs)
2919 .expect("active window must have a populated split layout")
2920 .get(&leaf_id)
2921 .map(|vs| vs.viewport.top_byte)
2922 .unwrap_or(0);
2923
2924 let compose_width = self
2926 .windows
2927 .get(&self.active_window)
2928 .and_then(|w| w.buffers.splits())
2929 .map(|(_, vs)| vs)
2930 .expect("active window must have a populated split layout")
2931 .get(&leaf_id)
2932 .and_then(|vs| vs.compose_width);
2933
2934 let drag_by_words = self.active_window_mut().mouse_state.drag_selection_by_words;
2938 let drag_word_end = self.active_window_mut().mouse_state.drag_selection_word_end;
2939
2940 let Some((target_position, new_position, anchor_position, new_sticky_column)) = self
2941 .active_window()
2942 .buffers
2943 .get(&buffer_id)
2944 .and_then(|state| {
2945 let gutter_width = state.margins.left_total_width() as u16;
2946 let target_position = super::click_geometry::screen_to_buffer_position(
2947 col,
2948 row,
2949 content_rect,
2950 gutter_width,
2951 &cached_mappings,
2952 fallback,
2953 true, compose_width,
2955 )?;
2956 let (new_position, anchor_pos) = if drag_by_words {
2957 if target_position >= anchor_position {
2958 (
2959 find_word_end(&state.buffer, target_position),
2960 anchor_position,
2961 )
2962 } else {
2963 let word_end = drag_word_end.unwrap_or(anchor_position);
2964 (find_word_start(&state.buffer, target_position), word_end)
2965 }
2966 } else {
2967 (target_position, anchor_position)
2968 };
2969 let new_sticky_column = state
2970 .buffer
2971 .offset_to_position(new_position)
2972 .map(|pos| pos.column);
2973 Some((target_position, new_position, anchor_pos, new_sticky_column))
2974 })
2975 else {
2976 return Ok(());
2977 };
2978 let _ = target_position;
2979
2980 let (primary_cursor_id, old_position, old_anchor, old_sticky_column) = self
2981 .active_window()
2982 .buffers
2983 .splits()
2984 .and_then(|(_, vs)| vs.get(&leaf_id))
2985 .map(|vs| {
2986 let cursor = vs.cursors.primary();
2987 (
2988 vs.cursors.primary_id(),
2989 cursor.position,
2990 cursor.anchor,
2991 cursor.sticky_column,
2992 )
2993 })
2994 .unwrap_or((CursorId(0), 0, None, 0));
2995
2996 let event = Event::MoveCursor {
2997 cursor_id: primary_cursor_id,
2998 old_position,
2999 new_position,
3000 old_anchor,
3001 new_anchor: Some(anchor_position),
3002 old_sticky_column,
3003 new_sticky_column: new_sticky_column.unwrap_or(old_sticky_column),
3004 };
3005
3006 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
3007 event_log.append(event.clone());
3008 }
3009 self.active_window_mut()
3010 .apply_event_to_buffer(buffer_id, leaf_id, &event);
3011
3012 Ok(())
3013 }
3014
3015 pub(super) fn handle_file_explorer_border_drag(&mut self, col: u16) -> AnyhowResult<()> {
3017 let Some((start_col, _start_row)) =
3018 self.active_window_mut().mouse_state.drag_start_position
3019 else {
3020 return Ok(());
3021 };
3022 let Some(start_width) = self
3023 .active_window_mut()
3024 .mouse_state
3025 .drag_start_explorer_width
3026 else {
3027 return Ok(());
3028 };
3029
3030 let delta = col as i32 - start_col as i32;
3031 let total_width = self.terminal_width as i32;
3032
3033 if total_width > 0 {
3037 use crate::config::ExplorerWidth;
3038 self.active_window_mut().file_explorer_width = match start_width {
3039 ExplorerWidth::Percent(start_pct) => {
3040 let percent_delta = (delta * 100) / total_width;
3041 let new_pct = (start_pct as i32 + percent_delta).clamp(0, 100) as u8;
3042 ExplorerWidth::Percent(new_pct)
3043 }
3044 ExplorerWidth::Columns(start_cols) => {
3045 let new_cols = (start_cols as i32 + delta).clamp(0, total_width) as u16;
3046 ExplorerWidth::Columns(new_cols)
3047 }
3048 };
3049 self.relayout();
3052 }
3053
3054 Ok(())
3055 }
3056
3057 pub(super) fn handle_separator_drag(
3059 &mut self,
3060 col: u16,
3061 row: u16,
3062 split_id: ContainerId,
3063 direction: SplitDirection,
3064 ) -> AnyhowResult<()> {
3065 let Some((start_col, start_row)) = self.active_window_mut().mouse_state.drag_start_position
3066 else {
3067 return Ok(());
3068 };
3069 let Some(start_ratio) = self.active_window_mut().mouse_state.drag_start_ratio else {
3070 return Ok(());
3071 };
3072 let Some(editor_area) = self.active_layout().editor_content_area else {
3073 return Ok(());
3074 };
3075
3076 let (delta, total_size) = match direction {
3078 SplitDirection::Horizontal => {
3079 let delta = row as i32 - start_row as i32;
3081 let total = editor_area.height as i32;
3082 (delta, total)
3083 }
3084 SplitDirection::Vertical => {
3085 let delta = col as i32 - start_col as i32;
3087 let total = editor_area.width as i32;
3088 (delta, total)
3089 }
3090 };
3091
3092 if total_size > 0 {
3095 let ratio_delta = delta as f32 / total_size as f32;
3096 let new_ratio = (start_ratio + ratio_delta).clamp(0.1, 0.9);
3097
3098 if self
3103 .windows
3104 .get(&self.active_window)
3105 .and_then(|w| w.buffers.splits())
3106 .map(|(mgr, _)| mgr)
3107 .expect("active window must have a populated split layout")
3108 .get_ratio(split_id.into())
3109 .is_some()
3110 {
3111 self.windows
3112 .get_mut(&self.active_window)
3113 .and_then(|w| w.split_manager_mut())
3114 .expect("active window must have a populated split layout")
3115 .set_ratio(split_id, new_ratio);
3116 } else {
3117 self.set_grouped_split_ratio(split_id, new_ratio);
3118 }
3119 self.relayout();
3122 }
3123
3124 Ok(())
3125 }
3126
3127 pub(super) fn handle_right_click(&mut self, col: u16, row: u16) -> AnyhowResult<()> {
3129 let frame_w = self.active_chrome().last_frame_width;
3130 let frame_h = self.active_chrome().last_frame_height;
3131 if let Some(ref menu) = self.active_window().file_explorer_context_menu {
3132 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3133 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3134 let menu_height = menu.height();
3135 if col >= menu_x
3136 && col < menu_x + menu_width
3137 && row >= menu_y
3138 && row < menu_y + menu_height
3139 {
3140 return Ok(());
3141 }
3142 }
3143
3144 if let Some(ref menu) = self.active_window_mut().tab_context_menu {
3146 let menu_x = menu.position.0;
3147 let menu_y = menu.position.1;
3148 let menu_width = 22u16; let menu_height = super::types::TabContextMenuItem::all().len() as u16 + 2; if col >= menu_x
3153 && col < menu_x + menu_width
3154 && row >= menu_y
3155 && row < menu_y + menu_height
3156 {
3157 return Ok(());
3159 }
3160 }
3161
3162 if let Some(explorer_area) = self.active_layout().file_explorer_area {
3163 if col >= explorer_area.x
3164 && col < explorer_area.x + explorer_area.width
3165 && row < explorer_area.y + explorer_area.height
3166 && row > explorer_area.y
3167 {
3169 let relative_row = row.saturating_sub(explorer_area.y + 1);
3170 let (is_multi, is_root_selected) =
3171 if let Some(explorer) = self.file_explorer_mut().as_mut() {
3172 let display_nodes = explorer.get_display_nodes();
3173 let scroll_offset = explorer.get_scroll_offset();
3174 let clicked_index = (relative_row as usize) + scroll_offset;
3175 let mut clicked_is_root = false;
3176 if clicked_index < display_nodes.len() {
3177 let (node_id, _) = display_nodes[clicked_index];
3178 explorer.set_selected(Some(node_id));
3179 clicked_is_root = node_id == explorer.tree().root_id();
3180 }
3181 (explorer.has_multi_selection(), clicked_is_root)
3182 } else {
3183 (false, false)
3184 };
3185 self.active_window_mut().key_context =
3186 crate::input::keybindings::KeyContext::FileExplorer;
3187 self.active_window_mut().tab_context_menu = None;
3188 self.active_window_mut().file_explorer_context_menu =
3189 Some(super::types::FileExplorerContextMenu::new(
3190 col,
3191 row + 1,
3192 is_multi,
3193 is_root_selected,
3194 ));
3195 return Ok(());
3196 }
3197 }
3198
3199 self.active_window_mut().file_explorer_context_menu = None;
3200
3201 let tab_hit = self
3203 .active_layout()
3204 .tab_layouts
3205 .iter()
3206 .find_map(
3207 |(split_id, tab_layout)| match tab_layout.hit_test(col, row) {
3208 Some(TabHit::TabName(target) | TabHit::CloseButton(target)) => {
3209 target.as_buffer().map(|bid| (*split_id, bid))
3212 }
3213 _ => None,
3214 },
3215 );
3216
3217 if let Some((split_id, buffer_id)) = tab_hit {
3218 self.active_window_mut().tab_context_menu =
3220 Some(TabContextMenu::new(buffer_id, split_id, col, row + 1));
3221 } else {
3222 self.active_window_mut().tab_context_menu = None;
3224 }
3225
3226 Ok(())
3227 }
3228
3229 pub(super) fn handle_tab_context_menu_click(
3231 &mut self,
3232 col: u16,
3233 row: u16,
3234 ) -> Option<AnyhowResult<()>> {
3235 let menu = self.active_window_mut().tab_context_menu.as_ref()?;
3236 let menu_x = menu.position.0;
3237 let menu_y = menu.position.1;
3238 let menu_width = 22u16;
3239 let items = super::types::TabContextMenuItem::all();
3240 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
3244 {
3245 self.active_window_mut().tab_context_menu = None;
3247 return Some(Ok(()));
3248 }
3249
3250 if row == menu_y || row == menu_y + menu_height - 1 {
3252 return Some(Ok(()));
3253 }
3254
3255 let item_idx = (row - menu_y - 1) as usize;
3257 if item_idx >= items.len() {
3258 return Some(Ok(()));
3259 }
3260
3261 let buffer_id = menu.buffer_id;
3263 let split_id = menu.split_id;
3264 let item = items[item_idx];
3265
3266 self.active_window_mut().tab_context_menu = None;
3268
3269 Some(self.execute_tab_context_menu_action(item, buffer_id, split_id))
3271 }
3272
3273 fn execute_tab_context_menu_action(
3275 &mut self,
3276 item: super::types::TabContextMenuItem,
3277 buffer_id: BufferId,
3278 leaf_id: LeafId,
3279 ) -> AnyhowResult<()> {
3280 use super::types::TabContextMenuItem;
3281 match item {
3282 TabContextMenuItem::Close => {
3283 self.close_tab_in_split(buffer_id, leaf_id);
3284 }
3285 TabContextMenuItem::CloseOthers => {
3286 self.close_other_tabs_in_split(buffer_id, leaf_id);
3287 }
3288 TabContextMenuItem::CloseToRight => {
3289 self.close_tabs_to_right_in_split(buffer_id, leaf_id);
3290 }
3291 TabContextMenuItem::CloseToLeft => {
3292 self.close_tabs_to_left_in_split(buffer_id, leaf_id);
3293 }
3294 TabContextMenuItem::CloseAll => {
3295 self.close_all_tabs_in_split(leaf_id);
3296 }
3297 TabContextMenuItem::CopyRelativePath => {
3298 self.copy_buffer_path(buffer_id, true);
3299 }
3300 TabContextMenuItem::CopyFullPath => {
3301 self.copy_buffer_path(buffer_id, false);
3302 }
3303 }
3304
3305 Ok(())
3306 }
3307
3308 pub(super) fn handle_file_explorer_context_menu_key(
3311 &mut self,
3312 code: crossterm::event::KeyCode,
3313 modifiers: crossterm::event::KeyModifiers,
3314 ) -> Option<AnyhowResult<()>> {
3315 use crossterm::event::KeyCode;
3316 use crossterm::event::KeyModifiers;
3317
3318 if modifiers != KeyModifiers::NONE {
3319 return None;
3320 }
3321
3322 match code {
3323 KeyCode::Up => {
3324 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3325 menu.prev_item();
3326 }
3327 Some(Ok(()))
3328 }
3329 KeyCode::Down => {
3330 if let Some(ref mut menu) = self.active_window_mut().file_explorer_context_menu {
3331 menu.next_item();
3332 }
3333 Some(Ok(()))
3334 }
3335 KeyCode::Enter => {
3336 let item = {
3337 let menu = self
3338 .active_window_mut()
3339 .file_explorer_context_menu
3340 .as_ref()?;
3341 menu.items()[menu.highlighted]
3342 };
3343 self.active_window_mut().file_explorer_context_menu = None;
3344 self.execute_file_explorer_context_menu_action(item);
3345 Some(Ok(()))
3346 }
3347 KeyCode::Esc => {
3348 self.active_window_mut().file_explorer_context_menu = None;
3349 Some(Ok(()))
3350 }
3351 _ => None,
3352 }
3353 }
3354
3355 pub(super) fn handle_file_explorer_context_menu_click(
3357 &mut self,
3358 col: u16,
3359 row: u16,
3360 ) -> Option<AnyhowResult<()>> {
3361 let frame_w = self.active_chrome().last_frame_width;
3363 let frame_h = self.active_chrome().last_frame_height;
3364 let clicked_item: Option<super::types::FileExplorerContextMenuItem> = {
3365 let menu = self.active_window().file_explorer_context_menu.as_ref()?;
3366 let (menu_x, menu_y) = menu.clamped_position(frame_w, frame_h);
3367 let menu_width = super::types::FILE_EXPLORER_CONTEXT_MENU_WIDTH;
3368 let menu_height = menu.height();
3369
3370 if col < menu_x
3371 || col >= menu_x + menu_width
3372 || row < menu_y
3373 || row >= menu_y + menu_height
3374 {
3375 self.active_window_mut().file_explorer_context_menu = None;
3376 return Some(Ok(()));
3377 }
3378
3379 if row == menu_y || row == menu_y + menu_height - 1 {
3380 return Some(Ok(()));
3381 }
3382
3383 let item_idx = (row - menu_y - 1) as usize;
3384 menu.items().get(item_idx).copied()
3385 };
3386
3387 self.active_window_mut().file_explorer_context_menu = None;
3388 if let Some(item) = clicked_item {
3389 self.execute_file_explorer_context_menu_action(item);
3390 }
3391 Some(Ok(()))
3392 }
3393
3394 fn execute_file_explorer_context_menu_action(
3395 &mut self,
3396 item: super::types::FileExplorerContextMenuItem,
3397 ) {
3398 use super::types::FileExplorerContextMenuItem;
3399 match item {
3400 FileExplorerContextMenuItem::NewFile => self.file_explorer_new_file(),
3401 FileExplorerContextMenuItem::NewDirectory => self.file_explorer_new_directory(),
3402 FileExplorerContextMenuItem::Rename => self.file_explorer_rename(),
3403 FileExplorerContextMenuItem::Cut => self.active_window_mut().file_explorer_cut(),
3404 FileExplorerContextMenuItem::Copy => self.active_window_mut().file_explorer_copy(),
3405 FileExplorerContextMenuItem::Paste => self.file_explorer_paste(),
3406 FileExplorerContextMenuItem::Duplicate => self.file_explorer_duplicate(),
3407 FileExplorerContextMenuItem::Delete => self.file_explorer_delete(),
3408 FileExplorerContextMenuItem::CopyFullPath => self.file_explorer_copy_path(false),
3409 FileExplorerContextMenuItem::CopyRelativePath => self.file_explorer_copy_path(true),
3410 }
3411 }
3412
3413 fn show_file_explorer_status_tooltip(&mut self, path: std::path::PathBuf, col: u16, row: u16) {
3415 use crate::view::popup::{Popup, PopupPosition};
3416 use ratatui::style::Style;
3417
3418 let is_directory = path.is_dir();
3419
3420 let decoration = self
3422 .active_window()
3423 .file_explorer_decoration_cache
3424 .direct_for_path(&path)
3425 .cloned();
3426
3427 let bubbled_decoration = if is_directory && decoration.is_none() {
3429 self.active_window()
3430 .file_explorer_decoration_cache
3431 .bubbled_for_path(&path)
3432 .cloned()
3433 } else {
3434 None
3435 };
3436
3437 let has_unsaved_changes = if is_directory {
3439 self.windows
3441 .get(&self.active_window)
3442 .map(|w| &w.buffers)
3443 .expect("active window present")
3444 .iter()
3445 .any(|(buffer_id, state)| {
3446 if state.buffer.is_modified() {
3447 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3448 {
3449 if let Some(file_path) = metadata.file_path() {
3450 return file_path.starts_with(&path);
3451 }
3452 }
3453 }
3454 false
3455 })
3456 } else {
3457 self.windows
3458 .get(&self.active_window)
3459 .map(|w| &w.buffers)
3460 .expect("active window present")
3461 .iter()
3462 .any(|(buffer_id, state)| {
3463 if state.buffer.is_modified() {
3464 if let Some(metadata) = self.active_window().buffer_metadata.get(buffer_id)
3465 {
3466 return metadata.file_path() == Some(&path);
3467 }
3468 }
3469 false
3470 })
3471 };
3472
3473 let mut lines: Vec<String> = Vec::new();
3475
3476 if let Some(decoration) = &decoration {
3477 let symbol = &decoration.symbol;
3478 let explanation = match symbol.as_str() {
3479 "U" => "Untracked - File is not tracked by git",
3480 "M" => "Modified - File has unstaged changes",
3481 "A" => "Added - File is staged for commit",
3482 "D" => "Deleted - File is staged for deletion",
3483 "R" => "Renamed - File has been renamed",
3484 "C" => "Copied - File has been copied",
3485 "!" => "Conflicted - File has merge conflicts",
3486 "●" => "Has changes - Contains modified files",
3487 _ => "Unknown status",
3488 };
3489 lines.push(format!("{} - {}", symbol, explanation));
3490 } else if bubbled_decoration.is_some() {
3491 lines.push("● - Contains modified files".to_string());
3492 } else if has_unsaved_changes {
3493 if is_directory {
3494 lines.push("● - Contains unsaved changes".to_string());
3495 } else {
3496 lines.push("● - Unsaved changes in editor".to_string());
3497 }
3498 } else {
3499 return; }
3501
3502 if is_directory {
3504 if let Some(modified_files) = self.get_modified_files_in_directory(&path) {
3506 lines.push(String::new()); lines.push("Modified files:".to_string());
3508 let resolved_path = path.canonicalize().unwrap_or_else(|_| path.clone());
3510 const MAX_FILES: usize = 8;
3511 for (i, file) in modified_files.iter().take(MAX_FILES).enumerate() {
3512 let display_name = file
3514 .strip_prefix(&resolved_path)
3515 .unwrap_or(file)
3516 .to_string_lossy()
3517 .to_string();
3518 lines.push(format!(" {}", display_name));
3519 if i == MAX_FILES - 1 && modified_files.len() > MAX_FILES {
3520 lines.push(format!(
3521 " ... and {} more",
3522 modified_files.len() - MAX_FILES
3523 ));
3524 break;
3525 }
3526 }
3527 }
3528 } else {
3529 if let Some(stats) = self.get_git_diff_stats(&path) {
3531 lines.push(String::new()); lines.push(stats);
3533 }
3534 }
3535
3536 if lines.is_empty() {
3537 return;
3538 }
3539
3540 let mut popup = Popup::text(lines, &*self.theme.read().unwrap());
3542 popup.title = Some("Git Status".to_string());
3543 popup.transient = true;
3544 popup.position = PopupPosition::Fixed { x: col, y: row + 1 };
3545 popup.width = 50;
3546 popup.max_height = 15;
3547 popup.border_style = Style::default().fg(self.theme.read().unwrap().popup_border_fg);
3548 popup.background_style = Style::default().bg(self.theme.read().unwrap().popup_bg);
3549
3550 let __buffer_id = self.active_buffer();
3552 if let Some(state) = self
3553 .windows
3554 .get_mut(&self.active_window)
3555 .map(|w| &mut w.buffers)
3556 .expect("active window present")
3557 .get_mut(&__buffer_id)
3558 {
3559 state.popups.show(popup);
3560 }
3561 }
3562
3563 fn dismiss_file_explorer_status_tooltip(&mut self) {
3565 let __buffer_id = self.active_buffer();
3567 if let Some(state) = self
3568 .windows
3569 .get_mut(&self.active_window)
3570 .map(|w| &mut w.buffers)
3571 .expect("active window present")
3572 .get_mut(&__buffer_id)
3573 {
3574 state.popups.dismiss_transient();
3575 }
3576 }
3577
3578 fn get_git_diff_stats(&self, path: &std::path::Path) -> Option<String> {
3580 use crate::services::process_hidden::HideWindow;
3581 use std::process::Command;
3582
3583 let output = Command::new("git")
3585 .args(["diff", "--numstat", "--"])
3586 .arg(path)
3587 .current_dir(self.working_dir())
3588 .hide_window()
3589 .output()
3590 .ok()?;
3591
3592 if !output.status.success() {
3593 return None;
3594 }
3595
3596 let stdout = String::from_utf8_lossy(&output.stdout);
3597 let line = stdout.lines().next()?;
3598 let parts: Vec<&str> = line.split('\t').collect();
3599
3600 if parts.len() >= 2 {
3601 let insertions = parts[0];
3602 let deletions = parts[1];
3603
3604 if insertions == "-" && deletions == "-" {
3606 return Some("Binary file changed".to_string());
3607 }
3608
3609 let ins: i32 = insertions.parse().unwrap_or(0);
3610 let del: i32 = deletions.parse().unwrap_or(0);
3611
3612 if ins > 0 || del > 0 {
3613 return Some(format!("+{} -{} lines", ins, del));
3614 }
3615 }
3616
3617 let staged_output = Command::new("git")
3619 .args(["diff", "--numstat", "--cached", "--"])
3620 .arg(path)
3621 .current_dir(self.working_dir())
3622 .hide_window()
3623 .output()
3624 .ok()?;
3625
3626 if staged_output.status.success() {
3627 let staged_stdout = String::from_utf8_lossy(&staged_output.stdout);
3628 if let Some(line) = staged_stdout.lines().next() {
3629 let parts: Vec<&str> = line.split('\t').collect();
3630 if parts.len() >= 2 {
3631 let insertions = parts[0];
3632 let deletions = parts[1];
3633
3634 if insertions == "-" && deletions == "-" {
3635 return Some("Binary file staged".to_string());
3636 }
3637
3638 let ins: i32 = insertions.parse().unwrap_or(0);
3639 let del: i32 = deletions.parse().unwrap_or(0);
3640
3641 if ins > 0 || del > 0 {
3642 return Some(format!("+{} -{} lines (staged)", ins, del));
3643 }
3644 }
3645 }
3646 }
3647
3648 None
3649 }
3650
3651 fn get_modified_files_in_directory(
3653 &self,
3654 dir_path: &std::path::Path,
3655 ) -> Option<Vec<std::path::PathBuf>> {
3656 use crate::services::process_hidden::HideWindow;
3657 use std::process::Command;
3658
3659 let resolved_path = dir_path
3661 .canonicalize()
3662 .unwrap_or_else(|_| dir_path.to_path_buf());
3663
3664 let output = Command::new("git")
3666 .args(["status", "--porcelain", "--"])
3667 .arg(&resolved_path)
3668 .current_dir(self.working_dir())
3669 .hide_window()
3670 .output()
3671 .ok()?;
3672
3673 if !output.status.success() {
3674 return None;
3675 }
3676
3677 let stdout = String::from_utf8_lossy(&output.stdout);
3678 let modified_files: Vec<std::path::PathBuf> = stdout
3679 .lines()
3680 .filter_map(|line| {
3681 if line.len() > 3 {
3684 let file_part = &line[3..];
3685 let file_name = if file_part.contains(" -> ") {
3687 file_part.split(" -> ").last().unwrap_or(file_part)
3688 } else {
3689 file_part
3690 };
3691 Some(self.working_dir().join(file_name))
3692 } else {
3693 None
3694 }
3695 })
3696 .collect();
3697
3698 if modified_files.is_empty() {
3699 None
3700 } else {
3701 Some(modified_files)
3702 }
3703 }
3704
3705 fn handle_floating_widget_panel_wheel(
3717 &mut self,
3718 slot: super::PanelSlot,
3719 col: u16,
3720 row: u16,
3721 delta: i32,
3722 ) -> bool {
3723 let inner = match self.panel(slot) {
3724 Some(fwp) => match fwp.last_inner_rect {
3725 Some(rect) => rect,
3726 None => return false,
3727 },
3728 None => return false,
3729 };
3730 if col < inner.x || col >= inner.x + inner.width {
3731 return false;
3732 }
3733 if row < inner.y || row >= inner.y + inner.height {
3734 return false;
3735 }
3736 let scrolled = self.handle_widget_panel_wheel(slot.buffer_id(), delta);
3737 let is_dock = matches!(
3741 self.panel(slot).map(|f| f.placement),
3742 Some(super::PanelPlacement::LeftDock { .. })
3743 );
3744 scrolled || is_dock
3745 }
3746
3747 fn try_widget_scrollbar_press(&mut self, slot: super::PanelSlot, col: u16, row: u16) -> bool {
3752 use crate::view::ui::scrollbar::ScrollbarState;
3753 let (panel_id, tracks) = match self.panel(slot) {
3754 Some(fwp) => (fwp.panel_id, fwp.scrollbar_tracks.clone()),
3755 None => return false,
3756 };
3757 for t in &tracks {
3758 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
3759 let pressed = self
3760 .panel_mut(slot)
3761 .and_then(|fwp| fwp.scrollbar_mouse.press(state, t.rect, col, row));
3762 if let Some(new_offset) = pressed {
3763 if let Some(fwp) = self.panel_mut(slot) {
3764 fwp.scrollbar_drag_key = Some(t.list_key.clone());
3765 }
3766 self.apply_widget_scroll(panel_id, &t.list_key, new_offset, t.visible);
3767 return true;
3768 }
3769 }
3770 false
3771 }
3772
3773 fn try_widget_scrollbar_drag(&mut self, slot: super::PanelSlot, row: u16) -> bool {
3776 use crate::view::ui::scrollbar::ScrollbarState;
3777 let (panel_id, key) = match self.panel(slot) {
3778 Some(fwp) => match &fwp.scrollbar_drag_key {
3779 Some(k) => (fwp.panel_id, k.clone()),
3780 None => return false,
3781 },
3782 None => return false,
3783 };
3784 let track = self.panel(slot).and_then(|fwp| {
3787 fwp.scrollbar_tracks
3788 .iter()
3789 .find(|t| t.list_key == key)
3790 .cloned()
3791 });
3792 let Some(t) = track else {
3793 return false;
3794 };
3795 let state = ScrollbarState::new(t.total, t.visible, t.scroll);
3796 let new_offset = self
3797 .panel_mut(slot)
3798 .and_then(|fwp| fwp.scrollbar_mouse.drag(state, t.rect, row));
3799 if let Some(off) = new_offset {
3800 self.apply_widget_scroll(panel_id, &key, off, t.visible);
3801 }
3802 true
3803 }
3804
3805 pub(super) fn release_widget_scrollbar(&mut self) {
3807 for fwp in [self.dock.as_mut(), self.floating_widget_panel.as_mut()]
3808 .into_iter()
3809 .flatten()
3810 {
3811 fwp.scrollbar_mouse.release();
3812 fwp.scrollbar_drag_key = None;
3813 }
3814 }
3815
3816 fn apply_widget_scroll(
3822 &mut self,
3823 panel_id: u64,
3824 list_key: &str,
3825 new_offset: usize,
3826 visible: usize,
3827 ) {
3828 let moved_sel = self.widget_registry.set_list_scroll(
3829 panel_id,
3830 list_key,
3831 new_offset as u32,
3832 visible as u32,
3833 );
3834 self.rerender_widget_panel(panel_id);
3835 if let Some(sel) = moved_sel {
3836 if self
3837 .plugin_manager
3838 .read()
3839 .unwrap()
3840 .has_hook_handlers("widget_event")
3841 {
3842 self.plugin_manager.read().unwrap().run_hook(
3843 "widget_event",
3844 crate::services::plugins::hooks::HookArgs::WidgetEvent {
3845 panel_id,
3846 widget_key: list_key.to_string(),
3847 event_type: "select".to_string(),
3848 payload: serde_json::json!({ "index": sel as i64 }),
3849 },
3850 );
3851 }
3852 }
3853 }
3854
3855 fn handle_floating_widget_click(&mut self, slot: super::PanelSlot, col: u16, row: u16) {
3858 if self.try_widget_scrollbar_press(slot, col, row) {
3861 return;
3862 }
3863 let (panel_id, inner) = match self.panel(slot) {
3864 Some(fwp) => match fwp.last_inner_rect {
3865 Some(rect) => (fwp.panel_id, rect),
3866 None => return,
3867 },
3868 None => return,
3869 };
3870 if col < inner.x || col >= inner.x + inner.width {
3871 return;
3872 }
3873 if row < inner.y || row >= inner.y + inner.height {
3874 return;
3875 }
3876 let brow = (row - inner.y) as u32;
3877 let entries = self
3878 .panel(slot)
3879 .map(|f| f.entries.clone())
3880 .unwrap_or_default();
3881 let local_screen_col = (col - inner.x) as usize;
3882 let bcol = match entries.get(brow as usize) {
3883 Some(entry) => screen_col_to_byte(&entry.text, local_screen_col),
3884 None => return,
3885 };
3886 let (hit_payload, hit_event, hit_key, hit_kind) =
3887 match self
3888 .widget_registry
3889 .hit_test(slot.buffer_id(), brow, bcol as u32)
3890 {
3891 Some((_, hit)) => (
3892 hit.payload.clone(),
3893 hit.event_type.to_string(),
3894 hit.widget_key.clone(),
3895 hit.widget_kind,
3896 ),
3897 None => {
3898 tracing::debug!(
3899 target: "fresh::dock",
3900 ?slot, col, row, brow, bcol,
3901 "handle_floating_widget_click: hit_test found no widget"
3902 );
3903 return;
3904 }
3905 };
3906 if !hit_key.is_empty() {
3907 let tabbable = self
3908 .widget_registry
3909 .get(panel_id)
3910 .map(|p| p.tabbable.iter().any(|k| k == &hit_key))
3911 .unwrap_or(false);
3912 tracing::debug!(
3913 target: "fresh::dock",
3914 hit_key = %hit_key,
3915 hit_kind,
3916 hit_event = %hit_event,
3917 tabbable,
3918 "handle_floating_widget_click: hit"
3919 );
3920 if tabbable {
3921 self.set_panel_focus_and_notify(panel_id, hit_key.clone());
3922 }
3923 self.rerender_widget_panel(panel_id);
3924 } else {
3925 tracing::debug!(
3926 target: "fresh::dock",
3927 hit_kind,
3928 hit_event = %hit_event,
3929 "handle_floating_widget_click: hit with empty key (not focusable)"
3930 );
3931 }
3932 let handled_specially = if hit_kind == "tree" && hit_event == "expand" {
3933 if let Some(item_key) = hit_payload.get("key").and_then(|v| v.as_str()) {
3934 self.handle_widget_tree_expand_toggle(panel_id, &hit_key, item_key);
3935 true
3936 } else {
3937 false
3938 }
3939 } else {
3940 false
3941 };
3942 if !handled_specially
3943 && self
3944 .plugin_manager
3945 .read()
3946 .unwrap()
3947 .has_hook_handlers("widget_event")
3948 {
3949 self.plugin_manager.read().unwrap().run_hook(
3950 "widget_event",
3951 crate::services::plugins::hooks::HookArgs::WidgetEvent {
3952 panel_id,
3953 widget_key: hit_key,
3954 event_type: hit_event,
3955 payload: hit_payload,
3956 },
3957 );
3958 }
3959 }
3960
3961 fn clear_active_window_drag_state(&mut self) {
3965 let ms = &mut self.active_window_mut().mouse_state;
3966 ms.dragging_scrollbar = None;
3967 ms.drag_start_row = None;
3968 ms.drag_start_top_byte = None;
3969 ms.dragging_horizontal_scrollbar = None;
3970 ms.drag_start_hcol = None;
3971 ms.drag_start_left_column = None;
3972 ms.dragging_separator = None;
3973 ms.drag_start_position = None;
3974 ms.drag_start_ratio = None;
3975 ms.dragging_file_explorer = false;
3976 ms.drag_start_explorer_width = None;
3977 ms.dragging_text_selection = false;
3978 ms.drag_selection_split = None;
3979 ms.drag_selection_anchor = None;
3980 ms.drag_selection_by_words = false;
3981 ms.drag_selection_word_end = None;
3982 ms.dragging_popup_scrollbar = None;
3983 ms.drag_start_popup_scroll = None;
3984 ms.dragging_prompt_scrollbar = false;
3985 ms.selecting_in_popup = None;
3986 }
3987}
3988
3989fn screen_col_to_byte(text: &str, target_col: usize) -> usize {
3994 use unicode_width::UnicodeWidthChar;
3995 let mut byte = 0;
3996 let mut col = 0usize;
3997 for ch in text.chars() {
3998 let w = UnicodeWidthChar::width(ch).unwrap_or(0);
3999 if col + w > target_col {
4000 return byte;
4001 }
4002 col += w;
4003 byte += ch.len_utf8();
4004 }
4005 byte
4006}