checkpoint: very basic scrolling works

Things that do not work

- Limiting how far back in the buffer it's possible to scroll
- Selections (need to transform to buffer offsets)
This commit is contained in:
Joe Wilm 2018-02-15 18:35:49 -08:00
parent 94796a70fc
commit 45c2b3fbf7
6 changed files with 296 additions and 179 deletions

View File

@ -55,6 +55,10 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> {
*self.size_info
}
fn scroll(&mut self, count: isize) {
self.terminal.scroll_display(count);
}
fn copy_selection(&self, buffer: ::copypasta::Buffer) {
if let Some(ref selection) = *self.selection {
if let Some(ref span) = selection.to_span(self.terminal) {

View File

@ -12,17 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! A generic 2d grid implementation optimized for use in a terminal.
//!
//! The current implementation uses a vector of vectors to store cell data.
//! Reimplementing the store as a single contiguous vector may be desirable in
//! the future. Rotation and indexing would need to be reconsidered at that
//! time; rotation currently reorganize Vecs in the lines Vec, and indexing with
//! ranges is currently supported.
//! A specialized 2d grid implementation optimized for use in a terminal.
use std::cmp::Ordering;
use std::iter::IntoIterator;
use std::ops::{Deref, Range, RangeTo, RangeFrom, RangeFull, Index, IndexMut};
use std::cmp::{max, Ordering};
use std::ops::{Deref, Range, Index, IndexMut, RangeTo, RangeFrom, RangeFull};
use index::{self, Point, Line, Column, IndexRange, RangeInclusive};
@ -36,7 +29,7 @@ mod storage;
use self::storage::Storage;
/// Lines to keep in scrollback buffer
const SCROLLBACK_LINES: usize = 100_000;
const SCROLLBACK_LINES: usize = 10_000;
/// Convert a type to a linear index range.
pub trait ToRange {
@ -105,8 +98,12 @@ pub struct GridIterator<'a, T: 'a> {
}
impl<T: Copy + Clone> Grid<T> {
pub fn scroll_display(&mut self, count: isize) {
self.display_offset = max((self.display_offset as isize) + count, 0isize) as usize;
}
pub fn new(lines: index::Line, cols: index::Column, template: T) -> Grid<T> {
let mut raw = Storage::with_capacity(*lines + SCROLLBACK_LINES);
let mut raw = Storage::with_capacity(*lines + SCROLLBACK_LINES, lines);
let template_row = Row::new(cols, &template);
// Allocate all lines in the buffer, including scrollback history
@ -125,6 +122,7 @@ impl<T: Copy + Clone> Grid<T> {
template_row,
template,
temp: Vec::new(),
display_offset: 0,
}
}
@ -147,12 +145,23 @@ impl<T: Copy + Clone> Grid<T> {
}
}
fn grow_lines(&mut self, lines: index::Line) {
for _ in IndexRange(self.num_lines()..lines) {
self.raw.push(self.template_row.clone());
}
/// Add lines to the visible area
///
/// The behavior in Terminal.app and iTerm.app is to keep the cursor at the
/// bottom of the screen as long as there is scrollback available. Once
/// scrollback is exhausted, new lines are simply added to the bottom of the
/// screen.
///
/// Alacritty takes a different approach. Rather than trying to move with
/// the scrollback, we simply pull additional lines from the back of the
/// buffer in order to populate the new area.
fn grow_lines(&mut self, target: index::Line) {
let delta = target - self.lines;
self.lines = lines;
self.raw.set_visible_lines(target);
self.lines = target;
self.scroll_up(&(Line(0)..target), delta);
}
fn grow_cols(&mut self, cols: index::Column) {
@ -167,6 +176,37 @@ impl<T: Copy + Clone> Grid<T> {
self.template_row.grow(cols, &self.template);
}
/// 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: index::Line) {
// TODO handle disabled scrollback
// while index::Line(self.raw.len()) != lines {
// self.raw.pop();
// }
let prev = self.lines;
self.raw.rotate(*prev as isize - *target as isize);
self.raw.set_visible_lines(target);
self.lines = target;
}
/// Convert a Line index (active region) to a buffer offset
///
/// # Panics
///
/// This method will panic if `Line` is larger than the grid dimensions
pub fn line_to_offset(&self, line: index::Line) -> usize {
assert!(line < self.num_lines());
*(self.num_lines() - line - 1)
}
#[inline]
pub fn scroll_down(&mut self, region: &Range<index::Line>, positions: index::Line) {
// Whether or not there is a scrolling region active, as long as it
@ -177,12 +217,12 @@ impl<T: Copy + Clone> Grid<T> {
if region.start == Line(0) {
// Rotate the entire line buffer. If there's a scrolling region
// active, the bottom lines are restored in the next step.
self.raw.rotate(-(*positions as isize));
self.raw.rotate_up(*positions);
// Now, restore any scroll region lines
for i in IndexRange(region.end .. self.num_lines()) {
// First do the swap
self.raw.swap(*i, *i + *positions);
self.raw.swap_lines(i, i + positions);
}
// Finally, reset recycled lines
@ -192,7 +232,7 @@ impl<T: Copy + Clone> Grid<T> {
} else {
// Subregion rotation
for line in IndexRange((region.start + positions)..region.end).rev() {
self.swap_lines(line, line - positions);
self.raw.swap_lines(line, line - positions);
}
for line in IndexRange(region.start .. (region.start + positions)) {
@ -214,29 +254,29 @@ impl<T: Copy + Clone> Grid<T> {
// Rotate the entire line buffer. If there's a scrolling region
// active, the bottom lines are restored in the next step.
self.raw.rotate_up(*positions);
self.raw.rotate(-(*positions as isize));
// Now, restore any lines outside the scroll region
for idx in (*region.end .. *self.num_lines()).rev() {
// First do the swap
self.raw.swap(idx, idx - *positions);
self.raw.swap_lines(Line(idx), Line(idx) - positions);
}
// Finally, reset recycled lines
//
// Recycled lines are just above the end of the scrolling region.
for i in 0..*positions {
self.raw[*region.end - i - 1].reset(&self.template_row);
self.raw[region.end - i - 1].reset(&self.template_row);
}
} else {
// Subregion rotation
for line in IndexRange(region.start..(region.end - positions)) {
self.swap_lines(line, line + positions);
self.raw.swap_lines(line, line + positions);
}
// Clear reused lines
for line in IndexRange((region.end - positions) .. region.end) {
self.raw[*line].reset(&self.template_row);
self.raw[line].reset(&self.template_row);
}
}
}
@ -248,6 +288,10 @@ impl<T> Grid<T> {
self.lines
}
pub fn display_iter(&self) -> DisplayIter<T> {
DisplayIter::new(self)
}
#[inline]
pub fn num_cols(&self) -> index::Column {
self.cols
@ -265,22 +309,14 @@ impl<T> Grid<T> {
self.lines > point.line && self.cols > point.col
}
/// Swap two lines in the grid
///
/// This could have used slice::swap internally, but we are able to have
/// better error messages by doing the bounds checking ourselves.
#[inline]
pub fn swap_lines(&mut self, src: index::Line, dst: index::Line) {
self.raw.swap(*src, *dst);
}
fn shrink_lines(&mut self, lines: index::Line) {
while index::Line(self.raw.len()) != lines {
self.raw.pop();
}
self.lines = lines;
}
// /// Swap two lines in the grid
// ///
// /// This could have used slice::swap internally, but we are able to have
// /// better error messages by doing the bounds checking ourselves.
// #[inline]
// pub fn swap_lines(&mut self, src: index::Line, dst: index::Line) {
// self.raw.swap(*src, *dst);
// }
fn shrink_cols(&mut self, cols: index::Column) {
for row in self.raw.iter_mut() {
@ -335,12 +371,22 @@ impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
}
}
/// Index active region by line
impl<T> Index<index::Line> for Grid<T> {
type Output = Row<T>;
#[inline]
fn index(&self, index: index::Line) -> &Row<T> {
let index = self.lines.0 - index.0;
&self.raw[index]
}
}
/// Index with buffer offset
impl<T> Index<usize> for Grid<T> {
type Output = Row<T>;
#[inline]
fn index(&self, index: usize) -> &Row<T> {
&self.raw[index]
}
}
@ -348,7 +394,6 @@ impl<T> Index<index::Line> for Grid<T> {
impl<T> IndexMut<index::Line> for Grid<T> {
#[inline]
fn index_mut(&mut self, index: index::Line) -> &mut Row<T> {
let index = self.lines.0 - index.0;
&mut self.raw[index]
}
}
@ -369,9 +414,9 @@ impl<'point, T> IndexMut<&'point Point> for Grid<T> {
}
}
// =================================================================================================
// Regions =========================================================================================
// =================================================================================================
// -------------------------------------------------------------------------------------------------
// REGIONS
// -------------------------------------------------------------------------------------------------
/// A subset of lines in the grid
///
@ -533,7 +578,7 @@ impl<'a, T> Iterator for RegionIter<'a, T> {
if self.cur < self.end {
let index = self.cur;
self.cur += 1;
Some(&self.raw[*index])
Some(&self.raw[index])
} else {
None
}
@ -547,10 +592,67 @@ impl<'a, T> Iterator for RegionIterMut<'a, T> {
let index = self.cur;
self.cur += 1;
unsafe {
Some(&mut *(&mut self.raw[index.0] as *mut _))
Some(&mut *(&mut self.raw[index] as *mut _))
}
} else {
None
}
}
}
// -------------------------------------------------------------------------------------------------
// DISPLAY ITERATOR
// -------------------------------------------------------------------------------------------------
/// Iterates over the visible area accounting for buffer transform
pub struct DisplayIter<'a, T: 'a> {
grid: &'a Grid<T>,
offset: usize,
limit: usize,
col: Column,
}
impl<'a, T: 'a> DisplayIter<'a, T> {
pub fn new(grid: &'a Grid<T>) -> DisplayIter<'a, T> {
let offset = grid.display_offset + *grid.num_lines() - 1;
let limit = grid.display_offset;
let col = Column(0);
DisplayIter { grid, offset, col, limit }
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn column(&self) -> Column {
self.col
}
}
impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> {
type Item = Indexed<T>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
// Make sure indices are valid. Return None if we've reached the end.
if self.col == self.grid.num_cols() {
if self.offset == self.limit {
return None;
}
self.col = Column(0);
self.offset -= 1;
}
// Return the next item.
let item = Some(Indexed {
inner: self.grid.raw[self.offset][self.col],
line: Line( *self.grid.lines - 1 - (self.offset - self.limit)),
column: self.col
});
self.col += 1;
item
}
}

View File

@ -22,10 +22,7 @@ use index::Column;
/// A row in the grid
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Row<T> {
data: Vec<T>,
id: u64
}
pub struct Row<T>(Vec<T>);
impl<T: Copy + Clone> Row<T> {
pub fn new(columns: Column, template: &T) -> Row<T> {
@ -40,9 +37,8 @@ impl<T: Copy + Clone> Row<T> {
/// Resets contents to the contents of `other`
#[inline]
pub fn reset(&mut self, other: &Row<T>, id: u64) {
pub fn reset(&mut self, other: &Row<T>) {
self.copy_from_slice(&**other);
self.id = id;
}
}
@ -52,16 +48,6 @@ impl<T> Row<T> {
self.pop();
}
}
#[inline]
pub fn cells(&self) -> slice::Iter<T> {
self.0.iter()
}
#[inline]
pub fn cells_mut(&mut self) -> slice::IterMut<T> {
self.0.iter_mut()
}
}

View File

@ -13,21 +13,34 @@
/// done so manually.
use std::ops::{Index, IndexMut};
use index::Line;
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct Storage<T> {
inner: Vec<T>,
zero: usize,
visible_lines: Line,
}
impl<T> Storage<T> {
#[inline]
pub fn with_capacity(cap: usize) -> Storage<T> {
pub fn with_capacity(cap: usize, lines: Line) -> Storage<T> {
Storage {
inner: Vec::with_capacity(cap),
zero: 0
zero: 0,
visible_lines: lines - 1,
}
}
#[inline]
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
pub fn set_visible_lines(&mut self, next: Line) {
self.visible_lines = next - 1;
}
#[inline]
pub fn push(&mut self, item: T) {
self.inner.push(item)
@ -55,6 +68,12 @@ impl<T> Storage<T> {
self.inner.swap(a, b);
}
pub fn swap_lines(&mut self, a: Line, b: Line) {
let a = self.visible_lines - a;
let b = self.visible_lines - b;
self.swap(*a, *b);
}
pub fn iter_mut(&mut self) -> IterMut<T> {
IterMut { storage: self, index: 0 }
}
@ -88,6 +107,23 @@ impl<T> IndexMut<usize> for Storage<T> {
}
}
impl<T> Index<Line> for Storage<T> {
type Output = T;
#[inline]
fn index(&self, index: Line) -> &T {
let index = self.visible_lines - index;
&self[*index]
}
}
impl<T> IndexMut<Line> for Storage<T> {
#[inline]
fn index_mut(&mut self, index: Line) -> &mut T {
let index = self.visible_lines - index;
&mut self[*index]
}
}
pub struct IterMut<'a, T: 'a> {
storage: &'a mut Storage<T>,
index: usize,

View File

@ -64,6 +64,7 @@ pub trait ActionContext {
fn last_modifiers(&mut self) -> &mut ModifiersState;
fn change_font_size(&mut self, delta: i8);
fn reset_font_size(&mut self);
fn scroll(&mut self, count: isize) {}
}
/// Describes a state and action to take in that state
@ -428,10 +429,6 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
pub fn on_mouse_wheel(&mut self, delta: MouseScrollDelta, phase: TouchPhase, modifiers: ModifiersState) {
let mouse_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if !self.ctx.terminal_mode().intersects(mouse_modes | TermMode::ALT_SCREEN) {
return;
}
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
let to_scroll = self.ctx.mouse_mut().lines_scrolled + lines;
@ -482,16 +479,19 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
let mouse_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if self.ctx.terminal_mode().intersects(mouse_modes) {
self.mouse_report(code, ElementState::Pressed, modifiers);
} else if faux_scrollback_lines > 0 {
// Faux scrolling
let cmd = code + 1; // 64 + 1 = A, 65 + 1 = B
let mut content = Vec::with_capacity(faux_scrollback_lines * 3);
for _ in 0..faux_scrollback_lines {
content.push(0x1b);
content.push(b'O');
content.push(cmd);
}
self.ctx.write_to_pty(content);
// } else if faux_scrollback_lines > 0 {
// // Faux scrolling
// let cmd = code + 1; // 64 + 1 = A, 65 + 1 = B
// let mut content = Vec::with_capacity(faux_scrollback_lines * 3);
// for _ in 0..faux_scrollback_lines {
// content.push(0x1b);
// content.push(b'O');
// content.push(cmd);
// }
// self.ctx.write_to_pty(content);
// }
} else {
self.ctx.scroll(-((code as isize) * 2 - 129));
}
}

View File

@ -24,8 +24,8 @@ use unicode_width::UnicodeWidthChar;
use font::{self, Size};
use ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle};
use grid::{BidirectionalIterator, Grid, ToRange, Indexed, IndexRegion};
use index::{self, Point, Column, Line, Linear, IndexRange, Contains, RangeInclusive};
use grid::{BidirectionalIterator, Grid, ToRange, Indexed, IndexRegion, DisplayIter};
use index::{self, Point, Column, Line, IndexRange, Contains, RangeInclusive};
use selection::{self, Span, Selection};
use config::{Config, VisualBellAnimation};
use {MouseCursor, Rgb};
@ -94,12 +94,11 @@ impl selection::Dimensions for Term {
/// This manages the cursor during a render. The cursor location is inverted to
/// draw it, and reverted after drawing to maintain state.
pub struct RenderableCellsIter<'a> {
inner: DisplayIter<'a, Cell>,
grid: &'a Grid<Cell>,
cursor: &'a Point,
cursor_index: index::Linear,
cursor_offset: usize,
mode: TermMode,
line: Line,
column: Column,
config: &'a Config,
colors: &'a color::List,
selection: Option<RangeInclusive<index::Linear>>,
@ -120,18 +119,18 @@ impl<'a> RenderableCellsIter<'a> {
selection: Option<RangeInclusive<index::Linear>>,
cursor_style: CursorStyle,
) -> RenderableCellsIter<'b> {
let cursor_index = Linear(cursor.line.0 * grid.num_cols().0 + cursor.col.0);
let cursor_offset = grid.line_to_offset(cursor.line);
let inner = grid.display_iter();
RenderableCellsIter {
grid,
cursor,
cursor_index,
mode,
line: Line(0),
column: Column(0),
selection,
config,
colors,
cursor: cursor,
cursor_offset: cursor_offset,
grid: grid,
inner: inner,
mode: mode,
selection: selection,
config: config,
colors: colors,
cursor_cells: ArrayDeque::new(),
}.initialize(cursor_style)
}
@ -318,6 +317,7 @@ impl<'a> RenderableCellsIter<'a> {
}
pub struct RenderableCell {
/// A _Display_ line (not necessarily an _Active_ line)
pub line: Line,
pub column: Column,
pub c: char,
@ -336,81 +336,70 @@ impl<'a> Iterator for RenderableCellsIter<'a> {
/// (eg. invert fg and bg colors).
#[inline]
fn next(&mut self) -> Option<Self::Item> {
while self.line < self.grid.num_lines() {
while self.column < self.grid.num_cols() {
// Grab current state for this iteration
let line = self.line;
let mut column = self.column;
let cell = &self.grid[line][column];
loop {
// Handle cursor
let (cell, selected) = if self.cursor_offset == self.inner.offset() &&
self.inner.column() == self.cursor.col
{
// Cursor cell
let cell = self.cursor_cells.pop_front().unwrap();
let index = Linear(line.0 * self.grid.num_cols().0 + column.0);
let (cell, selected) = if index == self.cursor_index {
// Cursor cell
let cell = self.cursor_cells.pop_front().unwrap();
column = cell.column;
// Since there may be multiple cursor cells (for a wide
// char), only update iteration position after all cursor
// cells have been drawn.
if self.cursor_cells.is_empty() {
self.line = cell.line;
self.column = cell.column + 1;
}
(cell.inner, false)
} else {
// Normal cell
self.column += 1;
let selected = self.selection.as_ref()
.map(|range| range.contains_(index))
.unwrap_or(false);
// Skip empty cells
if cell.is_empty() && !selected {
continue;
}
(*cell, selected)
};
// Apply inversion and lookup RGB values
let mut bg_alpha = 1.0;
let fg_rgb;
let bg_rgb;
let invert = selected ^ cell.inverse();
if invert {
if cell.fg == cell.bg {
bg_rgb = self.colors[NamedColor::Foreground];
fg_rgb = self.colors[NamedColor::Background];
bg_alpha = 1.0
} else {
bg_rgb = self.compute_fg_rgb(&cell.fg, &cell);
fg_rgb = self.compute_bg_rgb(&cell.bg);
}
} else {
fg_rgb = self.compute_fg_rgb(&cell.fg, &cell);
bg_rgb = self.compute_bg_rgb(&cell.bg);
bg_alpha = self.compute_bg_alpha(&cell.bg);
// Since there may be multiple cursor cells (for a wide
// char), only update iteration position after all cursor
// cells have been drawn.
if self.cursor_cells.is_empty() {
self.inner.next();
}
(cell, false)
} else {
let cell = self.inner.next()?;
return Some(RenderableCell {
line,
column,
flags: cell.flags,
c: cell.c,
fg: fg_rgb,
bg: bg_rgb,
bg_alpha,
})
// XXX (jwilm) selection temp disabled
//
// let selected = self.selection.as_ref()
// .map(|range| range.contains_(index))
// .unwrap_or(false);
let selected = false;
// Skip empty cells
if cell.is_empty() && !selected {
continue;
}
(cell, selected)
};
// Apply inversion and lookup RGB values
let mut bg_alpha = 1.0;
let fg_rgb;
let bg_rgb;
let invert = selected ^ cell.inverse();
if invert {
if cell.fg == cell.bg {
bg_rgb = self.colors[NamedColor::Foreground];
fg_rgb = self.colors[NamedColor::Background];
bg_alpha = 1.0
} else {
bg_rgb = self.compute_fg_rgb(&cell.fg, &cell);
fg_rgb = self.compute_bg_rgb(&cell.bg);
}
} else {
fg_rgb = self.compute_fg_rgb(&cell.fg, &cell);
bg_rgb = self.compute_bg_rgb(&cell.bg);
bg_alpha = self.compute_bg_alpha(&cell.bg);
}
self.column = Column(0);
self.line += 1;
return Some(RenderableCell {
line: cell.line,
column: cell.column,
flags: cell.flags,
c: cell.c,
fg: fg_rgb,
bg: bg_rgb,
bg_alpha: bg_alpha,
})
}
None
}
}
@ -789,6 +778,11 @@ impl Term {
self.next_title.take()
}
pub fn scroll_display(&mut self, count: isize) {
self.grid.scroll_display(count);
self.dirty = true;
}
#[inline]
pub fn get_next_mouse_cursor(&mut self) -> Option<MouseCursor> {
self.next_mouse_cursor.take()
@ -1047,10 +1041,6 @@ impl Term {
num_lines = Line(2);
}
// Scroll up to keep cursor and as much context as possible in grid.
// This only runs when the lines decreases.
self.scroll_region = Line(0)..self.grid.num_lines();
// Scroll up to keep cursor in terminal
if self.cursor.point.line >= num_lines {
let lines = self.cursor.point.line - num_lines + 1;
@ -1062,7 +1052,6 @@ impl Term {
let lines = self.cursor_save_alt.point.line - num_lines + 1;
self.alt_grid.scroll_up(&(Line(0)..old_lines), lines);
}
debug!("num_cols, num_lines = {}, {}", num_cols, num_lines);
// Resize grids to new size
@ -1085,16 +1074,16 @@ impl Term {
.map(|i| (*i as usize) % self.tabspaces == 0)
.collect::<Vec<bool>>();
if num_lines > old_lines {
// Make sure bottom of terminal is clear
let template = self.cursor.template;
self.grid
.region_mut((self.cursor.point.line + 1)..)
.each(|c| c.reset(&template));
self.alt_grid
.region_mut((self.cursor_save_alt.point.line + 1)..)
.each(|c| c.reset(&template));
}
// if num_lines > old_lines {
// // Make sure bottom of terminal is clear
// let template = self.cursor.template;
// self.grid
// .region_mut((self.cursor.point.line + 1)..)
// .each(|c| c.reset(&template));
// self.alt_grid
// .region_mut((self.cursor_save_alt.point.line + 1)..)
// .each(|c| c.reset(&template));
// }
}