gardesk/garterm / 0c9f13c

Browse files

fix: use absolute row coordinates for selection to handle scrollback correctly

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0c9f13c7a3cf36f4709fd980fdfdb34c97c15e35
Parents
80a593e
Tree
3d3f0f1

3 changed files

StatusFile+-
M garterm/src/app.rs 38 6
M garterm/src/input/selection.rs 6 5
M garterm/src/terminal/grid.rs 13 2
garterm/src/app.rsmodified
@@ -836,10 +836,36 @@ impl App {
836836
         if bounds.0.row == bounds.1.row && bounds.0.col == bounds.1.col {
837837
             return None;
838838
         }
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
+
839865
         Some(SelectionBounds {
840
-            start_row: bounds.0.row,
866
+            start_row,
841867
             start_col: bounds.0.col,
842
-            end_row: bounds.1.row,
868
+            end_row,
843869
             end_col: bounds.1.col,
844870
             is_block: matches!(self.selection.mode(), SelectionMode::Block),
845871
         })
@@ -1475,6 +1501,8 @@ impl App {
14751501
                 self.last_click = now;
14761502
 
14771503
                 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);
14781506
                     match self.click_count {
14791507
                         1 => {
14801508
                             // Single click: clear any existing selection, prepare for potential drag
@@ -1484,13 +1512,13 @@ impl App {
14841512
                             } else {
14851513
                                 SelectionMode::Normal
14861514
                             };
1487
-                            self.selection.start(row, col, mode);
1515
+                            self.selection.start(abs_row, col, mode);
14881516
                         }
14891517
                         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());
14911519
                         }
14921520
                         _ => {
1493
-                            self.selection.select_line(row, pane.terminal.cols());
1521
+                            self.selection.select_line(abs_row, pane.terminal.cols());
14941522
                         }
14951523
                     }
14961524
                 }
@@ -1638,7 +1666,11 @@ impl App {
16381666
 
16391667
         // Update selection during drag
16401668
         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
+            }
16421674
             if let Some(pane) = self.tabs.focused_pane_mut() {
16431675
                 pane.mark_dirty();
16441676
             }
garterm/src/input/selection.rsmodified
@@ -156,7 +156,8 @@ impl Selection {
156156
         match self.mode {
157157
             SelectionMode::Normal => {
158158
                 for row in start.row..=end.row {
159
-                    if let Some(line) = grid.line(row) {
159
+                    // Use line_absolute to access scrollback + active lines
160
+                    if let Some(line) = grid.line_absolute(row) {
160161
                         let start_col = if row == start.row { start.col } else { 0 };
161162
                         let end_col = if row == end.row { end.col } else { cols - 1 };
162163
 
@@ -176,7 +177,7 @@ impl Selection {
176177
             }
177178
             SelectionMode::Line => {
178179
                 for row in start.row..=end.row {
179
-                    if let Some(line) = grid.line(row) {
180
+                    if let Some(line) = grid.line_absolute(row) {
180181
                         // Find last non-space character
181182
                         let mut last_non_space = 0;
182183
                         for col in 0..cols {
@@ -206,7 +207,7 @@ impl Selection {
206207
                 };
207208
 
208209
                 for row in start.row..=end.row {
209
-                    if let Some(line) = grid.line(row) {
210
+                    if let Some(line) = grid.line_absolute(row) {
210211
                         for col in min_col..=max_col.min(cols - 1) {
211212
                             let c = line[col].c;
212213
                             if c != '\0' {
@@ -230,9 +231,9 @@ impl Selection {
230231
             .join("\n")
231232
     }
232233
 
233
-    /// Expand selection to word boundaries
234
+    /// Expand selection to word boundaries (row is absolute)
234235
     pub fn select_word(&mut self, row: usize, col: usize, grid: &Grid, cols: usize) {
235
-        let Some(line) = grid.line(row) else {
236
+        let Some(line) = grid.line_absolute(row) else {
236237
             return;
237238
         };
238239
 
garterm/src/terminal/grid.rsmodified
@@ -179,16 +179,27 @@ impl Grid {
179179
         })
180180
     }
181181
 
182
-    /// Get a line reference
182
+    /// Get a line reference (active grid only)
183183
     pub fn line(&self, row: usize) -> Option<&Line> {
184184
         self.lines.get(row)
185185
     }
186186
 
187
-    /// Get a mutable line reference
187
+    /// Get a mutable line reference (active grid only)
188188
     pub fn line_mut(&mut self, row: usize) -> Option<&mut Line> {
189189
         self.lines.get_mut(row)
190190
     }
191191
 
192
+    /// Get a line by absolute row number (scrollback + active)
193
+    /// Row 0 is the first line of scrollback, scrollback.len() is the first active line
194
+    pub fn line_absolute(&self, abs_row: usize) -> Option<&Line> {
195
+        let scrollback_len = self.scrollback.len();
196
+        if abs_row < scrollback_len {
197
+            self.scrollback.get(abs_row)
198
+        } else {
199
+            self.lines.get(abs_row - scrollback_len)
200
+        }
201
+    }
202
+
192203
     /// Scroll the grid up by n lines within a scroll region
193204
     /// Lines scrolled out of the top go to scrollback
194205
     pub fn scroll_up(&mut self, n: usize, top: usize, bottom: usize) {