1use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
7
8use turbo_vision::core::draw::{Cell, DrawBuffer};
9use turbo_vision::core::event::{
10 Event, EventType, KB_DOWN, KB_END, KB_ESC, KB_HOME, KB_PGDN, KB_PGUP, KB_UP, MB_LEFT_BUTTON,
11};
12use turbo_vision::core::geometry::{Point, Rect};
13use turbo_vision::core::palette::{Attr, TvColor};
14use turbo_vision::core::state::{GF_GROW_HI_X, GF_GROW_HI_Y, GrowFlags};
15use turbo_vision::terminal::Terminal;
16use turbo_vision::views::view::{View, write_line_to_terminal};
17
18#[derive(Clone, Copy, PartialEq, Eq, Debug)]
23struct SelPos {
24 row: usize,
25 col: usize,
26}
27
28#[derive(Clone, Copy, PartialEq, Eq, Debug)]
30enum SelMode {
31 Stream,
34 Block,
37}
38
39#[derive(Clone, Copy, PartialEq, Eq, Debug)]
45struct Selection {
46 anchor: SelPos,
47 head: SelPos,
48 mode: SelMode,
49}
50
51impl SelMode {
52 fn from_global() -> Self {
54 if turbo_vision::core::state::block_edit_mode() {
55 Self::Block
56 } else {
57 Self::Stream
58 }
59 }
60}
61
62fn reverse(attr: Attr) -> Attr {
65 Attr::new(attr.bg, attr.fg).with_style(attr.style)
66}
67
68fn order(a: SelPos, b: SelPos) -> (SelPos, SelPos) {
70 if (a.row, a.col) <= (b.row, b.col) {
71 (a, b)
72 } else {
73 (b, a)
74 }
75}
76
77fn block_bounds(sel: Selection) -> (usize, usize, usize, usize) {
80 let (a, b) = (sel.anchor, sel.head);
81 (
82 a.row.min(b.row),
83 a.row.max(b.row),
84 a.col.min(b.col),
85 a.col.max(b.col),
86 )
87}
88
89const PRESENTATION_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
101
102fn normalize_line(cells: &[Cell]) -> Vec<Cell> {
129 let mut out = Vec::with_capacity(cells.len());
130 let mut i = 0;
131 while i < cells.len() {
132 let cell = cells[i];
133 if cell.ch == '\0' {
134 out.push(cell);
135 i += 1;
136 continue;
137 }
138
139 let next = cells.get(i + 1).copied();
140 let selector = next.filter(|n| PRESENTATION_SELECTORS.contains(&n.ch));
141
142 let width = if let Some(sel) = selector {
143 let mut seq = String::with_capacity(cell.ch.len_utf8() + sel.ch.len_utf8());
144 seq.push(cell.ch);
145 seq.push(sel.ch);
146 seq.width()
147 } else {
148 cell.ch.width().unwrap_or(0)
149 };
150
151 if width == 0 {
152 i += if selector.is_some() { 2 } else { 1 };
153 continue;
154 }
155
156 out.push(cell);
157 if let Some(sel) = selector {
158 out.push(sel);
159 for _ in 2..width {
160 out.push(Cell::new('\0', cell.attr));
161 }
162 i += 2;
163 } else {
164 for _ in 1..width {
165 out.push(Cell::new('\0', cell.attr));
166 }
167 i += 1;
168 }
169 }
170 out
171}
172
173const WHEEL_STEP: usize = 3;
175
176const SCROLLBAR_WIDTH: usize = 1;
178
179pub const DEFAULT_MAX_LINES: usize = 10_000;
181
182fn wrap_cells(cells: &[Cell], width: usize) -> Vec<Vec<Cell>> {
197 if width == 0 || cells.is_empty() {
198 return vec![cells.to_vec()];
199 }
200
201 let mut rows = Vec::new();
202 let mut rest = cells;
203 while rest.len() > width {
204 let mut break_at = None;
209 for i in (0..width).rev() {
210 if rest[i].ch.is_whitespace() {
211 break_at = Some(i);
212 break;
213 }
214 }
215 if let Some(i) = break_at {
216 rows.push(rest[..i].to_vec());
217 rest = &rest[i + 1..]; } else {
219 let mut cut = width;
224 if cut > 1 && rest.get(cut).is_some_and(|c| c.ch == '\0') {
225 cut -= 1;
226 }
227 rows.push(rest[..cut].to_vec());
228 rest = &rest[cut..];
229 }
230 }
231 rows.push(rest.to_vec());
232 rows
233}
234
235#[derive(Debug)]
238pub struct StreamView {
239 bounds: Rect,
240 grow_mode: GrowFlags,
248 lines: Vec<Vec<Cell>>,
253 partial: Option<Vec<Cell>>,
255 wrapped: Vec<Vec<Cell>>,
259 row_counts: Vec<usize>,
263 partial_wrapped: Vec<Vec<Cell>>,
267 max_lines: usize,
273 top: usize,
275 follow: bool,
277 fill: Attr,
278 selection: Option<Selection>,
282 dragging_thumb: bool,
284}
285
286impl StreamView {
287 #[must_use]
288 pub fn new(bounds: Rect) -> Self {
289 Self {
290 bounds,
291 grow_mode: GF_GROW_HI_X | GF_GROW_HI_Y,
292 lines: Vec::new(),
293 partial: None,
294 wrapped: Vec::new(),
295 row_counts: Vec::new(),
296 partial_wrapped: Vec::new(),
297 max_lines: DEFAULT_MAX_LINES,
298 top: 0,
299 follow: true,
300 fill: Attr::new(TvColor::LightGray, TvColor::Black),
301 selection: None,
302 dragging_thumb: false,
303 }
304 }
305
306 fn width(&self) -> usize {
308 usize::try_from(self.bounds.width())
309 .unwrap_or(0)
310 .saturating_sub(SCROLLBAR_WIDTH)
311 }
312
313 fn scrollbar_x(&self) -> i16 {
315 self.bounds.b.x - 1
316 }
317
318 pub fn set_max_lines(&mut self, n: usize) {
319 self.max_lines = n.max(1);
320 self.trim();
321 }
322
323 pub fn push_line(&mut self, cells: &[Cell]) {
325 self.selection = None;
328 let normalized = normalize_line(cells);
329 let rows = wrap_cells(&normalized, self.width());
330 self.row_counts.push(rows.len());
331 self.wrapped.extend(rows);
332 self.lines.push(normalized);
333 self.trim();
334 if self.follow {
335 self.scroll_to_bottom();
336 }
337 }
338
339 pub fn set_partial(&mut self, cells: &[Cell]) {
342 let cells = normalize_line(cells);
343 if cells.is_empty() {
344 self.partial = None;
345 self.partial_wrapped.clear();
346 } else {
347 self.partial_wrapped = wrap_cells(&cells, self.width());
348 self.partial = Some(cells);
349 }
350 if self.follow {
351 self.scroll_to_bottom();
352 }
353 }
354
355 pub fn clear(&mut self) {
356 self.lines.clear();
357 self.partial = None;
358 self.wrapped.clear();
359 self.row_counts.clear();
360 self.partial_wrapped.clear();
361 self.top = 0;
362 self.follow = true;
363 self.selection = None;
364 }
365
366 #[must_use]
368 pub fn line_count(&self) -> usize {
369 self.lines.len() + usize::from(self.partial.is_some())
370 }
371
372 #[must_use]
377 pub fn row_count(&self) -> usize {
378 self.wrapped.len() + self.partial_wrapped.len()
379 }
380
381 fn page(&self) -> usize {
383 usize::try_from(self.bounds.height()).unwrap_or(0).max(1)
384 }
385
386 fn max_top(&self) -> usize {
387 self.row_count().saturating_sub(self.page())
388 }
389
390 fn rewrap(&mut self) {
395 let width = self.width();
396 self.wrapped.clear();
397 self.row_counts.clear();
398 for line in &self.lines {
399 let rows = wrap_cells(line, width);
400 self.row_counts.push(rows.len());
401 self.wrapped.extend(rows);
402 }
403 self.partial_wrapped = match &self.partial {
404 Some(cells) => wrap_cells(cells, width),
405 None => Vec::new(),
406 };
407 }
408
409 pub fn scroll_to_bottom(&mut self) {
410 self.top = self.max_top();
411 self.follow = true;
412 }
413
414 pub fn scroll_to_top(&mut self) {
415 self.top = 0;
416 self.follow = false;
417 }
418
419 pub fn scroll_up(&mut self, n: usize) {
420 self.top = self.top.saturating_sub(n);
421 self.follow = false;
422 }
423
424 pub fn scroll_down(&mut self, n: usize) {
425 self.set_top(self.top + n);
426 }
427
428 pub fn set_top(&mut self, top: usize) {
431 self.top = top.min(self.max_top());
432 self.follow = self.top == self.max_top();
433 }
434
435 #[must_use]
437 pub fn top(&self) -> usize {
438 self.top
439 }
440
441 fn track_len(&self) -> usize {
446 let page = self.page();
447 if page >= 3 { page - 2 } else { page }
448 }
449
450 fn track_start(&self) -> usize {
452 usize::from(self.page() >= 3)
453 }
454
455 fn thumb(&self) -> Option<(usize, usize)> {
458 let rows = self.row_count();
459 let page = self.page();
460 let track = self.track_len();
461 if rows <= page || track == 0 {
462 return None;
463 }
464 let len = (track * page / rows).clamp(1, track);
465 let usable = track - len;
466 let start = if usable == 0 {
467 0
468 } else {
469 (self.top * usable).div_ceil(self.max_top()).min(usable)
470 };
471 Some((start, len))
472 }
473
474 fn top_for_track_row(&self, y: i16) -> usize {
476 let Some((_, len)) = self.thumb() else {
477 return self.top;
478 };
479 let usable = self.track_len() - len;
480 if usable == 0 {
481 return self.top;
482 }
483 let rel = usize::try_from(y - self.bounds.a.y)
484 .unwrap_or(0)
485 .saturating_sub(self.track_start())
486 .min(usable);
487 rel * self.max_top() / usable
488 }
489
490 fn in_scrollbar(&self, pos: Point) -> bool {
491 pos.x == self.scrollbar_x()
492 && pos.y >= self.bounds.a.y
493 && pos.y < self.bounds.b.y
494 && self.bounds.width() > 0
495 }
496
497 fn in_view(&self, pos: Point) -> bool {
498 pos.x >= self.bounds.a.x
499 && pos.x < self.bounds.b.x
500 && pos.y >= self.bounds.a.y
501 && pos.y < self.bounds.b.y
502 }
503
504 fn scrollbar_click(&mut self, y: i16) {
507 let rel = usize::try_from(y - self.bounds.a.y).unwrap_or(0);
508 let page = self.page();
509 if page >= 3 && rel == 0 {
510 self.scroll_up(1);
511 } else if page >= 3 && rel == page - 1 {
512 self.scroll_down(1);
513 } else if let Some((start, len)) = self.thumb() {
514 let track_row = rel - self.track_start();
515 if track_row < start {
516 self.scroll_up(page);
517 } else if track_row >= start + len {
518 self.scroll_down(page);
519 } else {
520 self.dragging_thumb = true;
521 }
522 }
523 }
524
525 fn draw_scrollbar(&self, terminal: &mut Terminal) {
526 if self.bounds.width() <= 0 {
527 return;
528 }
529 let track_attr = Attr::new(TvColor::DarkGray, self.fill.bg);
530 let thumb_attr = Attr::new(TvColor::LightGray, self.fill.bg);
531 let page = self.page();
532 let thumb = self.thumb();
533 let track_start = self.track_start();
534 let x = self.scrollbar_x();
535 for row in 0..page {
536 let (ch, attr) = if page >= 3 && row == 0 {
537 ('▲', thumb_attr)
538 } else if page >= 3 && row == page - 1 {
539 ('▼', thumb_attr)
540 } else {
541 match thumb {
542 Some((start, len))
543 if row - track_start >= start && row - track_start < start + len =>
544 {
545 ('█', thumb_attr)
546 }
547 _ => ('░', track_attr),
548 }
549 };
550 let mut buf = DrawBuffer::new(1);
551 buf.put_char(0, ch, attr);
552 let y = self.bounds.a.y + i16::try_from(row).unwrap_or(i16::MAX);
553 write_line_to_terminal(terminal, x, y, &buf);
554 }
555 }
556
557 #[must_use]
558 pub fn is_at_bottom(&self) -> bool {
559 self.follow
560 }
561
562 pub fn select_all(&mut self) {
567 let rows = self.row_count();
568 if rows == 0 {
569 self.selection = None;
570 return;
571 }
572 let last = rows - 1;
573 let last_len = self.row_at(last).map_or(0, Vec::len);
574 self.selection = Some(Selection {
575 anchor: SelPos { row: 0, col: 0 },
576 head: SelPos {
577 row: last,
578 col: last_len,
579 },
580 mode: SelMode::Stream,
582 });
583 }
584
585 pub fn set_selection(&mut self, anchor: (usize, usize), head: (usize, usize)) {
588 self.selection = Some(Selection {
589 anchor: SelPos {
590 row: anchor.0,
591 col: anchor.1,
592 },
593 head: SelPos {
594 row: head.0,
595 col: head.1,
596 },
597 mode: SelMode::from_global(),
598 });
599 }
600
601 pub fn clear_selection(&mut self) {
602 self.selection = None;
603 }
604
605 #[must_use]
606 pub fn has_selection(&self) -> bool {
607 self.selection.is_some()
608 }
609
610 #[must_use]
613 pub fn selected_text(&self) -> Option<String> {
614 let sel = self.selection?;
615 if sel.mode == SelMode::Block {
616 return Some(self.block_text(sel));
617 }
618 let (start, end) = order(sel.anchor, sel.head);
619 let mut out = String::new();
620 for row in start.row..=end.row {
621 let Some(cells) = self.row_at(row) else {
622 continue;
623 };
624 let from = if row == start.row { start.col } else { 0 }.min(cells.len());
625 let to = if row == end.row { end.col } else { cells.len() }.min(cells.len());
626 if row > start.row && self.row_is_logical_start(row) {
627 out.push('\n');
628 }
629 out.extend(
630 cells[from..to.max(from)]
631 .iter()
632 .map(|c| c.ch)
633 .filter(|&ch| ch != '\0'),
634 );
635 }
636 Some(out)
637 }
638
639 fn block_text(&self, sel: Selection) -> String {
644 let (top, bottom, left, right) = block_bounds(sel);
645 let mut out = String::new();
646 for row in top..=bottom {
647 if row > top {
648 out.push('\n');
649 }
650 let Some(cells) = self.row_at(row) else {
651 continue;
652 };
653 let from = left.min(cells.len());
654 let to = right.min(cells.len());
655 out.extend(
656 cells[from..to.max(from)]
657 .iter()
658 .map(|c| c.ch)
659 .filter(|&ch| ch != '\0'),
660 );
661 }
662 out
663 }
664
665 fn row_at(&self, idx: usize) -> Option<&Vec<Cell>> {
666 self.iter_rows().nth(idx)
667 }
668
669 fn hit(&self, pos: Point, mode: SelMode) -> Option<SelPos> {
677 let x = usize::try_from(pos.x - self.bounds.a.x).ok()?;
678 let y = usize::try_from(pos.y - self.bounds.a.y).ok()?;
679 if y >= self.page() || x >= self.width() {
680 return None;
681 }
682 let abs_row = self.top + y;
683 let len = self.row_at(abs_row)?.len();
684 let col = match mode {
685 SelMode::Stream => x.min(len),
686 SelMode::Block => x,
687 };
688 Some(SelPos { row: abs_row, col })
689 }
690
691 fn is_selected(&self, abs_row: usize, col: usize) -> bool {
694 let Some(sel) = self.selection else {
695 return false;
696 };
697 if sel.mode == SelMode::Block {
698 let (top, bottom, left, right) = block_bounds(sel);
699 return (top..=bottom).contains(&abs_row) && (left..right).contains(&col);
700 }
701 let (s, e) = order(sel.anchor, sel.head);
702 (abs_row, col) >= (s.row, s.col) && (abs_row, col) < (e.row, e.col)
703 }
704
705 fn row_is_logical_start(&self, abs_row: usize) -> bool {
708 let mut offset = 0;
709 for &count in &self.row_counts {
710 if abs_row == offset {
711 return true;
712 }
713 offset += count;
714 }
715 abs_row == offset
717 }
718
719 #[must_use]
721 pub fn plain_text(&self) -> String {
722 let mut out = String::new();
723 for (i, line) in self.iter_lines().enumerate() {
724 if i > 0 {
725 out.push('\n');
726 }
727 out.extend(line.iter().map(|c| c.ch).filter(|&ch| ch != '\0'));
731 }
732 out
733 }
734
735 #[must_use]
737 pub fn styled_lines(&self) -> Vec<Vec<Cell>> {
738 self.iter_lines().cloned().collect()
739 }
740
741 fn iter_lines(&self) -> impl Iterator<Item = &Vec<Cell>> {
742 self.lines.iter().chain(self.partial.iter())
743 }
744
745 fn iter_rows(&self) -> impl Iterator<Item = &Vec<Cell>> {
748 self.wrapped.iter().chain(self.partial_wrapped.iter())
749 }
750
751 fn trim(&mut self) {
755 if self.lines.len() > self.max_lines {
756 let drop = self.lines.len() - self.max_lines;
757 self.lines.drain(..drop);
758 let dropped_rows: usize = self.row_counts.drain(..drop).sum();
759 self.wrapped.drain(..dropped_rows);
760 self.top = self.top.saturating_sub(dropped_rows);
761 }
762 }
763}
764
765impl View for StreamView {
766 fn bounds(&self) -> Rect {
767 self.bounds
768 }
769
770 fn set_bounds(&mut self, bounds: Rect) {
771 let width_changed = self.bounds.width() != bounds.width();
772 self.bounds = bounds;
773 if width_changed {
774 self.rewrap();
775 }
776 if self.follow {
777 self.scroll_to_bottom();
778 } else {
779 self.top = self.top.min(self.max_top());
780 }
781 }
782
783 fn draw(&mut self, terminal: &mut Terminal) {
784 if self.bounds.height() <= 0 {
785 return;
786 }
787 let width = self.width();
788 let page = self.page();
789 let rows: Vec<&Vec<Cell>> = self.iter_rows().skip(self.top).take(page).collect();
790
791 for row in 0..page {
792 let mut buf = DrawBuffer::new(width);
793 for i in 0..width {
794 buf.put_char(i, ' ', self.fill);
795 }
796 if let Some(line) = rows.get(row) {
797 let abs_row = self.top + row;
798 for (i, cell) in line.iter().take(width).enumerate() {
799 let attr = if self.is_selected(abs_row, i) {
800 reverse(cell.attr)
801 } else {
802 cell.attr
803 };
804 buf.put_char(i, cell.ch, attr);
805 }
806 }
807 let y = self.bounds.a.y + i16::try_from(row).unwrap_or(i16::MAX);
808 write_line_to_terminal(terminal, self.bounds.a.x, y, &buf);
809 }
810 self.draw_scrollbar(terminal);
811 }
812
813 fn handle_event(&mut self, event: &mut Event) {
814 match event.what {
815 EventType::Keyboard => {
816 let page = self.page();
817 match event.key_code {
818 KB_UP => self.scroll_up(1),
819 KB_DOWN => self.scroll_down(1),
820 KB_PGUP => self.scroll_up(page),
821 KB_PGDN => self.scroll_down(page),
822 KB_HOME => self.scroll_to_top(),
823 KB_END => self.scroll_to_bottom(),
824 KB_ESC if self.selection.is_some() => self.clear_selection(),
825 _ => return,
826 }
827 event.clear();
828 }
829 EventType::MouseWheelUp if self.in_view(event.mouse.pos) => {
830 self.scroll_up(WHEEL_STEP);
831 event.clear();
832 }
833 EventType::MouseWheelDown if self.in_view(event.mouse.pos) => {
834 self.scroll_down(WHEEL_STEP);
835 event.clear();
836 }
837 EventType::MouseDown
838 if event.mouse.buttons & MB_LEFT_BUTTON != 0
839 && self.in_scrollbar(event.mouse.pos) =>
840 {
841 self.scrollbar_click(event.mouse.pos.y);
842 event.clear();
843 }
844 EventType::MouseMove | EventType::MouseAuto if self.dragging_thumb => {
845 let top = self.top_for_track_row(event.mouse.pos.y);
846 self.set_top(top);
847 event.clear();
848 }
849 EventType::MouseDown if event.mouse.buttons & MB_LEFT_BUTTON != 0 => {
850 let mode = SelMode::from_global();
851 let Some(pos) = self.hit(event.mouse.pos, mode) else {
852 return;
853 };
854 self.selection = Some(Selection {
855 anchor: pos,
856 head: pos,
857 mode,
858 });
859 event.clear();
860 }
861 EventType::MouseMove | EventType::MouseAuto
862 if event.mouse.buttons & MB_LEFT_BUTTON != 0 =>
863 {
864 if let Some(mut sel) = self.selection
865 && let Some(pos) = self.hit(event.mouse.pos, sel.mode)
866 {
867 sel.head = pos;
868 self.selection = Some(sel);
869 event.clear();
870 }
871 }
872 EventType::MouseUp => {
873 self.dragging_thumb = false;
874 if let Some(sel) = self.selection
877 && sel.anchor == sel.head
878 {
879 self.selection = None;
880 }
881 event.clear();
882 }
883 _ => {}
884 }
885 }
886
887 fn grow_mode(&self) -> GrowFlags {
888 self.grow_mode
889 }
890
891 fn set_grow_mode(&mut self, grow_mode: GrowFlags) {
892 self.grow_mode = grow_mode;
893 }
894
895 fn can_focus(&self) -> bool {
896 true
897 }
898
899 fn get_palette(&self) -> Option<turbo_vision::core::palette::Palette> {
900 None
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909 use std::io;
910 use std::time::Duration;
911 use turbo_vision::core::palette::TvColor;
912 use turbo_vision::terminal::Backend;
913
914 fn line(s: &str) -> Vec<Cell> {
915 s.chars()
916 .map(|c| Cell::new(c, Attr::new(TvColor::LightGray, TvColor::Black)))
917 .collect()
918 }
919
920 fn view() -> StreamView {
923 StreamView::new(Rect::new(0, 0, 41, 10))
924 }
925
926 #[test]
927 fn select_all_extracts_logical_lines_without_soft_wrap_newlines() {
928 let mut v = StreamView::new(Rect::new(0, 0, 11, 10));
929 v.push_line(&line("hello"));
930 v.push_line(&line("abcdefghijABCDEFGHIJ")); v.select_all();
932 assert_eq!(
933 v.selected_text().unwrap(),
934 "hello\nabcdefghijABCDEFGHIJ",
935 "soft wraps within a logical line must not become newlines"
936 );
937 }
938
939 #[test]
940 fn stream_selection_spans_from_anchor_to_head_across_a_line_break() {
941 let mut v = view();
942 v.push_line(&line("hello"));
943 v.push_line(&line("world"));
944 v.set_selection((0, 2), (1, 3));
946 assert_eq!(v.selected_text().unwrap(), "llo\nwor");
947 }
948
949 #[test]
950 fn stream_selection_is_order_independent() {
951 let mut v = view();
952 v.push_line(&line("hello"));
953 v.push_line(&line("world"));
954 v.set_selection((1, 3), (0, 2)); assert_eq!(v.selected_text().unwrap(), "llo\nwor");
956 }
957
958 #[test]
959 fn no_selection_yields_no_text() {
960 let mut v = view();
961 v.push_line(&line("hello"));
962 assert!(v.selected_text().is_none());
963 assert!(!v.has_selection());
964 }
965
966 #[test]
967 fn selected_cells_render_reverse_video() {
968 let mut v = StreamView::new(Rect::new(0, 0, 8, 4));
969 v.push_line(&line("abcd"));
970 v.set_selection((0, 1), (0, 3)); let mut terminal = fake_terminal(20, 10);
972 v.draw(&mut terminal);
973 let a = terminal.read_cell(0, 0).unwrap(); let b = terminal.read_cell(1, 0).unwrap(); assert_eq!(b.ch, 'b');
976 assert_eq!(b.attr.fg, a.attr.bg, "selected fg is the normal bg");
977 assert_eq!(b.attr.bg, a.attr.fg, "selected bg is the normal fg");
978 let d = terminal.read_cell(3, 0).unwrap();
980 assert_eq!(d.attr.fg, a.attr.fg, "col 3 is outside [1,3), so normal");
981 }
982
983 #[test]
984 fn mouse_drag_creates_a_stream_selection() {
985 let mut v = view();
986 v.push_line(&line("hello"));
987 v.push_line(&line("world"));
988 let mut down = Event::mouse(
989 EventType::MouseDown,
990 Point::new(2, 0),
991 MB_LEFT_BUTTON,
992 false,
993 );
994 v.handle_event(&mut down);
995 let mut mv = Event::mouse(
996 EventType::MouseMove,
997 Point::new(3, 1),
998 MB_LEFT_BUTTON,
999 false,
1000 );
1001 v.handle_event(&mut mv);
1002 let mut up = Event::mouse(EventType::MouseUp, Point::new(3, 1), 0, false);
1003 v.handle_event(&mut up);
1004 assert_eq!(v.selected_text().unwrap(), "llo\nwor");
1005 }
1006
1007 struct BlockModeGuard(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
1010
1011 impl BlockModeGuard {
1012 fn on() -> Self {
1013 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1014 let guard = LOCK
1015 .lock()
1016 .unwrap_or_else(std::sync::PoisonError::into_inner);
1017 turbo_vision::core::state::set_block_edit_mode(true);
1018 Self(guard)
1019 }
1020 }
1021
1022 impl Drop for BlockModeGuard {
1023 fn drop(&mut self) {
1024 turbo_vision::core::state::set_block_edit_mode(false);
1025 }
1026 }
1027
1028 #[test]
1029 fn block_mode_selects_a_column_band_from_every_row_it_spans() {
1030 let _guard = BlockModeGuard::on();
1031 let mut v = view();
1032 v.push_line(&line("abcdef"));
1033 v.push_line(&line("gh"));
1034 v.push_line(&line("klmnop"));
1035 v.set_selection((0, 2), (2, 5));
1037 assert_eq!(
1038 v.selected_text().unwrap(),
1039 "cde
1040
1041mno"
1042 );
1043 }
1044
1045 #[test]
1046 fn a_block_drag_keeps_its_column_band_over_a_short_row() {
1047 let _guard = BlockModeGuard::on();
1048 let mut v = view();
1049 v.push_line(&line("abcdef"));
1050 v.push_line(&line("gh"));
1051 v.push_line(&line("klmnop"));
1052 let mut down = Event::mouse(
1053 EventType::MouseDown,
1054 Point::new(2, 0),
1055 MB_LEFT_BUTTON,
1056 false,
1057 );
1058 v.handle_event(&mut down);
1059 let mut mv = Event::mouse(
1062 EventType::MouseMove,
1063 Point::new(5, 1),
1064 MB_LEFT_BUTTON,
1065 false,
1066 );
1067 v.handle_event(&mut mv);
1068 let mut mv = Event::mouse(
1069 EventType::MouseMove,
1070 Point::new(5, 2),
1071 MB_LEFT_BUTTON,
1072 false,
1073 );
1074 v.handle_event(&mut mv);
1075 let mut up = Event::mouse(EventType::MouseUp, Point::new(5, 2), 0, false);
1076 v.handle_event(&mut up);
1077 assert_eq!(
1078 v.selected_text().unwrap(),
1079 "cde
1080
1081mno"
1082 );
1083 }
1084
1085 #[test]
1086 fn block_mode_highlights_only_the_column_band() {
1087 let _guard = BlockModeGuard::on();
1088 let mut v = view();
1089 v.push_line(&line("abcdef"));
1090 v.push_line(&line("klmnop"));
1091 v.set_selection((0, 2), (1, 5));
1092 assert!(v.is_selected(0, 3), "cols 2..5 of row 0 are in the band");
1093 assert!(!v.is_selected(0, 5), "col 5 is past the band");
1094 assert!(!v.is_selected(1, 1), "col 1 is before the band");
1095 assert!(
1096 !v.is_selected(0, 1),
1097 "a stream selection would have taken the whole tail of row 0"
1098 );
1099 }
1100
1101 #[test]
1102 fn select_all_stays_a_stream_selection_in_block_mode() {
1103 let _guard = BlockModeGuard::on();
1104 let mut v = view();
1105 v.push_line(&line("abcdef"));
1106 v.push_line(&line("gh"));
1107 v.select_all();
1108 assert_eq!(
1109 v.selected_text().unwrap(),
1110 "abcdef
1111gh"
1112 );
1113 }
1114
1115 #[test]
1116 fn a_plain_click_clears_any_selection() {
1117 let mut v = view();
1118 v.push_line(&line("hello"));
1119 v.select_all();
1120 assert!(v.has_selection());
1121 let mut down = Event::mouse(
1122 EventType::MouseDown,
1123 Point::new(2, 0),
1124 MB_LEFT_BUTTON,
1125 false,
1126 );
1127 v.handle_event(&mut down);
1128 let mut up = Event::mouse(EventType::MouseUp, Point::new(2, 0), 0, false);
1129 v.handle_event(&mut up);
1130 assert!(!v.has_selection(), "click without drag deselects");
1131 }
1132
1133 #[test]
1134 fn esc_clears_the_selection() {
1135 let mut v = view();
1136 v.push_line(&line("hello"));
1137 v.select_all();
1138 let mut esc = Event::keyboard(KB_ESC);
1139 v.handle_event(&mut esc);
1140 assert!(!v.has_selection());
1141 }
1142
1143 #[test]
1144 fn mutating_the_buffer_clears_the_selection() {
1145 let mut v = view();
1146 v.push_line(&line("hello"));
1147 v.select_all();
1148 assert!(v.has_selection());
1149 v.push_line(&line("more"));
1150 assert!(
1151 !v.has_selection(),
1152 "new content must drop a stale selection"
1153 );
1154 }
1155
1156 struct FakeBackend {
1161 width: u16,
1162 height: u16,
1163 }
1164
1165 impl Backend for FakeBackend {
1166 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1167 self
1168 }
1169
1170 fn init(&mut self) -> io::Result<()> {
1171 Ok(())
1172 }
1173
1174 fn cleanup(&mut self) -> io::Result<()> {
1175 Ok(())
1176 }
1177
1178 fn size(&self) -> io::Result<(u16, u16)> {
1179 Ok((self.width, self.height))
1180 }
1181
1182 fn poll_event(&mut self, _timeout: Duration) -> io::Result<Option<Event>> {
1183 Ok(None)
1184 }
1185
1186 fn write_raw(&mut self, _data: &[u8]) -> io::Result<()> {
1187 Ok(())
1188 }
1189
1190 fn flush(&mut self) -> io::Result<()> {
1191 Ok(())
1192 }
1193
1194 fn show_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> {
1195 Ok(())
1196 }
1197
1198 fn hide_cursor(&mut self) -> io::Result<()> {
1199 Ok(())
1200 }
1201 }
1202
1203 fn fake_terminal(width: u16, height: u16) -> Terminal {
1204 Terminal::with_backend(Box::new(FakeBackend { width, height }))
1205 .expect("fake backend never fails to init")
1206 }
1207
1208 #[derive(Clone, Default)]
1218 struct RecordingBackend {
1219 width: u16,
1220 height: u16,
1221 output: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
1222 }
1223
1224 impl Backend for RecordingBackend {
1225 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1226 self
1227 }
1228
1229 fn init(&mut self) -> io::Result<()> {
1230 Ok(())
1231 }
1232
1233 fn cleanup(&mut self) -> io::Result<()> {
1234 Ok(())
1235 }
1236
1237 fn size(&self) -> io::Result<(u16, u16)> {
1238 Ok((self.width, self.height))
1239 }
1240
1241 fn poll_event(&mut self, _timeout: Duration) -> io::Result<Option<Event>> {
1242 Ok(None)
1243 }
1244
1245 fn write_raw(&mut self, data: &[u8]) -> io::Result<()> {
1246 self.output.lock().unwrap().extend_from_slice(data);
1247 Ok(())
1248 }
1249
1250 fn flush(&mut self) -> io::Result<()> {
1251 Ok(())
1252 }
1253
1254 fn show_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> {
1255 Ok(())
1256 }
1257
1258 fn hide_cursor(&mut self) -> io::Result<()> {
1259 Ok(())
1260 }
1261 }
1262
1263 fn recording_terminal(
1267 width: u16,
1268 height: u16,
1269 ) -> (Terminal, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
1270 let output = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1271 let backend = RecordingBackend {
1272 width,
1273 height,
1274 output: output.clone(),
1275 };
1276 let terminal =
1277 Terminal::with_backend(Box::new(backend)).expect("fake backend never fails to init");
1278 (terminal, output)
1279 }
1280
1281 fn replay_onto_grid(bytes: &[u8], grid: &mut [Vec<char>]) {
1292 let text = std::str::from_utf8(bytes).expect("flush emits valid UTF-8");
1293 let mut chars = text.chars().peekable();
1294 let mut row = 0usize;
1295 let mut col = 0usize;
1296 while let Some(c) = chars.next() {
1297 if c == '\u{1b}' && chars.peek() == Some(&'[') {
1298 chars.next(); let mut params = String::new();
1300 let mut final_byte = ' ';
1301 for pc in chars.by_ref() {
1302 if pc.is_ascii_digit() || pc == ';' {
1303 params.push(pc);
1304 } else {
1305 final_byte = pc;
1306 break;
1307 }
1308 }
1309 if final_byte == 'H' {
1310 let mut parts = params.split(';');
1311 let r: usize = parts.next().and_then(|p| p.parse().ok()).unwrap_or(1);
1312 let cix: usize = parts.next().and_then(|p| p.parse().ok()).unwrap_or(1);
1313 row = r.saturating_sub(1);
1314 col = cix.saturating_sub(1);
1315 }
1316 continue;
1318 }
1319 let width = c.width().unwrap_or(0);
1320 if row < grid.len() && col < grid[row].len() {
1321 grid[row][col] = c;
1322 }
1323 col += width;
1324 }
1325 }
1326
1327 #[test]
1328 fn scrollback_cap_drops_oldest_lines() {
1329 let mut v = view();
1330 v.set_max_lines(3);
1331 for i in 0..5 {
1332 v.push_line(&line(&i.to_string()));
1333 }
1334 assert_eq!(v.line_count(), 3);
1335 assert_eq!(v.plain_text(), "2\n3\n4");
1336 }
1337
1338 #[test]
1339 fn autoscroll_holds_at_bottom_while_lines_arrive() {
1340 let mut v = view();
1341 for i in 0..50 {
1342 v.push_line(&line(&i.to_string()));
1343 }
1344 assert!(v.is_at_bottom());
1345 }
1346
1347 #[test]
1348 fn scrolling_up_releases_autoscroll_and_end_rearms_it() {
1349 let mut v = view();
1350 for i in 0..50 {
1351 v.push_line(&line(&i.to_string()));
1352 }
1353 v.scroll_up(5);
1354 assert!(!v.is_at_bottom());
1355 v.push_line(&line("new"));
1356 assert!(
1357 !v.is_at_bottom(),
1358 "a new line must not yank a scrolled-back reader to the bottom"
1359 );
1360 v.scroll_to_bottom();
1361 assert!(v.is_at_bottom());
1362 }
1363
1364 #[test]
1365 fn partial_line_is_replaced_not_appended() {
1366 let mut v = view();
1367 v.set_partial(&line("par"));
1368 v.set_partial(&line("part"));
1369 assert_eq!(v.plain_text(), "part");
1370 assert_eq!(v.line_count(), 1);
1371 }
1372
1373 #[test]
1374 fn plain_text_strips_attributes() {
1375 let mut v = view();
1376 v.push_line(&[Cell::new('x', Attr::new(TvColor::LightRed, TvColor::Blue))]);
1377 assert_eq!(v.plain_text(), "x");
1378 }
1379
1380 #[test]
1381 fn resize_larger_while_scrolled_back_reclamps_top_to_show_a_full_page() {
1382 let mut v = StreamView::new(Rect::new(0, 0, 40, 5));
1383 for i in 0..50 {
1384 v.push_line(&line(&i.to_string()));
1385 }
1386 v.scroll_to_top();
1389 v.scroll_down(40);
1390 assert!(!v.is_at_bottom());
1391 let old_top = v.top;
1392 assert!(old_top < v.max_top());
1393
1394 v.set_bounds(Rect::new(0, 0, 40, 48));
1399
1400 assert!(
1401 v.top <= v.max_top(),
1402 "top ({}) must not exceed max_top ({}) after growing",
1403 v.top,
1404 v.max_top()
1405 );
1406 let rows: Vec<&Vec<Cell>> = v.iter_rows().skip(v.top).take(v.page()).collect();
1407 assert_eq!(
1408 rows.len(),
1409 v.page().min(v.row_count()),
1410 "a full page of content should be visible after growing"
1411 );
1412 }
1413
1414 #[test]
1415 fn draw_clips_to_bounds_width() {
1416 let mut v = StreamView::new(Rect::new(2, 1, 9, 4));
1417 v.push_line(&line("short")); let mut terminal = fake_terminal(20, 10);
1420 v.draw(&mut terminal);
1421
1422 for (i, expected) in "short ".chars().enumerate() {
1425 let cell = terminal
1426 .read_cell(2 + i16::try_from(i).unwrap_or(i16::MAX), 1)
1427 .expect("cell within terminal bounds");
1428 assert_eq!(cell.ch, expected);
1429 }
1430 assert_eq!(terminal.read_cell(8, 1).unwrap().ch, '▲');
1433 assert_eq!(terminal.read_cell(9, 1).unwrap().ch, ' ');
1434
1435 assert_eq!(terminal.read_cell(2, 0).unwrap().ch, ' ');
1437 }
1438
1439 const WRENCH: &str = "\u{1F6E0}\u{FE0F}";
1449
1450 #[test]
1451 fn wide_character_row_paints_the_correct_total_number_of_columns() {
1452 let mut v = StreamView::new(Rect::new(0, 0, 11, 4));
1454 v.push_line(&line(&format!("{WRENCH} x")));
1455 let mut terminal = fake_terminal(20, 10);
1456 v.draw(&mut terminal);
1457
1458 assert_eq!(terminal.read_cell(0, 0).unwrap().ch, '\u{1F6E0}');
1460 assert_eq!(terminal.read_cell(1, 0).unwrap().ch, '\u{FE0F}');
1464 assert_eq!(terminal.read_cell(2, 0).unwrap().ch, ' ');
1466 assert_eq!(terminal.read_cell(3, 0).unwrap().ch, 'x');
1467 for x in 4..10 {
1469 assert_eq!(terminal.read_cell(x, 0).unwrap().ch, ' ');
1470 }
1471 }
1472
1473 #[test]
1474 fn text_after_a_wide_character_lands_at_the_right_column() {
1475 let mut v = StreamView::new(Rect::new(0, 0, 30, 4));
1476 v.push_line(&line(&format!("{WRENCH} Reading src/dsml.rs")));
1477 let mut terminal = fake_terminal(30, 10);
1478 v.draw(&mut terminal);
1479
1480 let expected = "\u{1F6E0}\u{FE0F} Reading src/dsml.rs";
1481 for (i, expected_ch) in expected.chars().enumerate() {
1482 let cell = terminal
1483 .read_cell(i16::try_from(i).unwrap(), 0)
1484 .expect("cell within terminal bounds");
1485 assert_eq!(cell.ch, expected_ch, "column {i} mismatch");
1486 }
1487 }
1488
1489 #[test]
1490 fn short_row_is_blank_padded_so_nothing_shows_through_from_beneath() {
1491 let mut v = StreamView::new(Rect::new(0, 0, 11, 4));
1492 v.push_line(&line("XXXXXXXXXX"));
1494 let mut terminal = fake_terminal(20, 10);
1495 v.draw(&mut terminal);
1496 v.clear();
1499 v.push_line(&line(&format!("{WRENCH}hi")));
1500 v.draw(&mut terminal);
1501
1502 assert_eq!(terminal.read_cell(0, 0).unwrap().ch, '\u{1F6E0}');
1503 assert_eq!(terminal.read_cell(1, 0).unwrap().ch, '\u{FE0F}');
1504 assert_eq!(terminal.read_cell(2, 0).unwrap().ch, 'h');
1505 assert_eq!(terminal.read_cell(3, 0).unwrap().ch, 'i');
1506 for x in 4..10 {
1507 assert_eq!(
1508 terminal.read_cell(x, 0).unwrap().ch,
1509 ' ',
1510 "column {x} must be blanked, not left over from the previous row"
1511 );
1512 }
1513 }
1514
1515 #[test]
1516 fn a_double_width_character_straddling_a_wrap_boundary_is_never_split() {
1517 let mut v = StreamView::new(Rect::new(0, 0, 4, 4));
1523 v.push_line(&line("ab中cd"));
1524
1525 assert_eq!(v.row_count(), 3, "the 6-column line wraps to three rows");
1526
1527 let mut terminal = fake_terminal(20, 10);
1528 v.draw(&mut terminal);
1529
1530 assert_eq!(terminal.read_cell(0, 0).unwrap().ch, 'a');
1533 assert_eq!(terminal.read_cell(1, 0).unwrap().ch, 'b');
1534
1535 assert_eq!(terminal.read_cell(0, 1).unwrap().ch, '中');
1537 assert_eq!(terminal.read_cell(1, 1).unwrap().ch, '\0');
1538 assert_eq!(terminal.read_cell(2, 1).unwrap().ch, 'c');
1539
1540 assert_eq!(terminal.read_cell(0, 2).unwrap().ch, 'd');
1542 }
1543
1544 #[test]
1545 fn plain_text_round_trips_a_wide_character_with_no_padding_artifacts() {
1546 let mut v = view();
1547 v.push_line(&line(&format!("{WRENCH} Reading src/dsml.rs")));
1548 assert_eq!(v.plain_text(), format!("{WRENCH} Reading src/dsml.rs"));
1549 }
1550
1551 #[test]
1562 fn a_covering_window_s_flush_fully_blanks_a_row_that_held_a_wide_character() {
1563 let (mut terminal, output) = recording_terminal(31, 4);
1564 let mut grid = vec![vec![' '; 31]; 4];
1565
1566 let mut lower = StreamView::new(Rect::new(0, 0, 31, 4));
1568 lower.push_line(&line(&format!("{WRENCH} Reading src/dsml.rs 1:500...")));
1569 lower.draw(&mut terminal);
1570 terminal
1571 .flush()
1572 .expect("flush never fails against a fake backend");
1573 replay_onto_grid(&output.lock().unwrap(), &mut grid);
1574 output.lock().unwrap().clear();
1575
1576 let mut upper = StreamView::new(Rect::new(0, 0, 31, 4));
1580 upper.draw(&mut terminal);
1581 terminal
1582 .flush()
1583 .expect("flush never fails against a fake backend");
1584 replay_onto_grid(&output.lock().unwrap(), &mut grid);
1585
1586 for (col, &ch) in grid[0].iter().take(30).enumerate() {
1589 assert_eq!(
1590 ch, ' ',
1591 "row 0 column {col} still shows a leftover character from \
1592 the window underneath: {grid:?}"
1593 );
1594 }
1595 }
1596
1597 #[test]
1598 fn a_line_longer_than_the_width_wraps_across_the_right_number_of_rows_with_complete_content() {
1599 let mut v = StreamView::new(Rect::new(0, 0, 11, 20));
1600 let text = "abcdefghijklmnopqrstuvwxy";
1602 v.push_line(&line(text));
1603
1604 assert_eq!(v.row_count(), 3);
1605 assert_eq!(
1606 v.plain_text(),
1607 text,
1608 "wrapping must not drop or duplicate any character"
1609 );
1610
1611 let mut terminal = fake_terminal(20, 20);
1614 v.draw(&mut terminal);
1615 let mut rendered = String::new();
1616 for row in 0..3 {
1617 for col in 0..10 {
1618 rendered.push(terminal.read_cell(col, row).unwrap().ch);
1619 }
1620 }
1621 assert_eq!(rendered, "abcdefghijklmnopqrstuvwxy ");
1622 }
1623
1624 #[test]
1625 fn a_wrap_breaks_at_a_space_rather_than_mid_word_when_one_is_available() {
1626 let mut v = StreamView::new(Rect::new(0, 0, 11, 20));
1627 v.push_line(&line("hello world"));
1628
1629 assert_eq!(v.row_count(), 2);
1634 let mut terminal = fake_terminal(20, 20);
1635 v.draw(&mut terminal);
1636 for (i, expected) in "hello ".chars().enumerate() {
1637 assert_eq!(
1638 terminal.read_cell(i16::try_from(i).unwrap(), 0).unwrap().ch,
1639 expected
1640 );
1641 }
1642 for (i, expected) in "world ".chars().enumerate() {
1643 assert_eq!(
1644 terminal.read_cell(i16::try_from(i).unwrap(), 1).unwrap().ch,
1645 expected
1646 );
1647 }
1648 }
1649
1650 #[test]
1651 fn a_single_token_longer_than_the_width_is_broken_rather_than_truncated() {
1652 let mut v = StreamView::new(Rect::new(0, 0, 5, 20));
1653 v.push_line(&line("abcdefghijkl"));
1657
1658 assert_eq!(v.row_count(), 3); assert_eq!(
1660 v.plain_text(),
1661 "abcdefghijkl",
1662 "the logical text is preserved even though it had to be broken mid-token"
1663 );
1664 }
1665
1666 #[test]
1667 fn plain_text_returns_the_original_unwrapped_logical_lines() {
1668 let mut v = StreamView::new(Rect::new(0, 0, 5, 20));
1669 v.push_line(&line("a much longer line than the five-column view"));
1670 v.push_line(&line("short"));
1671
1672 assert_eq!(
1673 v.plain_text(),
1674 "a much longer line than the five-column view\nshort",
1675 "Save As must get the original logical lines, not this window's wrap points"
1676 );
1677 }
1678
1679 #[test]
1680 fn resizing_narrower_then_wider_rewraps_and_content_survives_both() {
1681 let mut v = StreamView::new(Rect::new(0, 0, 21, 20));
1682 let text = "abcdefghijklmnopqrstuvwxyz";
1683 v.push_line(&line(text));
1684 assert_eq!(v.row_count(), 2); v.set_bounds(Rect::new(0, 0, 6, 20));
1687 assert_eq!(v.row_count(), 6); assert_eq!(v.plain_text(), text);
1689
1690 v.set_bounds(Rect::new(0, 0, 31, 20));
1691 assert_eq!(v.row_count(), 1); assert_eq!(v.plain_text(), text);
1693 }
1694
1695 #[test]
1696 fn scrolling_by_page_lands_correctly_when_wrapped_rows_are_present() {
1697 let mut v = StreamView::new(Rect::new(0, 0, 5, 5));
1699 let text: String = (0..80).map(|i| char::from(b'a' + (i % 26))).collect();
1700 v.push_line(&line(&text));
1701 assert_eq!(v.row_count(), 20);
1702
1703 v.scroll_to_top();
1704 assert_eq!(v.top, 0);
1705 v.scroll_down(v.page()); assert_eq!(
1707 v.top, 5,
1708 "paging must move by display rows, not logical lines"
1709 );
1710
1711 v.scroll_to_bottom();
1712 assert_eq!(v.top, v.row_count() - v.page());
1713 }
1714
1715 fn mouse(what: EventType, x: i16, y: i16, buttons: u8) -> Event {
1716 Event::mouse(what, Point::new(x, y), buttons, false)
1717 }
1718
1719 fn scrollable_view() -> StreamView {
1721 let mut v = view();
1722 for i in 0..50 {
1723 v.push_line(&line(&i.to_string()));
1724 }
1725 v
1726 }
1727
1728 #[test]
1729 fn mouse_wheel_scrolls_by_a_few_rows_and_releases_autoscroll() {
1730 let mut v = scrollable_view();
1731 assert!(v.is_at_bottom());
1732 let mut ev = mouse(EventType::MouseWheelUp, 5, 5, 0);
1733 v.handle_event(&mut ev);
1734 assert_eq!(ev.what, EventType::Nothing, "the wheel event is consumed");
1735 assert_eq!(v.top(), 40 - WHEEL_STEP);
1736 assert!(!v.is_at_bottom());
1737
1738 v.handle_event(&mut mouse(EventType::MouseWheelDown, 5, 5, 0));
1739 assert_eq!(v.top(), 40);
1740 assert!(
1741 v.is_at_bottom(),
1742 "wheeling back to the end re-arms autoscroll"
1743 );
1744 }
1745
1746 #[test]
1747 fn mouse_wheel_outside_the_view_is_ignored() {
1748 let mut v = scrollable_view();
1749 let mut ev = mouse(EventType::MouseWheelUp, 60, 5, 0);
1750 v.handle_event(&mut ev);
1751 assert_eq!(ev.what, EventType::MouseWheelUp);
1752 assert_eq!(v.top(), 40);
1753 }
1754
1755 #[test]
1756 fn scrollbar_arrows_step_one_row_and_track_pages() {
1757 let mut v = scrollable_view();
1758 let x = v.scrollbar_x();
1759 assert_eq!(x, 40);
1760
1761 v.handle_event(&mut mouse(EventType::MouseDown, x, 0, MB_LEFT_BUTTON));
1762 assert_eq!(v.top(), 39, "up arrow steps one row");
1763 v.handle_event(&mut mouse(EventType::MouseDown, x, 9, MB_LEFT_BUTTON));
1764 assert_eq!(v.top(), 40, "down arrow steps one row");
1765
1766 v.handle_event(&mut mouse(EventType::MouseDown, x, 1, MB_LEFT_BUTTON));
1769 assert_eq!(v.top(), 30, "track above the thumb pages up");
1770 v.scroll_to_top();
1771 v.handle_event(&mut mouse(EventType::MouseDown, x, 8, MB_LEFT_BUTTON));
1772 assert_eq!(v.top(), 10, "track below the thumb pages down");
1773 }
1774
1775 #[test]
1776 fn dragging_the_thumb_scrolls_and_a_click_on_the_scrollbar_never_selects() {
1777 let mut v = scrollable_view();
1778 let x = v.scrollbar_x();
1779 v.scroll_to_top();
1780 let (start, len) = v.thumb().expect("50 rows in a 10-row view scroll");
1781 assert_eq!((start, len), (0, 1));
1782
1783 v.handle_event(&mut mouse(EventType::MouseDown, x, 1, MB_LEFT_BUTTON));
1786 assert!(v.dragging_thumb);
1787 assert!(
1788 v.selection.is_none(),
1789 "a scrollbar press must not start a selection"
1790 );
1791 v.handle_event(&mut mouse(EventType::MouseMove, x, 8, MB_LEFT_BUTTON));
1792 assert_eq!(v.top(), 40);
1793 assert!(v.is_at_bottom());
1794 v.handle_event(&mut mouse(EventType::MouseMove, x, 4, MB_LEFT_BUTTON));
1795 assert!(v.top() > 0 && v.top() < 40);
1796 v.handle_event(&mut mouse(EventType::MouseUp, x, 4, 0));
1797 assert!(!v.dragging_thumb);
1798 assert!(v.selection.is_none());
1799 }
1800
1801 #[test]
1802 fn scrollbar_draws_arrows_and_a_thumb_that_tracks_the_position() {
1803 let mut v = scrollable_view();
1804 let x = v.scrollbar_x();
1805 let mut terminal = fake_terminal(50, 10);
1806 v.draw(&mut terminal);
1807 assert_eq!(terminal.read_cell(x, 0).unwrap().ch, '▲');
1808 assert_eq!(terminal.read_cell(x, 9).unwrap().ch, '▼');
1809 assert_eq!(terminal.read_cell(x, 8).unwrap().ch, '█');
1811 assert_eq!(terminal.read_cell(x, 1).unwrap().ch, '░');
1812
1813 v.scroll_to_top();
1814 v.draw(&mut terminal);
1815 assert_eq!(terminal.read_cell(x, 1).unwrap().ch, '█');
1816 assert_eq!(terminal.read_cell(x, 8).unwrap().ch, '░');
1817 }
1818
1819 #[test]
1820 fn scrollbar_has_no_thumb_when_everything_fits() {
1821 let mut v = view();
1822 v.push_line(&line("one"));
1823 assert!(v.thumb().is_none());
1824 let mut terminal = fake_terminal(50, 10);
1825 v.draw(&mut terminal);
1826 for y in 1..9 {
1827 assert_eq!(terminal.read_cell(v.scrollbar_x(), y).unwrap().ch, '░');
1828 }
1829 }
1830
1831 #[test]
1832 fn draw_on_zero_height_view_writes_nothing() {
1833 let mut v = StreamView::new(Rect::new(0, 0, 10, 0));
1834 v.push_line(&line("hello"));
1835 let mut terminal = fake_terminal(20, 10);
1836 v.draw(&mut terminal);
1837 for y in 0..10 {
1838 for x in 0..20 {
1839 assert_eq!(
1840 terminal.read_cell(x, y).unwrap().ch,
1841 ' ',
1842 "zero-height view must not write any cell"
1843 );
1844 }
1845 }
1846 }
1847}