//! Grid resize and reflow. use std::cmp::{max, min, Ordering}; use std::mem; use crate::index::{Boundary, Column, Line}; use crate::term::cell::{Flags, ResetDiscriminant}; use crate::grid::row::Row; use crate::grid::{Dimensions, Grid, GridCell}; impl Grid { /// Resize the grid's width and/or height. pub fn resize(&mut self, reflow: bool, lines: usize, columns: usize) where T: ResetDiscriminant, D: PartialEq, { // Use empty template cell for resetting cells due to resize. let template = mem::take(&mut self.cursor.template); match self.lines.cmp(&lines) { Ordering::Less => self.grow_lines(lines), Ordering::Greater => self.shrink_lines(lines), Ordering::Equal => (), } match self.columns.cmp(&columns) { Ordering::Less => self.grow_columns(reflow, columns), Ordering::Greater => self.shrink_columns(reflow, columns), Ordering::Equal => (), } // Restore template cell. self.cursor.template = template; } /// 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, target: usize) where T: ResetDiscriminant, D: PartialEq, { let lines_added = target - self.lines; // Need to resize before updating buffer. self.raw.grow_visible_lines(target); self.lines = target; let history_size = self.history_size(); let from_history = min(history_size, lines_added); // Move existing lines up for every line that couldn't be pulled from history. if from_history != lines_added { let delta = lines_added - from_history; self.scroll_up(&(Line(0)..Line(target as i32)), delta); } // 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: usize) where T: ResetDiscriminant, D: PartialEq, { // Scroll up to keep content inside the window. let required_scrolling = (self.cursor.point.line.0 as usize + 1).saturating_sub(target); if required_scrolling > 0 { self.scroll_up(&(Line(0)..Line(self.lines as i32)), required_scrolling); // Clamp cursors to the new viewport size. self.cursor.point.line = min(self.cursor.point.line, Line(target as i32 - 1)); } // Clamp saved cursor, since only primary cursor is scrolled into viewport. self.saved_cursor.point.line = min(self.saved_cursor.point.line, Line(target as i32 - 1)); self.raw.rotate((self.lines - target) as isize); self.raw.shrink_visible_lines(target); self.lines = target; } /// Grow number of columns in each row, reflowing if necessary. fn grow_columns(&mut self, reflow: bool, columns: usize) { // Check if a row needs to be wrapped. let should_reflow = |row: &Row| -> bool { let len = Column(row.len()); reflow && len.0 > 0 && len < columns && row[len - 1].flags().contains(Flags::WRAPLINE) }; self.columns = columns; let mut reversed: Vec> = Vec::with_capacity(self.raw.len()); let mut cursor_line_delta = 0; // Remove the linewrap special case, by moving the cursor outside of the grid. if self.cursor.input_needs_wrap && reflow { self.cursor.input_needs_wrap = false; self.cursor.point.column += 1; } 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 >= 1 && last_row[Column(last_len - 1)].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) { last_row.shrink(last_len - 1); last_len -= 1; } // Don't try to pull more cells from the next line than available. let mut num_wrapped = columns - last_len; let len = min(row.len(), num_wrapped); // 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) { num_wrapped -= 1; let mut cells = row.front_split_off(len - 1); let mut spacer = T::default(); spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER); cells.push(spacer); cells } else { row.front_split_off(len) }; // Add removed cells to previous row and reflow content. last_row.append(&mut cells); let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; if i == cursor_buffer_line && reflow { // Resize cursor's line and reflow the cursor if necessary. let mut target = self.cursor.point.sub(self, Boundary::Cursor, num_wrapped); // Clamp to the last column, if no content was reflown with the cursor. if target.column.0 == 0 && row.is_clear() { self.cursor.input_needs_wrap = true; target = target.sub(self, Boundary::Cursor, 1); } self.cursor.point.column = target.column; // Get required cursor line changes. Since `num_wrapped` is smaller than `columns` // this will always be either `0` or `1`. let line_delta = self.cursor.point.line - target.line; if line_delta != 0 && row.is_clear() { continue; } cursor_line_delta += line_delta.0 as usize; } else if row.is_clear() { if i <= self.display_offset { // Since we removed a line, rotate down the viewport. self.display_offset = self.display_offset.saturating_sub(1); } // Rotate cursor down if content below them was pulled from history. if i < cursor_buffer_line { self.cursor.point.line += 1; } // Don't push line into the new buffer. continue; } 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); } // Make sure we have at least the viewport filled. if reversed.len() < self.lines { let delta = (self.lines - reversed.len()) as i32; self.cursor.point.line = max(self.cursor.point.line - delta, Line(0)); reversed.resize_with(self.lines, || Row::new(columns)); } // Pull content down to put cursor in correct position, or move cursor up if there's no // more lines to delete below the cursor. if cursor_line_delta != 0 { let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; let available = min(cursor_buffer_line, reversed.len() - self.lines); let overflow = cursor_line_delta.saturating_sub(available); reversed.truncate(reversed.len() + overflow - cursor_line_delta); self.cursor.point.line = max(self.cursor.point.line - overflow, Line(0)); } // 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() < columns { row.grow(columns); } 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_columns(&mut self, reflow: bool, columns: usize) { self.columns = columns; // Remove the linewrap special case, by moving the cursor outside of the grid. if self.cursor.input_needs_wrap && reflow { self.cursor.input_needs_wrap = false; self.cursor.point.column += 1; } let mut new_raw = Vec::with_capacity(self.raw.len()); let mut buffered: Option> = None; let mut rows = self.raw.take_all(); for (i, mut row) in rows.drain(..).enumerate().rev() { // Append lines left over from the previous row. if let Some(buffered) = buffered.take() { // Add a column for every cell added before the cursor, if it goes beyond the new // width it is then later reflown. let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; if i == cursor_buffer_line { self.cursor.point.column += buffered.len(); } row.append_front(buffered); } loop { // Remove all cells which require reflowing. let mut wrapped = match row.shrink(columns) { Some(wrapped) if reflow => wrapped, _ => { let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; if reflow && i == cursor_buffer_line && self.cursor.point.column > columns { // If there are empty cells before the cursor, we assume it is explicit // whitespace and need to wrap it like normal content. Vec::new() } else { // Since it fits, just push the existing line without any reflow. new_raw.push(row); break; } }, }; // Insert spacer if a wide char would be wrapped into the last column. if row.len() >= columns && row[Column(columns - 1)].flags().contains(Flags::WIDE_CHAR) { let mut spacer = T::default(); spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER); let wide_char = mem::replace(&mut row[Column(columns - 1)], spacer); wrapped.insert(0, wide_char); } // Remove wide char spacer before shrinking. let len = wrapped.len(); if len > 0 && wrapped[len - 1].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) { if len == 1 { row[Column(columns - 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() < columns { // 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 { // Reflow cursor if a line below it is deleted. let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; if (i == cursor_buffer_line && self.cursor.point.column < columns) || i < cursor_buffer_line { self.cursor.point.line = max(self.cursor.point.line - 1, Line(0)); } // Reflow the cursor if it is on this line beyond the width. if i == cursor_buffer_line && self.cursor.point.column >= columns { // Since only a single new line is created, we subtract only `columns` // from the cursor instead of reflowing it completely. self.cursor.point.column -= columns; } // Make sure new row is at least as long as new width. let occ = wrapped.len(); if occ < columns { wrapped.resize_with(columns, T::default); } row = Row::from_vec(wrapped, occ); if i <= self.display_offset { // Since we added a new line, rotate up the viewport. self.display_offset += 1; } } } } // Reverse iterator and use it as the new grid storage. let mut reversed: Vec> = new_raw.drain(..).rev().collect(); reversed.truncate(self.max_scroll_limit + self.lines); self.raw.replace_inner(reversed); // Reflow the primary cursor, or clamp it if reflow is disabled. if !reflow { self.cursor.point.column = min(self.cursor.point.column, Column(columns - 1)); } else if self.cursor.point.column == columns && !self[self.cursor.point.line][Column(columns - 1)].flags().contains(Flags::WRAPLINE) { self.cursor.input_needs_wrap = true; self.cursor.point.column -= 1; } else { self.cursor.point = self.cursor.point.grid_clamp(self, Boundary::Cursor); } // Clamp the saved cursor to the grid. self.saved_cursor.point.column = min(self.saved_cursor.point.column, Column(columns - 1)); } }