use std::{
cell::RefCell,
io::{Error as IoError, Result as IoResult},
rc::Rc,
};
use ratatui::{
backend::WindowSize,
buffer::Cell,
layout::{Position, Size},
prelude::{backend::ClearType, Backend},
};
use web_sys::{window, Document, Element};
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{
cell_sized::CellSized,
event_callback::{
create_mouse_event, EventCallback, MouseConfig, KEY_EVENT_TYPES, MOUSE_EVENT_TYPES,
},
utils::*,
},
error::Error,
event::{KeyEvent, MouseEvent},
render::WebEventHandler,
CursorShape,
};
const DEFAULT_CELL_SIZE: (f64, f64) = (10.0, 20.0);
#[derive(Debug, Default)]
pub struct DomBackendOptions {
grid_id: Option<String>,
cursor_shape: CursorShape,
}
impl DomBackendOptions {
pub fn new(grid_id: Option<String>, cursor_shape: CursorShape) -> Self {
Self {
grid_id,
cursor_shape,
}
}
pub fn grid_id(&self) -> String {
match &self.grid_id {
Some(id) => format!("{id}_ratzilla_grid"),
None => "grid".to_string(),
}
}
pub fn cursor_shape(&self) -> &CursorShape {
&self.cursor_shape
}
}
pub struct DomBackend {
initialized: Rc<RefCell<bool>>,
cells: Vec<Element>,
grid: Element,
grid_parent: Element,
document: Document,
options: DomBackendOptions,
cursor_position: Option<Position>,
last_cursor_position: Option<Position>,
size: Size,
cell_size: (f64, f64),
_resize_callback: EventCallback<web_sys::Event>,
mouse_callback: Option<DomMouseCallbackState>,
key_callback: Option<EventCallback<web_sys::KeyboardEvent>>,
}
type DomMouseCallbackState = EventCallback<web_sys::MouseEvent>;
impl std::fmt::Debug for DomBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DomBackend")
.field("initialized", &self.initialized)
.field("cells", &format!("[{} cells]", self.cells.len()))
.field("size", &self.size)
.field("cell_size", &self.cell_size)
.field("cursor_position", &self.cursor_position)
.field("resize_callback", &"...")
.field("mouse_callback", &self.mouse_callback.is_some())
.field("key_callback", &self.key_callback.is_some())
.finish()
}
}
impl DomBackend {
pub fn new() -> Result<Self, Error> {
Self::new_with_options(DomBackendOptions::default())
}
pub fn new_by_id(id: &str) -> Result<Self, Error> {
Self::new_with_options(DomBackendOptions::new(
Some(id.to_string()),
CursorShape::default(),
))
}
pub fn set_cursor_shape(mut self, shape: CursorShape) -> Self {
self.options.cursor_shape = shape;
self
}
pub fn new_with_options(options: DomBackendOptions) -> Result<Self, Error> {
let window = window().ok_or(Error::UnableToRetrieveWindow)?;
let document = window.document().ok_or(Error::UnableToRetrieveDocument)?;
let grid_parent = get_element_by_id_or_body(options.grid_id.as_ref())?;
let cell_size =
Self::measure_cell_size(&document, &grid_parent).unwrap_or(DEFAULT_CELL_SIZE);
let size = Self::calculate_size(&grid_parent, cell_size);
let initialized = Rc::new(RefCell::new(false));
let initialized_cb = initialized.clone();
let resize_callback = EventCallback::new(
window.clone(),
Self::RESIZE_EVENT_TYPES,
move |_: web_sys::Event| {
initialized_cb.replace(false);
},
)?;
let mut backend = Self {
initialized,
cells: vec![],
grid: document.create_element("div")?,
grid_parent,
options,
document,
cursor_position: None,
last_cursor_position: None,
size,
cell_size,
_resize_callback: resize_callback,
mouse_callback: None,
key_callback: None,
};
backend.reset_grid()?;
Ok(backend)
}
fn measure_cell_size(document: &Document, parent: &Element) -> Result<(f64, f64), Error> {
let pre = document.create_element("pre")?;
pre.set_attribute(
"style",
"margin: 0; padding: 0; border: 0; line-height: normal;",
)?;
let span = document.create_element("span")?;
span.set_inner_html("\u{2588}");
span.set_attribute("style", "display: inline-block; width: 1ch;")?;
pre.append_child(&span)?;
parent.append_child(&pre)?;
let rect = span.get_bounding_client_rect();
let width = rect.width();
let height = rect.height();
parent.remove_child(&pre)?;
if width > 0.0 && height > 0.0 {
Ok((width, height))
} else {
Ok(DEFAULT_CELL_SIZE)
}
}
fn calculate_size(parent: &Element, cell_size: (f64, f64)) -> Size {
let rect = parent.get_bounding_client_rect();
let (parent_w, parent_h) = (rect.width(), rect.height());
let (w, h) = if parent_w > 0.0 && parent_h > 0.0 {
(parent_w, parent_h)
} else {
let (ww, wh) = get_raw_window_size();
(ww as f64, wh as f64)
};
Size::new((w / cell_size.0) as u16, (h / cell_size.1) as u16)
}
const RESIZE_EVENT_TYPES: &[&str] = &["resize"];
fn reset_grid(&mut self) -> Result<(), Error> {
self.grid = self.document.create_element("div")?;
self.grid.set_attribute("id", &self.options.grid_id())?;
self.cells.clear();
Ok(())
}
fn populate(&mut self) -> Result<(), Error> {
for _y in 0..self.size.height {
let mut line_cells: Vec<Element> = Vec::new();
for _x in 0..self.size.width {
let span = create_span(&self.document, &Cell::default())?;
self.cells.push(span.clone());
line_cells.push(span);
}
let pre = self.document.create_element("pre")?;
let line_height = format!("height: {}px;", self.cell_size.1);
pre.set_attribute("style", &line_height)?;
for elem in line_cells {
pre.append_child(&elem)?;
}
self.grid.append_child(&pre)?;
}
Ok(())
}
}
impl CellSized for DomBackend {
fn cell_size_px(&self) -> (f32, f32) {
let dpr = get_device_pixel_ratio();
(self.cell_size.0 as f32 * dpr, self.cell_size.1 as f32 * dpr)
}
fn cell_size_css_px(&self) -> (f32, f32) {
(self.cell_size.0 as f32, self.cell_size.1 as f32)
}
}
impl Backend for DomBackend {
type Error = IoError;
fn draw<'a, I>(&mut self, content: I) -> IoResult<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
if !*self.initialized.borrow() {
self.initialized.replace(true);
self.cursor_position = None;
self.last_cursor_position = None;
if self
.document
.get_element_by_id(&self.options.grid_id())
.is_some()
{
self.grid_parent.set_inner_html("");
self.reset_grid()?;
self.cell_size = Self::measure_cell_size(&self.document, &self.grid_parent)
.unwrap_or(DEFAULT_CELL_SIZE);
self.size = Self::calculate_size(&self.grid_parent, self.cell_size);
}
self.grid_parent
.append_child(&self.grid)
.map_err(Error::from)?;
self.populate()?;
}
for (x, y, cell) in content {
let cell_position = (y * self.size.width + x) as usize;
let elem = &self.cells[cell_position];
elem.set_inner_html(cell.symbol());
elem.set_attribute("style", &get_cell_style_as_css(cell))
.map_err(Error::from)?;
if cell.symbol().len() > 1 && cell.symbol().width() == 2 {
if (cell_position + 1) < self.cells.len() {
let next_elem = &self.cells[cell_position + 1];
next_elem.set_inner_html("");
next_elem
.set_attribute("style", &get_cell_style_as_css(&Cell::new("")))
.map_err(Error::from)?;
}
}
}
Ok(())
}
fn flush(&mut self) -> IoResult<()> {
Ok(())
}
fn hide_cursor(&mut self) -> IoResult<()> {
if let Some(pos) = self.cursor_position {
let cell_position = (pos.y * self.size.width + pos.x) as usize;
update_css_field(
CursorShape::None.get_css_attribute(),
&self.cells[cell_position],
)
.map_err(Error::from)?;
}
Ok(())
}
fn show_cursor(&mut self) -> IoResult<()> {
if let Some(pos) = self.last_cursor_position {
let cell_position = (pos.y * self.size.width + pos.x) as usize;
update_css_field(
CursorShape::None.get_css_attribute(),
&self.cells[cell_position],
)
.map_err(Error::from)?;
}
if let Some(pos) = self.cursor_position {
let cell_position = (pos.y * self.size.width + pos.x) as usize;
update_css_field(
self.options.cursor_shape.get_css_attribute(),
&self.cells[cell_position],
)
.map_err(Error::from)?;
}
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<()> {
Ok(())
}
fn size(&self) -> IoResult<Size> {
let size = get_size();
Ok(Size::new(
size.width.saturating_sub(1),
size.height.saturating_sub(1),
))
}
fn window_size(&mut self) -> IoResult<WindowSize> {
Ok(WindowSize {
columns_rows: self.size,
pixels: Size::new(
(self.size.width as f64 * self.cell_size.0) as u16,
(self.size.height as f64 * self.cell_size.1) as u16,
),
})
}
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<()> {
self.last_cursor_position = self.cursor_position;
self.cursor_position = Some(position.into());
Ok(())
}
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
match clear_type {
ClearType::All => self.clear(),
_ => Err(IoError::other("unimplemented")),
}
}
}
impl WebEventHandler for DomBackend {
fn on_mouse_event<F>(&mut self, mut callback: F) -> Result<(), Error>
where
F: FnMut(MouseEvent) + 'static,
{
self.clear_mouse_events();
let config = MouseConfig::new(self.size.width, self.size.height);
let element = self.grid.clone();
let mouse_callback = EventCallback::new(
self.grid.clone(),
MOUSE_EVENT_TYPES,
move |event: web_sys::MouseEvent| {
let mouse_event = create_mouse_event(&event, &element, &config);
callback(mouse_event);
},
)?;
self.mouse_callback = Some(mouse_callback);
Ok(())
}
fn clear_mouse_events(&mut self) {
self.mouse_callback = None;
}
fn on_key_event<F>(&mut self, mut callback: F) -> Result<(), Error>
where
F: FnMut(KeyEvent) + 'static,
{
self.clear_key_events();
self.grid.set_attribute("tabindex", "0")?;
self.key_callback = Some(EventCallback::new(
self.grid.clone(),
KEY_EVENT_TYPES,
move |event: web_sys::KeyboardEvent| {
callback(event.into());
},
)?);
Ok(())
}
fn clear_key_events(&mut self) {
self.key_callback = None;
}
}