use bitvec::{bitvec, prelude::BitVec};
use ratatui::{backend::ClearType, layout::Rect};
use std::io::{Error as IoError, Result as IoResult};
use crate::{
backend::{
color::{actual_bg_color, actual_fg_color},
utils::*,
},
error::Error,
CursorShape,
};
use ratatui::{
backend::WindowSize,
buffer::Cell,
layout::{Position, Size},
prelude::Backend,
style::{Color, Modifier},
};
use web_sys::{
js_sys::{Boolean, Map},
wasm_bindgen::{JsCast, JsValue},
};
const CELL_WIDTH: f64 = 10.0;
const CELL_HEIGHT: f64 = 19.0;
#[derive(Debug, Default)]
pub struct CanvasBackendOptions {
grid_id: Option<String>,
size: Option<(u32, u32)>,
always_clip_cells: bool,
}
impl CanvasBackendOptions {
pub fn new() -> Self {
Default::default()
}
pub fn grid_id(mut self, id: &str) -> Self {
self.grid_id = Some(id.to_string());
self
}
pub fn size(mut self, size: (u32, u32)) -> Self {
self.size = Some(size);
self
}
}
#[derive(Debug)]
struct Canvas {
inner: web_sys::HtmlCanvasElement,
context: web_sys::CanvasRenderingContext2d,
background_color: Color,
}
impl Canvas {
fn new(
parent_element: web_sys::Element,
width: u32,
height: u32,
background_color: Color,
) -> Result<Self, Error> {
let canvas = create_canvas_in_element(&parent_element, width, height)?;
let context_options = Map::new();
context_options.set(&JsValue::from_str("alpha"), &Boolean::from(JsValue::TRUE));
context_options.set(
&JsValue::from_str("desynchronized"),
&Boolean::from(JsValue::TRUE),
);
let context = canvas
.get_context_with_context_options("2d", &context_options)?
.ok_or_else(|| Error::UnableToRetrieveCanvasContext)?
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.expect("Unable to cast canvas context");
context.set_font("16px monospace");
context.set_text_baseline("top");
Ok(Self {
inner: canvas,
context,
background_color,
})
}
}
#[derive(Debug)]
pub struct CanvasBackend {
initialized: bool,
always_clip_cells: bool,
buffer: Vec<Vec<Cell>>,
prev_buffer: Vec<Vec<Cell>>,
changed_cells: BitVec,
canvas: Canvas,
cursor_position: Option<Position>,
cursor_shape: CursorShape,
debug_mode: Option<String>,
}
impl CanvasBackend {
pub fn new() -> Result<Self, Error> {
let (width, height) = get_raw_window_size();
Self::new_with_size(width.into(), height.into())
}
pub fn new_with_size(width: u32, height: u32) -> Result<Self, Error> {
Self::new_with_options(CanvasBackendOptions {
size: Some((width, height)),
..Default::default()
})
}
pub fn new_with_options(options: CanvasBackendOptions) -> Result<Self, Error> {
let parent = get_element_by_id_or_body(options.grid_id.as_ref())?;
let (width, height) = options
.size
.unwrap_or_else(|| (parent.client_width() as u32, parent.client_height() as u32));
let canvas = Canvas::new(parent, width, height, Color::Black)?;
let buffer = get_sized_buffer_from_canvas(&canvas.inner);
let changed_cells = bitvec![0; buffer.len() * buffer[0].len()];
Ok(Self {
prev_buffer: buffer.clone(),
always_clip_cells: options.always_clip_cells,
buffer,
initialized: false,
changed_cells,
canvas,
cursor_position: None,
cursor_shape: CursorShape::SteadyBlock,
debug_mode: None,
})
}
pub fn set_background_color(&mut self, color: Color) {
self.canvas.background_color = color;
}
pub fn cursor_shape(&self) -> &CursorShape {
&self.cursor_shape
}
pub fn set_cursor_shape(mut self, shape: CursorShape) -> Self {
self.cursor_shape = shape;
self
}
pub fn set_debug_mode<T: Into<String>>(&mut self, color: Option<T>) {
self.debug_mode = color.map(Into::into);
}
fn update_grid(&mut self, force_redraw: bool) -> Result<(), Error> {
if force_redraw {
self.canvas.context.clear_rect(
0.0,
0.0,
self.canvas.inner.client_width() as f64,
self.canvas.inner.client_height() as f64,
);
}
self.canvas.context.translate(5_f64, 5_f64)?;
self.resolve_changed_cells(force_redraw);
self.draw_background()?;
self.draw_symbols()?;
self.draw_cursor()?;
if self.debug_mode.is_some() {
self.draw_debug()?;
}
self.canvas.context.translate(-5_f64, -5_f64)?;
Ok(())
}
fn resolve_changed_cells(&mut self, force_redraw: bool) {
let mut index = 0;
for (y, line) in self.buffer.iter().enumerate() {
for (x, cell) in line.iter().enumerate() {
let prev_cell = &self.prev_buffer[y][x];
self.changed_cells
.set(index, force_redraw || cell != prev_cell);
index += 1;
}
}
}
fn draw_symbols(&mut self) -> Result<(), Error> {
let changed_cells = &self.changed_cells;
let mut index = 0;
self.canvas.context.save();
let mut last_color = None;
for (y, line) in self.buffer.iter().enumerate() {
for (x, cell) in line.iter().enumerate() {
if !changed_cells[index] || cell.symbol() == " " {
index += 1;
continue;
}
let color = actual_fg_color(cell);
if self.always_clip_cells || !cell.symbol().is_ascii() {
self.canvas.context.restore();
self.canvas.context.save();
self.canvas.context.begin_path();
self.canvas.context.rect(
x as f64 * CELL_WIDTH,
y as f64 * CELL_HEIGHT,
CELL_WIDTH,
CELL_HEIGHT,
);
self.canvas.context.clip();
last_color = None; let color = get_canvas_color(color, Color::White);
self.canvas.context.set_fill_style_str(&color);
} else if last_color != Some(color) {
self.canvas.context.restore();
self.canvas.context.save();
last_color = Some(color);
let color = get_canvas_color(color, Color::White);
self.canvas.context.set_fill_style_str(&color);
}
self.canvas.context.fill_text(
cell.symbol(),
x as f64 * CELL_WIDTH,
y as f64 * CELL_HEIGHT,
)?;
index += 1;
}
}
self.canvas.context.restore();
Ok(())
}
fn draw_background(&mut self) -> Result<(), Error> {
let changed_cells = &self.changed_cells;
self.canvas.context.save();
let draw_region = |(rect, color): (Rect, Color)| {
let color = get_canvas_color(color, self.canvas.background_color);
self.canvas.context.set_fill_style_str(&color);
self.canvas.context.fill_rect(
rect.x as f64 * CELL_WIDTH,
rect.y as f64 * CELL_HEIGHT,
rect.width as f64 * CELL_WIDTH,
rect.height as f64 * CELL_HEIGHT,
);
};
let mut index = 0;
for (y, line) in self.buffer.iter().enumerate() {
let mut row_renderer = RowColorOptimizer::new();
for (x, cell) in line.iter().enumerate() {
if changed_cells[index] {
row_renderer
.process_color((x, y), actual_bg_color(cell))
.map(draw_region);
} else {
row_renderer.flush().map(draw_region);
}
index += 1;
}
row_renderer.flush().map(draw_region);
}
self.canvas.context.restore();
Ok(())
}
fn draw_cursor(&mut self) -> Result<(), Error> {
if let Some(pos) = self.cursor_position {
let cell = &self.buffer[pos.y as usize][pos.x as usize];
if cell.modifier.contains(Modifier::UNDERLINED) {
self.canvas.context.save();
self.canvas.context.fill_text(
"_",
pos.x as f64 * CELL_WIDTH,
pos.y as f64 * CELL_HEIGHT,
)?;
self.canvas.context.restore();
}
}
Ok(())
}
fn draw_debug(&mut self) -> Result<(), Error> {
self.canvas.context.save();
let color = self.debug_mode.as_ref().unwrap();
for (y, line) in self.buffer.iter().enumerate() {
for (x, _) in line.iter().enumerate() {
self.canvas.context.set_stroke_style_str(color);
self.canvas.context.stroke_rect(
x as f64 * CELL_WIDTH,
y as f64 * CELL_HEIGHT,
CELL_WIDTH,
CELL_HEIGHT,
);
}
}
self.canvas.context.restore();
Ok(())
}
}
impl Backend for CanvasBackend {
type Error = IoError;
fn draw<'a, I>(&mut self, content: I) -> IoResult<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
let y = y as usize;
let x = x as usize;
let line = &mut self.buffer[y];
line.extend(std::iter::repeat_with(Cell::default).take(x.saturating_sub(line.len())));
line[x] = cell.clone();
}
if let Some(pos) = self.cursor_position {
let y = pos.y as usize;
let x = pos.x as usize;
let line = &mut self.buffer[y];
if x < line.len() {
let cursor_style = self.cursor_shape.show(line[x].style());
line[x].set_style(cursor_style);
}
}
Ok(())
}
fn flush(&mut self) -> IoResult<()> {
if !self.initialized {
self.update_grid(true)?;
self.prev_buffer = self.buffer.clone();
self.initialized = true;
return Ok(());
}
if self.buffer != self.prev_buffer {
self.update_grid(false)?;
}
self.prev_buffer = self.buffer.clone();
Ok(())
}
fn hide_cursor(&mut self) -> IoResult<()> {
if let Some(pos) = self.cursor_position {
let y = pos.y as usize;
let x = pos.x as usize;
let line = &mut self.buffer[y];
if x < line.len() {
let style = self.cursor_shape.hide(line[x].style());
line[x].set_style(style);
}
}
self.cursor_position = None;
Ok(())
}
fn show_cursor(&mut self) -> IoResult<()> {
Ok(())
}
fn get_cursor(&mut self) -> IoResult<(u16, u16)> {
Ok((0, 0))
}
fn set_cursor(&mut self, _x: u16, _y: u16) -> IoResult<()> {
Ok(())
}
fn clear(&mut self) -> IoResult<()> {
self.buffer = get_sized_buffer();
Ok(())
}
fn size(&self) -> IoResult<Size> {
Ok(Size::new(
self.buffer[0].len().saturating_sub(1) as u16,
self.buffer.len().saturating_sub(1) as u16,
))
}
fn window_size(&mut self) -> IoResult<WindowSize> {
unimplemented!()
}
fn get_cursor_position(&mut self) -> IoResult<Position> {
match self.cursor_position {
None => Ok((0, 0).into()),
Some(position) => Ok(position),
}
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> IoResult<()> {
let new_pos = position.into();
if let Some(old_pos) = self.cursor_position {
let y = old_pos.y as usize;
let x = old_pos.x as usize;
let line = &mut self.buffer[y];
if x < line.len() && old_pos != new_pos {
let style = self.cursor_shape.hide(line[x].style());
line[x].set_style(style);
}
}
self.cursor_position = Some(new_pos);
Ok(())
}
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
match clear_type {
ClearType::All => self.clear(),
_ => Err(IoError::other("unimplemented")),
}
}
}
struct RowColorOptimizer {
pending_region: Option<(Rect, Color)>,
}
impl RowColorOptimizer {
fn new() -> Self {
Self {
pending_region: None,
}
}
fn process_color(&mut self, pos: (usize, usize), color: Color) -> Option<(Rect, Color)> {
if let Some((active_rect, active_color)) = self.pending_region.as_mut() {
if active_color == &color {
active_rect.width += 1;
} else {
let region = *active_rect;
let region_color = *active_color;
*active_rect = Rect::new(pos.0 as _, pos.1 as _, 1, 1);
*active_color = color;
return Some((region, region_color));
}
} else {
let rect = Rect::new(pos.0 as _, pos.1 as _, 1, 1);
self.pending_region = Some((rect, color));
}
None
}
fn flush(&mut self) -> Option<(Rect, Color)> {
self.pending_region.take()
}
}