par_term/selection.rs
1/// Selection mode
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum SelectionMode {
4 /// Normal character-based selection
5 Normal,
6 /// Rectangular/block selection
7 Rectangular,
8 /// Full line selection (triple-click)
9 Line,
10 /// Entire terminal buffer (scrollback + visible screen).
11 ///
12 /// The stored `start`/`end` cover the visible screen so the on-screen
13 /// highlight works through the normal cell-marking path, but copy pulls the
14 /// whole buffer via `TerminalManager::export_text` rather than the viewport.
15 All,
16}
17
18/// Selection state for text selection
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct Selection {
21 /// Start position (col, row) in viewport-relative coordinates at `scroll_offset`
22 pub start: (usize, usize),
23 /// End position (col, row) in viewport-relative coordinates at `scroll_offset`
24 pub end: (usize, usize),
25 /// Selection mode
26 pub mode: SelectionMode,
27 /// Scroll offset at the time the selection was captured.
28 ///
29 /// Row coordinates are viewport-relative (0 = top of the visible screen) when
30 /// `scroll_offset` was the active viewport offset. The renderer adjusts the
31 /// rows by `(self.scroll_offset as isize - current_scroll_offset as isize)` so
32 /// the highlight tracks the content as the user scrolls.
33 pub scroll_offset: usize,
34}
35
36impl Selection {
37 /// Create a new selection.
38 ///
39 /// `scroll_offset` must be the current viewport scroll offset so the
40 /// renderer can compensate when the user scrolls after the selection.
41 pub fn new(
42 start: (usize, usize),
43 end: (usize, usize),
44 mode: SelectionMode,
45 scroll_offset: usize,
46 ) -> Self {
47 Self {
48 start,
49 end,
50 mode,
51 scroll_offset,
52 }
53 }
54
55 /// Return a copy of this selection with rows adjusted to `current_scroll_offset`.
56 ///
57 /// Rows that shift above the top of the viewport become `usize::MAX` so that
58 /// `is_cell_selected` never matches them. Rows shifted below the viewport are
59 /// left as-is (they exceed the row count and are also never matched).
60 pub fn viewport_adjusted(&self, current_scroll_offset: usize) -> Self {
61 let delta = current_scroll_offset as isize - self.scroll_offset as isize;
62 let adjust = |row: usize| -> usize {
63 let adjusted = row as isize + delta;
64 if adjusted < 0 {
65 usize::MAX
66 } else {
67 adjusted as usize
68 }
69 };
70 Self {
71 start: (self.start.0, adjust(self.start.1)),
72 end: (self.end.0, adjust(self.end.1)),
73 mode: self.mode,
74 scroll_offset: current_scroll_offset,
75 }
76 }
77
78 /// Get normalized selection (ensures start is before end)
79 pub fn normalized(&self) -> ((usize, usize), (usize, usize)) {
80 let (start_col, start_row) = self.start;
81 let (end_col, end_row) = self.end;
82
83 if start_row < end_row || (start_row == end_row && start_col <= end_col) {
84 (self.start, self.end)
85 } else {
86 (self.end, self.start)
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_selection_normalization() {
97 // Forward selection
98 let sel = Selection::new((0, 0), (10, 0), SelectionMode::Normal, 0);
99 assert_eq!(sel.normalized(), ((0, 0), (10, 0)));
100
101 // Backward selection (same line)
102 let sel = Selection::new((10, 0), (0, 0), SelectionMode::Normal, 0);
103 assert_eq!(sel.normalized(), ((0, 0), (10, 0)));
104
105 // Forward selection (multi-line)
106 let sel = Selection::new((10, 0), (5, 1), SelectionMode::Normal, 0);
107 assert_eq!(sel.normalized(), ((10, 0), (5, 1)));
108
109 // Backward selection (multi-line)
110 let sel = Selection::new((5, 1), (10, 0), SelectionMode::Normal, 0);
111 assert_eq!(sel.normalized(), ((10, 0), (5, 1)));
112 }
113}