114 lines
3.2 KiB
Rust
114 lines
3.2 KiB
Rust
//! The terminal's text: a grid of cells, a cursor, and which rows changed.
|
|
|
|
use alloc::vec;
|
|
use alloc::vec::Vec;
|
|
|
|
pub const MAX_COLS: usize = 160;
|
|
pub const MAX_ROWS: usize = 64;
|
|
|
|
/// On the heap: a full screen is more than a program stack holds.
|
|
pub struct Grid {
|
|
cells: Vec<u8>,
|
|
cols: usize,
|
|
rows: usize,
|
|
pub col: usize,
|
|
pub row: usize,
|
|
/// Rows to redraw.
|
|
dirty: Vec<bool>,
|
|
/// Rows everything moved up by since the screen last caught up: moving
|
|
/// the pixels is cheaper than drawing every row again.
|
|
scrolled: usize,
|
|
}
|
|
|
|
impl Grid {
|
|
pub fn new(cols: usize, rows: usize) -> Self {
|
|
let (cols, rows) = (cols.clamp(1, MAX_COLS), rows.clamp(1, MAX_ROWS));
|
|
Self { cells: vec![b' '; cols * rows], cols, rows, col: 0, row: 0, dirty: vec![true; rows], scrolled: 0 }
|
|
}
|
|
|
|
pub fn row(&self, r: usize) -> &[u8] {
|
|
&self.cells[r * self.cols..(r + 1) * self.cols]
|
|
}
|
|
|
|
pub fn rows(&self) -> usize {
|
|
self.rows
|
|
}
|
|
|
|
/// The rows changed since the last call, each once.
|
|
pub fn take_dirty(&mut self) -> impl Iterator<Item = usize> + '_ {
|
|
let rows = self.rows;
|
|
self.dirty[..rows].iter_mut().enumerate().filter_map(|(r, d)| core::mem::take(d).then_some(r))
|
|
}
|
|
|
|
/// How far to move the screen up before drawing the dirty rows, which
|
|
/// already account for it.
|
|
pub fn take_scrolled(&mut self) -> usize {
|
|
core::mem::take(&mut self.scrolled)
|
|
}
|
|
|
|
fn mark(&mut self, r: usize) {
|
|
self.dirty[r] = true;
|
|
}
|
|
|
|
fn newline(&mut self) {
|
|
// The row the cursor leaves, so its cursor cell is redrawn.
|
|
self.mark(self.row);
|
|
self.col = 0;
|
|
if self.row + 1 < self.rows {
|
|
self.row += 1;
|
|
return;
|
|
}
|
|
// At the bottom: everything moves up a row.
|
|
self.cells.copy_within(self.cols.., 0);
|
|
let last = (self.rows - 1) * self.cols;
|
|
self.cells[last..].fill(b' ');
|
|
// Rows still to redraw move up with their text.
|
|
self.dirty.copy_within(1.., 0);
|
|
self.mark(self.rows - 1);
|
|
if self.scrolled + 1 >= self.rows {
|
|
// Scrolled a whole screen: just draw it all.
|
|
self.touch_all();
|
|
self.scrolled = 0;
|
|
} else {
|
|
self.scrolled += 1;
|
|
}
|
|
}
|
|
|
|
/// Writes one byte at the cursor: `\n` moves to the next line, other
|
|
/// bytes wrap at the right edge.
|
|
pub fn put(&mut self, b: u8) {
|
|
if b == b'\n' {
|
|
self.newline();
|
|
return;
|
|
}
|
|
if self.col == self.cols {
|
|
self.newline();
|
|
}
|
|
self.cells[self.row * self.cols + self.col] = b;
|
|
self.mark(self.row);
|
|
self.col += 1;
|
|
}
|
|
|
|
/// Erases the cell before the cursor, on this row only.
|
|
pub fn back(&mut self) {
|
|
if self.col > 0 {
|
|
self.col -= 1;
|
|
self.cells[self.row * self.cols + self.col] = b' ';
|
|
self.mark(self.row);
|
|
}
|
|
}
|
|
|
|
/// Marks every row to be drawn again.
|
|
pub fn touch_all(&mut self) {
|
|
self.dirty.fill(true);
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.cells.fill(b' ');
|
|
self.col = 0;
|
|
self.row = 0;
|
|
self.dirty.fill(true);
|
|
self.scrolled = 0;
|
|
}
|
|
}
|