aboutsummaryrefslogtreecommitdiff
path: root/vendor/vt100/src/grid.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-31 20:49:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-31 20:49:05 +0300
commit41e009f9b7b46d335d41b365e41f9ad57d571ecb (patch)
tree42b3be90c336ad6d4487e9f86abc56ebb40ba210 /vendor/vt100/src/grid.rs
parentf826fb78dcac89b4ba23e399ac5d141459c14ae9 (diff)
fix alt-screen scrollback handling
Diffstat (limited to 'vendor/vt100/src/grid.rs')
-rw-r--r--vendor/vt100/src/grid.rs743
1 files changed, 743 insertions, 0 deletions
diff --git a/vendor/vt100/src/grid.rs b/vendor/vt100/src/grid.rs
new file mode 100644
index 00000000..b1c5d341
--- /dev/null
+++ b/vendor/vt100/src/grid.rs
@@ -0,0 +1,743 @@
+use crate::term::BufWrite as _;
+
+#[derive(Clone, Debug)]
+pub struct Grid {
+ size: Size,
+ pos: Pos,
+ saved_pos: Pos,
+ rows: Vec<crate::row::Row>,
+ scroll_top: u16,
+ scroll_bottom: u16,
+ origin_mode: bool,
+ saved_origin_mode: bool,
+ scrollback: std::collections::VecDeque<crate::row::Row>,
+ scrollback_len: usize,
+ scrollback_offset: usize,
+}
+
+impl Grid {
+ pub fn new(size: Size, scrollback_len: usize) -> Self {
+ Self {
+ size,
+ pos: Pos::default(),
+ saved_pos: Pos::default(),
+ rows: vec![],
+ scroll_top: 0,
+ scroll_bottom: size.rows - 1,
+ origin_mode: false,
+ saved_origin_mode: false,
+ scrollback: std::collections::VecDeque::new(),
+ scrollback_len,
+ scrollback_offset: 0,
+ }
+ }
+
+ pub fn allocate_rows(&mut self) {
+ if self.rows.is_empty() {
+ self.rows.extend(
+ std::iter::repeat_with(|| {
+ crate::row::Row::new(self.size.cols)
+ })
+ .take(usize::from(self.size.rows)),
+ );
+ }
+ }
+
+ fn new_row(&self) -> crate::row::Row {
+ crate::row::Row::new(self.size.cols)
+ }
+
+ pub fn clear(&mut self) {
+ self.pos = Pos::default();
+ self.saved_pos = Pos::default();
+ for row in self.drawing_rows_mut() {
+ row.clear(crate::attrs::Attrs::default());
+ }
+ self.scroll_top = 0;
+ self.scroll_bottom = self.size.rows - 1;
+ self.origin_mode = false;
+ self.saved_origin_mode = false;
+ }
+
+ pub fn clear_scrollback(&mut self) {
+ self.scrollback.clear();
+ self.scrollback_offset = 0;
+ }
+
+ pub fn size(&self) -> Size {
+ self.size
+ }
+
+ pub fn set_size(&mut self, size: Size) {
+ if size.cols != self.size.cols {
+ for row in &mut self.rows {
+ row.wrap(false);
+ }
+ }
+
+ if self.scroll_bottom == self.size.rows - 1 {
+ self.scroll_bottom = size.rows - 1;
+ }
+
+ self.size = size;
+ for row in &mut self.rows {
+ row.resize(size.cols, crate::Cell::new());
+ }
+ self.rows.resize(usize::from(size.rows), self.new_row());
+
+ if self.scroll_bottom >= size.rows {
+ self.scroll_bottom = size.rows - 1;
+ }
+ if self.scroll_bottom < self.scroll_top {
+ self.scroll_top = 0;
+ }
+
+ self.row_clamp_top(false);
+ self.row_clamp_bottom(false);
+ self.col_clamp();
+
+ if self.saved_pos.row > self.size.rows - 1 {
+ self.saved_pos.row = self.size.rows - 1;
+ }
+ if self.saved_pos.col > self.size.cols - 1 {
+ self.saved_pos.col = self.size.cols - 1;
+ }
+ }
+
+ pub fn pos(&self) -> Pos {
+ self.pos
+ }
+
+ pub fn set_pos(&mut self, mut pos: Pos) {
+ if self.origin_mode {
+ pos.row = pos.row.saturating_add(self.scroll_top);
+ }
+ self.pos = pos;
+ self.row_clamp_top(self.origin_mode);
+ self.row_clamp_bottom(self.origin_mode);
+ self.col_clamp();
+ }
+
+ pub fn save_cursor(&mut self) {
+ self.saved_pos = self.pos;
+ self.saved_origin_mode = self.origin_mode;
+ }
+
+ pub fn restore_cursor(&mut self) {
+ self.pos = self.saved_pos;
+ self.origin_mode = self.saved_origin_mode;
+ }
+
+ pub fn visible_rows(&self) -> impl Iterator<Item = &crate::row::Row> {
+ let scrollback_len = self.scrollback.len();
+ let rows_len = self.rows.len();
+ self.scrollback
+ .iter()
+ .skip(scrollback_len - self.scrollback_offset)
+ // when scrollback_offset > rows_len (e.g. rows = 3,
+ // scrollback_len = 10, offset = 9) the skip(10 - 9)
+ // will take 9 rows instead of 3. we need to set
+ // the upper bound to rows_len (e.g. 3)
+ .take(rows_len)
+ // same for rows_len - scrollback_offset (e.g. 3 - 9).
+ // it'll panic with overflow. we have to saturate the subtraction.
+ .chain(
+ self.rows
+ .iter()
+ .take(rows_len.saturating_sub(self.scrollback_offset)),
+ )
+ }
+
+ pub fn drawing_rows(&self) -> impl Iterator<Item = &crate::row::Row> {
+ self.rows.iter()
+ }
+
+ pub fn drawing_rows_mut(
+ &mut self,
+ ) -> impl Iterator<Item = &mut crate::row::Row> {
+ self.rows.iter_mut()
+ }
+
+ pub fn visible_row(&self, row: u16) -> Option<&crate::row::Row> {
+ self.visible_rows().nth(usize::from(row))
+ }
+
+ pub fn drawing_row(&self, row: u16) -> Option<&crate::row::Row> {
+ self.drawing_rows().nth(usize::from(row))
+ }
+
+ pub fn drawing_row_mut(
+ &mut self,
+ row: u16,
+ ) -> Option<&mut crate::row::Row> {
+ self.drawing_rows_mut().nth(usize::from(row))
+ }
+
+ pub fn current_row_mut(&mut self) -> &mut crate::row::Row {
+ self.drawing_row_mut(self.pos.row)
+ // we assume self.pos.row is always valid
+ .unwrap()
+ }
+
+ pub fn visible_cell(&self, pos: Pos) -> Option<&crate::Cell> {
+ self.visible_row(pos.row).and_then(|r| r.get(pos.col))
+ }
+
+ pub fn drawing_cell(&self, pos: Pos) -> Option<&crate::Cell> {
+ self.drawing_row(pos.row).and_then(|r| r.get(pos.col))
+ }
+
+ pub fn drawing_cell_mut(&mut self, pos: Pos) -> Option<&mut crate::Cell> {
+ self.drawing_row_mut(pos.row)
+ .and_then(|r| r.get_mut(pos.col))
+ }
+
+ pub fn scrollback_len(&self) -> usize {
+ self.scrollback_len
+ }
+
+ pub fn scrollback(&self) -> usize {
+ self.scrollback_offset
+ }
+
+ pub fn set_scrollback(&mut self, rows: usize) {
+ self.scrollback_offset = rows.min(self.scrollback.len());
+ }
+
+ pub fn write_contents(&self, contents: &mut String) {
+ let mut wrapping = false;
+ for row in self.visible_rows() {
+ row.write_contents(contents, 0, self.size.cols, wrapping);
+ if !row.wrapped() {
+ contents.push('\n');
+ }
+ wrapping = row.wrapped();
+ }
+
+ while contents.ends_with('\n') {
+ contents.truncate(contents.len() - 1);
+ }
+ }
+
+ pub fn write_contents_formatted(
+ &self,
+ contents: &mut Vec<u8>,
+ ) -> crate::attrs::Attrs {
+ crate::term::ClearAttrs.write_buf(contents);
+ crate::term::ClearScreen.write_buf(contents);
+
+ let mut prev_attrs = crate::attrs::Attrs::default();
+ let mut prev_pos = Pos::default();
+ let mut wrapping = false;
+ for (i, row) in self.visible_rows().enumerate() {
+ // we limit the number of cols to a u16 (see Size), so
+ // visible_rows() can never return more rows than will fit
+ let i = i.try_into().unwrap();
+ let (new_pos, new_attrs) = row.write_contents_formatted(
+ contents,
+ 0,
+ self.size.cols,
+ i,
+ wrapping,
+ Some(prev_pos),
+ Some(prev_attrs),
+ );
+ prev_pos = new_pos;
+ prev_attrs = new_attrs;
+ wrapping = row.wrapped();
+ }
+
+ self.write_cursor_position_formatted(
+ contents,
+ Some(prev_pos),
+ Some(prev_attrs),
+ );
+
+ prev_attrs
+ }
+
+ pub fn write_contents_diff(
+ &self,
+ contents: &mut Vec<u8>,
+ prev: &Self,
+ mut prev_attrs: crate::attrs::Attrs,
+ ) -> crate::attrs::Attrs {
+ let mut prev_pos = prev.pos;
+ let mut wrapping = false;
+ let mut prev_wrapping = false;
+ for (i, (row, prev_row)) in
+ self.visible_rows().zip(prev.visible_rows()).enumerate()
+ {
+ // we limit the number of cols to a u16 (see Size), so
+ // visible_rows() can never return more rows than will fit
+ let i = i.try_into().unwrap();
+ let (new_pos, new_attrs) = row.write_contents_diff(
+ contents,
+ prev_row,
+ 0,
+ self.size.cols,
+ i,
+ wrapping,
+ prev_wrapping,
+ prev_pos,
+ prev_attrs,
+ );
+ prev_pos = new_pos;
+ prev_attrs = new_attrs;
+ wrapping = row.wrapped();
+ prev_wrapping = prev_row.wrapped();
+ }
+
+ self.write_cursor_position_formatted(
+ contents,
+ Some(prev_pos),
+ Some(prev_attrs),
+ );
+
+ prev_attrs
+ }
+
+ pub fn write_cursor_position_formatted(
+ &self,
+ contents: &mut Vec<u8>,
+ prev_pos: Option<Pos>,
+ prev_attrs: Option<crate::attrs::Attrs>,
+ ) {
+ let prev_attrs = prev_attrs.unwrap_or_default();
+ // writing a character to the last column of a row doesn't wrap the
+ // cursor immediately - it waits until the next character is actually
+ // drawn. it is only possible for the cursor to have this kind of
+ // position after drawing a character though, so if we end in this
+ // position, we need to redraw the character at the end of the row.
+ if prev_pos != Some(self.pos) && self.pos.col >= self.size.cols {
+ let mut pos = Pos {
+ row: self.pos.row,
+ col: self.size.cols - 1,
+ };
+ if self
+ .drawing_cell(pos)
+ // we assume self.pos.row is always valid, and self.size.cols
+ // - 1 is always a valid column
+ .unwrap()
+ .is_wide_continuation()
+ {
+ pos.col = self.size.cols - 2;
+ }
+ let cell =
+ // we assume self.pos.row is always valid, and self.size.cols
+ // - 2 must be a valid column because self.size.cols - 1 is
+ // always valid and we just checked that the cell at
+ // self.size.cols - 1 is a wide continuation character, which
+ // means that the first half of the wide character must be
+ // before it
+ self.drawing_cell(pos).unwrap();
+ if cell.has_contents() {
+ if let Some(prev_pos) = prev_pos {
+ crate::term::MoveFromTo::new(prev_pos, pos)
+ .write_buf(contents);
+ } else {
+ crate::term::MoveTo::new(pos).write_buf(contents);
+ }
+ cell.attrs().write_escape_code_diff(contents, &prev_attrs);
+ contents.extend(cell.contents().as_bytes());
+ prev_attrs.write_escape_code_diff(contents, cell.attrs());
+ } else {
+ // if the cell doesn't have contents, we can't have gotten
+ // here by drawing a character in the last column. this means
+ // that as far as i'm aware, we have to have reached here from
+ // a newline when we were already after the end of an earlier
+ // row. in the case where we are already after the end of an
+ // earlier row, we can just write a few newlines, otherwise we
+ // also need to do the same as above to get ourselves to after
+ // the end of a row.
+ let mut found = false;
+ for i in (0..self.pos.row).rev() {
+ pos.row = i;
+ pos.col = self.size.cols - 1;
+ if self
+ .drawing_cell(pos)
+ // i is always less than self.pos.row, which we assume
+ // to be always valid, so it must also be valid.
+ // self.size.cols - 1 is always a valid col.
+ .unwrap()
+ .is_wide_continuation()
+ {
+ pos.col = self.size.cols - 2;
+ }
+ let cell = self
+ .drawing_cell(pos)
+ // i is always less than self.pos.row, which we assume
+ // to be always valid, so it must also be valid.
+ // self.size.cols - 2 is valid because self.size.cols
+ // - 1 is always valid, and col gets set to
+ // self.size.cols - 2 when the cell at self.size.cols
+ // - 1 is a wide continuation character, meaning that
+ // the first half of the wide character must be before
+ // it
+ .unwrap();
+ if cell.has_contents() {
+ if let Some(prev_pos) = prev_pos {
+ if prev_pos.row != i
+ || prev_pos.col < self.size.cols
+ {
+ crate::term::MoveFromTo::new(prev_pos, pos)
+ .write_buf(contents);
+ cell.attrs().write_escape_code_diff(
+ contents,
+ &prev_attrs,
+ );
+ contents.extend(cell.contents().as_bytes());
+ prev_attrs.write_escape_code_diff(
+ contents,
+ cell.attrs(),
+ );
+ }
+ } else {
+ crate::term::MoveTo::new(pos).write_buf(contents);
+ cell.attrs().write_escape_code_diff(
+ contents,
+ &prev_attrs,
+ );
+ contents.extend(cell.contents().as_bytes());
+ prev_attrs.write_escape_code_diff(
+ contents,
+ cell.attrs(),
+ );
+ }
+ contents.extend(
+ "\n".repeat(usize::from(self.pos.row - i))
+ .as_bytes(),
+ );
+ found = true;
+ break;
+ }
+ }
+
+ // this can happen if you get the cursor off the end of a row,
+ // and then do something to clear the end of the current row
+ // without moving the cursor (IL, DL, ED, EL, etc). we know
+ // there can't be something in the last column because we
+ // would have caught that above, so it should be safe to
+ // overwrite it.
+ if !found {
+ pos = Pos {
+ row: self.pos.row,
+ col: self.size.cols - 1,
+ };
+ if let Some(prev_pos) = prev_pos {
+ crate::term::MoveFromTo::new(prev_pos, pos)
+ .write_buf(contents);
+ } else {
+ crate::term::MoveTo::new(pos).write_buf(contents);
+ }
+ contents.push(b' ');
+ // we know that the cell has no contents, but it still may
+ // have drawing attributes (background color, etc)
+ let end_cell = self
+ .drawing_cell(pos)
+ // we assume self.pos.row is always valid, and
+ // self.size.cols - 1 is always a valid column
+ .unwrap();
+ end_cell
+ .attrs()
+ .write_escape_code_diff(contents, &prev_attrs);
+ crate::term::SaveCursor.write_buf(contents);
+ crate::term::Backspace.write_buf(contents);
+ crate::term::EraseChar::new(1).write_buf(contents);
+ crate::term::RestoreCursor.write_buf(contents);
+ prev_attrs
+ .write_escape_code_diff(contents, end_cell.attrs());
+ }
+ }
+ } else if let Some(prev_pos) = prev_pos {
+ crate::term::MoveFromTo::new(prev_pos, self.pos)
+ .write_buf(contents);
+ } else {
+ crate::term::MoveTo::new(self.pos).write_buf(contents);
+ }
+ }
+
+ pub fn erase_all(&mut self, attrs: crate::attrs::Attrs) {
+ for row in self.drawing_rows_mut() {
+ row.clear(attrs);
+ }
+ }
+
+ pub fn erase_all_forward(&mut self, attrs: crate::attrs::Attrs) {
+ let pos = self.pos;
+ for row in self.drawing_rows_mut().skip(usize::from(pos.row) + 1) {
+ row.clear(attrs);
+ }
+
+ self.erase_row_forward(attrs);
+ }
+
+ pub fn erase_all_backward(&mut self, attrs: crate::attrs::Attrs) {
+ let pos = self.pos;
+ for row in self.drawing_rows_mut().take(usize::from(pos.row)) {
+ row.clear(attrs);
+ }
+
+ self.erase_row_backward(attrs);
+ }
+
+ pub fn erase_row(&mut self, attrs: crate::attrs::Attrs) {
+ self.current_row_mut().clear(attrs);
+ }
+
+ pub fn erase_row_forward(&mut self, attrs: crate::attrs::Attrs) {
+ let size = self.size;
+ let pos = self.pos;
+ let row = self.current_row_mut();
+ for col in pos.col..size.cols {
+ row.erase(col, attrs);
+ }
+ }
+
+ pub fn erase_row_backward(&mut self, attrs: crate::attrs::Attrs) {
+ let size = self.size;
+ let pos = self.pos;
+ let row = self.current_row_mut();
+ for col in 0..=pos.col.min(size.cols - 1) {
+ row.erase(col, attrs);
+ }
+ }
+
+ pub fn insert_cells(&mut self, count: u16) {
+ let size = self.size;
+ let pos = self.pos;
+ let wide = pos.col < size.cols
+ && self
+ .drawing_cell(pos)
+ // we assume self.pos.row is always valid, and we know we are
+ // not off the end of a row because we just checked pos.col <
+ // size.cols
+ .unwrap()
+ .is_wide_continuation();
+ let row = self.current_row_mut();
+ for _ in 0..count {
+ if wide {
+ row.get_mut(pos.col).unwrap().set_wide_continuation(false);
+ }
+ row.insert(pos.col, crate::Cell::new());
+ if wide {
+ row.get_mut(pos.col).unwrap().set_wide_continuation(true);
+ }
+ }
+ row.truncate(size.cols);
+ }
+
+ pub fn delete_cells(&mut self, count: u16) {
+ let size = self.size;
+ let pos = self.pos;
+ let row = self.current_row_mut();
+ for _ in 0..(count.min(size.cols - pos.col)) {
+ row.remove(pos.col);
+ }
+ row.resize(size.cols, crate::Cell::new());
+ }
+
+ pub fn erase_cells(&mut self, count: u16, attrs: crate::attrs::Attrs) {
+ let size = self.size;
+ let pos = self.pos;
+ let row = self.current_row_mut();
+ for col in pos.col..((pos.col.saturating_add(count)).min(size.cols)) {
+ row.erase(col, attrs);
+ }
+ }
+
+ pub fn insert_lines(&mut self, count: u16) {
+ for _ in 0..count {
+ self.rows.remove(usize::from(self.scroll_bottom));
+ self.rows.insert(usize::from(self.pos.row), self.new_row());
+ // self.scroll_bottom is maintained to always be a valid row
+ self.rows[usize::from(self.scroll_bottom)].wrap(false);
+ }
+ }
+
+ pub fn delete_lines(&mut self, count: u16) {
+ for _ in 0..(count.min(self.size.rows - self.pos.row)) {
+ self.rows
+ .insert(usize::from(self.scroll_bottom) + 1, self.new_row());
+ self.rows.remove(usize::from(self.pos.row));
+ }
+ }
+
+ pub fn scroll_up(&mut self, count: u16) {
+ for _ in 0..(count.min(self.size.rows - self.scroll_top)) {
+ self.rows
+ .insert(usize::from(self.scroll_bottom) + 1, self.new_row());
+ let removed = self.rows.remove(usize::from(self.scroll_top));
+ if self.scrollback_len > 0 && self.scroll_top == 0 {
+ self.scrollback.push_back(removed);
+ while self.scrollback.len() > self.scrollback_len {
+ self.scrollback.pop_front();
+ }
+ if self.scrollback_offset > 0 {
+ self.scrollback_offset =
+ self.scrollback.len().min(self.scrollback_offset + 1);
+ }
+ }
+ }
+ }
+
+ pub fn scroll_down(&mut self, count: u16) {
+ for _ in 0..count {
+ self.rows.remove(usize::from(self.scroll_bottom));
+ self.rows
+ .insert(usize::from(self.scroll_top), self.new_row());
+ // self.scroll_bottom is maintained to always be a valid row
+ self.rows[usize::from(self.scroll_bottom)].wrap(false);
+ }
+ }
+
+ pub fn set_scroll_region(&mut self, top: u16, bottom: u16) {
+ let bottom = bottom.min(self.size().rows - 1);
+ if top < bottom {
+ self.scroll_top = top;
+ self.scroll_bottom = bottom;
+ } else {
+ self.scroll_top = 0;
+ self.scroll_bottom = self.size().rows - 1;
+ }
+ self.pos.row = self.scroll_top;
+ self.pos.col = 0;
+ }
+
+ fn in_scroll_region(&self) -> bool {
+ self.pos.row >= self.scroll_top && self.pos.row <= self.scroll_bottom
+ }
+
+ pub fn set_origin_mode(&mut self, mode: bool) {
+ self.origin_mode = mode;
+ self.set_pos(Pos { row: 0, col: 0 });
+ }
+
+ pub fn row_inc_clamp(&mut self, count: u16) {
+ let in_scroll_region = self.in_scroll_region();
+ self.pos.row = self.pos.row.saturating_add(count);
+ self.row_clamp_bottom(in_scroll_region);
+ }
+
+ pub fn row_inc_scroll(&mut self, count: u16) -> u16 {
+ let in_scroll_region = self.in_scroll_region();
+ self.pos.row = self.pos.row.saturating_add(count);
+ let lines = self.row_clamp_bottom(in_scroll_region);
+ if in_scroll_region {
+ self.scroll_up(lines);
+ lines
+ } else {
+ 0
+ }
+ }
+
+ pub fn row_dec_clamp(&mut self, count: u16) {
+ let in_scroll_region = self.in_scroll_region();
+ self.pos.row = self.pos.row.saturating_sub(count);
+ self.row_clamp_top(in_scroll_region);
+ }
+
+ pub fn row_dec_scroll(&mut self, count: u16) {
+ let in_scroll_region = self.in_scroll_region();
+ // need to account for clamping by both row_clamp_top and by
+ // saturating_sub
+ let extra_lines = count.saturating_sub(self.pos.row);
+ self.pos.row = self.pos.row.saturating_sub(count);
+ let lines = self.row_clamp_top(in_scroll_region);
+ self.scroll_down(lines + extra_lines);
+ }
+
+ pub fn row_set(&mut self, i: u16) {
+ self.pos.row = i;
+ self.row_clamp();
+ }
+
+ pub fn col_inc(&mut self, count: u16) {
+ self.pos.col = self.pos.col.saturating_add(count);
+ }
+
+ pub fn col_inc_clamp(&mut self, count: u16) {
+ self.pos.col = self.pos.col.saturating_add(count);
+ self.col_clamp();
+ }
+
+ pub fn col_dec(&mut self, count: u16) {
+ self.pos.col = self.pos.col.saturating_sub(count);
+ }
+
+ pub fn col_tab(&mut self) {
+ self.pos.col -= self.pos.col % 8;
+ self.pos.col += 8;
+ self.col_clamp();
+ }
+
+ pub fn col_set(&mut self, i: u16) {
+ self.pos.col = i;
+ self.col_clamp();
+ }
+
+ pub fn col_wrap(&mut self, width: u16, wrap: bool) {
+ if self.pos.col > self.size.cols - width {
+ let mut prev_pos = self.pos;
+ self.pos.col = 0;
+ let scrolled = self.row_inc_scroll(1);
+ prev_pos.row -= scrolled;
+ let new_pos = self.pos;
+ self.drawing_row_mut(prev_pos.row)
+ // we assume self.pos.row is always valid, and so prev_pos.row
+ // must be valid because it is always less than or equal to
+ // self.pos.row
+ .unwrap()
+ .wrap(wrap && prev_pos.row + 1 == new_pos.row);
+ }
+ }
+
+ fn row_clamp_top(&mut self, limit_to_scroll_region: bool) -> u16 {
+ if limit_to_scroll_region && self.pos.row < self.scroll_top {
+ let rows = self.scroll_top - self.pos.row;
+ self.pos.row = self.scroll_top;
+ rows
+ } else {
+ 0
+ }
+ }
+
+ fn row_clamp_bottom(&mut self, limit_to_scroll_region: bool) -> u16 {
+ let bottom = if limit_to_scroll_region {
+ self.scroll_bottom
+ } else {
+ self.size.rows - 1
+ };
+ if self.pos.row > bottom {
+ let rows = self.pos.row - bottom;
+ self.pos.row = bottom;
+ rows
+ } else {
+ 0
+ }
+ }
+
+ fn row_clamp(&mut self) {
+ if self.pos.row > self.size.rows - 1 {
+ self.pos.row = self.size.rows - 1;
+ }
+ }
+
+ fn col_clamp(&mut self) {
+ if self.pos.col > self.size.cols - 1 {
+ self.pos.col = self.size.cols - 1;
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub struct Size {
+ pub rows: u16,
+ pub cols: u16,
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub struct Pos {
+ pub row: u16,
+ pub col: u16,
+}