diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs index 2ed5898b..128aa43c 100644 --- a/tui/src/confirmation.rs +++ b/tui/src/confirmation.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use crate::{float::FloatContent, hint::Shortcut}; use ratatui::{ - crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, + crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}, layout::Alignment, prelude::*, widgets::{Block, Borders, Clear, List}, @@ -87,15 +87,24 @@ impl FloatContent for ConfirmPrompt { fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool { match event.kind { + MouseEventKind::Down(MouseButton::Left) => { + self.status = ConfirmStatus::Confirm; + true + } + MouseEventKind::Down(MouseButton::Right) => { + self.status = ConfirmStatus::Abort; + false + } MouseEventKind::ScrollDown => { self.scroll_down(); + false } MouseEventKind::ScrollUp => { self.scroll_up(); + false } - _ => {} + _ => false, } - false } fn handle_key_event(&mut self, key: &KeyEvent) -> bool { diff --git a/tui/src/float.rs b/tui/src/float.rs index 4d6ac006..afe32603 100644 --- a/tui/src/float.rs +++ b/tui/src/float.rs @@ -4,7 +4,7 @@ use ratatui::{ Frame, }; -use crate::hint::Shortcut; +use crate::{event::MouseButton, event::MouseEventKind, hint::Shortcut}; pub trait FloatContent { fn draw(&mut self, frame: &mut Frame, area: Rect); @@ -54,8 +54,14 @@ impl Float { self.content.draw(frame, popup_area); } - pub fn handle_mouse_event(&mut self, event: &MouseEvent) { - self.content.handle_mouse_event(event); + pub fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool { + match event.kind { + MouseEventKind::Down(MouseButton::Right) => { + self.content.handle_mouse_event(event); + true + } + _ => self.content.handle_mouse_event(event), + } } // Returns true if the floating window is finished. diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index 6a2546cc..80274bfb 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -9,7 +9,7 @@ use crate::{float::FloatContent, hint::Shortcut}; use linutil_core::Command; use ratatui::{ - crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, + crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}, layout::Rect, style::{Style, Stylize}, text::Line, @@ -33,6 +33,8 @@ pub struct FloatingText { mode_title: String, wrap_words: bool, frame_height: usize, + drag_start_y: Option, + drag_start_scroll: Option, } macro_rules! style { @@ -141,6 +143,8 @@ impl FloatingText { h_scroll: 0, wrap_words, frame_height: 0, + drag_start_y: None, + drag_start_scroll: None, } } @@ -165,6 +169,8 @@ impl FloatingText { v_scroll: 0, wrap_words: false, frame_height: 0, + drag_start_y: None, + drag_start_scroll: None, }) } @@ -206,6 +212,19 @@ impl FloatingText { }; } } + + fn handle_drag(&mut self, current_y: u16) { + if let (Some(start_y), Some(start_scroll)) = (self.drag_start_y, self.drag_start_scroll) { + let delta = start_y as i32 - current_y as i32; + let new_scroll = start_scroll as i32 + delta; + + let max_scroll = self + .wrapped_lines + .len() + .saturating_sub(self.frame_height.saturating_sub(2)); + self.v_scroll = new_scroll.clamp(0, max_scroll as i32) as usize; + } + } } impl FloatContent for FloatingText { @@ -285,6 +304,17 @@ impl FloatContent for FloatingText { fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool { match event.kind { + MouseEventKind::Down(MouseButton::Left) => { + self.drag_start_y = Some(event.row); + self.drag_start_scroll = Some(self.v_scroll); + } + MouseEventKind::Up(MouseButton::Left) => { + self.drag_start_y = None; + self.drag_start_scroll = None; + } + MouseEventKind::Drag(MouseButton::Left) => { + self.handle_drag(event.row); + } MouseEventKind::ScrollDown => self.scroll_down(), MouseEventKind::ScrollUp => self.scroll_up(), MouseEventKind::ScrollLeft => self.scroll_left(), diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index c3b3d3d4..a22c20f3 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -114,7 +114,7 @@ impl FloatContent for RunningCommand { } _ => {} } - true + false } /// Handle key events of the running command "window". Returns true when the "window" should be /// closed diff --git a/tui/src/state.rs b/tui/src/state.rs index 7f96aee9..0a53f4c4 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -13,7 +13,9 @@ use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList}; #[cfg(feature = "tips")] use rand::Rng; use ratatui::{ - crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind}, + crossterm::event::{ + KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, + }, layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect}, style::{Style, Stylize}, text::{Line, Span, Text}, @@ -481,7 +483,7 @@ impl AppState { match &mut self.focus { Focus::FloatingWindow(float) => float.draw(frame, chunks[1]), - Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1]), + Focus::ConfirmationPrompt(confirm) => confirm.draw(frame, chunks[1]), _ => {} } @@ -493,51 +495,138 @@ impl AppState { return true; } - if matches!(self.focus, Focus::TabList | Focus::List) { + match &mut self.focus { + Focus::FloatingWindow(float) => { + if float.handle_mouse_event(event) { + self.focus = Focus::List; + } + return true; + } + Focus::ConfirmationPrompt(confirm) => { + if confirm.handle_mouse_event(event) { + match confirm.content.status { + ConfirmStatus::Abort => { + self.focus = Focus::List; + if !self.multi_select { + self.selected_commands.clear() + } else if let Some(node) = self.get_selected_node() { + if !node.multi_select { + self.selected_commands.retain(|cmd| cmd.name != node.name); + } + } + } + ConfirmStatus::Confirm => self.handle_confirm_command(), + ConfirmStatus::None => {} + } + } + return true; + } + _ => {} + } + + if matches!(self.focus, Focus::TabList | Focus::List | Focus::Search) { let position = Position::new(event.column, event.row); let mouse_in_tab_list = self.areas.as_ref().unwrap().tab_list.contains(position); let mouse_in_list = self.areas.as_ref().unwrap().list.contains(position); + let mouse_in_search = if let Some(areas) = &self.areas { + position.y >= areas.list.y + && position.y < areas.list.y + 3 + && position.x >= areas.list.x + && position.x < areas.list.x + areas.list.width + } else { + false + }; + match event.kind { MouseEventKind::Moved => { - if mouse_in_list { - self.focus = Focus::List + if mouse_in_search { + if !matches!(self.focus, Focus::Search) { + self.focus = Focus::Search; + self.filter.activate_search(); + } + } else if mouse_in_list { + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } + self.focus = Focus::List; + if let Some(areas) = &self.areas { + let list_start = areas.list.y + 4; + let relative_y = position.y.saturating_sub(list_start); + let list_len = self.filter.item_list().len(); + let adjusted_len = if self.at_root() { + list_len + } else { + list_len + 1 + }; + if relative_y < adjusted_len as u16 { + self.selection.select(Some(relative_y as usize)); + } + } } else if mouse_in_tab_list { - self.focus = Focus::TabList + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } + self.focus = Focus::TabList; + if let Some(areas) = &self.areas { + let relative_y = position.y.saturating_sub(areas.tab_list.y + 1); + if relative_y < self.tabs.len() as u16 { + self.current_tab.select(Some(relative_y as usize)); + self.refresh_tab(); + } + } } } - MouseEventKind::ScrollDown => { + MouseEventKind::Down(button) => match button { + MouseButton::Left => { + if mouse_in_search { + self.enter_search(); + } else if mouse_in_list { + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } + self.handle_enter(); + } else if mouse_in_tab_list { + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } + self.focus = Focus::TabList; + } + } + MouseButton::Right if mouse_in_list => { + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } + self.enable_preview(); + } + MouseButton::Middle if mouse_in_list => { + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } + self.enable_description(); + } + _ => {} + }, + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { + if matches!(self.focus, Focus::Search) { + self.exit_search(); + } if mouse_in_tab_list { if self.current_tab.selected().unwrap() != self.tabs.len() - 1 { self.current_tab.select_next(); } self.refresh_tab(); } else if mouse_in_list { - self.selection.select_next() - } - } - MouseEventKind::ScrollUp => { - if mouse_in_tab_list { - if self.current_tab.selected().unwrap() != 0 { - self.current_tab.select_previous(); + if event.kind == MouseEventKind::ScrollDown { + self.scroll_down(); + } else { + self.scroll_up(); } - self.refresh_tab(); - } else if mouse_in_list { - self.selection.select_previous() } } _ => {} } } - match &mut self.focus { - Focus::FloatingWindow(float) => { - float.content.handle_mouse_event(event); - } - Focus::ConfirmationPrompt(confirm) => { - confirm.content.handle_mouse_event(event); - } - _ => {} - } true }