diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index 8e042e2..64d71de 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -99,6 +99,14 @@ impl Editor { self.text.is_selecting() } + pub fn move_cursor_to(&mut self, row: u16, col: u16) { + self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col)); + } + + pub fn scroll_offset(&self) -> u16 { + self.scroll_offset.get() + } + pub fn copy(&mut self) { self.text.copy(); } diff --git a/src/bin/desktop/main.rs b/src/bin/desktop/main.rs index d58005a..ae1ea27 100644 --- a/src/bin/desktop/main.rs +++ b/src/bin/desktop/main.rs @@ -22,7 +22,7 @@ use cagire::engine::{ }; use cagire::init::{init, InitArgs}; use cagire::input::{handle_key, handle_mouse, InputContext, InputResult}; -use cagire::input_egui::{convert_egui_events, convert_egui_mouse}; +use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState}; use cagire::settings::Settings; use cagire::views; use crossbeam_channel::Receiver; @@ -168,6 +168,7 @@ struct CagireDesktop { mouse_y: Arc, mouse_down: Arc, last_frame: std::time::Instant, + egui_mouse: EguiMouseState, } impl CagireDesktop { @@ -212,6 +213,7 @@ impl CagireDesktop { mouse_y: b.mouse_y, mouse_down: b.mouse_down, last_frame: std::time::Instant::now(), + egui_mouse: EguiMouseState::default(), } } @@ -322,7 +324,7 @@ impl CagireDesktop { let term = self.terminal.get_frame().area(); let widget_rect = ctx.content_rect(); - for mouse in convert_egui_mouse(ctx, widget_rect, term) { + for mouse in convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse) { let mut input_ctx = InputContext { app: &mut self.app, link: &self.link, diff --git a/src/input/mouse.rs b/src/input/mouse.rs index 7094ff7..903dbef 100644 --- a/src/input/mouse.rs +++ b/src/input/mouse.rs @@ -26,6 +26,12 @@ pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) { match kind { MouseEventKind::Down(MouseButton::Left) => handle_click(ctx, col, row, term), + MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Moved => { + handle_editor_drag(ctx, col, row, term); + } + MouseEventKind::Up(MouseButton::Left) => { + ctx.app.editor_ctx.mouse_selecting = false; + } MouseEventKind::ScrollUp => handle_scroll(ctx, col, row, term, true), MouseEventKind::ScrollDown => handle_scroll(ctx, col, row, term, false), _ => {} @@ -59,6 +65,55 @@ fn contains(area: Rect, col: u16, row: u16) -> bool { col >= area.x && col < area.x + area.width && row >= area.y && row < area.y + area.height } +fn handle_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: Rect) { + if ctx.app.editor_ctx.mouse_selecting { + handle_editor_mouse(ctx, col, row, term, true); + } +} + +fn handle_editor_mouse(ctx: &mut InputContext, col: u16, row: u16, term: Rect, dragging: bool) { + // Reconstruct editor area (mirrors render_modal_editor / ModalFrame::render_centered) + let width = (term.width * 80 / 100).max(40); + let height = (term.height * 60 / 100).max(10); + let modal_w = width.min(term.width.saturating_sub(4)); + let modal_h = height.min(term.height.saturating_sub(4)); + let mx = term.x + (term.width.saturating_sub(modal_w)) / 2; + let my = term.y + (term.height.saturating_sub(modal_h)) / 2; + // inner = area inside 1-cell border + let inner_x = mx + 1; + let inner_y = my + 1; + let inner_w = modal_w.saturating_sub(2); + let inner_h = modal_h.saturating_sub(2); + + let show_search = ctx.app.editor_ctx.editor.search_active() + || !ctx.app.editor_ctx.editor.search_query().is_empty(); + let reserved = 1 + if show_search { 1 } else { 0 }; + let editor_y = inner_y + if show_search { 1 } else { 0 }; + let editor_h = inner_h.saturating_sub(reserved); + + if col < inner_x || col >= inner_x + inner_w || row < editor_y || row >= editor_y + editor_h { + return; + } + + let scroll = ctx.app.editor_ctx.editor.scroll_offset(); + let text_row = (row - editor_y) + scroll; + let text_col = col - inner_x; + + if dragging { + if !ctx.app.editor_ctx.editor.is_selecting() { + ctx.app.editor_ctx.editor.start_selection(); + } + } else { + ctx.app.editor_ctx.mouse_selecting = true; + ctx.app.editor_ctx.editor.cancel_selection(); + } + + ctx.app + .editor_ctx + .editor + .move_cursor_to(text_row, text_col); +} + fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) { // Sticky minimap intercepts all clicks if matches!(ctx.app.ui.minimap, MinimapMode::Sticky) { @@ -745,8 +800,11 @@ fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) { fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) { match &ctx.app.ui.modal { - Modal::Editor | Modal::Preview => { - // Don't dismiss editor/preview on click + Modal::Editor => { + handle_editor_mouse(ctx, col, row, term, false); + } + Modal::Preview => { + // Don't dismiss preview on click } Modal::Confirm { .. } => { handle_confirm_click(ctx, col, row, term); diff --git a/src/input_egui.rs b/src/input_egui.rs index e208773..05b6bbe 100644 --- a/src/input_egui.rs +++ b/src/input_egui.rs @@ -1,13 +1,25 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use ratatui::layout::Rect; +#[derive(Default)] +pub struct EguiMouseState { + prev_down: bool, + prev_col: u16, + prev_row: u16, +} + pub fn convert_egui_mouse( ctx: &egui::Context, widget_rect: egui::Rect, term: Rect, + state: &mut EguiMouseState, ) -> Vec { let mut events = Vec::new(); - if widget_rect.width() < 1.0 || widget_rect.height() < 1.0 || term.width == 0 || term.height == 0 { + if widget_rect.width() < 1.0 + || widget_rect.height() < 1.0 + || term.width == 0 + || term.height == 0 + { return events; } @@ -16,6 +28,15 @@ pub fn convert_egui_mouse( return; }; if !widget_rect.contains(pos) { + if state.prev_down && !i.pointer.primary_down() { + events.push(MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + column: state.prev_col, + row: state.prev_row, + modifiers: KeyModifiers::empty(), + }); + state.prev_down = false; + } return; } @@ -26,15 +47,43 @@ pub fn convert_egui_mouse( let col = col.min(term.width.saturating_sub(1)); let row = row.min(term.height.saturating_sub(1)); - if i.pointer.button_clicked(egui::PointerButton::Primary) { + let is_down = i.pointer.primary_down(); + let moved = col != state.prev_col || row != state.prev_row; + + if !state.prev_down && is_down { events.push(MouseEvent { kind: MouseEventKind::Down(MouseButton::Left), column: col, row, modifiers: KeyModifiers::empty(), }); + } else if state.prev_down && is_down && moved { + events.push(MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + column: col, + row, + modifiers: KeyModifiers::empty(), + }); + } else if state.prev_down && !is_down { + events.push(MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + column: col, + row, + modifiers: KeyModifiers::empty(), + }); + } else if !is_down && moved { + events.push(MouseEvent { + kind: MouseEventKind::Moved, + column: col, + row, + modifiers: KeyModifiers::empty(), + }); } + state.prev_down = is_down; + state.prev_col = col; + state.prev_row = row; + let scroll = i.raw_scroll_delta.y; if scroll > 1.0 { events.push(MouseEvent { diff --git a/src/state/editor.rs b/src/state/editor.rs index 60cd499..6f6f5e3 100644 --- a/src/state/editor.rs +++ b/src/state/editor.rs @@ -99,6 +99,7 @@ pub struct EditorContext { pub stack_cache: RefCell>, pub target: EditorTarget, pub steps_per_page: Cell, + pub mouse_selecting: bool, } #[derive(Clone)] @@ -150,6 +151,7 @@ impl Default for EditorContext { stack_cache: RefCell::new(None), target: EditorTarget::default(), steps_per_page: Cell::new(32), + mouse_selecting: false, } } }