diff options
Diffstat (limited to 'alacritty_terminal/src/grid/resize.rs')
-rw-r--r-- | alacritty_terminal/src/grid/resize.rs | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/alacritty_terminal/src/grid/resize.rs b/alacritty_terminal/src/grid/resize.rs new file mode 100644 index 00000000..796c5859 --- /dev/null +++ b/alacritty_terminal/src/grid/resize.rs @@ -0,0 +1,274 @@ +//! Grid resize and reflow. + +use std::cmp::{min, Ordering}; + +use crate::index::{Column, Line}; +use crate::term::cell::Flags; + +use crate::grid::row::Row; +use crate::grid::{Grid, GridCell}; + +impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { + /// Resize the grid's width and/or height. + pub fn resize(&mut self, reflow: bool, lines: Line, cols: Column) { + match self.lines.cmp(&lines) { + Ordering::Less => self.grow_lines(lines), + Ordering::Greater => self.shrink_lines(lines), + Ordering::Equal => (), + } + + match self.cols.cmp(&cols) { + Ordering::Less => self.grow_cols(cols, reflow), + Ordering::Greater => self.shrink_cols(cols, reflow), + Ordering::Equal => (), + } + } + + /// Add lines to the visible area. + /// + /// Alacritty keeps the cursor at the bottom of the terminal as long as there + /// is scrollback available. Once scrollback is exhausted, new lines are + /// simply added to the bottom of the screen. + fn grow_lines(&mut self, new_line_count: Line) { + let lines_added = new_line_count - self.lines; + + // Need to resize before updating buffer. + self.raw.grow_visible_lines(new_line_count, Row::new(self.cols, T::default())); + self.lines = new_line_count; + + let history_size = self.history_size(); + let from_history = min(history_size, lines_added.0); + + // Move existing lines up for every line that couldn't be pulled from history. + if from_history != lines_added.0 { + let delta = lines_added - from_history; + self.scroll_up(&(Line(0)..new_line_count), delta, T::default()); + } + + // Move cursor down for every line pulled from history. + self.saved_cursor.point.line += from_history; + self.cursor.point.line += from_history; + + self.display_offset = self.display_offset.saturating_sub(*lines_added); + self.decrease_scroll_limit(*lines_added); + } + + /// Remove lines from the visible area. + /// + /// The behavior in Terminal.app and iTerm.app is to keep the cursor at the + /// bottom of the screen. This is achieved by pushing history "out the top" + /// of the terminal window. + /// + /// Alacritty takes the same approach. + fn shrink_lines(&mut self, target: Line) { + // Scroll up to keep content inside the window. + let required_scrolling = (self.cursor.point.line + 1).saturating_sub(target.0); + if required_scrolling > 0 { + self.scroll_up(&(Line(0)..self.lines), Line(required_scrolling), T::default()); + + // Clamp cursors to the new viewport size. + self.saved_cursor.point.line = min(self.saved_cursor.point.line, target - 1); + self.cursor.point.line = min(self.cursor.point.line, target - 1); + } + + self.raw.rotate((self.lines - target).0 as isize); + self.raw.shrink_visible_lines(target); + self.lines = target; + } + + /// Grow number of columns in each row, reflowing if necessary. + fn grow_cols(&mut self, cols: Column, reflow: bool) { + // Check if a row needs to be wrapped. + let should_reflow = |row: &Row<T>| -> bool { + let len = Column(row.len()); + reflow && len < cols && row[len - 1].flags().contains(Flags::WRAPLINE) + }; + + self.cols = cols; + + let mut reversed: Vec<Row<T>> = Vec::with_capacity(self.raw.len()); + let mut new_empty_lines = 0; + + let mut rows = self.raw.take_all(); + + for (i, mut row) in rows.drain(..).enumerate().rev() { + // Check if reflowing should be performed. + let last_row = match reversed.last_mut() { + Some(last_row) if should_reflow(last_row) => last_row, + _ => { + reversed.push(row); + continue; + }, + }; + + // Remove wrap flag before appending additional cells. + if let Some(cell) = last_row.last_mut() { + cell.flags_mut().remove(Flags::WRAPLINE); + } + + // Remove leading spacers when reflowing wide char to the previous line. + let mut last_len = last_row.len(); + if last_len >= 2 + && !last_row[Column(last_len - 2)].flags().contains(Flags::WIDE_CHAR) + && last_row[Column(last_len - 1)].flags().contains(Flags::WIDE_CHAR_SPACER) + { + last_row.shrink(Column(last_len - 1)); + last_len -= 1; + } + + // Don't try to pull more cells from the next line than available. + let len = min(row.len(), cols.0 - last_len); + + // Insert leading spacer when there's not enough room for reflowing wide char. + let mut cells = if row[Column(len - 1)].flags().contains(Flags::WIDE_CHAR) { + let mut cells = row.front_split_off(len - 1); + + let mut spacer = T::default(); + spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER); + cells.push(spacer); + + cells + } else { + row.front_split_off(len) + }; + + // Reflow cells to previous row. + last_row.append(&mut cells); + + if row.is_empty() { + if i + reversed.len() < self.lines.0 { + // Add new line and move everything up if we can't pull from history. + self.saved_cursor.point.line.0 = self.saved_cursor.point.line.saturating_sub(1); + self.cursor.point.line.0 = self.cursor.point.line.saturating_sub(1); + new_empty_lines += 1; + } else { + // Since we removed a line, rotate down the viewport. + self.display_offset = self.display_offset.saturating_sub(1); + } + + // Don't push line into the new buffer. + continue; + } else if let Some(cell) = last_row.last_mut() { + // Set wrap flag if next line still has cells. + cell.flags_mut().insert(Flags::WRAPLINE); + } + + reversed.push(row); + } + + // Add all new empty lines in one go. + reversed.append(&mut vec![Row::new(cols, T::default()); new_empty_lines]); + + // Reverse iterator and fill all rows that are still too short. + let mut new_raw = Vec::with_capacity(reversed.len()); + for mut row in reversed.drain(..).rev() { + if row.len() < cols.0 { + row.grow(cols, T::default()); + } + new_raw.push(row); + } + + self.raw.replace_inner(new_raw); + + // Clamp display offset in case lines above it got merged. + self.display_offset = min(self.display_offset, self.history_size()); + } + + /// Shrink number of columns in each row, reflowing if necessary. + fn shrink_cols(&mut self, cols: Column, reflow: bool) { + self.cols = cols; + + let mut rows = self.raw.take_all(); + + let mut new_raw = Vec::with_capacity(self.raw.len()); + let mut buffered: Option<Vec<T>> = None; + + for (i, mut row) in rows.drain(..).enumerate().rev() { + // Append lines left over from the previous row. + if let Some(buffered) = buffered.take() { + row.append_front(buffered); + } + + loop { + // Remove all cells which require reflowing. + let mut wrapped = match row.shrink(cols) { + Some(wrapped) if reflow => wrapped, + _ => { + new_raw.push(row); + break; + }, + }; + + // Insert spacer if a wide char would be wrapped into the last column. + if row.len() >= cols.0 && row[cols - 1].flags().contains(Flags::WIDE_CHAR) { + wrapped.insert(0, row[cols - 1]); + + let mut spacer = T::default(); + spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER); + row[cols - 1] = spacer; + } + + // Remove wide char spacer before shrinking. + let len = wrapped.len(); + if (len == 1 || (len >= 2 && !wrapped[len - 2].flags().contains(Flags::WIDE_CHAR))) + && wrapped[len - 1].flags().contains(Flags::WIDE_CHAR_SPACER) + { + if len == 1 { + // Delete the wrapped content if it contains only a leading spacer. + row[cols - 1].flags_mut().insert(Flags::WRAPLINE); + new_raw.push(row); + break; + } else { + // Remove the leading spacer from the end of the wrapped row. + wrapped[len - 2].flags_mut().insert(Flags::WRAPLINE); + wrapped.truncate(len - 1); + } + } + + new_raw.push(row); + + // Set line as wrapped if cells got removed. + if let Some(cell) = new_raw.last_mut().and_then(|r| r.last_mut()) { + cell.flags_mut().insert(Flags::WRAPLINE); + } + + if wrapped + .last() + .map(|c| c.flags().contains(Flags::WRAPLINE) && i >= 1) + .unwrap_or(false) + && wrapped.len() < cols.0 + { + // Make sure previous wrap flag doesn't linger around. + if let Some(cell) = wrapped.last_mut() { + cell.flags_mut().remove(Flags::WRAPLINE); + } + + // Add removed cells to start of next row. + buffered = Some(wrapped); + break; + } else { + // Since we added a line, rotate up the viewport. + if i < self.display_offset { + self.display_offset = min(self.display_offset + 1, self.max_scroll_limit); + } + + // Make sure new row is at least as long as new width. + let occ = wrapped.len(); + if occ < cols.0 { + wrapped.append(&mut vec![T::default(); cols.0 - occ]); + } + row = Row::from_vec(wrapped, occ); + } + } + } + + // Reverse iterator and use it as the new grid storage. + let mut reversed: Vec<Row<T>> = new_raw.drain(..).rev().collect(); + reversed.truncate(self.max_scroll_limit + self.lines.0); + self.raw.replace_inner(reversed); + + // Wrap content going beyond new width if necessary. + self.saved_cursor.point.col = min(self.saved_cursor.point.col, self.cols - 1); + self.cursor.point.col = min(self.cursor.point.col, self.cols - 1); + } +} |