use crate::cell::{Cell, CellAttributes};
use crate::color::ColorAttribute;
use crate::image::ImageCell;
use crate::surface::line::CellRef;
use finl_unicode::grapheme_clusters::Graphemes;
use ordered_float::NotNan;
#[cfg(feature = "use_serde")]
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::cmp::min;
use wezterm_dynamic::{FromDynamic, ToDynamic};
pub mod change;
pub mod line;
pub use self::change::{Change, Image, TextureCoordinate};
pub use self::line::Line;
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Position {
Relative(isize),
Absolute(usize),
EndRelative(usize),
}
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Hash, Copy, PartialEq, Eq, FromDynamic, ToDynamic)]
pub enum CursorVisibility {
Hidden,
Visible,
}
impl Default for CursorVisibility {
fn default() -> CursorVisibility {
CursorVisibility::Visible
}
}
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromDynamic, ToDynamic)]
pub enum CursorShape {
Default,
BlinkingBlock,
SteadyBlock,
BlinkingUnderline,
SteadyUnderline,
BlinkingBar,
SteadyBar,
}
impl Default for CursorShape {
fn default() -> CursorShape {
CursorShape::Default
}
}
impl CursorShape {
pub fn is_blinking(self) -> bool {
matches!(
self,
Self::BlinkingBlock | Self::BlinkingUnderline | Self::BlinkingBar
)
}
}
pub type SequenceNo = usize;
pub const SEQ_ZERO: SequenceNo = 0;
#[derive(Default)]
pub struct Surface {
width: usize,
height: usize,
lines: Vec<Line>,
attributes: CellAttributes,
xpos: usize,
ypos: usize,
seqno: SequenceNo,
changes: Vec<Change>,
cursor_shape: Option<CursorShape>,
cursor_visibility: CursorVisibility,
cursor_color: ColorAttribute,
title: String,
}
#[derive(Default)]
struct DiffState {
changes: Vec<Change>,
cursor: Option<(usize, usize)>,
attr: Option<CellAttributes>,
}
impl DiffState {
#[inline]
fn diff_cells(&mut self, col_num: usize, row_num: usize, cell: CellRef, other_cell: CellRef) {
if cell.same_contents(&other_cell) {
return;
}
self.cursor = match self.cursor.take() {
Some((cursor_row, cursor_col))
if cursor_row == row_num && cursor_col == col_num - 1 =>
{
Some((row_num, col_num))
}
_ => {
self.changes.push(Change::CursorPosition {
y: Position::Absolute(row_num),
x: Position::Absolute(col_num),
});
Some((row_num, col_num))
}
};
self.attr = match self.attr.take() {
Some(ref attr) if attr == other_cell.attrs() => {
Some(attr.clone())
}
_ => {
self.changes
.push(Change::AllAttributes(other_cell.attrs().clone()));
Some(other_cell.attrs().clone())
}
};
let result_len = self.changes.len();
if result_len > 0 && self.changes[result_len - 1].is_text() {
if let Some(Change::Text(ref mut prefix)) = self.changes.get_mut(result_len - 1) {
prefix.push_str(other_cell.str());
}
} else {
self.changes
.push(Change::Text(other_cell.str().to_string()));
}
}
}
impl Surface {
pub fn new(width: usize, height: usize) -> Self {
let mut scr = Surface {
width,
height,
..Default::default()
};
scr.resize(width, height);
scr
}
pub fn dimensions(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn cursor_position(&self) -> (usize, usize) {
(self.xpos, self.ypos)
}
pub fn cursor_shape(&self) -> Option<CursorShape> {
self.cursor_shape
}
pub fn cursor_visibility(&self) -> CursorVisibility {
self.cursor_visibility
}
pub fn title(&self) -> &str {
&self.title
}
pub fn resize(&mut self, width: usize, height: usize) {
if !self.changes.is_empty() {
self.seqno += 1;
self.changes.clear();
}
self.lines
.resize(height, Line::with_width(width, self.seqno));
for line in &mut self.lines {
line.resize(width, self.seqno);
}
self.width = width;
self.height = height;
self.xpos = compute_position_change(self.xpos, &Position::Relative(0), self.width);
self.ypos = compute_position_change(self.ypos, &Position::Relative(0), self.height);
}
pub fn add_changes(&mut self, mut changes: Vec<Change>) -> SequenceNo {
let seq = self.seqno.saturating_sub(1) + changes.len();
for change in &changes {
self.apply_change(&change);
}
self.seqno += changes.len();
self.changes.append(&mut changes);
seq
}
pub fn add_change<C: Into<Change>>(&mut self, change: C) -> SequenceNo {
let seq = self.seqno;
self.seqno += 1;
let change = change.into();
self.apply_change(&change);
self.changes.push(change);
seq
}
fn apply_change(&mut self, change: &Change) {
match change {
Change::AllAttributes(attr) => self.attributes = attr.clone(),
Change::Text(text) => self.print_text(text),
Change::Attribute(change) => self.attributes.apply_change(change),
Change::CursorPosition { x, y } => self.set_cursor_pos(x, y),
Change::ClearScreen(color) => self.clear_screen(*color),
Change::ClearToEndOfLine(color) => self.clear_eol(*color),
Change::ClearToEndOfScreen(color) => self.clear_eos(*color),
Change::CursorColor(color) => self.cursor_color = *color,
Change::CursorShape(shape) => self.cursor_shape = Some(*shape),
Change::CursorVisibility(visibility) => self.cursor_visibility = *visibility,
Change::Image(image) => self.add_image(image),
Change::Title(text) => self.title = text.to_owned(),
Change::ScrollRegionUp {
first_row,
region_size,
scroll_count,
} => self.scroll_region_up(*first_row, *region_size, *scroll_count),
Change::ScrollRegionDown {
first_row,
region_size,
scroll_count,
} => self.scroll_region_down(*first_row, *region_size, *scroll_count),
}
}
fn add_image(&mut self, image: &Image) {
let xsize = (image.bottom_right.x - image.top_left.x) / image.width as f32;
let ysize = (image.bottom_right.y - image.top_left.y) / image.height as f32;
if self.ypos + image.height > self.height {
let scroll = (self.ypos + image.height) - self.height;
for _ in 0..scroll {
self.scroll_screen_up();
}
self.ypos -= scroll;
}
let mut ypos = NotNan::new(0.0).unwrap();
for y in 0..image.height {
let mut xpos = NotNan::new(0.0).unwrap();
for x in 0..image.width {
self.lines[self.ypos + y].set_cell(
self.xpos + x,
Cell::new(
' ',
self.attributes
.clone()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new(
image.top_left.x + xpos,
image.top_left.y + ypos,
),
TextureCoordinate::new(
image.top_left.x + xpos + xsize,
image.top_left.y + ypos + ysize,
),
image.image.clone(),
)))
.clone(),
),
self.seqno,
);
xpos += xsize;
}
ypos += ysize;
}
self.xpos += image.width;
}
fn clear_screen(&mut self, color: ColorAttribute) {
self.attributes = CellAttributes::default().set_background(color).clone();
let cleared = Cell::new(' ', self.attributes.clone());
for line in &mut self.lines {
line.fill_range(0..self.width, &cleared, self.seqno);
}
self.xpos = 0;
self.ypos = 0;
}
fn clear_eos(&mut self, color: ColorAttribute) {
self.attributes = CellAttributes::default().set_background(color).clone();
let cleared = Cell::new(' ', self.attributes.clone());
self.lines[self.ypos].fill_range(self.xpos..self.width, &cleared, self.seqno);
for line in &mut self.lines.iter_mut().skip(self.ypos + 1) {
line.fill_range(0..self.width, &cleared, self.seqno);
}
}
fn clear_eol(&mut self, color: ColorAttribute) {
self.attributes = CellAttributes::default().set_background(color).clone();
let cleared = Cell::new(' ', self.attributes.clone());
self.lines[self.ypos].fill_range(self.xpos..self.width, &cleared, self.seqno);
}
fn scroll_screen_up(&mut self) {
self.lines.remove(0);
self.lines.push(Line::with_width(self.width, self.seqno));
}
fn scroll_region_up(&mut self, start: usize, size: usize, count: usize) {
for index in start..start + min(count, size) {
self.lines[index] = Line::with_width(self.width, self.seqno);
}
if 0 < count && count < size {
self.lines[start..start + size].rotate_left(count);
}
}
fn scroll_region_down(&mut self, start: usize, size: usize, count: usize) {
for index in start + size - min(count, size)..start + size {
self.lines[index] = Line::with_width(self.width, self.seqno);
}
if 0 < count && count < size {
self.lines[start..start + size].rotate_right(count);
}
}
fn print_text(&mut self, text: &str) {
for g in Graphemes::new(text) {
if g == "\r\n" {
self.xpos = 0;
let new_y = self.ypos + 1;
if new_y >= self.height {
self.scroll_screen_up();
} else {
self.ypos = new_y;
}
continue;
}
if g == "\r" {
self.xpos = 0;
continue;
}
if g == "\n" {
let new_y = self.ypos + 1;
if new_y >= self.height {
self.scroll_screen_up();
} else {
self.ypos = new_y;
}
continue;
}
if self.xpos >= self.width {
let new_y = self.ypos + 1;
if new_y >= self.height {
self.scroll_screen_up();
} else {
self.ypos = new_y;
}
self.xpos = 0;
}
let cell = Cell::new_grapheme(g, self.attributes.clone(), None);
let width = cell.width().max(1);
self.lines[self.ypos].set_cell(self.xpos, cell, self.seqno);
self.xpos += width;
}
}
fn set_cursor_pos(&mut self, x: &Position, y: &Position) {
self.xpos = compute_position_change(self.xpos, x, self.width);
self.ypos = compute_position_change(self.ypos, y, self.height);
}
pub fn screen_chars_to_string(&self) -> String {
let mut s = String::new();
for line in &self.lines {
for cell in line.visible_cells() {
s.push_str(cell.str());
}
s.push('\n');
}
s
}
pub fn screen_cells(&mut self) -> Vec<&mut [Cell]> {
let mut lines = Vec::new();
for line in &mut self.lines {
lines.push(line.cells_mut());
}
lines
}
pub fn screen_lines(&self) -> Vec<Cow<Line>> {
self.lines.iter().map(|line| Cow::Borrowed(line)).collect()
}
pub fn get_changes(&self, seq: SequenceNo) -> (SequenceNo, Cow<[Change]>) {
let first = self.seqno.saturating_sub(self.changes.len());
if seq == 0 || first > seq || self.seqno == 0 {
return (self.seqno, Cow::Owned(self.repaint_all()));
}
let delta_cost = self.seqno - seq;
let full_cost = self.estimate_full_paint_cost();
if delta_cost > full_cost {
(self.seqno, Cow::Owned(self.repaint_all()))
} else {
(self.seqno, Cow::Borrowed(&self.changes[seq - first..]))
}
}
pub fn has_changes(&self, seq: SequenceNo) -> bool {
self.seqno != seq
}
pub fn current_seqno(&self) -> SequenceNo {
self.seqno
}
pub fn flush_changes_older_than(&mut self, seq: SequenceNo) {
let first = self.seqno.saturating_sub(self.changes.len());
let idx = seq.saturating_sub(first);
if idx > self.changes.len() {
return;
}
self.changes = self.changes.split_off(idx);
}
fn estimate_full_paint_cost(&self) -> usize {
3 + (((self.width * self.height) as f64) * 1.2) as usize
}
fn repaint_all(&self) -> Vec<Change> {
let mut result = vec![
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
];
if !self.title.is_empty() {
result.push(Change::Title(self.title.to_owned()));
}
let mut attr = CellAttributes::default();
let crlf = Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(1),
};
let mut trailing_color = None;
let mut trailing_idx = None;
for (idx, line) in self.lines.iter().rev().enumerate() {
let changes = line.changes(&attr);
if changes.is_empty() {
match trailing_color {
Some(other) if other != Default::default() => {
break;
}
Some(_) => continue,
None => {
trailing_color = Some(Default::default());
trailing_idx = Some(idx);
continue;
}
}
} else {
let last_change = changes.len() - 1;
match (&changes[last_change], trailing_color) {
(&Change::ClearToEndOfLine(ref color), None) => {
trailing_color = Some(*color);
trailing_idx = Some(idx);
}
(&Change::ClearToEndOfLine(ref color), Some(other)) => {
if other == *color {
trailing_idx = Some(idx);
continue;
} else {
break;
}
}
_ => break,
}
}
}
for (idx, line) in self.lines.iter().enumerate() {
match trailing_idx {
Some(t) if self.height - t == idx => {
let color =
trailing_color.expect("didn't set trailing_color along with trailing_idx");
let last_result = result.len() - 1;
match result[last_result] {
Change::ClearToEndOfLine(col) if col == color => {
result.remove(last_result);
}
_ => {}
}
result.push(Change::ClearToEndOfScreen(color));
break;
}
_ => {}
}
let mut changes = line.changes(&attr);
if idx != 0 {
result.push(crlf.clone());
}
result.append(&mut changes);
if let Some(c) = line.visible_cells().last() {
attr = c.attrs().clone();
}
}
loop {
let result_len = result.len();
if result_len == 0 {
break;
}
match result[result_len - 1] {
Change::CursorPosition { .. } => {
result.remove(result_len - 1);
}
_ => break,
}
}
let moved_cursor = result.len() != 2;
if moved_cursor || self.xpos != 0 || self.ypos != 0 {
result.push(Change::CursorPosition {
x: Position::Absolute(self.xpos),
y: Position::Absolute(self.ypos),
});
}
if self.cursor_visibility != CursorVisibility::Hidden {
result.push(Change::CursorVisibility(CursorVisibility::Visible));
if let Some(shape) = self.cursor_shape {
result.push(Change::CursorShape(shape));
}
}
result
}
#[allow(clippy::too_many_arguments)]
pub fn diff_region(
&self,
x: usize,
y: usize,
width: usize,
height: usize,
other: &Surface,
other_x: usize,
other_y: usize,
) -> Vec<Change> {
let mut diff_state = DiffState::default();
for ((row_num, line), other_line) in self
.lines
.iter()
.enumerate()
.skip(y)
.take_while(|(row_num, _)| *row_num < y + height)
.zip(other.lines.iter().skip(other_y))
{
for (cell, other_cell) in line
.visible_cells()
.skip(x)
.take_while(|cell| cell.cell_index() < x + width)
.zip(other_line.visible_cells().skip(other_x))
{
diff_state.diff_cells(cell.cell_index(), row_num, cell, other_cell);
}
}
diff_state.changes
}
pub fn diff_lines(&self, other_lines: Vec<&Line>) -> Vec<Change> {
let mut diff_state = DiffState::default();
for ((row_num, line), other_line) in self.lines.iter().enumerate().zip(other_lines.iter()) {
for (cell, other_cell) in line.visible_cells().zip(other_line.visible_cells()) {
diff_state.diff_cells(cell.cell_index(), row_num, cell, other_cell);
}
}
diff_state.changes
}
pub fn diff_against_numbered_line(&self, row_num: usize, other_line: &Line) -> Vec<Change> {
let mut diff_state = DiffState::default();
if let Some(line) = self.lines.get(row_num) {
for (cell, other_cell) in line.visible_cells().zip(other_line.visible_cells()) {
diff_state.diff_cells(cell.cell_index(), row_num, cell, other_cell);
}
}
diff_state.changes
}
pub fn diff_screens(&self, other: &Surface) -> Vec<Change> {
self.diff_region(0, 0, self.width, self.height, other, 0, 0)
}
pub fn draw_from_screen(&mut self, other: &Surface, x: usize, y: usize) -> SequenceNo {
let attrs = self.attributes.clone();
let cursor = (self.xpos, self.ypos);
let changes = self.diff_region(x, y, other.width, other.height, other, 0, 0);
let seq = self.add_changes(changes);
self.xpos = cursor.0;
self.ypos = cursor.1;
self.attributes = attrs;
seq
}
pub fn copy_region(
&mut self,
src_x: usize,
src_y: usize,
width: usize,
height: usize,
dest_x: usize,
dest_y: usize,
) -> SequenceNo {
let changes = self.diff_region(dest_x, dest_y, width, height, self, src_x, src_y);
self.add_changes(changes)
}
}
fn compute_position_change(current: usize, pos: &Position, limit: usize) -> usize {
use self::Position::*;
match pos {
Relative(delta) => {
if *delta >= 0 {
min(
current.saturating_add(*delta as usize),
limit.saturating_sub(1),
)
} else {
current.saturating_sub((*delta).abs() as usize)
}
}
Absolute(abs) => min(*abs, limit.saturating_sub(1)),
EndRelative(delta) => limit.saturating_sub(*delta),
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::cell::{AttributeChange, Intensity};
use crate::color::AnsiColor;
use crate::image::ImageData;
use std::sync::Arc;
#[test]
fn basic_print() {
let mut s = Surface::new(4, 3);
assert_eq!(
s.screen_chars_to_string(),
"\x20\x20\x20\x20\n\
\x20\x20\x20\x20\n\
\x20\x20\x20\x20\n"
);
s.add_change("w00t");
assert_eq!(
s.screen_chars_to_string(),
"w00t\n\
\x20\x20\x20\x20\n\
\x20\x20\x20\x20\n"
);
s.add_change("foo");
assert_eq!(
s.screen_chars_to_string(),
"w00t\n\
foo\x20\n\
\x20\x20\x20\x20\n"
);
s.add_change("baar");
assert_eq!(
s.screen_chars_to_string(),
"w00t\n\
foob\n\
aar\x20\n"
);
s.add_change("baz");
assert_eq!(
s.screen_chars_to_string(),
"foob\n\
aarb\n\
az\x20\x20\n"
);
}
#[test]
fn newline() {
let mut s = Surface::new(4, 4);
s.add_change("bloo\rwat\n hey\r\nho");
assert_eq!(
s.screen_chars_to_string(),
"wato\n\
\x20\x20\x20\x20\n\
hey \n\
ho \n"
);
}
#[test]
fn clear_screen() {
let mut s = Surface::new(2, 2);
s.add_change("hello");
assert_eq!(s.xpos, 1);
assert_eq!(s.ypos, 1);
s.add_change(Change::ClearScreen(Default::default()));
assert_eq!(s.xpos, 0);
assert_eq!(s.ypos, 0);
assert_eq!(s.screen_chars_to_string(), " \n \n");
}
#[test]
fn clear_eol() {
let mut s = Surface::new(3, 3);
s.add_change("helwowfoo");
s.add_change(Change::ClearToEndOfLine(Default::default()));
assert_eq!(s.screen_chars_to_string(), "hel\nwow\nfoo\n");
s.add_change(Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(0),
});
s.add_change(Change::ClearToEndOfLine(Default::default()));
assert_eq!(s.screen_chars_to_string(), " \nwow\nfoo\n");
s.add_change(Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
});
s.add_change(Change::ClearToEndOfLine(Default::default()));
assert_eq!(s.screen_chars_to_string(), " \nw\nfoo\n");
}
#[test]
fn clear_eos() {
let mut s = Surface::new(3, 3);
s.add_change("helwowfoo");
s.add_change(Change::ClearToEndOfScreen(Default::default()));
assert_eq!(s.screen_chars_to_string(), "hel\nwow\nfoo\n");
s.add_change(Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
});
s.add_change(Change::ClearToEndOfScreen(Default::default()));
assert_eq!(s.screen_chars_to_string(), "hel\nw\n \n");
let (_seq, changes) = s.get_changes(0);
assert_eq!(
&[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::Text("hel".into()),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(1),
},
Change::Text("w".into()),
Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
},
Change::CursorVisibility(CursorVisibility::Visible),
],
&*changes
);
}
#[test]
fn clear_eos_back_color() {
let mut s = Surface::new(3, 3);
s.add_change(Change::ClearScreen(AnsiColor::Red.into()));
s.add_change("helwowfoo");
assert_eq!(s.screen_chars_to_string(), "hel\nwow\nfoo\n");
s.add_change(Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
});
s.add_change(Change::ClearToEndOfScreen(AnsiColor::Red.into()));
assert_eq!(s.screen_chars_to_string(), "hel\nw \n \n");
let (_seq, changes) = s.get_changes(0);
assert_eq!(
&[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::AllAttributes(
CellAttributes::default()
.set_background(AnsiColor::Red)
.clone()
),
Change::Text("hel".into()),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(1),
},
Change::Text("w".into()),
Change::ClearToEndOfScreen(AnsiColor::Red.into()),
Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
},
Change::CursorVisibility(CursorVisibility::Visible),
],
&*changes
);
}
#[test]
fn clear_eol_opt() {
let mut s = Surface::new(3, 3);
s.add_change(Change::Attribute(AttributeChange::Background(
AnsiColor::Red.into(),
)));
s.add_change("111 333");
let (_seq, changes) = s.get_changes(0);
assert_eq!(
&[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::AllAttributes(
CellAttributes::default()
.set_background(AnsiColor::Red)
.clone()
),
Change::Text("111".into()),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(1),
},
Change::ClearToEndOfLine(AnsiColor::Red.into()),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(1),
},
Change::Text("333".into()),
Change::CursorPosition {
x: Position::Absolute(3),
y: Position::Absolute(2),
},
Change::CursorVisibility(CursorVisibility::Visible),
],
&*changes
);
}
#[test]
fn clear_and_move_cursor() {
let mut s = Surface::new(4, 3);
s.add_change(Change::CursorPosition {
x: Position::Absolute(3),
y: Position::Absolute(2),
});
let (_seq, changes) = s.get_changes(0);
assert_eq!(
&[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::CursorPosition {
x: Position::Absolute(3),
y: Position::Absolute(2),
},
Change::CursorVisibility(CursorVisibility::Visible),
],
&*changes
);
}
#[test]
fn cursor_movement() {
let mut s = Surface::new(4, 3);
s.add_change(Change::CursorPosition {
x: Position::Absolute(3),
y: Position::Absolute(2),
});
s.add_change("X");
assert_eq!(
s.screen_chars_to_string(),
"\x20\x20\x20\x20\n\
\x20\x20\x20\x20\n\
\x20\x20\x20X\n"
);
s.add_change(Change::CursorPosition {
x: Position::Relative(-2),
y: Position::Relative(-1),
});
s.add_change("-");
assert_eq!(
s.screen_chars_to_string(),
"\x20\x20\x20\x20\n\
\x20\x20-\x20\n\
\x20\x20\x20X\n"
);
s.add_change(Change::CursorPosition {
x: Position::Relative(1),
y: Position::Relative(-1),
});
s.add_change("-");
assert_eq!(
s.screen_chars_to_string(),
"\x20\x20\x20-\n\
\x20\x20-\x20\n\
\x20\x20\x20X\n"
);
}
#[test]
fn attribute_setting() {
use crate::cell::Intensity;
let mut s = Surface::new(3, 1);
s.add_change("n");
s.add_change(AttributeChange::Intensity(Intensity::Bold));
s.add_change("b");
let mut bold = CellAttributes::default();
bold.set_intensity(Intensity::Bold);
assert_eq!(
s.screen_cells(),
[[
Cell::new('n', CellAttributes::default()),
Cell::new('b', bold),
Cell::default(),
]]
);
}
#[test]
fn empty_changes() {
let s = Surface::new(4, 3);
let empty = &[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::CursorVisibility(CursorVisibility::Visible),
];
let (seq, changes) = s.get_changes(0);
assert_eq!(seq, 0);
assert_eq!(empty, &*changes);
let (seq, changes) = s.get_changes(1);
assert_eq!(seq, 0);
assert_eq!(empty, &*changes);
}
#[test]
fn add_changes_empty() {
let mut s = Surface::new(2, 2);
let last_seq = s.add_change("foo");
assert_eq!(0, last_seq);
assert_eq!(last_seq, s.add_changes(vec![]));
assert_eq!(last_seq + 1, s.add_changes(vec![Change::Text("a".into())]));
}
#[test]
fn resize_delta_flush() {
let mut s = Surface::new(4, 3);
s.add_change("a");
let (seq, _) = s.get_changes(0);
s.resize(2, 2);
let full = &[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::Text("a".to_string()),
Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(0),
},
Change::CursorVisibility(CursorVisibility::Visible),
];
let (_seq, changes) = s.get_changes(seq);
assert_eq!(full, &*changes);
}
#[test]
fn dont_lose_first_char_on_attr_change() {
let mut s = Surface::new(2, 2);
s.add_change(Change::Attribute(AttributeChange::Foreground(
AnsiColor::Maroon.into(),
)));
s.add_change("ab");
let (_seq, changes) = s.get_changes(0);
assert_eq!(
&[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::AllAttributes(
CellAttributes::default()
.set_foreground(AnsiColor::Maroon)
.clone()
),
Change::Text("ab".into()),
Change::CursorPosition {
x: Position::Absolute(2),
y: Position::Absolute(0),
},
Change::CursorVisibility(CursorVisibility::Visible),
],
&*changes
);
}
#[test]
fn resize_cursor_position() {
let mut s = Surface::new(4, 4);
s.add_change(" a");
s.add_change(Change::CursorPosition {
x: Position::Absolute(3),
y: Position::Absolute(3),
});
assert_eq!(s.xpos, 3);
assert_eq!(s.ypos, 3);
s.resize(2, 2);
assert_eq!(s.xpos, 1);
assert_eq!(s.ypos, 1);
let full = &[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::Text(" a".to_string()),
Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
},
Change::CursorVisibility(CursorVisibility::Visible),
];
let (_seq, changes) = s.get_changes(0);
assert_eq!(full, &*changes);
}
#[test]
fn delta_change() {
let mut s = Surface::new(4, 3);
s.flush_changes_older_than(0);
s.flush_changes_older_than(1);
let initial = &[
Change::CursorVisibility(CursorVisibility::Hidden),
Change::ClearScreen(Default::default()),
Change::Text("a".to_string()),
Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(0),
},
Change::CursorVisibility(CursorVisibility::Visible),
];
let seq_pos = {
let next_seq = s.add_change("a");
let (seq, changes) = s.get_changes(0);
assert_eq!(seq, next_seq + 1);
assert_eq!(initial, &*changes);
seq
};
let seq_pos = {
let next_seq = s.add_change("b");
let (seq, changes) = s.get_changes(seq_pos);
assert_eq!(seq, next_seq + 1);
assert_eq!(&[Change::Text("b".to_string())], &*changes);
seq
};
{
s.add_change(Change::Attribute(AttributeChange::Intensity(
Intensity::Bold,
)));
s.add_change("c");
s.add_change(Change::Attribute(AttributeChange::Intensity(
Intensity::Normal,
)));
s.add_change("d");
}
for _ in 0..3 {
{
let (_seq, changes) = s.get_changes(seq_pos);
assert_eq!(
&[
Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Text("c".to_string()),
Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
Change::Text("d".to_string()),
],
&*changes
);
}
s.flush_changes_older_than(seq_pos);
}
}
#[test]
fn diff_screens() {
let mut s = Surface::new(4, 3);
s.add_change("w00t");
s.add_change("foo");
s.add_change("baar");
s.add_change("baz");
assert_eq!(
s.screen_chars_to_string(),
"foob\n\
aarb\n\
az \n"
);
let s2 = Surface::new(2, 2);
{
let changes = s2.diff_region(0, 0, 2, 2, &s, 0, 0);
assert_eq!(
vec![
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(0),
},
Change::AllAttributes(CellAttributes::default()),
Change::Text("fo".into()),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(1),
},
Change::Text("aa".into()),
],
changes
);
}
s.add_change(Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(1),
});
s.add_change(Change::Attribute(AttributeChange::Intensity(
Intensity::Bold,
)));
s.add_change("XO");
{
let changes = s2.diff_region(0, 0, 2, 2, &s, 1, 1);
assert_eq!(
vec![
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(0),
},
Change::AllAttributes(
CellAttributes::default()
.set_intensity(Intensity::Bold)
.clone(),
),
Change::Text("XO".into()),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Absolute(1),
},
Change::AllAttributes(CellAttributes::default()),
Change::Text("z".into()),
],
changes
);
}
}
#[test]
fn draw_screens() {
let mut s = Surface::new(4, 4);
let mut s1 = Surface::new(2, 2);
s1.add_change("1234");
let mut s2 = Surface::new(2, 2);
s2.add_change("XYZA");
s.draw_from_screen(&s1, 0, 0);
s.draw_from_screen(&s2, 2, 2);
assert_eq!(
s.screen_chars_to_string(),
"12 \n\
34 \n\
\x20\x20XY\n\
\x20\x20ZA\n"
);
}
#[test]
fn draw_colored_region() {
let mut dest = Surface::new(4, 4);
dest.add_change("A");
let mut src = Surface::new(2, 2);
src.add_change(Change::ClearScreen(AnsiColor::Blue.into()));
dest.draw_from_screen(&src, 2, 2);
assert_eq!(
dest.screen_chars_to_string(),
"A \n\
\x20 \n\
\x20 \n\
\x20 \n"
);
let blue_space = Cell::new(
' ',
CellAttributes::default()
.set_background(AnsiColor::Blue)
.clone(),
);
assert_eq!(
dest.screen_cells(),
[
[
Cell::new('A', CellAttributes::default()),
Cell::default(),
Cell::default(),
Cell::default(),
],
[
Cell::default(),
Cell::default(),
Cell::default(),
Cell::default(),
],
[
Cell::default(),
Cell::default(),
blue_space.clone(),
blue_space.clone(),
],
[
Cell::default(),
Cell::default(),
blue_space.clone(),
blue_space.clone(),
]
]
);
assert_eq!(dest.xpos, 1);
assert_eq!(dest.ypos, 0);
assert_eq!(dest.attributes, Default::default());
dest.add_change("B");
assert_eq!(
dest.screen_chars_to_string(),
"AB \n\
\x20 \n\
\x20 \n\
\x20 \n"
);
}
#[test]
fn copy_region() {
let mut s = Surface::new(4, 3);
s.add_change("w00t");
s.add_change("foo");
s.add_change("baar");
s.add_change("baz");
assert_eq!(
s.screen_chars_to_string(),
"foob\n\
aarb\n\
az \n"
);
s.copy_region(0, 0, 2, 2, 2, 1);
assert_eq!(
s.screen_chars_to_string(),
"foob\n\
aafo\n\
azaa\n"
);
}
#[test]
fn double_width() {
let mut s = Surface::new(4, 1);
s.add_change("🤷12");
assert_eq!(s.screen_chars_to_string(), "🤷12\n");
s.add_change(Change::CursorPosition {
x: Position::Absolute(1),
y: Position::Absolute(0),
});
s.add_change("a🤷");
assert_eq!(s.screen_chars_to_string(), " a🤷\n");
s.add_change(Change::CursorPosition {
x: Position::Absolute(2),
y: Position::Absolute(0),
});
s.add_change("x");
assert_eq!(s.screen_chars_to_string(), " ax \n");
}
#[test]
fn zero_width() {
let mut s = Surface::new(4, 1);
s.add_change("A\u{200b}B");
assert_eq!(s.screen_chars_to_string(), "A\u{200b}B \n");
}
#[test]
fn images() {
let data = Arc::new(ImageData::with_raw_data(vec![]));
let mut s = Surface::new(2, 2);
s.add_change(Change::Image(Image {
top_left: TextureCoordinate::new_f32(0.0, 0.0),
bottom_right: TextureCoordinate::new_f32(1.0, 1.0),
image: data.clone(),
width: 4,
height: 2,
}));
assert_eq!(
s.screen_cells(),
[
[
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.0, 0.0),
TextureCoordinate::new_f32(0.25, 0.5),
data.clone()
)))
.clone()
),
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.25, 0.0),
TextureCoordinate::new_f32(0.5, 0.5),
data.clone()
)))
.clone()
),
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.5, 0.0),
TextureCoordinate::new_f32(0.75, 0.5),
data.clone()
)))
.clone()
),
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.75, 0.0),
TextureCoordinate::new_f32(1.0, 0.5),
data.clone()
)))
.clone()
),
],
[
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.0, 0.5),
TextureCoordinate::new_f32(0.25, 1.0),
data.clone()
)))
.clone()
),
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.25, 0.5),
TextureCoordinate::new_f32(0.5, 1.0),
data.clone()
)))
.clone()
),
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.5, 0.5),
TextureCoordinate::new_f32(0.75, 1.0),
data.clone()
)))
.clone()
),
Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.75, 0.5),
TextureCoordinate::new_f32(1.0, 1.0),
data.clone()
)))
.clone()
),
],
]
);
let mut other = Surface::new(1, 1);
other.add_change(Change::Image(Image {
top_left: TextureCoordinate::new_f32(0.25, 0.3),
bottom_right: TextureCoordinate::new_f32(0.75, 0.8),
image: data.clone(),
width: 1,
height: 1,
}));
assert_eq!(
other.screen_cells(),
[[Cell::new(
' ',
CellAttributes::default()
.set_image(Box::new(ImageCell::new(
TextureCoordinate::new_f32(0.25, 0.3),
TextureCoordinate::new_f32(0.75, 0.8),
data.clone()
)))
.clone()
),]]
);
}
}