@@ -836,10 +836,36 @@ impl App { |
| 836 | 836 | if bounds.0.row == bounds.1.row && bounds.0.col == bounds.1.col { |
| 837 | 837 | return None; |
| 838 | 838 | } |
| 839 | + |
| 840 | + // Convert absolute rows to visible rows for rendering |
| 841 | + // Selection stores absolute rows, but renderer uses viewport-relative rows |
| 842 | + let pane = self.tabs.focused_pane()?; |
| 843 | + let grid = pane.terminal.grid(); |
| 844 | + |
| 845 | + // Convert absolute rows to visible, skip if not in viewport |
| 846 | + let start_visible = grid.absolute_row_to_visible(bounds.0.row); |
| 847 | + let end_visible = grid.absolute_row_to_visible(bounds.1.row); |
| 848 | + |
| 849 | + // At least part of selection must be visible |
| 850 | + let start_row = start_visible.unwrap_or(0); |
| 851 | + let end_row = end_visible.unwrap_or(pane.terminal.rows().saturating_sub(1)); |
| 852 | + |
| 853 | + // If both are None and not overlapping viewport, skip |
| 854 | + if start_visible.is_none() && end_visible.is_none() { |
| 855 | + // Check if selection spans the viewport |
| 856 | + let scrollback_len = grid.scrollback_len(); |
| 857 | + let scroll_offset = grid.scroll_offset(); |
| 858 | + let viewport_start = scrollback_len.saturating_sub(scroll_offset); |
| 859 | + let viewport_end = viewport_start + pane.terminal.rows(); |
| 860 | + if bounds.1.row < viewport_start || bounds.0.row >= viewport_end { |
| 861 | + return None; |
| 862 | + } |
| 863 | + } |
| 864 | + |
| 839 | 865 | Some(SelectionBounds { |
| 840 | | - start_row: bounds.0.row, |
| 866 | + start_row, |
| 841 | 867 | start_col: bounds.0.col, |
| 842 | | - end_row: bounds.1.row, |
| 868 | + end_row, |
| 843 | 869 | end_col: bounds.1.col, |
| 844 | 870 | is_block: matches!(self.selection.mode(), SelectionMode::Block), |
| 845 | 871 | }) |
@@ -1475,6 +1501,8 @@ impl App { |
| 1475 | 1501 | self.last_click = now; |
| 1476 | 1502 | |
| 1477 | 1503 | if let Some(pane) = self.tabs.focused_pane() { |
| 1504 | + // Convert viewport row to absolute row (accounts for scrollback) |
| 1505 | + let abs_row = pane.terminal.grid().visible_row_to_absolute(row); |
| 1478 | 1506 | match self.click_count { |
| 1479 | 1507 | 1 => { |
| 1480 | 1508 | // Single click: clear any existing selection, prepare for potential drag |
@@ -1484,13 +1512,13 @@ impl App { |
| 1484 | 1512 | } else { |
| 1485 | 1513 | SelectionMode::Normal |
| 1486 | 1514 | }; |
| 1487 | | - self.selection.start(row, col, mode); |
| 1515 | + self.selection.start(abs_row, col, mode); |
| 1488 | 1516 | } |
| 1489 | 1517 | 2 => { |
| 1490 | | - self.selection.select_word(row, col, pane.terminal.grid(), pane.terminal.cols()); |
| 1518 | + self.selection.select_word(abs_row, col, pane.terminal.grid(), pane.terminal.cols()); |
| 1491 | 1519 | } |
| 1492 | 1520 | _ => { |
| 1493 | | - self.selection.select_line(row, pane.terminal.cols()); |
| 1521 | + self.selection.select_line(abs_row, pane.terminal.cols()); |
| 1494 | 1522 | } |
| 1495 | 1523 | } |
| 1496 | 1524 | } |
@@ -1638,7 +1666,11 @@ impl App { |
| 1638 | 1666 | |
| 1639 | 1667 | // Update selection during drag |
| 1640 | 1668 | if self.selection.is_active() && state & 0x100 != 0 { |
| 1641 | | - self.selection.update(row, col); |
| 1669 | + if let Some(pane) = self.tabs.focused_pane() { |
| 1670 | + // Convert viewport row to absolute row |
| 1671 | + let abs_row = pane.terminal.grid().visible_row_to_absolute(row); |
| 1672 | + self.selection.update(abs_row, col); |
| 1673 | + } |
| 1642 | 1674 | if let Some(pane) = self.tabs.focused_pane_mut() { |
| 1643 | 1675 | pane.mark_dirty(); |
| 1644 | 1676 | } |