diff --git a/term/src/screen.rs b/term/src/screen.rs index 2a88aec9e..59320bfbd 100644 --- a/term/src/screen.rs +++ b/term/src/screen.rs @@ -76,11 +76,73 @@ impl Screen { scrollback_size(&self.config, self.allow_scrollback) } + fn rewrap_lines(&mut self, physical_cols: usize) { + let mut rewrapped = VecDeque::new(); + let mut logical_line: Option = None; + for mut line in self.lines.drain(..) { + line.invalidate_implicit_hyperlinks(); + if line.last_cell_was_wrapped() { + line.set_last_cell_was_wrapped(false); + logical_line = Some(match logical_line.take() { + None => line, + Some(mut prior) => { + prior.append_line(line); + prior + } + }); + continue; + } + + let line = match logical_line.take() { + None => line, + Some(mut prior) => { + prior.append_line(line); + prior + } + }; + + if line.cells().len() <= physical_cols { + rewrapped.push_back(line); + } else { + for line in line.wrap(physical_cols) { + rewrapped.push_back(line); + } + } + } + self.lines = rewrapped; + + // If we resized narrower and generated additional lines, + // we may need to scroll the lines to make room. However, + // if the bottom line(s) are whitespace, we'll prune those + // out first in the rewrap case so that we don't lose any + // real information off the top of the scrollback + while self.lines.len() > self.physical_rows + && self.lines.back().map(Line::is_whitespace).unwrap_or(false) + { + self.lines.pop_back(); + } + + // If we resized wider and the rewrap resulted in fewer + // lines than the viewport size, pad us back out to the + // viewport size + while self.lines.len() < self.physical_rows { + self.lines.push_back(Line::with_width(physical_cols)); + } + } + /// Resize the physical, viewable portion of the screen pub fn resize(&mut self, physical_rows: usize, physical_cols: usize) { let physical_rows = physical_rows.max(1); let physical_cols = physical_cols.max(1); + if physical_cols != self.physical_cols { + // Check to see if we need to rewrap lines that were + // wrapped due to reaching the right hand side of the terminal. + // For each one that we find, we need to join it with its + // successor and then re-split it + self.rewrap_lines(physical_cols); + } + let capacity = physical_rows + self.scrollback_size(); let current_capacity = self.lines.capacity(); if capacity > current_capacity { @@ -211,11 +273,11 @@ impl Screen { } }; - let last = match self.stable_row_to_phys(range.end - 1) { + let last = match self.stable_row_to_phys(range.end.saturating_sub(1)) { Some(last) => last, None => { let last = self.lines.len() - 1; - return last - range_len..last + 1; + return last.saturating_sub(range_len)..last + 1; } }; diff --git a/term/src/terminalstate.rs b/term/src/terminalstate.rs index ddbc2eb70..c1006e284 100644 --- a/term/src/terminalstate.rs +++ b/term/src/terminalstate.rs @@ -1296,12 +1296,16 @@ impl TerminalState { let position = position.max(0); let rows = self.screen().physical_rows; - let avail_scrollback = self.screen().lines.len() - rows; + let avail_scrollback = self.screen().lines.len().saturating_sub(rows); let position = position.min(avail_scrollback as i64); self.viewport_offset = position; - let top = self.screen().lines.len() - (rows + position as usize); + let top = self + .screen() + .lines + .len() + .saturating_sub(rows + position as usize); { let screen = self.screen_mut(); for y in top..top + rows { diff --git a/term/src/test/mod.rs b/term/src/test/mod.rs index fe3737010..cfc210036 100644 --- a/term/src/test/mod.rs +++ b/term/src/test/mod.rs @@ -524,6 +524,73 @@ fn test_delete_lines() { term.assert_dirty_lines(&[1, 2, 3, 4], None); } +/// Test the behavior of wrapped lines when we resize the terminal +/// wider and then narrower. +#[test] +fn test_resize_wrap() { + const LINES: usize = 8; + let mut term = TestTerm::new(LINES, 4, 0); + term.print("111\r\n2222aa\r\n333\r\n"); + assert_visible_contents( + &term, + &[ + "111 ", "2222", "aa ", "333 ", " ", " ", " ", " ", + ], + ); + term.resize(LINES, 5, 0, 0); + assert_visible_contents( + &term, + &["111 ", "2222a", "a", "333 ", " ", " ", " ", " "], + ); + term.resize(LINES, 6, 0, 0); + assert_visible_contents( + &term, + &[ + "111 ", "2222aa", "333 ", " ", " ", " ", " ", " ", + ], + ); + term.resize(LINES, 7, 0, 0); + assert_visible_contents( + &term, + &[ + "111 ", "2222aa", "333 ", " ", " ", " ", " ", " ", + ], + ); + term.resize(LINES, 8, 0, 0); + assert_visible_contents( + &term, + &[ + "111 ", "2222aa", "333 ", " ", " ", " ", " ", " ", + ], + ); + + // Resize smaller again + term.resize(LINES, 7, 0, 0); + assert_visible_contents( + &term, + &[ + "111 ", "2222aa", "333 ", " ", " ", " ", " ", " ", + ], + ); + term.resize(LINES, 6, 0, 0); + assert_visible_contents( + &term, + &[ + "111 ", "2222aa", "333 ", " ", " ", " ", " ", " ", + ], + ); + term.resize(LINES, 5, 0, 0); + assert_visible_contents( + &term, + &["111 ", "2222a", "a", "333 ", " ", " ", " ", " "], + ); + term.resize(LINES, 4, 0, 0); + assert_visible_contents( + &term, + &["111 ", "2222", "aa", "333 ", " ", " ", " ", " "], + ); +} + #[test] fn test_scrollup() { let mut term = TestTerm::new(2, 1, 4); diff --git a/termwiz/src/surface/line.rs b/termwiz/src/surface/line.rs index bb7338ea2..41d45ec57 100644 --- a/termwiz/src/surface/line.rs +++ b/termwiz/src/surface/line.rs @@ -81,6 +81,37 @@ impl Line { self.bits |= LineBits::DIRTY; } + /// Wrap the line so that it fits within the provided width. + /// Returns the list of resultant line(s) + pub fn wrap(mut self, width: usize) -> Vec { + if let Some(end_idx) = self.cells.iter().rposition(|c| c.str() != " ") { + self.cells.resize(end_idx + 1, Cell::default()); + + let mut lines: Vec<_> = self + .cells + .chunks_mut(width) + .map(|chunk| { + let mut line = Line { + cells: chunk.to_vec(), + bits: LineBits::DIRTY, + }; + if line.cells.len() == width { + // Ensure that we don't forget that we wrapped + line.set_last_cell_was_wrapped(true); + } + line + }) + .collect(); + // The last of the chunks wasn't actually wrapped + lines + .last_mut() + .map(|line| line.set_last_cell_was_wrapped(false)); + lines + } else { + vec![self] + } + } + /// Check whether the dirty bit is set. /// If it is set, then something about the line has changed since /// the dirty bit was last cleared. @@ -348,6 +379,36 @@ impl Line { &self.cells } + /// Return true if the line consists solely of whitespace cells + pub fn is_whitespace(&self) -> bool { + self.cells.iter().all(|c| c.str() == " ") + } + + /// Return true if the last cell in the line has the wrapped attribute, + /// indicating that the following line is logically a part of this one. + pub fn last_cell_was_wrapped(&self) -> bool { + self.cells + .last() + .map(|c| c.attrs().wrapped()) + .unwrap_or(false) + } + + /// Adjust the value of the wrapped attribute on the last cell of this + /// line. + pub fn set_last_cell_was_wrapped(&mut self, wrapped: bool) { + if let Some(cell) = self.cells.last_mut() { + cell.attrs_mut().set_wrapped(wrapped); + } + } + + /// Concatenate the cells from other with this line, appending them + /// to this line. + /// This function is used by rewrapping logic when joining wrapped + /// lines back together. + pub fn append_line(&mut self, mut other: Line) { + self.cells.append(&mut other.cells); + } + /// mutable access the cell data, but the caller must take care /// to only mutate attributes rather than the cell textual content. /// Use set_cell if you need to modify the textual content of the