Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use std::{cell::RefCell, io::Result, rc::Rc};

use app::App;
use examples_shared::backend::{BackendType, MultiBackendBuilder};
use ratzilla::event::KeyCode;
use ratzilla::WebRenderer;
use ratzilla::backend::webgl2::WebGl2BackendOptions;
use ratzilla::event::KeyCode;

mod app;

Expand Down
185 changes: 150 additions & 35 deletions src/backend/canvas.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use bitvec::{bitvec, prelude::BitVec};
use ratatui::{backend::ClearType, layout::Rect};
use std::io::{Error as IoError, Result as IoResult};
use std::{
io::{Error as IoError, Result as IoResult},
rc::Rc,
sync::atomic::{AtomicBool, AtomicU16, Ordering},
};

use crate::{
backend::{
Expand Down Expand Up @@ -77,21 +81,46 @@ impl CanvasBackendOptions {
struct Canvas {
/// Canvas element.
inner: web_sys::HtmlCanvasElement,
/// Canvas parent element.
parent: Option<web_sys::Element>,
/// Rendering context.
context: web_sys::CanvasRenderingContext2d,
/// Background color.
background_color: Color,
/// If the canvas is a fixed size, that size
size: Option<(u32, u32)>,
}

impl Canvas {
/// Constructs a new [`Canvas`].
fn new(
parent_element: web_sys::Element,
width: u32,
height: u32,
parent_element: Option<web_sys::Element>,
size: Option<(u32, u32)>,
background_color: Color,
) -> Result<Self, Error> {
let canvas = create_canvas_in_element(&parent_element, width, height)?;
let (width, height) = size
.or_else(|| {
parent_element
.as_ref()
.map(|p| (p.client_width() as u32, p.client_height() as u32))
})
.unwrap_or_else(|| {
let (width, height) = get_raw_window_size();
(width as u32, height as u32)
});

let canvas = if let Some(element) = parent_element.as_ref() {
create_canvas_in_element(element, width, height)?
} else {
create_canvas_in_element(
&get_document()?
.body()
.ok_or(Error::UnableToRetrieveBody)?
.into(),
width,
height,
)?
};

let context_options = Map::new();
context_options.set(&JsValue::from_str("alpha"), &Boolean::from(JsValue::TRUE));
Expand All @@ -104,15 +133,60 @@ impl Canvas {
.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,
parent: parent_element,
context,
background_color,
size,
})
}

/// Returns true if the size changed
fn init_ctx_and_resize(&self) -> Result<Option<(usize, usize)>, Error> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function would benefit from being multiple functions. it's does a few too many things atm.

let (width, height) = self
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this snippet is duplicated in new(), should prob go into its own function. would also improve readability.

.size
.or_else(|| {
self.parent
.as_ref()
.map(|p| (p.client_width() as u32, p.client_height() as u32))
})
.unwrap_or_else(|| {
let (width, height) = get_raw_window_size();
(width as u32, height as u32)
});

let ratio = web_sys::window()
.ok_or(Error::UnableToRetrieveWindow)?
Comment on lines +160 to +161
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let ratio = web_sys::window()
.ok_or(Error::UnableToRetrieveWindow)?
let ratio = get_window()?

.device_pixel_ratio();

let source_w = (width as f64 / CELL_WIDTH).floor();
let source_h = (height as f64 / CELL_HEIGHT).floor();

let canvas_w = source_w * CELL_WIDTH;
let canvas_h = source_h * CELL_HEIGHT;

let change_size = self.inner.width() != (canvas_w * ratio) as u32
|| self.inner.height() != (canvas_h * ratio) as u32;

if change_size {
self.inner.set_width((canvas_w * ratio) as u32);
self.inner.set_height((canvas_h * ratio) as u32);
self.inner
.style()
.set_property("width", &format!("{}px", canvas_w))?;
self.inner
.style()
.set_property("height", &format!("{}px", canvas_h))?;

self.context.set_font("16px monospace");
self.context.set_text_baseline("top");
self.context.scale(ratio, ratio)?;
}

Ok(change_size.then_some((source_w as usize, source_h as usize)))
}
}

/// Canvas backend.
Expand All @@ -121,12 +195,14 @@ impl Canvas {
#[derive(Debug)]
pub struct CanvasBackend {
/// Whether the canvas has been initialized.
initialized: bool,
initialized: Rc<AtomicBool>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rc<RefCell<bool>> is preferred, as in DomBackend since everything is single-threaded.

/// Always clip foreground drawing to the cell rectangle. Helpful when
/// dealing with out-of-bounds rendering from problematic fonts. Enabling
/// this option may cause some performance issues when dealing with large
/// numbers of simultaneous changes.
always_clip_cells: bool,
/// The size of the buffer in cells
grid_size: Rc<(AtomicU16, AtomicU16)>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, Rc<RefCell<(u16, u16)>>

/// Current buffer.
buffer: Vec<Vec<Cell>>,
/// Previous buffer.
Expand All @@ -145,6 +221,8 @@ pub struct CanvasBackend {
mouse_callback: Option<MouseCallbackState>,
/// Key event callback handler.
key_callback: Option<EventCallback<web_sys::KeyboardEvent>>,
/// Resize event callback handler
_resize_callback: EventCallback<web_sys::Event>,
}

/// Type alias for mouse event callback state.
Expand All @@ -168,27 +246,47 @@ impl CanvasBackend {
/// Constructs a new [`CanvasBackend`] with the given options.
pub fn new_with_options(options: CanvasBackendOptions) -> Result<Self, Error> {
// Parent element of canvas (uses <body> unless specified)
let parent = get_element_by_id_or_body(options.grid_id.as_ref())?;
let parent = if let Some(id) = options.grid_id.as_ref() {
Some(get_element_by_id_or_body(Some(id))?)
} else {
None
};

let (width, height) = options
.size
.unwrap_or_else(|| (parent.client_width() as u32, parent.client_height() as u32));
let canvas = Canvas::new(parent, options.size, Color::Black)?;

let initialized = Rc::new(AtomicBool::new(false));

let resize_callback = EventCallback::new(
web_sys::window().ok_or(Error::UnableToRetrieveWindow)?,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
web_sys::window().ok_or(Error::UnableToRetrieveWindow)?,
get_window()?,

from backend/utils.rs

&["resize"],
{
let initialized = Rc::clone(&initialized);
move |_: web_sys::Event| {
initialized.store(false, Ordering::Relaxed);
}
},
)?;

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()];
let buffer_size = Rc::new((
AtomicU16::new(buffer[0].len() as u16),
AtomicU16::new(buffer.len() as u16),
));
Ok(Self {
prev_buffer: buffer.clone(),
always_clip_cells: options.always_clip_cells,
buffer,
initialized: false,
grid_size: buffer_size,
initialized,
changed_cells,
canvas,
cursor_position: None,
cursor_shape: CursorShape::SteadyBlock,
debug_mode: None,
mouse_callback: None,
key_callback: None,
_resize_callback: resize_callback,
})
}

Expand Down Expand Up @@ -241,7 +339,6 @@ impl CanvasBackend {
self.canvas.inner.client_height() as f64,
);
}
self.canvas.context.translate(5_f64, 5_f64)?;

// NOTE: The draw_* functions each traverse the buffer once, instead of
// traversing it once per cell; this is done to reduce the number of
Expand All @@ -254,7 +351,6 @@ impl CanvasBackend {
self.draw_debug()?;
}

self.canvas.context.translate(-5_f64, -5_f64)?;
Ok(())
}

Expand Down Expand Up @@ -474,10 +570,31 @@ impl Backend for CanvasBackend {
/// actually render the content to the screen.
fn flush(&mut self) -> IoResult<()> {
// Only runs once.
if !self.initialized {
if !self.initialized.swap(true, Ordering::Relaxed) {
if let Some(new_buffer_size) = self.canvas.init_ctx_and_resize()? {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new_buffer_size is not necessarily a new terminal size. resizing the window using a mouse will trigger re-init every time we process an event, affecting responsiveness.

self.buffer.resize_with(new_buffer_size.1, || {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, shouldn't the resize check run in draw()? as in, ratatui has already resized the buffer in draw(), so any cells in the diff will be lost since they fall outside the internal cell buffer - but i'm a bit tired, i might be missing something.

vec![Cell::default(); new_buffer_size.0]
});
self.prev_buffer.resize_with(new_buffer_size.1, || {
vec![Cell::default(); new_buffer_size.0]
});
for line in self.buffer.iter_mut() {
line.resize_with(new_buffer_size.0, || Cell::default());
}
for line in self.prev_buffer.iter_mut() {
line.resize_with(new_buffer_size.0, || Cell::default());
}
self.changed_cells
.resize(new_buffer_size.0 * new_buffer_size.1, true);
self.grid_size
.0
.store(new_buffer_size.0 as u16, Ordering::Relaxed);
self.grid_size
.1
.store(new_buffer_size.1 as u16, Ordering::Relaxed);
}
self.update_grid(true)?;
self.prev_buffer = self.buffer.clone();
self.initialized = true;
return Ok(());
}

Expand Down Expand Up @@ -517,14 +634,17 @@ impl Backend for CanvasBackend {
}

fn clear(&mut self) -> IoResult<()> {
self.buffer = get_sized_buffer();
self.buffer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

.iter_mut()
.flatten()
.for_each(|c| *c = Cell::default());
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,
self.grid_size.0.load(Ordering::Relaxed),
self.grid_size.1.load(Ordering::Relaxed),
))
}

Expand Down Expand Up @@ -570,27 +690,22 @@ impl WebEventHandler for CanvasBackend {
// Clear any existing handlers first
self.clear_mouse_events();

// Get grid dimensions from the buffer
let grid_width = self.buffer[0].len() as u16;
let grid_height = self.buffer.len() as u16;

// Configure coordinate translation for canvas backend
let config = MouseConfig::new(grid_width, grid_height)
.with_offset(5.0) // Canvas translation offset
.with_cell_dimensions(CELL_WIDTH, CELL_HEIGHT);

let element: web_sys::Element = self.canvas.inner.clone().into();
let element_for_closure = element.clone();

// Create mouse event callback
let mouse_callback = EventCallback::new(
element,
MOUSE_EVENT_TYPES,
let mouse_callback = EventCallback::new(element, MOUSE_EVENT_TYPES, {
let grid_size = Rc::clone(&self.grid_size);
move |event: web_sys::MouseEvent| {
let config = MouseConfig::new(
grid_size.0.load(Ordering::Relaxed),
grid_size.1.load(Ordering::Relaxed),
)
.with_cell_dimensions(CELL_WIDTH, CELL_HEIGHT);
let mouse_event = create_mouse_event(&event, &element_for_closure, &config);
callback(mouse_event);
},
)?;
}
})?;

self.mouse_callback = Some(mouse_callback);

Expand Down