toss/src/buffer.rs

484 lines
12 KiB
Rust

use std::ops::{Add, AddAssign, Neg, Range, Sub, SubAssign};
use crossterm::style::ContentStyle;
use crate::styled::Styled;
use crate::widthdb::WidthDB;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Size {
pub width: u16,
pub height: u16,
}
impl Size {
pub const ZERO: Self = Self::new(0, 0);
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
}
}
impl Add for Size {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.width + rhs.width, self.height + rhs.height)
}
}
impl AddAssign for Size {
fn add_assign(&mut self, rhs: Self) {
self.width += rhs.width;
self.height += rhs.height;
}
}
impl Sub for Size {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::new(self.width - rhs.width, self.height - rhs.height)
}
}
impl SubAssign for Size {
fn sub_assign(&mut self, rhs: Self) {
self.width -= rhs.width;
self.height -= rhs.height;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pos {
pub x: i32,
pub y: i32,
}
impl Pos {
pub const ZERO: Self = Self::new(0, 0);
pub const fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
impl From<Size> for Pos {
fn from(s: Size) -> Self {
Self::new(s.width.into(), s.height.into())
}
}
impl Add for Pos {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl Add<Size> for Pos {
type Output = Self;
fn add(self, rhs: Size) -> Self {
Self::new(self.x + rhs.width as i32, self.y + rhs.height as i32)
}
}
impl AddAssign for Pos {
fn add_assign(&mut self, rhs: Self) {
self.x += rhs.x;
self.y += rhs.y;
}
}
impl AddAssign<Size> for Pos {
fn add_assign(&mut self, rhs: Size) {
self.x += rhs.width as i32;
self.y += rhs.height as i32;
}
}
impl Sub for Pos {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl Sub<Size> for Pos {
type Output = Self;
fn sub(self, rhs: Size) -> Self {
Self::new(self.x - rhs.width as i32, self.y - rhs.height as i32)
}
}
impl SubAssign for Pos {
fn sub_assign(&mut self, rhs: Self) {
self.x -= rhs.x;
self.y -= rhs.y;
}
}
impl SubAssign<Size> for Pos {
fn sub_assign(&mut self, rhs: Size) {
self.x -= rhs.width as i32;
self.y -= rhs.height as i32;
}
}
impl Neg for Pos {
type Output = Self;
fn neg(self) -> Self {
Self::new(-self.x, -self.y)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
pub content: Box<str>,
pub style: ContentStyle,
pub width: u8,
pub offset: u8,
}
impl Default for Cell {
fn default() -> Self {
Self {
content: " ".to_string().into_boxed_str(),
style: ContentStyle::default(),
width: 1,
offset: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
struct StackFrame {
pub pos: Pos,
pub size: Size,
pub drawable_area: Option<(Pos, Size)>,
}
impl StackFrame {
fn intersect_areas(
a_start: Pos,
a_size: Size,
b_start: Pos,
b_size: Size,
) -> Option<(Pos, Size)> {
// The first row/column that is not part of the area any more
let a_end = a_start + a_size;
let b_end = b_start + b_size;
let x_start = a_start.x.max(b_start.x);
let x_end = a_end.x.min(b_end.x);
let y_start = a_start.y.max(b_start.y);
let y_end = a_end.y.min(b_end.y);
if x_start < x_end && y_start < y_end {
let start = Pos::new(x_start, y_start);
let size = Size::new((x_end - x_start) as u16, (y_end - y_start) as u16);
Some((start, size))
} else {
None
}
}
fn then(&self, pos: Pos, size: Size) -> Self {
let pos = self.local_to_global(pos);
let drawable_area = self
.drawable_area
.and_then(|(da_pos, da_size)| Self::intersect_areas(da_pos, da_size, pos, size));
StackFrame {
pos,
size,
drawable_area,
}
}
fn local_to_global(&self, local_pos: Pos) -> Pos {
local_pos + self.pos
}
fn global_to_local(&self, global_pos: Pos) -> Pos {
global_pos - self.pos
}
/// Ranges along the x and y axis where drawing is allowed, in global
/// coordinates.
fn legal_ranges(&self) -> Option<(Range<i32>, Range<i32>)> {
if let Some((pos, size)) = self.drawable_area {
let xrange = pos.x..pos.x + size.width as i32;
let yrange = pos.y..pos.y + size.height as i32;
Some((xrange, yrange))
} else {
None
}
}
}
#[derive(Debug, Default)]
pub struct Buffer {
size: Size,
data: Vec<Cell>,
cursor: Option<Pos>,
/// A stack of rectangular drawing areas.
///
/// When rendering to the buffer with a nonempty stack, it behaves as if it
/// was the size of the topmost stack element, and characters are translated
/// by the position of the topmost stack element. No characters can be
/// placed outside the area described by the topmost stack element.
stack: Vec<StackFrame>,
}
impl Buffer {
/// Index in `data` of the cell at the given position. The position must
/// be inside the buffer.
///
/// Ignores the stack.
fn index(&self, x: u16, y: u16) -> usize {
assert!(x < self.size.width);
assert!(y < self.size.height);
let x: usize = x.into();
let y: usize = y.into();
let width: usize = self.size.width.into();
y * width + x
}
/// A reference to the cell at the given position. The position must be
/// inside the buffer.
///
/// Ignores the stack.
pub fn at(&self, x: u16, y: u16) -> &Cell {
assert!(x < self.size.width);
assert!(y < self.size.height);
let i = self.index(x, y);
&self.data[i]
}
/// A mutable reference to the cell at the given position. The position must
/// be inside the buffer.
///
/// Ignores the stack.
fn at_mut(&mut self, x: u16, y: u16) -> &mut Cell {
assert!(x < self.size.width);
assert!(y < self.size.height);
let i = self.index(x, y);
&mut self.data[i]
}
fn current_frame(&self) -> StackFrame {
self.stack.last().copied().unwrap_or(StackFrame {
pos: Pos::ZERO,
size: self.size,
drawable_area: Some((Pos::ZERO, self.size)),
})
}
pub fn push(&mut self, pos: Pos, size: Size) {
self.stack.push(self.current_frame().then(pos, size));
}
pub fn pop(&mut self) {
self.stack.pop();
}
/// Size of the current drawable area, respecting the stack.
pub fn size(&self) -> Size {
self.current_frame().size
}
pub fn cursor(&self) -> Option<Pos> {
self.cursor.map(|p| self.current_frame().global_to_local(p))
}
pub fn set_cursor(&mut self, pos: Option<Pos>) {
self.cursor = pos.map(|p| self.current_frame().local_to_global(p));
}
/// Resize the buffer and reset its contents.
///
/// The buffer's contents are reset even if the buffer is already the
/// correct size. The stack is reset as well.
pub fn resize(&mut self, size: Size) {
if size == self.size {
self.data.fill_with(Cell::default);
} else {
let width: usize = size.width.into();
let height: usize = size.height.into();
let len = width * height;
self.size = size;
self.data.clear();
self.data.resize_with(len, Cell::default);
}
self.cursor = None;
self.stack.clear();
}
/// Reset the contents and stack of the buffer.
///
/// `buf.reset()` is equivalent to `buf.resize(buf.size())`.
pub fn reset(&mut self) {
self.resize(self.size);
}
/// Remove the grapheme at the specified coordinates from the buffer.
///
/// Removes the entire grapheme, not just the cell at the coordinates.
/// Preserves the style of the affected cells. Preserves the cursor. Works
/// even if the coordinates don't point to the beginning of the grapheme.
///
/// Ignores the stack.
fn erase(&mut self, x: u16, y: u16) {
let cell = self.at(x, y);
let width: u16 = cell.width.into();
let offset: u16 = cell.offset.into();
for x in (x - offset)..(x - offset + width) {
let cell = self.at_mut(x, y);
let style = cell.style;
*cell = Cell::default();
cell.style = style;
}
}
/// Write styled text to the buffer, respecting the width of individual
/// graphemes.
///
/// The initial x position is considered the first column for tab width
/// calculations.
pub fn write(&mut self, widthdb: &mut WidthDB, pos: Pos, styled: &Styled) {
let frame = self.current_frame();
let (xrange, yrange) = match frame.legal_ranges() {
Some(ranges) => ranges,
None => return, // No drawable area
};
let pos = frame.local_to_global(pos);
if !yrange.contains(&pos.y) {
return; // Outside of drawable area
}
let y = pos.y as u16;
let mut col: usize = 0;
for (_, styled_grapheme) in styled.styled_grapheme_indices() {
let x = pos.x + col as i32;
let g = *styled_grapheme.content();
let style = *styled_grapheme.style();
let width = widthdb.grapheme_width(g, col);
col += width as usize;
if g == "\t" {
for dx in 0..width {
self.write_grapheme(&xrange, x + dx as i32, y, 1, " ", style);
}
} else if width > 0 {
self.write_grapheme(&xrange, x, y, width, g, style);
}
}
}
/// Write a single grapheme to the buffer, respecting its width.
///
/// Assumes that `pos.y` is in range.
fn write_grapheme(
&mut self,
xrange: &Range<i32>,
x: i32,
y: u16,
width: u8,
grapheme: &str,
style: ContentStyle,
) {
let min_x = xrange.start;
let max_x = xrange.end - 1; // Last possible cell
let start_x = x;
let end_x = x + width as i32 - 1; // Coordinate of last cell
if start_x > max_x || end_x < min_x {
return; // Not visible
}
if start_x >= min_x && end_x <= max_x {
// Fully visible, write actual grapheme
for offset in 0..width {
let x = start_x as u16 + offset as u16;
self.erase(x, y);
*self.at_mut(x, y) = Cell {
content: grapheme.to_string().into_boxed_str(),
style,
width,
offset,
};
}
} else {
// Partially visible, write empty cells with correct style
let start_x = start_x.max(0) as u16;
let end_x = end_x.min(max_x) as u16;
for x in start_x..=end_x {
self.erase(x, y);
*self.at_mut(x, y) = Cell {
style,
..Default::default()
};
}
}
if let Some(pos) = self.cursor {
if pos.y == y as i32 && start_x <= pos.x && pos.x <= end_x {
// The cursor lies within the bounds of the current grapheme and
self.cursor = None;
}
}
}
pub fn cells(&self) -> Cells<'_> {
Cells {
buffer: self,
x: 0,
y: 0,
}
}
}
pub struct Cells<'a> {
buffer: &'a Buffer,
x: u16,
y: u16,
}
impl<'a> Iterator for Cells<'a> {
type Item = (u16, u16, &'a Cell);
fn next(&mut self) -> Option<Self::Item> {
if self.y >= self.buffer.size.height {
return None;
}
let (x, y) = (self.x, self.y);
let cell = self.buffer.at(self.x, self.y);
assert!(cell.offset == 0);
self.x += cell.width as u16;
if self.x >= self.buffer.size.width {
self.x = 0;
self.y += 1;
}
Some((x, y, cell))
}
}