ratatui_toolkit/primitives/termtui/
copy_mode.rs1use crate::primitives::termtui::screen::Screen;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct CopyPos {
12 pub x: i32,
13 pub y: i32,
14}
15
16impl CopyPos {
17 pub fn new(x: i32, y: i32) -> Self {
18 Self { x, y }
19 }
20
21 pub fn to_low_high(start: &CopyPos, end: &CopyPos) -> (CopyPos, CopyPos) {
23 if start.y < end.y || (start.y == end.y && start.x <= end.x) {
24 (*start, *end)
25 } else {
26 (*end, *start)
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug)]
33pub enum CopyMoveDir {
34 Up,
35 Down,
36 Left,
37 Right,
38 LineStart,
39 LineEnd,
40 PageUp,
41 PageDown,
42 Top,
43 Bottom,
44 WordLeft,
45 WordRight,
46}
47
48impl CopyMoveDir {
49 pub fn delta(&self) -> (i32, i32) {
51 match self {
52 CopyMoveDir::Up => (0, -1),
53 CopyMoveDir::Down => (0, 1),
54 CopyMoveDir::Left => (-1, 0),
55 CopyMoveDir::Right => (1, 0),
56 _ => (0, 0), }
58 }
59}
60
61#[derive(Clone, Default)]
63pub enum CopyMode {
64 #[default]
66 None,
67 Active {
69 frozen_screen: Box<Screen>,
71 cursor: CopyPos,
73 anchor: Option<CopyPos>,
75 screen_height: i32,
77 screen_width: i32,
79 scrollback_available: i32,
81 },
82}
83
84impl CopyMode {
85 pub fn is_active(&self) -> bool {
87 matches!(self, CopyMode::Active { .. })
88 }
89
90 pub fn enter(screen: Screen, start: CopyPos) -> Self {
92 let size = screen.size();
93 let scrollback = screen.primary_grid().scrollback_available() as i32;
94
95 CopyMode::Active {
96 frozen_screen: Box::new(screen),
97 cursor: start,
98 anchor: None,
99 screen_height: size.rows as i32,
100 screen_width: size.cols as i32,
101 scrollback_available: scrollback,
102 }
103 }
104
105 pub fn move_cursor(&mut self, dx: i32, dy: i32) {
107 if let CopyMode::Active {
108 cursor,
109 screen_width,
110 screen_height,
111 scrollback_available,
112 ..
113 } = self
114 {
115 let new_x = (cursor.x + dx).clamp(0, *screen_width - 1);
116 let new_y = (cursor.y + dy).clamp(-*scrollback_available, *screen_height - 1);
117
118 cursor.x = new_x;
119 cursor.y = new_y;
120 }
121 }
122
123 pub fn move_dir(&mut self, dir: CopyMoveDir) {
125 if let CopyMode::Active {
126 cursor,
127 screen_width,
128 screen_height,
129 scrollback_available,
130 ..
131 } = self
132 {
133 match dir {
134 CopyMoveDir::Up | CopyMoveDir::Down | CopyMoveDir::Left | CopyMoveDir::Right => {
135 let (dx, dy) = dir.delta();
136 self.move_cursor(dx, dy);
137 }
138 CopyMoveDir::LineStart => {
139 cursor.x = 0;
140 }
141 CopyMoveDir::LineEnd => {
142 cursor.x = *screen_width - 1;
143 }
144 CopyMoveDir::PageUp => {
145 let page = *screen_height / 2;
146 cursor.y = (cursor.y - page).max(-*scrollback_available);
147 }
148 CopyMoveDir::PageDown => {
149 let page = *screen_height / 2;
150 cursor.y = (cursor.y + page).min(*screen_height - 1);
151 }
152 CopyMoveDir::Top => {
153 cursor.y = -*scrollback_available;
154 cursor.x = 0;
155 }
156 CopyMoveDir::Bottom => {
157 cursor.y = *screen_height - 1;
158 cursor.x = *screen_width - 1;
159 }
160 CopyMoveDir::WordLeft | CopyMoveDir::WordRight => {
161 let delta = if matches!(dir, CopyMoveDir::WordLeft) {
163 -5
164 } else {
165 5
166 };
167 cursor.x = (cursor.x + delta).clamp(0, *screen_width - 1);
168 }
169 }
170 }
171 }
172
173 pub fn set_anchor(&mut self) {
175 if let CopyMode::Active { cursor, anchor, .. } = self {
176 if anchor.is_some() {
177 *anchor = None;
179 } else {
180 *anchor = Some(*cursor);
182 }
183 }
184 }
185
186 pub fn set_end(&mut self) {
188 if let CopyMode::Active { cursor, anchor, .. } = self {
189 if anchor.is_none() {
190 *anchor = Some(*cursor);
192 }
193 }
194 }
195
196 pub fn get_selection(&self) -> Option<(CopyPos, CopyPos)> {
198 if let CopyMode::Active { cursor, anchor, .. } = self {
199 anchor.map(|a| (a, *cursor))
200 } else {
201 None
202 }
203 }
204
205 pub fn get_selected_text(&self) -> Option<String> {
207 if let CopyMode::Active {
208 frozen_screen,
209 cursor,
210 anchor,
211 ..
212 } = self
213 {
214 let anchor = (*anchor)?;
215 let (low, high) = CopyPos::to_low_high(&anchor, cursor);
216
217 Some(frozen_screen.get_selected_text(low.x, low.y, high.x, high.y))
218 } else {
219 None
220 }
221 }
222
223 pub fn frozen_screen(&self) -> Option<&Screen> {
225 if let CopyMode::Active { frozen_screen, .. } = self {
226 Some(frozen_screen)
227 } else {
228 None
229 }
230 }
231
232 pub fn cursor(&self) -> Option<CopyPos> {
234 if let CopyMode::Active { cursor, .. } = self {
235 Some(*cursor)
236 } else {
237 None
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn make_test_screen() -> Screen {
247 Screen::new(24, 80, 1000)
248 }
249
250 #[test]
251 fn test_copy_mode_enter() {
252 let screen = make_test_screen();
253 let mode = CopyMode::enter(screen, CopyPos::new(0, 0));
254
255 assert!(mode.is_active());
256 }
257
258 #[test]
259 fn test_copy_mode_move_cursor() {
260 let screen = make_test_screen();
261 let mut mode = CopyMode::enter(screen, CopyPos::new(10, 10));
262
263 mode.move_cursor(5, 2);
264
265 if let CopyMode::Active { cursor, .. } = mode {
266 assert_eq!(cursor.x, 15);
267 assert_eq!(cursor.y, 12);
268 } else {
269 panic!("Expected active mode");
270 }
271 }
272
273 #[test]
274 fn test_copy_mode_bounds() {
275 let screen = make_test_screen();
276 let mut mode = CopyMode::enter(screen, CopyPos::new(0, 0));
277
278 mode.move_cursor(-10, -10);
280
281 if let CopyMode::Active { cursor, .. } = mode {
282 assert_eq!(cursor.x, 0);
283 assert!(cursor.y <= 0);
284 } else {
285 panic!("Expected active mode");
286 }
287 }
288
289 #[test]
290 fn test_copy_mode_selection() {
291 let screen = make_test_screen();
292 let mut mode = CopyMode::enter(screen, CopyPos::new(5, 5));
293
294 assert!(mode.get_selection().is_none());
296
297 mode.set_anchor();
299
300 mode.move_cursor(10, 0);
302
303 let selection = mode.get_selection();
305 assert!(selection.is_some());
306
307 let (start, end) = selection.unwrap();
308 assert_eq!(start.x, 5);
309 assert_eq!(end.x, 15);
310 }
311
312 #[test]
313 fn test_copy_pos_to_low_high() {
314 let a = CopyPos::new(10, 5);
315 let b = CopyPos::new(5, 10);
316
317 let (low, high) = CopyPos::to_low_high(&a, &b);
318 assert_eq!(low.y, 5);
319 assert_eq!(high.y, 10);
320 }
321}