diff --git a/src/terminal/panel.rs b/src/terminal/panel.rs index 898061e..e24d8c6 100644 --- a/src/terminal/panel.rs +++ b/src/terminal/panel.rs @@ -81,9 +81,9 @@ pub struct TerminalPanel { last_cols: u16, last_rows: u16, spawn_error: Option, - // Selection state — viewport cell coordinates at the time of selection. - // Rows are relative to the viewport when selection_display_offset was captured. - selection: Option<(usize, usize, usize, usize)>, // (start_col, start_row, end_col, end_row) + // Selection state — cell coordinates relative to selection_display_offset. + // Rows can be negative (content scrolled above viewport). + selection: Option<(i32, i32, i32, i32)>, // (start_col, start_row, end_col, end_row) selection_display_offset: usize, selecting: bool, scroll_remainder: f32, @@ -409,6 +409,25 @@ impl TerminalPanel { } } + /// Scroll the terminal during active selection, keeping selection coordinates in sync. + fn auto_scroll_selection(&mut self, lines: i32) { + if let Some(pty) = &self.pty { + if let Ok(mut term) = pty.term.lock() { + let old_offset = term.grid().display_offset(); + term.scroll_display(alacritty_terminal::grid::Scroll::Delta(lines)); + let new_offset = term.grid().display_offset(); + let delta = new_offset as i32 - old_offset as i32; + if delta != 0 { + if let Some(ref mut sel) = self.selection { + sel.1 += delta; + sel.3 += delta; + } + self.selection_display_offset = new_offset; + } + } + } + } + pub fn handle_input(&mut self, ctx: &egui::Context) { if !self.focused { return; @@ -489,14 +508,14 @@ impl TerminalPanel { } /// Convert pointer position to terminal cell (col, row), clamped to grid bounds. - fn pos_to_cell(&self, pos: Pos2, body: Rect, ctx: &egui::Context) -> (usize, usize) { + fn pos_to_cell(&self, pos: Pos2, body: Rect, ctx: &egui::Context) -> (i32, i32) { let (cw, ch) = crate::terminal::renderer::cell_size(ctx); let col = ((pos.x - body.min.x - crate::terminal::renderer::PAD_X) / cw) .floor() - .clamp(0.0, (self.last_cols as f32) - 1.0) as usize; + .clamp(0.0, (self.last_cols as f32) - 1.0) as i32; let row = ((pos.y - body.min.y - crate::terminal::renderer::PAD_Y) / ch) .floor() - .clamp(0.0, (self.last_rows as f32) - 1.0) as usize; + .clamp(0.0, (self.last_rows as f32) - 1.0) as i32; (col, row) } @@ -504,7 +523,14 @@ impl TerminalPanel { let (sc, sr, ec, er) = self.selection?; let pty = self.pty.as_ref()?; let term = pty.term.lock().ok()?; - let text = extract_selection_text(&term, sc, sr, ec, er, self.selection_display_offset); + let text = extract_selection_text( + &term, + sc.max(0) as usize, + sr.max(0) as usize, + ec.max(0) as usize, + er.max(0) as usize, + self.selection_display_offset, + ); if text.is_empty() { None } else { @@ -772,31 +798,21 @@ impl TerminalPanel { let (cw, ch) = crate::terminal::renderer::cell_size(ui.ctx()); let pad_x = crate::terminal::renderer::PAD_X; let pad_y = crate::terminal::renderer::PAD_Y; - let max_col = self.last_cols as usize; + let max_col = self.last_cols as i32; let max_row = self.last_rows as i32; let (start_row, start_col, end_row, end_col) = if sr < er || (sr == er && sc <= ec) { - ( - sr, - sc.min(max_col.saturating_sub(1)), - er, - ec.min(max_col.saturating_sub(1)), - ) + (sr, sc.min(max_col - 1), er, ec.min(max_col - 1)) } else { - ( - er, - ec.min(max_col.saturating_sub(1)), - sr, - sc.min(max_col.saturating_sub(1)), - ) + (er, ec.min(max_col - 1), sr, sc.min(max_col - 1)) }; - // Adjust rows for scroll: positive delta = scrolled back = rows move down + // Adjust rows for scroll delta since selection was made let current_offset = scrollbar_state.map(|s| s.display_offset).unwrap_or(0) as i32; let offset_delta = current_offset - self.selection_display_offset as i32; - let adj_start = start_row as i32 + offset_delta; - let adj_end = end_row as i32 + offset_delta; + let adj_start = start_row + offset_delta; + let adj_end = end_row + offset_delta; let screen_content = Rect::from_min_max(transform * content_rect.min, transform * content_rect.max) @@ -809,21 +825,20 @@ impl TerminalPanel { if row_i < 0 || row_i >= max_row { continue; } - let row = row_i as usize; - let orig_row = row_i - offset_delta; // original selection row - let c0 = if orig_row == start_row as i32 { - start_col + let orig_row = row_i - offset_delta; + let c0 = if orig_row == start_row { + start_col.max(0) } else { 0 }; - let c1 = (if orig_row == end_row as i32 { + let c1 = (if orig_row == end_row { end_col + 1 } else { max_col }) .min(max_col); let x0 = content_rect.min.x + pad_x + c0 as f32 * cw; - let y0 = content_rect.min.y + pad_y + row as f32 * ch; + let y0 = content_rect.min.y + pad_y + row_i as f32 * ch; let x1 = content_rect.min.x + pad_x + c1 as f32 * cw; let sel_rect = Rect::from_min_max( transform * Pos2::new(x0, y0), @@ -1078,8 +1093,10 @@ impl TerminalPanel { // Double-click: select word if let Some(pos) = body_resp.interact_pointer_pos() { let (col, row) = self.pos_to_cell(pos, content_rect, ui.ctx()); - if let Some((start, end)) = self.word_boundaries_at(col, row) { - self.selection = Some((start, row, end, row)); + if let Some((start, end)) = + self.word_boundaries_at(col as usize, row as usize) + { + self.selection = Some((start as i32, row, end as i32, row)); self.selection_display_offset = scrollbar_state.map(|s| s.display_offset).unwrap_or(0); } @@ -1089,7 +1106,7 @@ impl TerminalPanel { // Triple-click: select entire line if let Some(pos) = body_resp.interact_pointer_pos() { let (_, row) = self.pos_to_cell(pos, content_rect, ui.ctx()); - let last_col = (self.last_cols as usize).saturating_sub(1); + let last_col = (self.last_cols as i32) - 1; self.selection = Some((0, row, last_col, row)); self.selection_display_offset = scrollbar_state.map(|s| s.display_offset).unwrap_or(0); @@ -1112,21 +1129,59 @@ impl TerminalPanel { self.selecting = true; } } + if local_interactions_enabled && self.selecting && body_resp.dragged_by(egui::PointerButton::Primary) { - if let Some(pos) = body_resp.hover_pos() { - let (col, row) = self.pos_to_cell(pos, content_rect, ui.ctx()); - if let Some(ref mut sel) = self.selection { - sel.2 = col; - sel.3 = row; + // Check for auto-scroll when pointer is above or below terminal + let mut auto_scrolled = false; + if let Some(screen_pos) = ui.ctx().input(|i| i.pointer.latest_pos()) { + let canvas_pos = transform.inverse() * screen_pos; + let (_, row_height) = crate::terminal::renderer::cell_size(ui.ctx()); + + if canvas_pos.y < content_rect.min.y { + // Pointer above terminal: auto-scroll up (towards history) + let distance = content_rect.min.y - canvas_pos.y; + let lines = ((distance / (row_height * 3.0)).ceil() as i32).max(1); + self.auto_scroll_selection(lines); + if let Some(ref mut sel) = self.selection { + sel.2 = 0; + sel.3 = 0; + } + auto_scrolled = true; + ui.ctx().request_repaint(); + } else if canvas_pos.y > content_rect.max.y { + // Pointer below terminal: auto-scroll down (towards recent) + let distance = canvas_pos.y - content_rect.max.y; + let lines = -(((distance / (row_height * 3.0)).ceil() as i32).max(1)); + self.auto_scroll_selection(lines); + if let Some(ref mut sel) = self.selection { + sel.2 = (self.last_cols as i32) - 1; + sel.3 = (self.last_rows as i32) - 1; + } + auto_scrolled = true; + ui.ctx().request_repaint(); } - } else if let Some(pos) = body_resp.interact_pointer_pos() { - let (col, row) = self.pos_to_cell(pos, content_rect, ui.ctx()); - if let Some(ref mut sel) = self.selection { - sel.2 = col; - sel.3 = row; + } + + if !auto_scrolled { + // Convert viewport row to original selection offset coordinate + let current_offset = scrollbar_state.map(|s| s.display_offset).unwrap_or(0) as i32; + let offset_delta = current_offset - self.selection_display_offset as i32; + + if let Some(pos) = body_resp.hover_pos() { + let (col, row) = self.pos_to_cell(pos, content_rect, ui.ctx()); + if let Some(ref mut sel) = self.selection { + sel.2 = col; + sel.3 = row - offset_delta; + } + } else if let Some(pos) = body_resp.interact_pointer_pos() { + let (col, row) = self.pos_to_cell(pos, content_rect, ui.ctx()); + if let Some(ref mut sel) = self.selection { + sel.2 = col; + sel.3 = row - offset_delta; + } } } } @@ -1143,7 +1198,9 @@ impl TerminalPanel { + if mods.ctrl { 16 } else { 0 }; // Helper: send SGR mouse event - let send_mouse = |btn: u8, col: usize, row: usize, press: bool| { + let send_mouse = |btn: u8, col: i32, row: i32, press: bool| { + let col = col.max(0) as usize; + let row = row.max(0) as usize; let suffix = if press { 'M' } else { 'm' }; let seq = format!("\x1b[<{};{};{}{}", btn + mod_bits, col + 1, row + 1, suffix); pty.write(seq.as_bytes()); @@ -1257,8 +1314,8 @@ impl TerminalPanel { ui.close_menu(); } if ui.button("Select All").clicked() { - let last_col = (self.last_cols as usize).saturating_sub(1); - let last_row = (self.last_rows as usize).saturating_sub(1); + let last_col = (self.last_cols as i32) - 1; + let last_row = (self.last_rows as i32) - 1; self.selection = Some((0, 0, last_col, last_row)); self.selection_display_offset = scrollbar_state.map(|s| s.display_offset).unwrap_or(0);