1use crate::{
3 config_handle,
4 dot::{Dot, Range},
5 editor::{Action, Editor},
6 fsys::LogEvent,
7 key::{MouseButton, MouseEvent, MouseEventKind, MouseMod},
8 system::System,
9 ui::{Border, SCRATCH_ID},
10};
11use ad_event::Source;
12use std::time::Instant;
13
14const FAST_SCROLL_MS: u128 = 10;
16const FAST_SCROLL_ROWS: usize = 5;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Click {
23 Text {
24 btn: MouseButton,
26 selection: Range,
30 cut_handled: bool,
31 paste_handled: bool,
32 },
33 ResizeColumn {
34 last_x: usize,
35 },
36 ResizeWindow {
37 last_y: usize,
38 },
39}
40
41impl Click {
42 pub(super) fn text(btn: MouseButton, selection: Range) -> Self {
43 Self::Text {
44 btn,
45 selection,
46 cut_handled: false,
47 paste_handled: false,
48 }
49 }
50}
51
52impl<S> Editor<S>
53where
54 S: System,
55{
56 fn scroll_rows(&self, last_click_time: Instant) -> usize {
66 let delta = (self.last_click_time - last_click_time).as_millis();
67 if delta < FAST_SCROLL_MS {
68 FAST_SCROLL_ROWS
69 } else {
70 1
71 }
72 }
73
74 pub(super) fn handle_mouse_event(&mut self, MouseEvent { k, m, b, x, y }: MouseEvent) {
91 use MouseButton::*;
92 use MouseEventKind::*;
93 use MouseMod::*;
94
95 let last_click_time = self.last_click_time;
96 self.last_click_time = Instant::now();
97
98 match (k, m, b) {
99 (Press, NoMod, Left) => {
100 if self.held_click.is_some() {
102 self.held_click = None;
103 return;
104 }
105
106 if let Some(border) = self.layout.border_at_coords(x, y) {
109 match border {
110 Border::Vertical { col_idx } => {
111 self.layout.focus_column_for_resize(col_idx);
112 self.held_click = Some(Click::ResizeColumn { last_x: x });
113 }
114 Border::Horizontal { col_idx, win_idx } => {
115 self.layout
116 .focus_column_and_window_for_resize(col_idx, win_idx);
117 self.held_click = Some(Click::ResizeWindow { last_y: y });
118 }
119 }
120 return;
121 }
122
123 let click_in_active_buffer = self.layout.set_dot_from_screen_coords(x, y);
124 let b = self.layout.active_buffer_mut();
125 if !click_in_active_buffer && b.id != SCRATCH_ID {
126 _ = self.tx_fsys.send(LogEvent::Focus(b.id));
127 }
128
129 if self.last_click_was_left && click_in_active_buffer {
130 let delta = (self.last_click_time - last_click_time).as_millis();
131 if delta < config_handle!(self).double_click_ms as u128 {
132 b.try_expand_delimited();
133 return;
134 }
135 }
136
137 self.held_click = Some(Click::text(Left, b.dot.as_range()));
138 self.last_click_was_left = true;
139 }
140
141 (Press, NoMod, Right) => self.handle_right_or_middle_click(true, x, y),
142 (Press, Alt, Right) => self.handle_right_or_middle_click(true, x, y),
143
144 (Press, NoMod, Middle) | (Press, Ctrl, Left) => {
145 self.handle_right_or_middle_click(false, x, y)
146 }
147
148 (Hold, _, _) => match &mut self.held_click {
149 Some(Click::Text {
150 btn,
151 selection,
152 cut_handled,
153 paste_handled,
154 }) => {
155 if *btn == Left && (*cut_handled || *paste_handled) {
156 return;
157 }
158
159 match self.layout.try_active_cur_from_screen_coords(x, y) {
160 Some(cur) => selection.set_active_cursor(cur),
161 None => return,
162 }
163
164 if *btn == Left {
165 self.layout.active_buffer_mut().dot = Dot::from(*selection);
166 }
167 }
168
169 Some(Click::ResizeColumn { last_x }) => {
170 let delta = x as i16 - *last_x as i16;
171 if delta != 0 {
172 self.layout.resize_active_column_against_next(delta);
173 *last_x = x;
174 }
175 }
176
177 Some(Click::ResizeWindow { last_y }) => {
178 let delta = y as i16 - *last_y as i16;
179 if delta != 0 {
180 self.layout.resize_active_window_against_next(delta);
181 *last_y = y;
182 }
183 }
184
185 None => (),
186 },
187
188 (Press, _, WheelUp) => {
189 self.last_click_was_left = false;
190 self.layout
191 .scroll_view(x, y, true, self.scroll_rows(last_click_time));
192 }
193
194 (Press, _, WheelDown) => {
195 self.last_click_was_left = false;
196 self.layout
197 .scroll_view(x, y, false, self.scroll_rows(last_click_time));
198 }
199
200 (Release, m, b) => {
201 if let Some(Click::Text { btn, .. }) = self.held_click
202 && btn == Left
203 && (b == Right || b == Middle)
204 {
205 return; }
207
208 let held = match self.held_click.take() {
209 Some(held) => held,
210 None => return,
211 };
212
213 let (btn, mut selection, cut_handled, paste_handled) = match held {
215 Click::Text {
216 btn,
217 selection,
218 cut_handled,
219 paste_handled,
220 } => (btn, selection, cut_handled, paste_handled),
221 Click::ResizeColumn { .. } | Click::ResizeWindow { .. } => return,
222 };
223
224 if btn == Left && (cut_handled || paste_handled) {
225 return;
226 }
227
228 if let Some(cur) = self.layout.try_active_cur_from_screen_coords(x, y) {
231 selection.set_active_cursor(cur);
232 }
233
234 match btn {
235 Left | WheelUp | WheelDown => (),
236 Right | Middle => {
237 self.handle_right_or_middle_release(btn == Right, selection, m == Alt)
238 }
239 }
240 }
241
242 _ => (),
243 }
244 }
245
246 #[inline]
247 fn handle_right_or_middle_click(&mut self, is_right: bool, x: usize, y: usize) {
248 use MouseButton::*;
249
250 self.last_click_was_left = false;
251
252 match &mut self.held_click {
253 Some(Click::Text {
254 btn,
255 selection,
256 cut_handled,
257 paste_handled,
258 }) => {
259 if *btn == Left {
261 if is_right && !*paste_handled {
262 *paste_handled = true;
263 self.paste_from_clipboard(Source::Mouse);
264 } else if !is_right && !*cut_handled {
265 *selection = self.layout.active_buffer().dot.as_range();
266 *cut_handled = true;
267 self.forward_action_to_active_buffer(Action::Delete, Source::Mouse);
268 }
269 } else if (is_right && *btn == Middle) || (!is_right && *btn == Right) {
270 self.held_click = None;
271 }
272 }
273
274 Some(_) => {
275 self.held_click = None;
277 }
278
279 None => {
280 let btn = if is_right { Right } else { Middle };
281 let (id, cur) = self.layout.focus_cur_from_screen_coords(x, y);
282 _ = self.tx_fsys.send(LogEvent::Focus(id));
283 self.held_click = Some(Click::text(btn, Range::from_cursors(cur, cur, false)));
284 }
285 };
286 }
287
288 #[inline]
289 fn handle_right_or_middle_release(
290 &mut self,
291 is_right: bool,
292 selection: Range,
293 load_in_new_window: bool,
294 ) {
295 if selection.start != selection.end {
296 if is_right {
300 self.layout.active_buffer_mut().dot = Dot::from(selection);
301 self.default_load_dot(Source::Mouse, load_in_new_window);
302 } else {
303 let dot = self.layout.active_buffer().dot;
304 self.layout.active_buffer_mut().dot = Dot::from(selection);
305
306 if dot.is_range() {
307 let arg = dot.content(self.layout.active_buffer()).trim().to_string();
309 self.default_execute_dot(Some((dot.as_range(), arg)), Source::Mouse);
310 self.layout.active_buffer_mut().dot = dot;
311 } else {
312 self.default_execute_dot(None, Source::Mouse);
313 }
314 }
315 } else {
316 if !self.layout.active_buffer().dot.contains(&selection.start) {
320 self.layout.active_buffer_mut().dot = Dot::from(selection.start);
321 }
322
323 if is_right {
324 self.default_load_dot(Source::Mouse, load_in_new_window);
325 } else {
326 self.default_execute_dot(None, Source::Mouse);
327 }
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::{
336 dot::{Cur, Range},
337 editor::EditorMode,
338 fsys::InputFilter,
339 key::{MouseButton::*, MouseEvent, MouseEventKind::*, MouseMod::*},
340 log::LogBuffer,
341 };
342 use ad_event::{FsysEvent, Kind, Source};
343 use simple_test_case::test_case;
344 use std::{io, sync::mpsc::channel};
345
346 #[derive(Debug, Default)]
347 struct TestSystem {
348 clipboard: String,
349 }
350
351 impl System for TestSystem {
352 fn set_clipboard(&mut self, s: &str) -> io::Result<()> {
353 self.clipboard = s.to_string();
354
355 Ok(())
356 }
357
358 fn read_clipboard(&self) -> io::Result<String> {
359 Ok(self.clipboard.clone())
360 }
361
362 fn store_child_handle(&mut self, _: &str, _: std::process::Child) {}
363 fn running_children(&self) -> Vec<String> {
364 vec![]
365 }
366 fn cleanup_child(&mut self, _: u32) {}
367 fn kill_child(&mut self, _: usize) {}
368 }
369
370 fn r(start: usize, end: usize, start_active: bool) -> Range {
371 Range {
372 start: Cur { idx: start },
373 end: Cur { idx: end },
374 start_active,
375 }
376 }
377
378 #[test_case(
381 &[
382 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
383 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
384 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
385 ],
386 None,
387 "some",
388 "some text to test with",
389 "X",
390 &[];
391 "left click drag selection complete"
392 )]
393 #[test_case(
394 &[
395 MouseEvent { k: Press, m: NoMod, b: Right, x: 3, y: 1 },
396 MouseEvent { k: Hold, m: NoMod, b: Right, x: 7, y: 1 },
397 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
398 ],
399 None,
400 "some",
401 "some text to test with",
402 "X",
403 &[
404 FsysEvent::new(Source::Mouse, Kind::LoadBody, 0, 3, "some"),
405 ];
406 "right click drag selection complete"
407 )]
408 #[test_case(
409 &[
410 MouseEvent { k: Press, m: NoMod, b: Middle, x: 3, y: 1 },
411 MouseEvent { k: Hold, m: NoMod, b: Middle, x: 7, y: 1 },
412 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
413 ],
414 None,
415 "some",
416 "some text to test with",
417 "X",
418 &[
419 FsysEvent::new(Source::Mouse, Kind::ExecuteBody, 0, 3, "some"),
420 ];
421 "middle click drag selection complete"
422 )]
423 #[test_case(
424 &[
425 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
426 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
427 ],
428 Some(Click::text(Left, r(0, 3, false))),
429 "some",
430 "some text to test with",
431 "X",
432 &[];
433 "left click drag selection without release"
434 )]
435 #[test_case(
436 &[
437 MouseEvent { k: Press, m: NoMod, b: Right, x: 3, y: 1 },
438 MouseEvent { k: Hold, m: NoMod, b: Right, x: 7, y: 1 },
439 ],
440 Some(Click::text(Right, r(0, 3, false))),
441 "t", "some text to test with",
443 "X",
444 &[];
445 "right click drag selection without release"
446 )]
447 #[test_case(
448 &[
449 MouseEvent { k: Press, m: NoMod, b: Middle, x: 3, y: 1 },
450 MouseEvent { k: Hold, m: NoMod, b: Middle, x: 7, y: 1 },
451 ],
452 Some(Click::text(Middle, r(0, 3, false))),
453 "t", "some text to test with",
455 "X",
456 &[];
457 "middle click drag selection without release"
458 )]
459 #[test_case(
460 &[
461 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
462 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
463 MouseEvent { k: Press, m: NoMod, b: Right, x: 4, y: 1 },
464 MouseEvent { k: Release, m: NoMod, b: Right, x: 4, y: 1 },
465 ],
466 None,
467 "some",
468 "some text to test with",
469 "X",
470 &[
471 FsysEvent::new(Source::Mouse, Kind::LoadBody, 0, 3, "some"),
472 ];
473 "right click expand in existing selection"
474 )]
475 #[test_case(
476 &[
477 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
478 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
479 MouseEvent { k: Press, m: NoMod, b: Middle, x: 4, y: 1 },
480 MouseEvent { k: Release, m: NoMod, b: Middle, x: 4, y: 1 },
481 ],
482 None,
483 "some",
484 "some text to test with",
485 "X",
486 &[
487 FsysEvent::new(Source::Mouse, Kind::ExecuteBody, 0, 3, "some"),
488 ];
489 "middle click expand in existing selection"
490 )]
491 #[test_case(
492 &[
493 MouseEvent { k: Press, m: NoMod, b: Left, x: 9, y: 1 },
494 MouseEvent { k: Hold, m: NoMod, b: Left, x: 12, y: 1 },
495 MouseEvent { k: Release, m: NoMod, b: Left, x: 12, y: 1 },
496 MouseEvent { k: Press, m: NoMod, b: Middle, x: 3, y: 1 },
497 MouseEvent { k: Hold, m: NoMod, b: Middle, x: 7, y: 1 },
498 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
499 ],
500 None,
501 "text",
502 "some text to test with",
503 "X",
504 &[
505 FsysEvent::new(Source::Mouse, Kind::ChordedArgument, 5, 8, "text"),
506 FsysEvent::new(Source::Mouse, Kind::ExecuteBody, 0, 3, "some"),
507 ];
508 "middle click with dot arg"
509 )]
510 #[test_case(
511 &[
512 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
513 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
514 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
515 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
516 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
517 ],
518 None,
519 " ",
520 " text to test with",
521 "some",
522 &[
523 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
524 ];
525 "chord cut"
526 )]
527 #[test_case(
528 &[
529 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
530 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
531 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
532 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
533 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
534 ],
535 None,
536 " ",
537 "X text to test with",
538 "X",
539 &[
540 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
541 FsysEvent::new(Source::Mouse, Kind::InsertBody, 0, 1, "X"),
542 ];
543 "chord paste"
544 )]
545 #[test_case(
546 &[
547 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
548 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
549 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
550 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
551 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
552 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
553 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
554 ],
555 None,
556 " ",
557 "some text to test with",
558 "some",
559 &[
560 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
561 FsysEvent::new(Source::Mouse, Kind::InsertBody, 0, 4, "some"),
562 ];
563 "chord cut then paste"
564 )]
565 #[test_case(
568 &[
569 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
570 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
571 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
572 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
573 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
574 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
575 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
576 ],
577 None,
578 "t",
579 "Xtext to test with",
580 "X",
581 &[
582 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
583 FsysEvent::new(Source::Mouse, Kind::InsertBody, 0, 1, "X"),
584 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 1, 2, " "),
585 ];
586 "chord paste then cut"
587 )]
588 #[test_case(
589 &[
590 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
591 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
592 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
593 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
594 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
595 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
596 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
597 ],
598 None,
599 " ",
600 " text to test with",
601 "some",
602 &[
603 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
604 ];
605 "repeated chord cut"
606 )]
607 #[test_case(
608 &[
609 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
610 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
611 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
612 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
613 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
614 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
615 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
616 ],
617 None,
618 " ",
619 "X text to test with",
620 "X",
621 &[
622 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
623 FsysEvent::new(Source::Mouse, Kind::InsertBody, 0, 1, "X"),
624 ];
625 "repeated chord paste"
626 )]
627 #[test_case(
628 &[
629 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
630 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
631 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
632 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
633 MouseEvent { k: Hold, m: NoMod, b: Left, x: 3, y: 1 },
634 MouseEvent { k: Release, m: NoMod, b: Left, x: 3, y: 1 },
635 ],
636 None,
637 " ",
638 "X text to test with",
639 "X",
640 &[
641 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
642 FsysEvent::new(Source::Mouse, Kind::InsertBody, 0, 1, "X"),
643 ];
644 "motion after chord paste is ignored"
645 )]
646 #[test_case(
647 &[
648 MouseEvent { k: Press, m: NoMod, b: Left, x: 3, y: 1 },
649 MouseEvent { k: Hold, m: NoMod, b: Left, x: 7, y: 1 },
650 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
651 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
652 MouseEvent { k: Hold, m: NoMod, b: Left, x: 2, y: 1 },
653 MouseEvent { k: Release, m: NoMod, b: Left, x: 2, y: 1 },
654 ],
655 None,
656 " ",
657 " text to test with",
658 "some",
659 &[
660 FsysEvent::new(Source::Mouse, Kind::DeleteBody, 0, 4, ""),
661 ];
662 "motion after chord cut is ignored"
663 )]
664 #[test_case(
665 &[
666 MouseEvent { k: Press, m: NoMod, b: Right, x: 3, y: 1 },
667 MouseEvent { k: Hold, m: NoMod, b: Right, x: 7, y: 1 },
668 MouseEvent { k: Press, m: NoMod, b: Left, x: 7, y: 1 },
669 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
670 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
671 ],
672 None,
673 "t",
674 "some text to test with",
675 "X",
676 &[];
677 "right click cancel with left"
678 )]
679 #[test_case(
680 &[
681 MouseEvent { k: Press, m: NoMod, b: Right, x: 3, y: 1 },
682 MouseEvent { k: Hold, m: NoMod, b: Right, x: 7, y: 1 },
683 MouseEvent { k: Press, m: NoMod, b: Middle, x: 7, y: 1 },
684 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
685 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
686 ],
687 None,
688 "t",
689 "some text to test with",
690 "X",
691 &[];
692 "right click cancel with middle"
693 )]
694 #[test_case(
695 &[
696 MouseEvent { k: Press, m: NoMod, b: Middle, x: 3, y: 1 },
697 MouseEvent { k: Hold, m: NoMod, b: Middle, x: 7, y: 1 },
698 MouseEvent { k: Press, m: NoMod, b: Left, x: 7, y: 1 },
699 MouseEvent { k: Release, m: NoMod, b: Left, x: 7, y: 1 },
700 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
701 ],
702 None,
703 "t",
704 "some text to test with",
705 "X",
706 &[];
707 "middle click cancel with left"
708 )]
709 #[test_case(
710 &[
711 MouseEvent { k: Press, m: NoMod, b: Middle, x: 3, y: 1 },
712 MouseEvent { k: Hold, m: NoMod, b: Middle, x: 7, y: 1 },
713 MouseEvent { k: Press, m: NoMod, b: Right, x: 7, y: 1 },
714 MouseEvent { k: Release, m: NoMod, b: Right, x: 7, y: 1 },
715 MouseEvent { k: Release, m: NoMod, b: Middle, x: 7, y: 1 },
716 ],
717 None,
718 "t",
719 "some text to test with",
720 "X",
721 &[];
722 "middle click cancel with right"
723 )]
724 #[test_case(
725 &[
726 MouseEvent { k: Press, m: NoMod, b: Left, x: 9, y: 1 },
727 MouseEvent { k: Release, m: NoMod, b: Left, x: 9, y: 1 },
728 MouseEvent { k: Press, m: NoMod, b: Left, x: 9, y: 1 },
729 MouseEvent { k: Release, m: NoMod, b: Left, x: 9, y: 1 },
730 ],
731 None,
732 "text",
733 "some text to test with",
734 "X",
735 &[];
736 "double left click"
737 )]
738 #[test]
739 fn mouse_interactions_work(
740 evts: &[MouseEvent],
741 click: Option<Click>,
742 dot: &str,
743 content: &str,
744 clipboard: &str,
745 fsys_events: &[FsysEvent],
746 ) {
747 let mut ed = Editor::new_with_system(
748 Default::default(),
749 Default::default(),
750 EditorMode::Headless,
751 LogBuffer::default(),
752 TestSystem {
753 clipboard: "X".to_string(),
754 },
755 );
756 ed.update_window_size(100, 80); ed.layout
758 .open_virtual("test", "some text to test with", false);
759 ed.layout.active_buffer_mut().dot = Dot::Cur { c: Cur { idx: 5 } };
760
761 let (tx, rx) = channel();
763 let filter = InputFilter::new(tx);
764 ed.layout
765 .try_set_input_filter(ed.active_buffer_id(), filter);
766
767 for evt in evts.iter() {
768 ed.handle_mouse_event(*evt);
769 }
770
771 let recvd_fsys_events: Vec<_> = rx.try_iter().collect();
772 let b = ed.layout.active_buffer();
773
774 assert_eq!(ed.held_click, click, "click");
775 assert_eq!(b.dot.content(b), dot, "dot content");
776 assert_eq!(b.str_contents(), content, "buffer content");
777 assert_eq!(ed.system.clipboard, clipboard, "clipboard content");
778 assert_eq!(fsys_events, &recvd_fsys_events, "fsys events");
779 }
780
781 fn editor_with_layout(n_cols: usize, n_wins: usize) -> Editor<TestSystem> {
787 let mut ed = Editor::new_with_system(
788 Default::default(),
789 Default::default(),
790 EditorMode::Headless,
791 LogBuffer::default(),
792 TestSystem::default(),
793 );
794 ed.update_window_size(80, 100);
795 ed.layout.open_virtual("test", "test content", false);
796
797 for _ in 1..n_cols {
798 ed.layout.new_column();
799 }
800 for _ in 1..n_wins {
801 ed.layout.new_window();
802 }
803
804 ed
805 }
806
807 #[test_case(5; "drag right grows first column")]
808 #[test_case(-5; "drag left shrinks first column")]
809 #[test]
810 fn resize_column_drag(delta: i16) {
811 let mut ed = editor_with_layout(2, 1);
812 let initial_sizes = ed.layout.column_widths();
813 let border_x = initial_sizes[0] + 1;
814
815 ed.handle_mouse_event(MouseEvent {
816 k: Press,
817 m: NoMod,
818 b: Left,
819 x: border_x,
820 y: 10,
821 });
822 assert!(matches!(ed.held_click, Some(Click::ResizeColumn { .. })));
823
824 let target_x = (border_x as i16 + delta) as usize;
825 ed.handle_mouse_event(MouseEvent {
826 k: Hold,
827 m: NoMod,
828 b: Left,
829 x: target_x,
830 y: 10,
831 });
832 ed.handle_mouse_event(MouseEvent {
833 k: Release,
834 m: NoMod,
835 b: Left,
836 x: target_x,
837 y: 10,
838 });
839
840 assert!(ed.held_click.is_none());
841
842 let final_sizes = ed.layout.column_widths();
843 assert_eq!(
844 final_sizes[0] as i16,
845 initial_sizes[0] as i16 + delta,
846 "first column"
847 );
848 assert_eq!(
849 final_sizes[1] as i16,
850 initial_sizes[1] as i16 - delta,
851 "second column"
852 );
853 }
854
855 #[test_case(5; "drag down grows first window")]
856 #[test_case(-5; "drag up shrinks first window")]
857 #[test]
858 fn resize_window_drag(delta: i16) {
859 let mut ed = editor_with_layout(1, 2);
860 let initial_sizes = ed.layout.window_heights();
861 let border_y = initial_sizes[0] + 1;
862
863 ed.handle_mouse_event(MouseEvent {
864 k: Press,
865 m: NoMod,
866 b: Left,
867 x: 10,
868 y: border_y,
869 });
870 assert!(matches!(ed.held_click, Some(Click::ResizeWindow { .. })));
871
872 let target_y = (border_y as i16 + delta) as usize;
873 ed.handle_mouse_event(MouseEvent {
874 k: Hold,
875 m: NoMod,
876 b: Left,
877 x: 10,
878 y: target_y,
879 });
880 ed.handle_mouse_event(MouseEvent {
881 k: Release,
882 m: NoMod,
883 b: Left,
884 x: 10,
885 y: target_y,
886 });
887
888 assert!(ed.held_click.is_none());
889
890 let final_sizes = ed.layout.window_heights();
891 assert_eq!(
892 final_sizes[0] as i16,
893 initial_sizes[0] as i16 + delta,
894 "first window"
895 );
896 assert_eq!(
897 final_sizes[1] as i16,
898 initial_sizes[1] as i16 - delta,
899 "second window"
900 );
901 }
902
903 #[test]
904 fn resize_sets_correct_focus() {
905 let mut ed = editor_with_layout(3, 1);
906
907 ed.layout.focus_column_for_resize(0);
908 assert_eq!(ed.layout.cols_before_focus(), 0, "initially on column 0");
909
910 let widths = ed.layout.column_widths();
911 let border_x = widths[0] + widths[1] + 2;
912
913 ed.handle_mouse_event(MouseEvent {
914 k: Press,
915 m: NoMod,
916 b: Left,
917 x: border_x,
918 y: 10,
919 });
920
921 assert_eq!(ed.layout.cols_before_focus(), 1, "focus moved to column 1");
922 }
923
924 #[test]
925 fn click_on_border_without_drag_is_noop() {
926 let mut ed = editor_with_layout(2, 1);
927 let initial_sizes = ed.layout.column_widths();
928 let border_x = initial_sizes[0] + 1;
929
930 ed.handle_mouse_event(MouseEvent {
931 k: Press,
932 m: NoMod,
933 b: Left,
934 x: border_x,
935 y: 10,
936 });
937 ed.handle_mouse_event(MouseEvent {
938 k: Release,
939 m: NoMod,
940 b: Left,
941 x: border_x,
942 y: 10,
943 });
944
945 let final_sizes = ed.layout.column_widths();
946 assert_eq!(initial_sizes, final_sizes, "sizes unchanged");
947 }
948
949 #[test_case(Right; "right click")]
950 #[test_case(Middle; "middle click")]
951 #[test]
952 fn non_left_click_on_border_is_noop(btn: MouseButton) {
953 let mut ed = editor_with_layout(2, 1);
954 let initial_sizes = ed.layout.column_widths();
955 let border_x = initial_sizes[0] + 1;
956
957 ed.handle_mouse_event(MouseEvent {
958 k: Press,
959 m: NoMod,
960 b: btn,
961 x: border_x,
962 y: 10,
963 });
964 ed.handle_mouse_event(MouseEvent {
965 k: Release,
966 m: NoMod,
967 b: btn,
968 x: border_x,
969 y: 10,
970 });
971
972 assert!(ed.held_click.is_none());
973
974 let final_sizes = ed.layout.column_widths();
975 assert_eq!(initial_sizes, final_sizes, "sizes unchanged");
976 }
977
978 #[test]
979 fn resize_cancelled_by_right_click() {
980 let mut ed = editor_with_layout(2, 1);
981 let initial_sizes = ed.layout.column_widths();
982
983 let border_x = initial_sizes[0] + 1;
984
985 ed.handle_mouse_event(MouseEvent {
986 k: Press,
987 m: NoMod,
988 b: Left,
989 x: border_x,
990 y: 10,
991 });
992 assert!(matches!(ed.held_click, Some(Click::ResizeColumn { .. })));
993
994 ed.handle_mouse_event(MouseEvent {
995 k: Press,
996 m: NoMod,
997 b: Right,
998 x: border_x,
999 y: 10,
1000 });
1001 assert!(ed.held_click.is_none(), "resize cancelled");
1002
1003 let final_sizes = ed.layout.column_widths();
1004 assert_eq!(initial_sizes, final_sizes, "sizes unchanged after cancel");
1005 }
1006}