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_HOME, KB_PGDN, KB_PGUP, KB_UP,
11};
12use turbo_vision::core::geometry::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
18const PRESENTATION_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
30
31fn normalize_line(cells: &[Cell]) -> Vec<Cell> {
58 let mut out = Vec::with_capacity(cells.len());
59 let mut i = 0;
60 while i < cells.len() {
61 let cell = cells[i];
62 if cell.ch == '\0' {
63 out.push(cell);
64 i += 1;
65 continue;
66 }
67
68 let next = cells.get(i + 1).copied();
69 let selector = next.filter(|n| PRESENTATION_SELECTORS.contains(&n.ch));
70
71 let width = if let Some(sel) = selector {
72 let mut seq = String::with_capacity(cell.ch.len_utf8() + sel.ch.len_utf8());
73 seq.push(cell.ch);
74 seq.push(sel.ch);
75 seq.width()
76 } else {
77 cell.ch.width().unwrap_or(0)
78 };
79
80 if width == 0 {
81 i += if selector.is_some() { 2 } else { 1 };
82 continue;
83 }
84
85 out.push(cell);
86 if let Some(sel) = selector {
87 out.push(sel);
88 for _ in 2..width {
89 out.push(Cell::new('\0', cell.attr));
90 }
91 i += 2;
92 } else {
93 for _ in 1..width {
94 out.push(Cell::new('\0', cell.attr));
95 }
96 i += 1;
97 }
98 }
99 out
100}
101
102pub const DEFAULT_MAX_LINES: usize = 10_000;
104
105fn wrap_cells(cells: &[Cell], width: usize) -> Vec<Vec<Cell>> {
120 if width == 0 || cells.is_empty() {
121 return vec![cells.to_vec()];
122 }
123
124 let mut rows = Vec::new();
125 let mut rest = cells;
126 while rest.len() > width {
127 let mut break_at = None;
132 for i in (0..width).rev() {
133 if rest[i].ch.is_whitespace() {
134 break_at = Some(i);
135 break;
136 }
137 }
138 if let Some(i) = break_at {
139 rows.push(rest[..i].to_vec());
140 rest = &rest[i + 1..]; } else {
142 let mut cut = width;
147 if cut > 1 && rest.get(cut).is_some_and(|c| c.ch == '\0') {
148 cut -= 1;
149 }
150 rows.push(rest[..cut].to_vec());
151 rest = &rest[cut..];
152 }
153 }
154 rows.push(rest.to_vec());
155 rows
156}
157
158#[derive(Debug)]
161pub struct StreamView {
162 bounds: Rect,
163 grow_mode: GrowFlags,
171 lines: Vec<Vec<Cell>>,
176 partial: Option<Vec<Cell>>,
178 wrapped: Vec<Vec<Cell>>,
182 row_counts: Vec<usize>,
186 partial_wrapped: Vec<Vec<Cell>>,
190 max_lines: usize,
196 top: usize,
198 follow: bool,
200 fill: Attr,
201}
202
203impl StreamView {
204 #[must_use]
205 pub fn new(bounds: Rect) -> Self {
206 Self {
207 bounds,
208 grow_mode: GF_GROW_HI_X | GF_GROW_HI_Y,
209 lines: Vec::new(),
210 partial: None,
211 wrapped: Vec::new(),
212 row_counts: Vec::new(),
213 partial_wrapped: Vec::new(),
214 max_lines: DEFAULT_MAX_LINES,
215 top: 0,
216 follow: true,
217 fill: Attr::new(TvColor::LightGray, TvColor::Black),
218 }
219 }
220
221 fn width(&self) -> usize {
222 usize::try_from(self.bounds.width()).unwrap_or(0)
223 }
224
225 pub fn set_max_lines(&mut self, n: usize) {
226 self.max_lines = n.max(1);
227 self.trim();
228 }
229
230 pub fn push_line(&mut self, cells: &[Cell]) {
232 let normalized = normalize_line(cells);
233 let rows = wrap_cells(&normalized, self.width());
234 self.row_counts.push(rows.len());
235 self.wrapped.extend(rows);
236 self.lines.push(normalized);
237 self.trim();
238 if self.follow {
239 self.scroll_to_bottom();
240 }
241 }
242
243 pub fn set_partial(&mut self, cells: &[Cell]) {
246 let cells = normalize_line(cells);
247 if cells.is_empty() {
248 self.partial = None;
249 self.partial_wrapped.clear();
250 } else {
251 self.partial_wrapped = wrap_cells(&cells, self.width());
252 self.partial = Some(cells);
253 }
254 if self.follow {
255 self.scroll_to_bottom();
256 }
257 }
258
259 pub fn clear(&mut self) {
260 self.lines.clear();
261 self.partial = None;
262 self.wrapped.clear();
263 self.row_counts.clear();
264 self.partial_wrapped.clear();
265 self.top = 0;
266 self.follow = true;
267 }
268
269 #[must_use]
271 pub fn line_count(&self) -> usize {
272 self.lines.len() + usize::from(self.partial.is_some())
273 }
274
275 #[must_use]
280 pub fn row_count(&self) -> usize {
281 self.wrapped.len() + self.partial_wrapped.len()
282 }
283
284 fn page(&self) -> usize {
286 usize::try_from(self.bounds.height()).unwrap_or(0).max(1)
287 }
288
289 fn max_top(&self) -> usize {
290 self.row_count().saturating_sub(self.page())
291 }
292
293 fn rewrap(&mut self) {
298 let width = self.width();
299 self.wrapped.clear();
300 self.row_counts.clear();
301 for line in &self.lines {
302 let rows = wrap_cells(line, width);
303 self.row_counts.push(rows.len());
304 self.wrapped.extend(rows);
305 }
306 self.partial_wrapped = match &self.partial {
307 Some(cells) => wrap_cells(cells, width),
308 None => Vec::new(),
309 };
310 }
311
312 pub fn scroll_to_bottom(&mut self) {
313 self.top = self.max_top();
314 self.follow = true;
315 }
316
317 pub fn scroll_to_top(&mut self) {
318 self.top = 0;
319 self.follow = false;
320 }
321
322 pub fn scroll_up(&mut self, n: usize) {
323 self.top = self.top.saturating_sub(n);
324 self.follow = false;
325 }
326
327 pub fn scroll_down(&mut self, n: usize) {
328 self.top = (self.top + n).min(self.max_top());
329 self.follow = self.top == self.max_top();
330 }
331
332 #[must_use]
333 pub fn is_at_bottom(&self) -> bool {
334 self.follow
335 }
336
337 #[must_use]
339 pub fn plain_text(&self) -> String {
340 let mut out = String::new();
341 for (i, line) in self.iter_lines().enumerate() {
342 if i > 0 {
343 out.push('\n');
344 }
345 out.extend(line.iter().map(|c| c.ch).filter(|&ch| ch != '\0'));
349 }
350 out
351 }
352
353 #[must_use]
355 pub fn styled_lines(&self) -> Vec<Vec<Cell>> {
356 self.iter_lines().cloned().collect()
357 }
358
359 fn iter_lines(&self) -> impl Iterator<Item = &Vec<Cell>> {
360 self.lines.iter().chain(self.partial.iter())
361 }
362
363 fn iter_rows(&self) -> impl Iterator<Item = &Vec<Cell>> {
366 self.wrapped.iter().chain(self.partial_wrapped.iter())
367 }
368
369 fn trim(&mut self) {
373 if self.lines.len() > self.max_lines {
374 let drop = self.lines.len() - self.max_lines;
375 self.lines.drain(..drop);
376 let dropped_rows: usize = self.row_counts.drain(..drop).sum();
377 self.wrapped.drain(..dropped_rows);
378 self.top = self.top.saturating_sub(dropped_rows);
379 }
380 }
381}
382
383impl View for StreamView {
384 fn bounds(&self) -> Rect {
385 self.bounds
386 }
387
388 fn set_bounds(&mut self, bounds: Rect) {
389 let width_changed = self.width() != usize::try_from(bounds.width()).unwrap_or(0);
390 self.bounds = bounds;
391 if width_changed {
392 self.rewrap();
393 }
394 if self.follow {
395 self.scroll_to_bottom();
396 } else {
397 self.top = self.top.min(self.max_top());
398 }
399 }
400
401 fn draw(&mut self, terminal: &mut Terminal) {
402 if self.bounds.height() <= 0 {
403 return;
404 }
405 let width = usize::try_from(self.bounds.width()).unwrap_or(0);
406 let page = self.page();
407 let rows: Vec<&Vec<Cell>> = self.iter_rows().skip(self.top).take(page).collect();
408
409 for row in 0..page {
410 let mut buf = DrawBuffer::new(width);
411 for i in 0..width {
412 buf.put_char(i, ' ', self.fill);
413 }
414 if let Some(line) = rows.get(row) {
415 for (i, cell) in line.iter().take(width).enumerate() {
416 buf.put_char(i, cell.ch, cell.attr);
417 }
418 }
419 let y = self.bounds.a.y + i16::try_from(row).unwrap_or(i16::MAX);
420 write_line_to_terminal(terminal, self.bounds.a.x, y, &buf);
421 }
422 }
423
424 fn handle_event(&mut self, event: &mut Event) {
425 if event.what != EventType::Keyboard {
426 return;
427 }
428 let page = self.page();
429 match event.key_code {
430 KB_UP => self.scroll_up(1),
431 KB_DOWN => self.scroll_down(1),
432 KB_PGUP => self.scroll_up(page),
433 KB_PGDN => self.scroll_down(page),
434 KB_HOME => self.scroll_to_top(),
435 KB_END => self.scroll_to_bottom(),
436 _ => return,
437 }
438 event.clear();
439 }
440
441 fn grow_mode(&self) -> GrowFlags {
442 self.grow_mode
443 }
444
445 fn set_grow_mode(&mut self, grow_mode: GrowFlags) {
446 self.grow_mode = grow_mode;
447 }
448
449 fn can_focus(&self) -> bool {
450 true
451 }
452
453 fn get_palette(&self) -> Option<turbo_vision::core::palette::Palette> {
454 None
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use std::io;
464 use std::time::Duration;
465 use turbo_vision::core::palette::TvColor;
466 use turbo_vision::terminal::Backend;
467
468 fn line(s: &str) -> Vec<Cell> {
469 s.chars()
470 .map(|c| Cell::new(c, Attr::new(TvColor::LightGray, TvColor::Black)))
471 .collect()
472 }
473
474 fn view() -> StreamView {
475 StreamView::new(Rect::new(0, 0, 40, 10))
476 }
477
478 struct FakeBackend {
483 width: u16,
484 height: u16,
485 }
486
487 impl Backend for FakeBackend {
488 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
489 self
490 }
491
492 fn init(&mut self) -> io::Result<()> {
493 Ok(())
494 }
495
496 fn cleanup(&mut self) -> io::Result<()> {
497 Ok(())
498 }
499
500 fn size(&self) -> io::Result<(u16, u16)> {
501 Ok((self.width, self.height))
502 }
503
504 fn poll_event(&mut self, _timeout: Duration) -> io::Result<Option<Event>> {
505 Ok(None)
506 }
507
508 fn write_raw(&mut self, _data: &[u8]) -> io::Result<()> {
509 Ok(())
510 }
511
512 fn flush(&mut self) -> io::Result<()> {
513 Ok(())
514 }
515
516 fn show_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> {
517 Ok(())
518 }
519
520 fn hide_cursor(&mut self) -> io::Result<()> {
521 Ok(())
522 }
523 }
524
525 fn fake_terminal(width: u16, height: u16) -> Terminal {
526 Terminal::with_backend(Box::new(FakeBackend { width, height }))
527 .expect("fake backend never fails to init")
528 }
529
530 #[derive(Clone, Default)]
540 struct RecordingBackend {
541 width: u16,
542 height: u16,
543 output: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
544 }
545
546 impl Backend for RecordingBackend {
547 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
548 self
549 }
550
551 fn init(&mut self) -> io::Result<()> {
552 Ok(())
553 }
554
555 fn cleanup(&mut self) -> io::Result<()> {
556 Ok(())
557 }
558
559 fn size(&self) -> io::Result<(u16, u16)> {
560 Ok((self.width, self.height))
561 }
562
563 fn poll_event(&mut self, _timeout: Duration) -> io::Result<Option<Event>> {
564 Ok(None)
565 }
566
567 fn write_raw(&mut self, data: &[u8]) -> io::Result<()> {
568 self.output.lock().unwrap().extend_from_slice(data);
569 Ok(())
570 }
571
572 fn flush(&mut self) -> io::Result<()> {
573 Ok(())
574 }
575
576 fn show_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> {
577 Ok(())
578 }
579
580 fn hide_cursor(&mut self) -> io::Result<()> {
581 Ok(())
582 }
583 }
584
585 fn recording_terminal(
589 width: u16,
590 height: u16,
591 ) -> (Terminal, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
592 let output = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
593 let backend = RecordingBackend {
594 width,
595 height,
596 output: output.clone(),
597 };
598 let terminal =
599 Terminal::with_backend(Box::new(backend)).expect("fake backend never fails to init");
600 (terminal, output)
601 }
602
603 fn replay_onto_grid(bytes: &[u8], grid: &mut [Vec<char>]) {
614 let text = std::str::from_utf8(bytes).expect("flush emits valid UTF-8");
615 let mut chars = text.chars().peekable();
616 let mut row = 0usize;
617 let mut col = 0usize;
618 while let Some(c) = chars.next() {
619 if c == '\u{1b}' && chars.peek() == Some(&'[') {
620 chars.next(); let mut params = String::new();
622 let mut final_byte = ' ';
623 for pc in chars.by_ref() {
624 if pc.is_ascii_digit() || pc == ';' {
625 params.push(pc);
626 } else {
627 final_byte = pc;
628 break;
629 }
630 }
631 if final_byte == 'H' {
632 let mut parts = params.split(';');
633 let r: usize = parts.next().and_then(|p| p.parse().ok()).unwrap_or(1);
634 let cix: usize = parts.next().and_then(|p| p.parse().ok()).unwrap_or(1);
635 row = r.saturating_sub(1);
636 col = cix.saturating_sub(1);
637 }
638 continue;
640 }
641 let width = c.width().unwrap_or(0);
642 if row < grid.len() && col < grid[row].len() {
643 grid[row][col] = c;
644 }
645 col += width;
646 }
647 }
648
649 #[test]
650 fn scrollback_cap_drops_oldest_lines() {
651 let mut v = view();
652 v.set_max_lines(3);
653 for i in 0..5 {
654 v.push_line(&line(&i.to_string()));
655 }
656 assert_eq!(v.line_count(), 3);
657 assert_eq!(v.plain_text(), "2\n3\n4");
658 }
659
660 #[test]
661 fn autoscroll_holds_at_bottom_while_lines_arrive() {
662 let mut v = view();
663 for i in 0..50 {
664 v.push_line(&line(&i.to_string()));
665 }
666 assert!(v.is_at_bottom());
667 }
668
669 #[test]
670 fn scrolling_up_releases_autoscroll_and_end_rearms_it() {
671 let mut v = view();
672 for i in 0..50 {
673 v.push_line(&line(&i.to_string()));
674 }
675 v.scroll_up(5);
676 assert!(!v.is_at_bottom());
677 v.push_line(&line("new"));
678 assert!(
679 !v.is_at_bottom(),
680 "a new line must not yank a scrolled-back reader to the bottom"
681 );
682 v.scroll_to_bottom();
683 assert!(v.is_at_bottom());
684 }
685
686 #[test]
687 fn partial_line_is_replaced_not_appended() {
688 let mut v = view();
689 v.set_partial(&line("par"));
690 v.set_partial(&line("part"));
691 assert_eq!(v.plain_text(), "part");
692 assert_eq!(v.line_count(), 1);
693 }
694
695 #[test]
696 fn plain_text_strips_attributes() {
697 let mut v = view();
698 v.push_line(&[Cell::new('x', Attr::new(TvColor::LightRed, TvColor::Blue))]);
699 assert_eq!(v.plain_text(), "x");
700 }
701
702 #[test]
703 fn resize_larger_while_scrolled_back_reclamps_top_to_show_a_full_page() {
704 let mut v = StreamView::new(Rect::new(0, 0, 40, 5));
705 for i in 0..50 {
706 v.push_line(&line(&i.to_string()));
707 }
708 v.scroll_to_top();
711 v.scroll_down(40);
712 assert!(!v.is_at_bottom());
713 let old_top = v.top;
714 assert!(old_top < v.max_top());
715
716 v.set_bounds(Rect::new(0, 0, 40, 48));
721
722 assert!(
723 v.top <= v.max_top(),
724 "top ({}) must not exceed max_top ({}) after growing",
725 v.top,
726 v.max_top()
727 );
728 let rows: Vec<&Vec<Cell>> = v.iter_rows().skip(v.top).take(v.page()).collect();
729 assert_eq!(
730 rows.len(),
731 v.page().min(v.row_count()),
732 "a full page of content should be visible after growing"
733 );
734 }
735
736 #[test]
737 fn draw_clips_to_bounds_width() {
738 let mut v = StreamView::new(Rect::new(2, 1, 8, 4));
739 v.push_line(&line("short")); let mut terminal = fake_terminal(20, 10);
742 v.draw(&mut terminal);
743
744 for (i, expected) in "short ".chars().enumerate() {
747 let cell = terminal
748 .read_cell(2 + i16::try_from(i).unwrap_or(i16::MAX), 1)
749 .expect("cell within terminal bounds");
750 assert_eq!(cell.ch, expected);
751 }
752 assert_eq!(terminal.read_cell(8, 1).unwrap().ch, ' ');
754
755 assert_eq!(terminal.read_cell(2, 0).unwrap().ch, ' ');
757 }
758
759 const WRENCH: &str = "\u{1F6E0}\u{FE0F}";
769
770 #[test]
771 fn wide_character_row_paints_the_correct_total_number_of_columns() {
772 let mut v = StreamView::new(Rect::new(0, 0, 10, 4));
774 v.push_line(&line(&format!("{WRENCH} x")));
775 let mut terminal = fake_terminal(20, 10);
776 v.draw(&mut terminal);
777
778 assert_eq!(terminal.read_cell(0, 0).unwrap().ch, '\u{1F6E0}');
780 assert_eq!(terminal.read_cell(1, 0).unwrap().ch, '\u{FE0F}');
784 assert_eq!(terminal.read_cell(2, 0).unwrap().ch, ' ');
786 assert_eq!(terminal.read_cell(3, 0).unwrap().ch, 'x');
787 for x in 4..10 {
789 assert_eq!(terminal.read_cell(x, 0).unwrap().ch, ' ');
790 }
791 }
792
793 #[test]
794 fn text_after_a_wide_character_lands_at_the_right_column() {
795 let mut v = StreamView::new(Rect::new(0, 0, 30, 4));
796 v.push_line(&line(&format!("{WRENCH} Reading src/dsml.rs")));
797 let mut terminal = fake_terminal(30, 10);
798 v.draw(&mut terminal);
799
800 let expected = "\u{1F6E0}\u{FE0F} Reading src/dsml.rs";
801 for (i, expected_ch) in expected.chars().enumerate() {
802 let cell = terminal
803 .read_cell(i16::try_from(i).unwrap(), 0)
804 .expect("cell within terminal bounds");
805 assert_eq!(cell.ch, expected_ch, "column {i} mismatch");
806 }
807 }
808
809 #[test]
810 fn short_row_is_blank_padded_so_nothing_shows_through_from_beneath() {
811 let mut v = StreamView::new(Rect::new(0, 0, 10, 4));
812 v.push_line(&line("XXXXXXXXXX"));
814 let mut terminal = fake_terminal(20, 10);
815 v.draw(&mut terminal);
816 v.clear();
819 v.push_line(&line(&format!("{WRENCH}hi")));
820 v.draw(&mut terminal);
821
822 assert_eq!(terminal.read_cell(0, 0).unwrap().ch, '\u{1F6E0}');
823 assert_eq!(terminal.read_cell(1, 0).unwrap().ch, '\u{FE0F}');
824 assert_eq!(terminal.read_cell(2, 0).unwrap().ch, 'h');
825 assert_eq!(terminal.read_cell(3, 0).unwrap().ch, 'i');
826 for x in 4..10 {
827 assert_eq!(
828 terminal.read_cell(x, 0).unwrap().ch,
829 ' ',
830 "column {x} must be blanked, not left over from the previous row"
831 );
832 }
833 }
834
835 #[test]
836 fn a_double_width_character_straddling_a_wrap_boundary_is_never_split() {
837 let mut v = StreamView::new(Rect::new(0, 0, 3, 4));
843 v.push_line(&line("ab中cd"));
844
845 assert_eq!(v.row_count(), 3, "the 6-column line wraps to three rows");
846
847 let mut terminal = fake_terminal(20, 10);
848 v.draw(&mut terminal);
849
850 assert_eq!(terminal.read_cell(0, 0).unwrap().ch, 'a');
853 assert_eq!(terminal.read_cell(1, 0).unwrap().ch, 'b');
854
855 assert_eq!(terminal.read_cell(0, 1).unwrap().ch, '中');
857 assert_eq!(terminal.read_cell(1, 1).unwrap().ch, '\0');
858 assert_eq!(terminal.read_cell(2, 1).unwrap().ch, 'c');
859
860 assert_eq!(terminal.read_cell(0, 2).unwrap().ch, 'd');
862 }
863
864 #[test]
865 fn plain_text_round_trips_a_wide_character_with_no_padding_artifacts() {
866 let mut v = view();
867 v.push_line(&line(&format!("{WRENCH} Reading src/dsml.rs")));
868 assert_eq!(v.plain_text(), format!("{WRENCH} Reading src/dsml.rs"));
869 }
870
871 #[test]
882 fn a_covering_window_s_flush_fully_blanks_a_row_that_held_a_wide_character() {
883 let (mut terminal, output) = recording_terminal(30, 4);
884 let mut grid = vec![vec![' '; 30]; 4];
885
886 let mut lower = StreamView::new(Rect::new(0, 0, 30, 4));
888 lower.push_line(&line(&format!("{WRENCH} Reading src/dsml.rs 1:500...")));
889 lower.draw(&mut terminal);
890 terminal
891 .flush()
892 .expect("flush never fails against a fake backend");
893 replay_onto_grid(&output.lock().unwrap(), &mut grid);
894 output.lock().unwrap().clear();
895
896 let mut upper = StreamView::new(Rect::new(0, 0, 30, 4));
900 upper.draw(&mut terminal);
901 terminal
902 .flush()
903 .expect("flush never fails against a fake backend");
904 replay_onto_grid(&output.lock().unwrap(), &mut grid);
905
906 for (col, &ch) in grid[0].iter().enumerate() {
909 assert_eq!(
910 ch, ' ',
911 "row 0 column {col} still shows a leftover character from \
912 the window underneath: {grid:?}"
913 );
914 }
915 }
916
917 #[test]
918 fn a_line_longer_than_the_width_wraps_across_the_right_number_of_rows_with_complete_content() {
919 let mut v = StreamView::new(Rect::new(0, 0, 10, 20));
920 let text = "abcdefghijklmnopqrstuvwxy";
922 v.push_line(&line(text));
923
924 assert_eq!(v.row_count(), 3);
925 assert_eq!(
926 v.plain_text(),
927 text,
928 "wrapping must not drop or duplicate any character"
929 );
930
931 let mut terminal = fake_terminal(20, 20);
934 v.draw(&mut terminal);
935 let mut rendered = String::new();
936 for row in 0..3 {
937 for col in 0..10 {
938 rendered.push(terminal.read_cell(col, row).unwrap().ch);
939 }
940 }
941 assert_eq!(rendered, "abcdefghijklmnopqrstuvwxy ");
942 }
943
944 #[test]
945 fn a_wrap_breaks_at_a_space_rather_than_mid_word_when_one_is_available() {
946 let mut v = StreamView::new(Rect::new(0, 0, 10, 20));
947 v.push_line(&line("hello world"));
948
949 assert_eq!(v.row_count(), 2);
954 let mut terminal = fake_terminal(20, 20);
955 v.draw(&mut terminal);
956 for (i, expected) in "hello ".chars().enumerate() {
957 assert_eq!(
958 terminal.read_cell(i16::try_from(i).unwrap(), 0).unwrap().ch,
959 expected
960 );
961 }
962 for (i, expected) in "world ".chars().enumerate() {
963 assert_eq!(
964 terminal.read_cell(i16::try_from(i).unwrap(), 1).unwrap().ch,
965 expected
966 );
967 }
968 }
969
970 #[test]
971 fn a_single_token_longer_than_the_width_is_broken_rather_than_truncated() {
972 let mut v = StreamView::new(Rect::new(0, 0, 5, 20));
973 v.push_line(&line("abcdefghijkl"));
977
978 assert_eq!(v.row_count(), 3); assert_eq!(
980 v.plain_text(),
981 "abcdefghijkl",
982 "the logical text is preserved even though it had to be broken mid-token"
983 );
984 }
985
986 #[test]
987 fn plain_text_returns_the_original_unwrapped_logical_lines() {
988 let mut v = StreamView::new(Rect::new(0, 0, 5, 20));
989 v.push_line(&line("a much longer line than the five-column view"));
990 v.push_line(&line("short"));
991
992 assert_eq!(
993 v.plain_text(),
994 "a much longer line than the five-column view\nshort",
995 "Save As must get the original logical lines, not this window's wrap points"
996 );
997 }
998
999 #[test]
1000 fn resizing_narrower_then_wider_rewraps_and_content_survives_both() {
1001 let mut v = StreamView::new(Rect::new(0, 0, 20, 20));
1002 let text = "abcdefghijklmnopqrstuvwxyz";
1003 v.push_line(&line(text));
1004 assert_eq!(v.row_count(), 2); v.set_bounds(Rect::new(0, 0, 5, 20));
1007 assert_eq!(v.row_count(), 6); assert_eq!(v.plain_text(), text);
1009
1010 v.set_bounds(Rect::new(0, 0, 30, 20));
1011 assert_eq!(v.row_count(), 1); assert_eq!(v.plain_text(), text);
1013 }
1014
1015 #[test]
1016 fn scrolling_by_page_lands_correctly_when_wrapped_rows_are_present() {
1017 let mut v = StreamView::new(Rect::new(0, 0, 4, 5));
1019 let text: String = (0..80).map(|i| char::from(b'a' + (i % 26))).collect();
1020 v.push_line(&line(&text));
1021 assert_eq!(v.row_count(), 20);
1022
1023 v.scroll_to_top();
1024 assert_eq!(v.top, 0);
1025 v.scroll_down(v.page()); assert_eq!(
1027 v.top, 5,
1028 "paging must move by display rows, not logical lines"
1029 );
1030
1031 v.scroll_to_bottom();
1032 assert_eq!(v.top, v.row_count() - v.page());
1033 }
1034
1035 #[test]
1036 fn draw_on_zero_height_view_writes_nothing() {
1037 let mut v = StreamView::new(Rect::new(0, 0, 10, 0));
1038 v.push_line(&line("hello"));
1039 let mut terminal = fake_terminal(20, 10);
1040 v.draw(&mut terminal);
1041 for y in 0..10 {
1042 for x in 0..20 {
1043 assert_eq!(
1044 terminal.read_cell(x, y).unwrap().ch,
1045 ' ',
1046 "zero-height view must not write any cell"
1047 );
1048 }
1049 }
1050 }
1051}