Feat: text selection using mouse
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<AtomicU32>,
|
||||
mouse_down: Arc<AtomicU32>,
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<MouseEvent> {
|
||||
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 {
|
||||
|
||||
@@ -99,6 +99,7 @@ pub struct EditorContext {
|
||||
pub stack_cache: RefCell<Option<StackCache>>,
|
||||
pub target: EditorTarget,
|
||||
pub steps_per_page: Cell<usize>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user