1use crate::Bounds;
2
3#[derive(Debug, Clone)]
13pub struct MouseDimensions {
14 screen_bounds: Bounds,
16 content_bounds: Bounds,
18 scroll_offset: (usize, usize),
20 content_size: (usize, usize),
22}
23
24impl MouseDimensions {
25 pub fn new(
27 screen_bounds: Bounds,
28 content_bounds: Bounds,
29 scroll_offset: (usize, usize),
30 content_size: (usize, usize),
31 ) -> Self {
32 Self {
33 screen_bounds,
34 content_bounds,
35 scroll_offset,
36 content_size,
37 }
38 }
39
40 pub fn set_scroll_offset(&mut self, horizontal: usize, vertical: usize) {
42 self.scroll_offset = (horizontal, vertical);
43 }
44
45 pub fn crossterm_to_internal(&self, column: u16, row: u16) -> (usize, usize) {
48 (column as usize, row as usize)
49 }
50
51 pub fn internal_to_crossterm(&self, x: usize, y: usize) -> (u16, u16) {
53 (x as u16, y as u16)
54 }
55
56 pub fn is_within_screen_bounds(&self, x: usize, y: usize) -> bool {
58 self.screen_bounds.contains_point(x, y)
59 }
60
61 pub fn is_within_content_bounds(&self, x: usize, y: usize) -> bool {
63 self.content_bounds.contains_point(x, y)
64 }
65
66 pub fn screen_to_content(&self, screen_x: usize, screen_y: usize) -> Option<(usize, usize)> {
69 if !self.content_bounds.contains_point(screen_x, screen_y) {
71 return None;
72 }
73
74 let relative_x = screen_x.saturating_sub(self.content_bounds.x1);
76 let relative_y = screen_y.saturating_sub(self.content_bounds.y1);
77
78 let content_x = relative_x + self.scroll_offset.0;
80 let content_y = relative_y + self.scroll_offset.1;
81
82 if content_x >= self.content_size.0 || content_y >= self.content_size.1 {
84 return None;
85 }
86
87 Some((content_x, content_y))
88 }
89
90 pub fn content_to_screen(&self, content_x: usize, content_y: usize) -> Option<(usize, usize)> {
92 if content_x >= self.content_size.0 || content_y >= self.content_size.1 {
94 return None;
95 }
96
97 let viewable_width = self.content_bounds.width();
100 let viewable_height = self.content_bounds.height();
101
102 if content_x < self.scroll_offset.0 || content_y < self.scroll_offset.1 {
103 return None; }
105
106 if content_x >= self.scroll_offset.0 + viewable_width
107 || content_y >= self.scroll_offset.1 + viewable_height
108 {
109 return None; }
111
112 let screen_relative_x = content_x - self.scroll_offset.0;
114 let screen_relative_y = content_y - self.scroll_offset.1;
115
116 let screen_x = self.content_bounds.x1 + screen_relative_x;
118 let screen_y = self.content_bounds.y1 + screen_relative_y;
119
120 Some((screen_x, screen_y))
121 }
122
123 pub fn get_visible_content_range(&self) -> ((usize, usize), (usize, usize)) {
125 let start_x = self.scroll_offset.0;
126 let start_y = self.scroll_offset.1;
127 let end_x = (start_x + self.content_bounds.width()).min(self.content_size.0);
128 let end_y = (start_y + self.content_bounds.height()).min(self.content_size.1);
129
130 ((start_x, start_y), (end_x, end_y))
131 }
132
133 pub fn is_content_coordinate_visible(&self, content_x: usize, content_y: usize) -> bool {
135 let ((start_x, start_y), (end_x, end_y)) = self.get_visible_content_range();
136
137 content_x >= start_x && content_x < end_x && content_y >= start_y && content_y < end_y
138 }
139
140 pub fn screen_y_to_content_line(&self, screen_y: usize) -> Option<usize> {
143 if !self.is_within_content_bounds(0, screen_y) {
144 return None;
145 }
146
147 let relative_y = screen_y.saturating_sub(self.content_bounds.y1);
148 let content_line = relative_y + self.scroll_offset.1;
149
150 if content_line >= self.content_size.1 {
151 None
152 } else {
153 Some(content_line)
154 }
155 }
156
157 pub fn screen_x_to_content_column(&self, screen_x: usize) -> Option<usize> {
160 if !self.is_within_content_bounds(screen_x, 0) {
161 return None;
162 }
163
164 let relative_x = screen_x.saturating_sub(self.content_bounds.x1);
165 let content_column = relative_x + self.scroll_offset.0;
166
167 if content_column >= self.content_size.0 {
168 None
169 } else {
170 Some(content_column)
171 }
172 }
173
174 pub fn detect_click_region(&self, x: usize, y: usize) -> ClickRegion {
177 if !self.is_within_screen_bounds(x, y) {
178 return ClickRegion::Outside;
179 }
180
181 if self.is_within_content_bounds(x, y) {
183 return ClickRegion::Content;
184 }
185
186 let bounds = &self.screen_bounds;
188
189 if x == bounds.x2 && y >= bounds.y1 && y <= bounds.y2 {
191 return ClickRegion::VerticalScrollbar;
192 }
193
194 if y == bounds.y2 && x >= bounds.x1 && x <= bounds.x2 {
195 return ClickRegion::HorizontalScrollbar;
196 }
197
198 if y == bounds.y1 + 1 && x > bounds.x1 && x < bounds.x2 {
200 return ClickRegion::TabBar;
201 }
202
203 if y == bounds.y1 || y == bounds.y2 || x == bounds.x1 || x == bounds.x2 {
205 return ClickRegion::Border;
206 }
207
208 ClickRegion::Content }
210
211 pub fn calculate_distance(&self, x1: usize, y1: usize, x2: usize, y2: usize) -> f64 {
213 let dx = (x2 as f64) - (x1 as f64);
214 let dy = (y2 as f64) - (y1 as f64);
215 (dx * dx + dy * dy).sqrt()
216 }
217
218 pub fn is_drag_operation(
220 &self,
221 start_x: usize,
222 start_y: usize,
223 current_x: usize,
224 current_y: usize,
225 threshold: f64,
226 ) -> bool {
227 self.calculate_distance(start_x, start_y, current_x, current_y) > threshold
228 }
229
230 pub fn calculate_selection_bounds(
232 &self,
233 start_x: usize,
234 start_y: usize,
235 end_x: usize,
236 end_y: usize,
237 ) -> SelectionBounds {
238 let min_x = start_x.min(end_x);
239 let max_x = start_x.max(end_x);
240 let min_y = start_y.min(end_y);
241 let max_y = start_y.max(end_y);
242
243 SelectionBounds {
244 start_x: min_x,
245 start_y: min_y,
246 end_x: max_x,
247 end_y: max_y,
248 width: max_x.saturating_sub(min_x) + 1,
249 height: max_y.saturating_sub(min_y) + 1,
250 }
251 }
252
253 pub fn clamp_to_screen_bounds(&self, x: usize, y: usize) -> (usize, usize) {
256 let clamped_x = x.clamp(self.screen_bounds.x1, self.screen_bounds.x2);
257 let clamped_y = y.clamp(self.screen_bounds.y1, self.screen_bounds.y2);
258 (clamped_x, clamped_y)
259 }
260
261 pub fn clamp_to_content_bounds(&self, x: usize, y: usize) -> (usize, usize) {
263 let clamped_x = x.clamp(self.content_bounds.x1, self.content_bounds.x2);
264 let clamped_y = y.clamp(self.content_bounds.y1, self.content_bounds.y2);
265 (clamped_x, clamped_y)
266 }
267
268 pub fn calculate_scroll_to_visible(&self, content_x: usize, content_y: usize) -> (i32, i32) {
270 let mut delta_x = 0i32;
271 let mut delta_y = 0i32;
272
273 let viewable_width = self.content_bounds.width();
274 let viewable_height = self.content_bounds.height();
275
276 if content_x < self.scroll_offset.0 {
278 delta_x = -((self.scroll_offset.0 - content_x) as i32);
280 } else if content_x >= self.scroll_offset.0 + viewable_width {
281 delta_x = (content_x - (self.scroll_offset.0 + viewable_width - 1)) as i32;
283 }
284
285 if content_y < self.scroll_offset.1 {
287 delta_y = -((self.scroll_offset.1 - content_y) as i32);
289 } else if content_y >= self.scroll_offset.1 + viewable_height {
290 delta_y = (content_y - (self.scroll_offset.1 + viewable_height - 1)) as i32;
292 }
293
294 (delta_x, delta_y)
295 }
296
297 pub fn update_bounds(&mut self, screen_bounds: Bounds, content_bounds: Bounds) {
299 self.screen_bounds = screen_bounds;
300 self.content_bounds = content_bounds;
301 }
302
303 pub fn update_content_size(&mut self, width: usize, height: usize) {
305 self.content_size = (width, height);
306 }
307}
308
309#[derive(Debug, Clone, PartialEq)]
310pub enum ClickRegion {
311 Content,
312 Border,
313 TabBar,
314 VerticalScrollbar,
315 HorizontalScrollbar,
316 CloseButton,
317 ResizeHandle,
318 Outside,
319}
320
321#[derive(Debug, Clone, PartialEq)]
322pub struct SelectionBounds {
323 pub start_x: usize,
324 pub start_y: usize,
325 pub end_x: usize,
326 pub end_y: usize,
327 pub width: usize,
328 pub height: usize,
329}
330
331impl SelectionBounds {
332 pub fn contains(&self, x: usize, y: usize) -> bool {
334 x >= self.start_x && x <= self.end_x && y >= self.start_y && y <= self.end_y
335 }
336
337 pub fn get_coordinates(&self) -> Vec<(usize, usize)> {
339 let mut coords = Vec::new();
340 for y in self.start_y..=self.end_y {
341 for x in self.start_x..=self.end_x {
342 coords.push((x, y));
343 }
344 }
345 coords
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 fn create_test_mouse_dims() -> MouseDimensions {
354 let screen_bounds = Bounds::new(0, 0, 20, 10); let content_bounds = Bounds::new(2, 2, 18, 8); let scroll_offset = (5, 3); let content_size = (50, 30); MouseDimensions::new(screen_bounds, content_bounds, scroll_offset, content_size)
360 }
361
362 #[test]
363 fn test_crossterm_coordinate_conversion() {
364 let mouse_dims = create_test_mouse_dims();
365
366 let (x, y) = mouse_dims.crossterm_to_internal(15, 7);
367 assert_eq!(x, 15);
368 assert_eq!(y, 7);
369
370 let (col, row) = mouse_dims.internal_to_crossterm(15, 7);
371 assert_eq!(col, 15);
372 assert_eq!(row, 7);
373 }
374
375 #[test]
376 fn test_bounds_checking() {
377 let mouse_dims = create_test_mouse_dims();
378
379 assert!(mouse_dims.is_within_screen_bounds(10, 5));
381
382 assert!(!mouse_dims.is_within_screen_bounds(25, 5));
384 assert!(!mouse_dims.is_within_screen_bounds(10, 15));
385
386 assert!(mouse_dims.is_within_content_bounds(10, 5));
388
389 assert!(!mouse_dims.is_within_content_bounds(1, 1)); }
392
393 #[test]
394 fn test_screen_to_content_translation() {
395 let mouse_dims = create_test_mouse_dims();
396
397 if let Some((content_x, content_y)) = mouse_dims.screen_to_content(10, 5) {
399 assert_eq!(content_x, 13);
402 assert_eq!(content_y, 6);
403 } else {
404 panic!("Should successfully translate coordinates");
405 }
406
407 assert!(mouse_dims.screen_to_content(1, 1).is_none());
409 }
410
411 #[test]
412 fn test_content_to_screen_translation() {
413 let mouse_dims = create_test_mouse_dims();
414
415 if let Some((screen_x, screen_y)) = mouse_dims.content_to_screen(13, 6) {
417 assert_eq!(screen_x, 10);
420 assert_eq!(screen_y, 5);
421 } else {
422 panic!("Should successfully translate back to screen coordinates");
423 }
424
425 assert!(mouse_dims.content_to_screen(0, 0).is_none()); }
428
429 #[test]
430 fn test_visible_content_range() {
431 let mouse_dims = create_test_mouse_dims();
432
433 let ((start_x, start_y), (end_x, end_y)) = mouse_dims.get_visible_content_range();
434
435 assert_eq!(start_x, 5); assert_eq!(start_y, 3); assert_eq!(end_x, 5 + 17); assert_eq!(end_y, 3 + 7); }
441
442 #[test]
443 fn test_click_region_detection() {
444 let mouse_dims = create_test_mouse_dims();
445
446 assert_eq!(mouse_dims.detect_click_region(10, 5), ClickRegion::Content);
448
449 assert_eq!(mouse_dims.detect_click_region(0, 0), ClickRegion::Border);
451
452 assert_eq!(
454 mouse_dims.detect_click_region(20, 5),
455 ClickRegion::VerticalScrollbar
456 );
457
458 assert_eq!(
460 mouse_dims.detect_click_region(10, 10),
461 ClickRegion::HorizontalScrollbar
462 );
463
464 assert_eq!(mouse_dims.detect_click_region(25, 15), ClickRegion::Outside);
466 }
467
468 #[test]
469 fn test_drag_detection() {
470 let mouse_dims = create_test_mouse_dims();
471
472 assert!(!mouse_dims.is_drag_operation(10, 5, 11, 6, 3.0));
474
475 assert!(mouse_dims.is_drag_operation(10, 5, 15, 10, 3.0));
477 }
478
479 #[test]
480 fn test_selection_bounds() {
481 let mouse_dims = create_test_mouse_dims();
482
483 let selection = mouse_dims.calculate_selection_bounds(5, 3, 10, 7);
484
485 assert_eq!(selection.start_x, 5);
486 assert_eq!(selection.start_y, 3);
487 assert_eq!(selection.end_x, 10);
488 assert_eq!(selection.end_y, 7);
489 assert_eq!(selection.width, 6); assert_eq!(selection.height, 5); assert!(selection.contains(7, 5));
494 assert!(!selection.contains(2, 2));
495 assert!(!selection.contains(12, 8));
496 }
497
498 #[test]
499 fn test_coordinate_clamping() {
500 let mouse_dims = create_test_mouse_dims();
501
502 let (x, y) = mouse_dims.clamp_to_screen_bounds(25, 15);
504 assert_eq!(x, 20); assert_eq!(y, 10); let (x, y) = mouse_dims.clamp_to_content_bounds(25, 15);
509 assert_eq!(x, 18); assert_eq!(y, 8); }
512
513 #[test]
514 fn test_scroll_to_visible_calculation() {
515 let mouse_dims = create_test_mouse_dims();
516
517 let (dx, dy) = mouse_dims.calculate_scroll_to_visible(10, 5);
519 assert_eq!(dx, 0);
520 assert_eq!(dy, 0);
521
522 let (dx, dy) = mouse_dims.calculate_scroll_to_visible(2, 5);
524 assert!(dx < 0); let (dx, dy) = mouse_dims.calculate_scroll_to_visible(25, 5);
528 assert!(dx > 0); }
530}