fix: autoscroll and clear mouse text selections

This commit is contained in:
Ogulcan Celik 2026-04-09 17:51:28 +03:00
parent be271f8dbe
commit 953ff58e9f
5 changed files with 468 additions and 96 deletions

View File

@ -357,33 +357,45 @@ impl AppState {
}
pub fn copy_selection(&mut self) {
let sel = match self.selection.as_mut() {
Some(s) => {
if !s.finish() {
let text = {
let sel = match self.selection.as_mut() {
Some(s) => {
if !s.finish() {
self.selection = None;
return;
}
s
}
None => return,
};
let ws = match self.active.and_then(|i| self.workspaces.get(i)) {
Some(ws) => ws,
None => {
self.selection = None;
return;
}
s
}
None => return,
};
let rt = match ws.runtime(sel.pane_id) {
Some(r) => r,
None => {
self.selection = None;
return;
}
};
rt.extract_selection(sel)
};
let ws = match self.active.and_then(|i| self.workspaces.get(i)) {
Some(ws) => ws,
None => return,
};
let rt = match ws.runtime(sel.pane_id) {
Some(r) => r,
None => return,
};
if let Some(text) = rt.extract_selection(sel) {
if let Some(text) = text {
if !text.is_empty() {
crate::selection::write_osc52(&text);
info!(len = text.len(), "copied selection to clipboard");
}
}
self.selection = None;
}
}

View File

@ -2164,7 +2164,12 @@ impl AppState {
mouse.row - info.inner_rect.y,
mouse.column - info.inner_rect.x,
);
self.selection = Some(Selection::anchor(info.id, row, col, info.inner_rect));
self.selection = Some(Selection::anchor(
info.id,
row,
col,
self.pane_scroll_metrics(info.id),
));
self.focus_pane(info.id);
if self.mode != Mode::Terminal {
@ -2185,6 +2190,11 @@ impl AppState {
}
MouseEventKind::Drag(MouseButton::Left) => {
if self.selection.is_some() {
self.update_selection_drag(mouse.column, mouse.row);
return None;
}
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
if self.forward_pane_mouse_button(&info, mouse) {
self.selection = None;
@ -2273,12 +2283,22 @@ impl AppState {
DragTarget::ReleaseNotesScrollbar { .. }
| DragTarget::KeybindHelpScrollbar { .. } => {}
}
} else if let Some(sel) = &mut self.selection {
sel.drag(mouse.column, mouse.row);
}
}
MouseEventKind::Up(MouseButton::Left) => {
if self.selection.is_some() {
self.workspace_press = None;
self.drag = None;
let was_click = self.selection.as_ref().is_some_and(|s| s.was_just_click());
if was_click {
self.selection = None;
} else {
self.copy_selection();
}
return None;
}
if let Some(info) = self.pane_mouse_target(mouse.column, mouse.row).cloned() {
if self.forward_pane_mouse_button(&info, mouse) {
self.selection = None;
@ -2328,8 +2348,10 @@ impl AppState {
}
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown if !in_sidebar => {
self.selection = None;
self.handle_terminal_wheel(mouse);
if !self.scroll_selection_with_wheel(mouse) {
self.selection = None;
self.handle_terminal_wheel(mouse);
}
}
MouseEventKind::ScrollUp if in_sidebar => {
@ -2769,6 +2791,10 @@ impl AppState {
.or_else(|| self.pane_frame_at(col, row))
}
fn pane_info_by_id(&self, pane_id: crate::layout::PaneId) -> Option<&PaneInfo> {
self.view.pane_infos.iter().find(|info| info.id == pane_id)
}
fn pane_frame_at(&self, col: u16, row: u16) -> Option<&PaneInfo> {
self.view.pane_infos.iter().find(|p| {
col >= p.rect.x
@ -2803,6 +2829,79 @@ impl AppState {
}
}
fn pane_scroll_metrics(
&self,
pane_id: crate::layout::PaneId,
) -> Option<crate::pane::ScrollMetrics> {
self.active
.and_then(|i| self.workspaces.get(i))
.and_then(|ws| ws.runtime(pane_id))
.and_then(crate::pane::PaneRuntime::scroll_metrics)
}
fn update_selection_cursor(
&mut self,
pane_id: crate::layout::PaneId,
screen_col: u16,
screen_row: u16,
) {
let Some(info) = self.pane_info_by_id(pane_id).cloned() else {
return;
};
let metrics = self.pane_scroll_metrics(pane_id);
if let Some(selection) = self.selection.as_mut() {
selection.drag(screen_col, screen_row, info.inner_rect, metrics);
}
}
fn selection_edge_scroll_lines(distance: u16) -> usize {
usize::from(distance).saturating_mul(3).clamp(3, 15)
}
fn update_selection_drag(&mut self, screen_col: u16, screen_row: u16) {
let Some(pane_id) = self.selection.as_ref().map(|selection| selection.pane_id) else {
return;
};
let Some(info) = self.pane_info_by_id(pane_id).cloned() else {
return;
};
let bottom = info.inner_rect.y + info.inner_rect.height.saturating_sub(1);
if screen_row < info.inner_rect.y {
self.scroll_pane_up(
pane_id,
Self::selection_edge_scroll_lines(info.inner_rect.y - screen_row),
);
} else if screen_row > bottom {
self.scroll_pane_down(
pane_id,
Self::selection_edge_scroll_lines(screen_row - bottom),
);
}
self.update_selection_cursor(pane_id, screen_col, screen_row);
}
fn scroll_selection_with_wheel(&mut self, mouse: MouseEvent) -> bool {
const LINES_PER_NOTCH: usize = 3;
let Some(selection) = self.selection.as_ref() else {
return false;
};
if !selection.is_in_progress() {
return false;
}
let pane_id = selection.pane_id;
self.focus_pane(pane_id);
match mouse.kind {
MouseEventKind::ScrollUp => self.scroll_pane_up(pane_id, LINES_PER_NOTCH),
MouseEventKind::ScrollDown => self.scroll_pane_down(pane_id, LINES_PER_NOTCH),
_ => return false,
}
self.update_selection_cursor(pane_id, mouse.column, mouse.row);
true
}
fn handle_terminal_wheel(&mut self, mouse: MouseEvent) {
const LINES_PER_NOTCH: usize = 3;
@ -3051,6 +3150,13 @@ mod tests {
}
}
fn numbered_lines_bytes(count: usize) -> Vec<u8> {
(0..count)
.map(|i| format!("{i:06}\r\n"))
.collect::<String>()
.into_bytes()
}
fn capture_snapshot(state: &AppState) -> crate::persist::SessionSnapshot {
crate::persist::capture(
&state.workspaces,
@ -4060,6 +4166,166 @@ mod tests {
assert!(!app.state.sidebar_collapsed);
}
#[tokio::test]
async fn dragging_selection_above_pane_autoscrolls_and_extends_into_scrollback() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let pane_id = ws.tabs[0].root_pane;
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
let info = pane_infos[0].clone();
ws.tabs[0].runtimes.insert(
pane_id,
crate::pane::PaneRuntime::test_with_scrollback_bytes(
info.inner_rect.width,
info.inner_rect.height,
16 * 1024,
&numbered_lines_bytes(64),
),
);
app.state.workspaces = vec![ws];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
let start_metrics = app.state.workspaces[0]
.runtime(pane_id)
.and_then(crate::pane::PaneRuntime::scroll_metrics)
.expect("initial scroll metrics");
let start_row = info.inner_rect.y;
let start_col = info.inner_rect.x + 2;
app.handle_mouse(mouse(
MouseEventKind::Down(MouseButton::Left),
start_col,
start_row,
));
app.handle_mouse(mouse(
MouseEventKind::Drag(MouseButton::Left),
start_col,
info.inner_rect.y.saturating_sub(1),
));
let end_metrics = app.state.workspaces[0]
.runtime(pane_id)
.and_then(crate::pane::PaneRuntime::scroll_metrics)
.expect("scroll metrics after drag");
assert_eq!(
end_metrics.offset_from_bottom,
start_metrics.offset_from_bottom + 3
);
let selection = app.state.selection.as_ref().expect("selection after drag");
assert!(selection.is_visible());
assert_eq!(
selection.ordered_cells(),
(
(
(start_metrics.max_offset_from_bottom - end_metrics.offset_from_bottom) as u32,
2,
),
(start_metrics.max_offset_from_bottom as u32, 2),
)
);
}
#[tokio::test]
async fn releasing_dragged_selection_clears_highlight_after_copy() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let pane_id = ws.tabs[0].root_pane;
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
let info = pane_infos[0].clone();
ws.tabs[0].runtimes.insert(
pane_id,
crate::pane::PaneRuntime::test_with_scrollback_bytes(
info.inner_rect.width,
info.inner_rect.height,
16 * 1024,
&numbered_lines_bytes(64),
),
);
app.state.workspaces = vec![ws];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
let row = info.inner_rect.y;
let start_col = info.inner_rect.x + 1;
let end_col = info.inner_rect.x + 4;
app.handle_mouse(mouse(
MouseEventKind::Down(MouseButton::Left),
start_col,
row,
));
app.handle_mouse(mouse(MouseEventKind::Drag(MouseButton::Left), end_col, row));
assert!(app.state.selection.is_some());
app.handle_mouse(mouse(MouseEventKind::Up(MouseButton::Left), end_col, row));
assert!(app.state.selection.is_none());
}
#[tokio::test]
async fn wheel_scroll_keeps_in_progress_selection_and_extends_it() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let pane_id = ws.tabs[0].root_pane;
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
let info = pane_infos[0].clone();
ws.tabs[0].runtimes.insert(
pane_id,
crate::pane::PaneRuntime::test_with_scrollback_bytes(
info.inner_rect.width,
info.inner_rect.height,
16 * 1024,
&numbered_lines_bytes(64),
),
);
app.state.workspaces = vec![ws];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
let start_metrics = app.state.workspaces[0]
.runtime(pane_id)
.and_then(crate::pane::PaneRuntime::scroll_metrics)
.expect("initial scroll metrics");
let top_row = info.inner_rect.y;
let col = info.inner_rect.x + 2;
app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), col, top_row));
app.handle_mouse(mouse(MouseEventKind::ScrollUp, col, top_row));
let end_metrics = app.state.workspaces[0]
.runtime(pane_id)
.and_then(crate::pane::PaneRuntime::scroll_metrics)
.expect("scroll metrics after wheel");
assert_eq!(
end_metrics.offset_from_bottom,
start_metrics.offset_from_bottom + 3
);
let selection = app.state.selection.as_ref().expect("selection after wheel");
assert!(selection.is_visible());
assert_eq!(
selection.ordered_cells(),
(
(
(start_metrics.max_offset_from_bottom - end_metrics.offset_from_bottom) as u32,
2,
),
(start_metrics.max_offset_from_bottom as u32, 2),
)
);
}
#[test]
fn clicking_workspace_switches_on_mouse_up() {
let mut app = app_for_mouse_test();

View File

@ -1112,11 +1112,8 @@ fn ghostty_extract_selection(
selection: &crate::selection::Selection,
) -> Result<String, crate::ghostty::Error> {
let ((start_row, start_col), (end_row, end_col)) = selection.ordered_cells();
core.terminal.read_text_viewport(
(start_col, u32::from(start_row)),
(end_col, u32::from(end_row)),
false,
)
core.terminal
.read_text_screen((start_col, start_row), (end_col, end_row), false)
}
fn ghostty_screen_row(
@ -1868,9 +1865,19 @@ impl PaneRuntime {
#[cfg(test)]
impl PaneRuntime {
pub(crate) fn test_with_screen_bytes(cols: u16, rows: u16, bytes: &[u8]) -> Self {
Self::test_with_scrollback_bytes(cols, rows, 0, bytes)
}
pub(crate) fn test_with_scrollback_bytes(
cols: u16,
rows: u16,
scrollback_limit_bytes: usize,
bytes: &[u8],
) -> Self {
let (tx, _rx) = mpsc::channel(4);
let (resize_tx, _resize_rx) = mpsc::channel(1);
let mut terminal = crate::ghostty::Terminal::new(cols, rows, 0).unwrap();
let mut terminal =
crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes).unwrap();
terminal.write(bytes);
Self {
@ -2166,6 +2173,29 @@ mod tests {
assert_eq!(pane.detection_text(), bottom_snapshot);
}
#[test]
fn extract_selection_reads_screen_rows_not_current_viewport() {
let (tx, _rx) = mpsc::channel(4);
let mut terminal = crate::ghostty::Terminal::new(8, 3, 1024).unwrap();
write_numbered_lines(&mut terminal, 8);
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
pane.set_scroll_offset_from_bottom(3);
let metrics = pane
.scroll_metrics()
.expect("scroll metrics after initial scroll");
let mut selection =
crate::selection::Selection::anchor(PaneId::from_raw(1), 0, 0, Some(metrics));
selection.drag(5, 2, Rect::new(0, 0, 8, 3), Some(metrics));
pane.scroll_reset();
let text = pane
.extract_selection(&selection)
.expect("selection should extract text");
assert_eq!(text, "000003\n000004\n000005");
}
#[test]
fn recent_unwrapped_text_ignores_soft_wraps() {
let (tx, _rx) = mpsc::channel(4);

View File

@ -7,13 +7,13 @@
//! MouseUp → Text extracted, copied via OSC 52, highlight stays
//! Next click / key → Selection cleared
//!
//! Coordinates are stored relative to the pane's inner area (the region
//! where terminal content is rendered, excluding borders).
//! Rows are stored in screen-buffer coordinates instead of viewport-relative
//! coordinates. That keeps selection stable while the pane scrolls.
use ratatui::layout::Rect;
use std::io::Write;
use crate::layout::PaneId;
use crate::{layout::PaneId, pane::ScrollMetrics};
/// Current phase of a selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -33,36 +33,44 @@ enum Phase {
pub struct Selection {
/// Which pane the selection belongs to.
pub pane_id: PaneId,
/// Anchor position in pane-relative coordinates (row, col).
anchor: (u16, u16),
/// Current/final position in pane-relative coordinates (row, col).
cursor: (u16, u16),
/// Anchor position in screen-buffer coordinates (row, col).
anchor: (u32, u16),
/// Current/final position in screen-buffer coordinates (row, col).
cursor: (u32, u16),
/// Selection phase.
phase: Phase,
/// The inner rect of the pane (for clamping during drag).
/// This is the content area, excluding borders.
pane_inner: Rect,
}
impl Selection {
/// Start a potential selection. This records the anchor but doesn't
/// make anything visible yet — the user might just be clicking.
pub fn anchor(pane_id: PaneId, row: u16, col: u16, pane_inner: Rect) -> Self {
pub fn anchor(
pane_id: PaneId,
viewport_row: u16,
col: u16,
metrics: Option<ScrollMetrics>,
) -> Self {
let anchor = (absolute_row_for_viewport_row(viewport_row, metrics), col);
Self {
pane_id,
anchor: (row, col),
cursor: (row, col),
anchor,
cursor: anchor,
phase: Phase::Anchored,
pane_inner,
}
}
/// Extend the selection as the mouse drags. Activates highlighting
/// once the cursor moves to a different cell than the anchor.
/// Screen coordinates are clamped to the pane boundary.
pub fn drag(&mut self, screen_col: u16, screen_row: u16) {
let (row, col) = self.clamp_to_pane(screen_col, screen_row);
self.cursor = (row, col);
pub fn drag(
&mut self,
screen_col: u16,
screen_row: u16,
pane_inner: Rect,
metrics: Option<ScrollMetrics>,
) {
let (viewport_row, col) = clamp_to_pane(screen_col, screen_row, pane_inner);
self.cursor = (absolute_row_for_viewport_row(viewport_row, metrics), col);
if self.cursor != self.anchor {
self.phase = Phase::Dragging;
}
@ -89,8 +97,13 @@ impl Selection {
self.phase == Phase::Anchored
}
/// Whether the pointer is still down and the selection can keep extending.
pub fn is_in_progress(&self) -> bool {
matches!(self.phase, Phase::Anchored | Phase::Dragging)
}
/// Returns (start, end) in reading order (top-left to bottom-right).
fn ordered(&self) -> ((u16, u16), (u16, u16)) {
fn ordered(&self) -> ((u32, u16), (u32, u16)) {
let (ar, ac) = self.anchor;
let (cr, cc) = self.cursor;
if ar < cr || (ar == cr && ac <= cc) {
@ -100,42 +113,56 @@ impl Selection {
}
}
pub(crate) fn ordered_cells(&self) -> ((u16, u16), (u16, u16)) {
pub(crate) fn ordered_cells(&self) -> ((u32, u16), (u32, u16)) {
self.ordered()
}
/// Check whether a pane-relative cell (row, col) is inside the selection.
pub fn contains(&self, row: u16, col: u16) -> bool {
pub fn contains(&self, viewport_row: u16, col: u16, metrics: Option<ScrollMetrics>) -> bool {
if !self.is_visible() {
return false;
}
let row = absolute_row_for_viewport_row(viewport_row, metrics);
let ((sr, sc), (er, ec)) = self.ordered();
if row < sr || row > er {
return false;
}
if sr == er {
// Single-line: from sc to ec (inclusive)
col >= sc && col <= ec
} else if row == sr {
// First line: from sc to end of line
col >= sc
} else if row == er {
// Last line: from start to ec
col <= ec
} else {
// Middle rows: fully selected
true
}
}
}
/// Clamp screen coordinates to the pane's inner area and convert
/// to pane-relative coordinates.
fn clamp_to_pane(&self, screen_col: u16, screen_row: u16) -> (u16, u16) {
let r = &self.pane_inner;
let clamped_col = screen_col.clamp(r.x, r.x + r.width.saturating_sub(1));
let clamped_row = screen_row.clamp(r.y, r.y + r.height.saturating_sub(1));
(clamped_row - r.y, clamped_col - r.x)
}
fn viewport_top_row(metrics: Option<ScrollMetrics>) -> u32 {
metrics
.map(|metrics| {
metrics
.max_offset_from_bottom
.saturating_sub(metrics.offset_from_bottom)
})
.unwrap_or(0) as u32
}
fn absolute_row_for_viewport_row(viewport_row: u16, metrics: Option<ScrollMetrics>) -> u32 {
viewport_top_row(metrics) + u32::from(viewport_row)
}
fn clamp_to_pane(screen_col: u16, screen_row: u16, pane_inner: Rect) -> (u16, u16) {
let clamped_col = screen_col.clamp(
pane_inner.x,
pane_inner.x + pane_inner.width.saturating_sub(1),
);
let clamped_row = screen_row.clamp(
pane_inner.y,
pane_inner.y + pane_inner.height.saturating_sub(1),
);
(clamped_row - pane_inner.y, clamped_col - pane_inner.x)
}
/// Write text to the system clipboard via OSC 52.
@ -158,8 +185,9 @@ pub fn write_osc52(text: &str) {
mod tests {
use super::*;
fn make_sel(sr: u16, sc: u16, er: u16, ec: u16) -> Selection {
let mut sel = Selection::anchor(PaneId::from_raw(0), sr, sc, Rect::new(0, 0, 80, 24));
fn make_sel(sr: u32, sc: u16, er: u32, ec: u16) -> Selection {
let mut sel = Selection::anchor(PaneId::from_raw(0), sr as u16, sc, None);
sel.anchor = (sr, sc);
sel.cursor = (er, ec);
sel.phase = Phase::Dragging;
sel
@ -180,43 +208,38 @@ mod tests {
#[test]
fn single_line_contains() {
let sel = make_sel(2, 5, 2, 15);
assert!(!sel.contains(2, 4));
assert!(sel.contains(2, 5));
assert!(sel.contains(2, 10));
assert!(sel.contains(2, 15)); // inclusive
assert!(!sel.contains(2, 16));
assert!(!sel.contains(1, 10));
assert!(!sel.contains(3, 10));
assert!(!sel.contains(2, 4, None));
assert!(sel.contains(2, 5, None));
assert!(sel.contains(2, 10, None));
assert!(sel.contains(2, 15, None));
assert!(!sel.contains(2, 16, None));
assert!(!sel.contains(1, 10, None));
assert!(!sel.contains(3, 10, None));
}
#[test]
fn multi_line_contains() {
let sel = make_sel(2, 5, 4, 10);
// Row 2: from col 5 to end
assert!(!sel.contains(2, 4));
assert!(sel.contains(2, 5));
assert!(sel.contains(2, 79));
// Row 3: fully selected
assert!(sel.contains(3, 0));
assert!(sel.contains(3, 79));
// Row 4: from start to col 10
assert!(sel.contains(4, 0));
assert!(sel.contains(4, 10));
assert!(!sel.contains(4, 11));
assert!(!sel.contains(2, 4, None));
assert!(sel.contains(2, 5, None));
assert!(sel.contains(2, 79, None));
assert!(sel.contains(3, 0, None));
assert!(sel.contains(3, 79, None));
assert!(sel.contains(4, 0, None));
assert!(sel.contains(4, 10, None));
assert!(!sel.contains(4, 11, None));
}
#[test]
fn anchored_not_visible() {
let sel = Selection::anchor(PaneId::from_raw(0), 5, 10, Rect::new(0, 0, 80, 24));
let sel = Selection::anchor(PaneId::from_raw(0), 5, 10, None);
assert!(!sel.is_visible());
assert!(!sel.contains(5, 10));
assert!(!sel.contains(5, 10, None));
}
#[test]
fn click_without_drag() {
let mut sel = Selection::anchor(PaneId::from_raw(0), 5, 10, Rect::new(0, 0, 80, 24));
let mut sel = Selection::anchor(PaneId::from_raw(0), 5, 10, None);
assert!(sel.was_just_click());
let copied = sel.finish();
assert!(!copied);
@ -224,8 +247,8 @@ mod tests {
#[test]
fn drag_then_finish() {
let mut sel = Selection::anchor(PaneId::from_raw(0), 5, 10, Rect::new(10, 5, 80, 24));
sel.drag(20, 7); // screen coords → clamped to pane
let mut sel = Selection::anchor(PaneId::from_raw(0), 5, 10, None);
sel.drag(20, 7, Rect::new(10, 5, 80, 24), None);
assert!(sel.is_visible());
assert!(!sel.was_just_click());
let copied = sel.finish();
@ -233,15 +256,54 @@ mod tests {
}
#[test]
fn clamp_to_pane_bounds() {
let sel = Selection::anchor(PaneId::from_raw(0), 0, 0, Rect::new(10, 5, 80, 24));
// Drag way outside pane bounds
let (row, col) = sel.clamp_to_pane(200, 100);
assert_eq!(row, 23); // clamped to height - 1
assert_eq!(col, 79); // clamped to width - 1
fn drag_uses_buffer_rows_when_scrolled() {
let mut sel = Selection::anchor(
PaneId::from_raw(0),
0,
10,
Some(ScrollMetrics {
offset_from_bottom: 1,
max_offset_from_bottom: 10,
viewport_rows: 4,
}),
);
// Drag left of pane
let (row, col) = sel.clamp_to_pane(0, 0);
sel.drag(
10,
5,
Rect::new(10, 5, 80, 4),
Some(ScrollMetrics {
offset_from_bottom: 2,
max_offset_from_bottom: 10,
viewport_rows: 4,
}),
);
assert_eq!(sel.ordered_cells(), ((8, 0), (9, 10)));
}
#[test]
fn contains_tracks_current_viewport_after_scroll() {
let sel = make_sel(8, 2, 10, 4);
let metrics = Some(ScrollMetrics {
offset_from_bottom: 2,
max_offset_from_bottom: 10,
viewport_rows: 4,
});
assert!(sel.contains(0, 2, metrics));
assert!(sel.contains(1, 40, metrics));
assert!(sel.contains(2, 4, metrics));
assert!(!sel.contains(3, 4, metrics));
}
#[test]
fn clamp_to_pane_bounds() {
let (row, col) = clamp_to_pane(200, 100, Rect::new(10, 5, 80, 24));
assert_eq!(row, 23);
assert_eq!(col, 79);
let (row, col) = clamp_to_pane(0, 0, Rect::new(10, 5, 80, 24));
assert_eq!(row, 0);
assert_eq!(col, 0);
}

View File

@ -1240,6 +1240,7 @@ fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) {
frame,
info.id,
info.inner_rect,
rt.scroll_metrics(),
&app.palette,
);
}
@ -1459,6 +1460,7 @@ fn render_selection_highlight(
frame: &mut Frame,
pane_id: crate::layout::PaneId,
inner: Rect,
scroll_metrics: Option<crate::pane::ScrollMetrics>,
p: &Palette,
) {
if let Some(sel) = selection {
@ -1466,7 +1468,7 @@ fn render_selection_highlight(
let buf = frame.buffer_mut();
for y in 0..inner.height {
for x in 0..inner.width {
if sel.contains(y, x) {
if sel.contains(y, x, scroll_metrics) {
let cell = &mut buf[(inner.x + x, inner.y + y)];
cell.set_style(Style::default().fg(p.panel_bg).bg(p.blue));
}