From 3bcbd53bb0a57080740e3a71ef44b7ceb9b74403 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Fri, 18 Jul 2025 15:01:49 -0600 Subject: [PATCH 01/35] Implement resizing for canvas backend --- examples/demo/Cargo.lock | 1 + examples/demo/Cargo.toml | 2 + examples/demo/index.html | 60 ++++++----- examples/demo/src/main.rs | 5 +- src/backend/canvas.rs | 202 +++++++++++++++++++++++--------------- src/backend/utils.rs | 16 ++- 6 files changed, 181 insertions(+), 105 deletions(-) diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index ed34c963..78a73c85 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -288,6 +288,7 @@ name = "demo" version = "0.1.0" dependencies = [ "clap", + "console_error_panic_hook", "rand", "ratzilla", "tachyonfx", diff --git a/examples/demo/Cargo.toml b/examples/demo/Cargo.toml index 0766d560..2d1dd5fb 100644 --- a/examples/demo/Cargo.toml +++ b/examples/demo/Cargo.toml @@ -10,4 +10,6 @@ rand = { version = "0.8.5", features = ["small_rng"], default-features = false } ratzilla = { path = "../../" } tachyonfx = { version = "0.15.0", default-features = false, features = ["web-time"] } web-time = "1.1.0" +console_error_panic_hook = "0.1.7" + # tui-big-text = "0.6.1" diff --git a/examples/demo/index.html b/examples/demo/index.html index 1d67f957..91a9d9b1 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -1,25 +1,39 @@ - - - - Ratzilla Demo - - - - + + + + + Ratzilla Demo + + + + + + \ No newline at end of file diff --git a/examples/demo/src/main.rs b/examples/demo/src/main.rs index b873f39a..778c2686 100644 --- a/examples/demo/src/main.rs +++ b/examples/demo/src/main.rs @@ -33,8 +33,11 @@ struct Cli { } fn main() -> Result<()> { + console_error_panic_hook::set_once(); + let app_state = Rc::new(RefCell::new(App::new("Demo", false))); - let mut backend = CanvasBackend::new_with_size(1600, 900)?; + // let mut backend = CanvasBackend::new_with_size(1600, 900)?; + let mut backend = CanvasBackend::new()?; backend.set_background_color(Color::Rgb(18, 18, 18)); let terminal = Terminal::new(backend)?; terminal.on_key_event({ diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 33dea067..283a5321 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -1,6 +1,11 @@ use bitvec::{bitvec, prelude::BitVec}; use ratatui::layout::Rect; -use std::io::Result as IoResult; +use std::{ + io::Result as IoResult, + mem::ManuallyDrop, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; use crate::{ backend::{ @@ -19,7 +24,8 @@ use ratatui::{ }; use web_sys::{ js_sys::{Boolean, Map}, - wasm_bindgen::{JsCast, JsValue}, + wasm_bindgen::{prelude::Closure, JsCast, JsValue}, + Element, }; /// Width of a single cell. @@ -70,6 +76,10 @@ impl CanvasBackendOptions { /// Canvas renderer. #[derive(Debug)] struct Canvas { + /// The canvas's parent element + parent: Element, + /// Whether the canvas has been initialized. + initialized: Rc, /// Canvas element. inner: web_sys::HtmlCanvasElement, /// Rendering context. @@ -78,6 +88,26 @@ struct Canvas { background_color: Color, } +fn init_ctx( + canvas: &web_sys::HtmlCanvasElement, +) -> Result { + 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::() + .expect("Unable to cast canvas context"); + context.set_font("16px monospace"); + context.set_text_baseline("top"); + Ok(context) +} + impl Canvas { /// Constructs a new [`Canvas`]. fn new( @@ -88,26 +118,30 @@ impl Canvas { ) -> Result { 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::() - .expect("Unable to cast canvas context"); - context.set_font("16px monospace"); - context.set_text_baseline("top"); + let initialized: Rc = Rc::new(false.into()); + let closure = ManuallyDrop::new(Closure::::new({ + let initialized = Rc::clone(&initialized); + move |_: web_sys::Event| { + initialized.store(false, Ordering::Relaxed); + } + })); + web_sys::window() + .unwrap() + .set_onresize(Some(closure.as_ref().unchecked_ref())); Ok(Self { + parent: parent_element, + initialized, + context: init_ctx(&canvas)?, inner: canvas, - context, background_color, }) } + + fn re_init_ctx(&mut self) -> Result<(), Error> { + self.context = init_ctx(&self.inner)?; + Ok(()) + } } /// Canvas backend. @@ -115,17 +149,10 @@ impl Canvas { /// This backend renders the buffer onto a HTML canvas element. #[derive(Debug)] pub struct CanvasBackend { - /// Whether the canvas has been initialized. - initialized: bool, - /// 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 options passed to the backend upon instantiation + options: CanvasBackendOptions, /// Current buffer. buffer: Vec>, - /// Previous buffer. - prev_buffer: Vec>, /// Changed buffer cells changed_cells: BitVec, /// Canvas. @@ -141,8 +168,7 @@ pub struct CanvasBackend { impl CanvasBackend { /// Constructs a new [`CanvasBackend`]. pub fn new() -> Result { - let (width, height) = get_raw_window_size(); - Self::new_with_size(width.into(), height.into()) + Self::new_with_options(CanvasBackendOptions::default()) } /// Constructs a new [`CanvasBackend`] with the given size. @@ -164,12 +190,10 @@ impl CanvasBackend { 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 changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { - prev_buffer: buffer.clone(), - always_clip_cells: options.always_clip_cells, + options, buffer, - initialized: false, changed_cells, canvas, cursor_position: None, @@ -178,6 +202,40 @@ impl CanvasBackend { }) } + fn buffer_size(&self) -> Size { + Size::new(self.buffer[0].len() as u16, self.buffer.len() as u16) + } + + fn initialize(&mut self) -> Result<(), Error> { + let (width, height) = self.options.size.unwrap_or_else(|| { + ( + self.canvas.parent.client_width() as u32, + self.canvas.parent.client_height() as u32, + ) + }); + + self.canvas.inner.set_width(width); + self.canvas.inner.set_height(height); + self.canvas.re_init_ctx()?; + + let new_buffer_size = size_to_buffer_size(Size { + width: width as u16, + height: height as u16, + }); + if self.buffer_size() != new_buffer_size { + for line in &mut self.buffer { + line.resize_with(new_buffer_size.width as usize, || Cell::default()); + } + self.buffer + .resize_with(new_buffer_size.height as usize, || { + vec![Cell::default(); new_buffer_size.width as usize] + }); + self.changed_cells = bitvec![usize::MAX; self.buffer.len() * self.buffer[0].len()]; + } + + Ok(()) + } + /// Sets the background color of the canvas. pub fn set_background_color(&mut self, color: Color) { self.canvas.background_color = color; @@ -226,13 +284,14 @@ impl CanvasBackend { self.canvas.inner.client_width() as f64, self.canvas.inner.client_height() as f64, ); + self.initialize()?; + // NOTE: The draw_* functions each traverse the buffer once, instead of + // traversing it once per cell; this is done to reduce the number of + // WASM calls per cell. + self.changed_cells.set_elements(usize::MAX); } 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 - // WASM calls per cell. - self.resolve_changed_cells(force_redraw); self.draw_background()?; self.draw_symbols()?; self.draw_cursor()?; @@ -241,25 +300,10 @@ impl CanvasBackend { } self.canvas.context.translate(-5_f64, -5_f64)?; + self.changed_cells.set_elements(0x00); Ok(()) } - /// Updates the representation of the changed cells. - /// - /// This function updates the `changed_cells` vector to indicate which cells - /// have changed. - 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; - } - } - } - /// Draws the text symbols on the canvas. /// /// This method renders the textual content of each cell in the buffer, optimizing canvas operations @@ -283,7 +327,7 @@ impl CanvasBackend { for (y, line) in self.buffer.iter().enumerate() { for (x, cell) in line.iter().enumerate() { // Skip empty cells - if !changed_cells[index] || cell.symbol() == " " { + if !changed_cells.get(index).map(|c| *c).unwrap_or(true) || cell.symbol() == " " { index += 1; continue; } @@ -292,7 +336,7 @@ impl CanvasBackend { // We need to reset the canvas context state in two scenarios: // 1. When we need to create a clipping path (for potentially problematic glyphs) // 2. When the text color changes - if self.always_clip_cells || !cell.symbol().is_ascii() { + if self.options.always_clip_cells || !cell.symbol().is_ascii() { self.canvas.context.restore(); self.canvas.context.save(); @@ -359,7 +403,7 @@ impl CanvasBackend { 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] { + if changed_cells.get(index).map(|c| *c).unwrap_or(true) { // Only calls `draw_region` if the color is different from the previous one row_renderer .process_color((x, y), actual_bg_color(cell)) @@ -433,19 +477,26 @@ impl Backend for CanvasBackend { 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(line) = self.buffer.get_mut(y) { + line.get_mut(x).map(|c| *c = cell.clone()); + if let Some(mut cell) = self.changed_cells.get_mut((y * line.len()) + x) { + cell.set(true); + } + } } // Draw the cursor if set 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); + if let Some(line) = self.buffer.get_mut(y) { + if x < line.len() { + let cursor_style = self.cursor_shape.show(line[x].style()); + line.get_mut(x).map(|c| c.set_style(cursor_style)); + if let Some(mut cell) = self.changed_cells.get_mut((y * line.len()) + x) { + cell.set(true); + } + } } } @@ -457,20 +508,11 @@ impl Backend for CanvasBackend { /// This function is called after the [`CanvasBackend::draw`] function to /// actually render the content to the screen. fn flush(&mut self) -> IoResult<()> { - // Only runs once. - 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)?; + let initialized = self.canvas.initialized.swap(true, Ordering::Relaxed); + if self.changed_cells.any() || !initialized { + self.update_grid(!initialized)?; } - self.prev_buffer = self.buffer.clone(); - Ok(()) } @@ -501,15 +543,19 @@ impl Backend for CanvasBackend { } fn clear(&mut self) -> IoResult<()> { - self.buffer = get_sized_buffer(); + self.buffer + .iter_mut() + .flatten() + .for_each(|c| *c = Cell::default()); Ok(()) } fn size(&self) -> IoResult { - Ok(Size::new( - self.buffer[0].len().saturating_sub(1) as u16, - self.buffer.len().saturating_sub(1) as u16, - )) + let size = self.buffer_size(); + Ok(Size { + width: size.width.saturating_sub(1), + height: size.height.saturating_sub(1), + }) } fn window_size(&mut self) -> IoResult { diff --git a/src/backend/utils.rs b/src/backend/utils.rs index 1596bf7f..6f0e6bad 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -6,6 +6,7 @@ use crate::{ use compact_str::{format_compact, CompactString}; use ratatui::{ buffer::Cell, + layout::Size, style::{Color, Modifier}, }; use web_sys::{ @@ -126,11 +127,20 @@ pub(crate) fn get_sized_buffer() -> Vec> { vec![vec![Cell::default(); size.width as usize]; size.height as usize] } +pub(crate) fn size_to_buffer_size(size: Size) -> Size { + Size { + width: size.width / 10_u16, + height: size.height / 19_u16, + } +} + /// Returns a buffer based on the canvas size. pub(crate) fn get_sized_buffer_from_canvas(canvas: &HtmlCanvasElement) -> Vec> { - let width = canvas.client_width() as u16 / 10_u16; - let height = canvas.client_height() as u16 / 19_u16; - vec![vec![Cell::default(); width as usize]; height as usize] + let width = canvas.client_width() as u16; + let height = canvas.client_height() as u16; + + let size = size_to_buffer_size(Size { width, height }); + vec![vec![Cell::default(); size.width as usize]; size.height as usize] } /// Returns the document object from the window. From 36750509970326ddc0f53625325236f9afdfd3ef Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Fri, 18 Jul 2025 21:02:30 -0600 Subject: [PATCH 02/35] Account for font metrics in sizing --- Cargo.toml | 1 + src/backend/canvas.rs | 84 +++++++++++++++++++++++++------------------ src/backend/utils.rs | 13 ++++--- 3 files changed, 59 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eba627e4..96d283b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ web-sys = { version = "0.3.77", features = [ 'Node', 'Performance', 'Screen', + 'TextMetrics', 'WebGl2RenderingContext', 'WebGlBuffer', 'WebGlProgram', diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 283a5321..98e17afa 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -28,18 +28,6 @@ use web_sys::{ Element, }; -/// Width of a single cell. -/// -/// This will be used for multiplying the cell's x position to get the actual pixel -/// position on the canvas. -const CELL_WIDTH: f64 = 10.0; - -/// Height of a single cell. -/// -/// This will be used for multiplying the cell's y position to get the actual pixel -/// position on the canvas. -const CELL_HEIGHT: f64 = 19.0; - /// Options for the [`CanvasBackend`]. #[derive(Debug, Default)] pub struct CanvasBackendOptions { @@ -86,6 +74,16 @@ struct Canvas { context: web_sys::CanvasRenderingContext2d, /// Background color. background_color: Color, + /// Width of a single cell. + /// + /// This will be used for multiplying the cell's x position to get the actual pixel + /// position on the canvas. + cell_width: f64, + /// Height of a single cell. + /// + /// This will be used for multiplying the cell's y position to get the actual pixel + /// position on the canvas. + cell_height: f64, } fn init_ctx( @@ -129,15 +127,30 @@ impl Canvas { .unwrap() .set_onresize(Some(closure.as_ref().unchecked_ref())); + let context = init_ctx(&canvas)?; + + let font_measurement = context.measure_text("|")?; + Ok(Self { parent: parent_element, initialized, - context: init_ctx(&canvas)?, + context, inner: canvas, background_color, + // cell_width: font_measurement.actual_bounding_box_left().abs() + // + font_measurement.actual_bounding_box_right().abs(), + cell_width: font_measurement.width().floor(), + cell_height: font_measurement.font_bounding_box_descent().abs().floor(), }) } + fn font_metrics(&self) -> Size { + Size { + width: self.cell_width as u16, + height: self.cell_height as u16, + } + } + fn re_init_ctx(&mut self) -> Result<(), Error> { self.context = init_ctx(&self.inner)?; Ok(()) @@ -189,7 +202,7 @@ impl CanvasBackend { .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 buffer = get_sized_buffer_from_canvas(&canvas.inner, canvas.font_metrics()); let changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { options, @@ -218,10 +231,13 @@ impl CanvasBackend { self.canvas.inner.set_height(height); self.canvas.re_init_ctx()?; - let new_buffer_size = size_to_buffer_size(Size { - width: width as u16, - height: height as u16, - }); + let new_buffer_size = size_to_buffer_size( + Size { + width: width as u16, + height: height as u16, + }, + self.canvas.font_metrics(), + ); if self.buffer_size() != new_buffer_size { for line in &mut self.buffer { line.resize_with(new_buffer_size.width as usize, || Cell::default()); @@ -342,10 +358,10 @@ impl CanvasBackend { self.canvas.context.begin_path(); self.canvas.context.rect( - x as f64 * CELL_WIDTH, - y as f64 * CELL_HEIGHT, - CELL_WIDTH, - CELL_HEIGHT, + x as f64 * self.canvas.cell_width, + y as f64 * self.canvas.cell_height, + self.canvas.cell_width, + self.canvas.cell_height, ); self.canvas.context.clip(); @@ -364,8 +380,8 @@ impl CanvasBackend { self.canvas.context.fill_text( cell.symbol(), - x as f64 * CELL_WIDTH, - y as f64 * CELL_HEIGHT, + x as f64 * self.canvas.cell_width, + y as f64 * self.canvas.cell_height, )?; index += 1; @@ -392,10 +408,10 @@ impl CanvasBackend { 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, + rect.x as f64 * self.canvas.cell_width, + rect.y as f64 * self.canvas.cell_height, + rect.width as f64 * self.canvas.cell_width, + rect.height as f64 * self.canvas.cell_height, ); }; @@ -434,8 +450,8 @@ impl CanvasBackend { self.canvas.context.fill_text( "_", - pos.x as f64 * CELL_WIDTH, - pos.y as f64 * CELL_HEIGHT, + pos.x as f64 * self.canvas.cell_width, + pos.y as f64 * self.canvas.cell_height, )?; self.canvas.context.restore(); @@ -454,10 +470,10 @@ impl CanvasBackend { 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, + x as f64 * self.canvas.cell_width, + y as f64 * self.canvas.cell_height, + self.canvas.cell_width, + self.canvas.cell_height, ); } } diff --git a/src/backend/utils.rs b/src/backend/utils.rs index 6f0e6bad..fc1c1963 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -127,19 +127,22 @@ pub(crate) fn get_sized_buffer() -> Vec> { vec![vec![Cell::default(); size.width as usize]; size.height as usize] } -pub(crate) fn size_to_buffer_size(size: Size) -> Size { +pub(crate) fn size_to_buffer_size(size: Size, font_metrics: Size) -> Size { Size { - width: size.width / 10_u16, - height: size.height / 19_u16, + width: size.width / font_metrics.width, + height: size.height / font_metrics.height, } } /// Returns a buffer based on the canvas size. -pub(crate) fn get_sized_buffer_from_canvas(canvas: &HtmlCanvasElement) -> Vec> { +pub(crate) fn get_sized_buffer_from_canvas( + canvas: &HtmlCanvasElement, + font_metrics: Size, +) -> Vec> { let width = canvas.client_width() as u16; let height = canvas.client_height() as u16; - let size = size_to_buffer_size(Size { width, height }); + let size = size_to_buffer_size(Size { width, height }, font_metrics); vec![vec![Cell::default(); size.width as usize]; size.height as usize] } From 0a2ce45420c00ec8f1ea8a3ecce8c28fdccbfa83 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Fri, 18 Jul 2025 21:39:23 -0600 Subject: [PATCH 03/35] Adjust canvas margins based on font metrics --- src/backend/canvas.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 98e17afa..288e6c9c 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -306,7 +306,9 @@ impl CanvasBackend { // WASM calls per cell. self.changed_cells.set_elements(usize::MAX); } - self.canvas.context.translate(5_f64, 5_f64)?; + let left_margin = (self.canvas.cell_width / 2.0).floor(); + let top_margin = (self.canvas.cell_height / 2.0).floor(); + self.canvas.context.translate(left_margin, top_margin)?; self.draw_background()?; self.draw_symbols()?; @@ -315,7 +317,7 @@ impl CanvasBackend { self.draw_debug()?; } - self.canvas.context.translate(-5_f64, -5_f64)?; + self.canvas.context.translate(-left_margin, -top_margin)?; self.changed_cells.set_elements(0x00); Ok(()) } From 0a153be226dc6623e66da69ccb95eaf98289b68d Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Fri, 18 Jul 2025 23:09:34 -0600 Subject: [PATCH 04/35] Make mouse events relative to the canvas --- src/backend/canvas.rs | 13 ++++++++++--- src/backend/dom.rs | 11 ++++++++++- src/backend/webgl2.rs | 7 +++++++ src/event.rs | 4 ++-- src/render.rs | 25 ++++++++++++++++++++----- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 288e6c9c..8645a58b 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -2,7 +2,6 @@ use bitvec::{bitvec, prelude::BitVec}; use ratatui::layout::Rect; use std::{ io::Result as IoResult, - mem::ManuallyDrop, rc::Rc, sync::atomic::{AtomicBool, Ordering}, }; @@ -13,6 +12,7 @@ use crate::{ utils::*, }, error::Error, + render::WebBackend, CursorShape, }; use ratatui::{ @@ -117,15 +117,16 @@ impl Canvas { let canvas = create_canvas_in_element(&parent_element, width, height)?; let initialized: Rc = Rc::new(false.into()); - let closure = ManuallyDrop::new(Closure::::new({ + let closure = Closure::::new({ let initialized = Rc::clone(&initialized); move |_: web_sys::Event| { initialized.store(false, Ordering::Relaxed); } - })); + }); web_sys::window() .unwrap() .set_onresize(Some(closure.as_ref().unchecked_ref())); + closure.forget(); let context = init_ctx(&canvas)?; @@ -603,6 +604,12 @@ impl Backend for CanvasBackend { } } +impl WebBackend for CanvasBackend { + fn listening_element(&self) -> &Element { + &self.canvas.inner + } +} + /// Optimizes canvas rendering by batching adjacent cells with the same color into a single rectangle. /// /// This reduces the number of draw calls to the canvas API by coalescing adjacent cells diff --git a/src/backend/dom.rs b/src/backend/dom.rs index 2ce52ebd..c66235c5 100644 --- a/src/backend/dom.rs +++ b/src/backend/dom.rs @@ -11,7 +11,10 @@ use web_sys::{ window, Document, Element, Window, }; -use crate::{backend::utils::*, error::Error, widgets::hyperlink::HYPERLINK_MODIFIER, CursorShape}; +use crate::{ + backend::utils::*, error::Error, render::WebBackend, widgets::hyperlink::HYPERLINK_MODIFIER, + CursorShape, +}; /// Options for the [`DomBackend`]. #[derive(Debug, Default)] @@ -340,3 +343,9 @@ impl Backend for DomBackend { Ok(()) } } + +impl WebBackend for DomBackend { + fn listening_element(&self) -> &Element { + &self.grid_parent + } +} diff --git a/src/backend/webgl2.rs b/src/backend/webgl2.rs index bc5e0267..98ab7c31 100644 --- a/src/backend/webgl2.rs +++ b/src/backend/webgl2.rs @@ -1,6 +1,7 @@ use crate::{ backend::{color::to_rgb, utils::*}, error::Error, + render::WebBackend, CursorShape, }; use beamterm_renderer::{CellData, FontAtlas, Renderer, TerminalGrid}; @@ -429,6 +430,12 @@ impl Backend for WebGl2Backend { } } +impl WebBackend for WebGl2Backend { + fn listening_element(&self) -> &web_sys::Element { + self.context.renderer.canvas() + } +} + /// Resizes the cell grid to the new size, copying existing cells where possible. /// /// When the terminal dimensions change, this function creates a new cell buffer and diff --git a/src/event.rs b/src/event.rs index 07f9590e..8b21dbe6 100644 --- a/src/event.rs +++ b/src/event.rs @@ -170,8 +170,8 @@ impl From for MouseEvent { event.button().into() }, event: event_type, - x: event.client_x() as u32, - y: event.client_y() as u32, + x: event.offset_x() as u32, + y: event.offset_y() as u32, ctrl, alt, shift, diff --git a/src/render.rs b/src/render.rs index bc559ff8..8468ef0a 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,14 +1,20 @@ use ratatui::{prelude::Backend, Frame, Terminal}; use std::{cell::RefCell, rc::Rc}; -use web_sys::{wasm_bindgen::prelude::*, window}; +use web_sys::{wasm_bindgen::prelude::*, window, Element}; use crate::event::{KeyEvent, MouseEvent}; +pub trait WebBackend { + /// This is the element that event listeners will be added + /// to to capture mouse and keyboard events + fn listening_element(&self) -> ∈ +} + /// Trait for rendering on the web. /// /// It provides all the necessary methods to render the terminal on the web /// and also interact with the browser such as handling key events. -pub trait WebRenderer { +pub trait WebRenderer: WebBackend { /// Renders the terminal on the web. /// /// This method takes a closure that will be called on every update @@ -53,10 +59,10 @@ pub trait WebRenderer { }); let window = window().unwrap(); let document = window.document().unwrap(); - document + self.listening_element() .add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref()) .unwrap(); - document + self.listening_element() .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref()) .unwrap(); document @@ -74,12 +80,21 @@ pub trait WebRenderer { } } +impl WebBackend for Terminal +where + T: Backend + WebBackend + 'static, +{ + fn listening_element(&self) -> &Element { + self.backend().listening_element() + } +} + /// Implement [`WebRenderer`] for Ratatui's [`Terminal`]. /// /// This implementation creates a loop that calls the [`Terminal::draw`] method. impl WebRenderer for Terminal where - T: Backend + 'static, + T: Backend + WebBackend + 'static, { fn draw_web(mut self, mut render_callback: F) where From c19f3f64781eea425fb2a22f1b64520c2711f445 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 09:41:04 -0600 Subject: [PATCH 05/35] Use better font metrics for rendering to canvas --- examples/demo/src/app.rs | 23 +++++++++++++++++------ examples/demo/src/main.rs | 4 ++++ src/backend/canvas.rs | 15 +++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/examples/demo/src/app.rs b/examples/demo/src/app.rs index 55c0cc49..8b9d2fc7 100644 --- a/examples/demo/src/app.rs +++ b/examples/demo/src/app.rs @@ -225,6 +225,7 @@ pub struct Server<'a> { pub struct App<'a> { pub title: &'a str, pub should_quit: bool, + pub paused: bool, pub tabs: TabsState<'a>, pub show_chart: bool, pub progress: f64, @@ -260,6 +261,7 @@ impl<'a> App<'a> { App { title, should_quit: false, + paused: false, tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]), show_chart: true, progress: 0.0, @@ -347,6 +349,17 @@ impl<'a> App<'a> { } pub fn on_tick(&mut self) -> Duration { + // calculate elapsed time since last frame + let now = web_time::Instant::now(); + let elapsed = now.duration_since(self.last_frame).as_millis() as u32; + self.last_frame = now; + + let elapsed = Duration::from_millis(elapsed); + + if self.paused { + return elapsed; + } + // Update progress self.progress += 0.001; if self.progress > 1.0 { @@ -361,13 +374,11 @@ impl<'a> App<'a> { let event = self.barchart.pop().unwrap(); self.barchart.insert(0, event); + elapsed + } - // calculate elapsed time since last frame - let now = web_time::Instant::now(); - let elapsed = now.duration_since(self.last_frame).as_millis() as u32; - self.last_frame = now; - - Duration::from_millis(elapsed) + pub fn pause_unpause(&mut self) { + self.paused = !self.paused; } fn add_transition_tab_effect(&mut self) { diff --git a/examples/demo/src/main.rs b/examples/demo/src/main.rs index 778c2686..e120f56d 100644 --- a/examples/demo/src/main.rs +++ b/examples/demo/src/main.rs @@ -38,6 +38,7 @@ fn main() -> Result<()> { let app_state = Rc::new(RefCell::new(App::new("Demo", false))); // let mut backend = CanvasBackend::new_with_size(1600, 900)?; let mut backend = CanvasBackend::new()?; + // backend.set_debug_mode(Some("red")); backend.set_background_color(Color::Rgb(18, 18, 18)); let terminal = Terminal::new(backend)?; terminal.on_key_event({ @@ -57,6 +58,9 @@ fn main() -> Result<()> { KeyCode::Down => { app_state.on_down(); } + KeyCode::Char(' ') => { + app_state.pause_unpause(); + } KeyCode::Char(c) => app_state.on_key(c), _ => {} } diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 8645a58b..6069a18c 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -84,6 +84,8 @@ struct Canvas { /// This will be used for multiplying the cell's y position to get the actual pixel /// position on the canvas. cell_height: f64, + /// The font ascent of the `|` character as measured by the canvas + cell_ascent: f64, } fn init_ctx( @@ -130,7 +132,7 @@ impl Canvas { let context = init_ctx(&canvas)?; - let font_measurement = context.measure_text("|")?; + let font_measurement = context.measure_text("█")?; Ok(Self { parent: parent_element, @@ -138,10 +140,11 @@ impl Canvas { context, inner: canvas, background_color, - // cell_width: font_measurement.actual_bounding_box_left().abs() - // + font_measurement.actual_bounding_box_right().abs(), cell_width: font_measurement.width().floor(), - cell_height: font_measurement.font_bounding_box_descent().abs().floor(), + cell_height: (font_measurement.font_bounding_box_ascent().abs() + + font_measurement.font_bounding_box_descent().abs()) + .floor(), + cell_ascent: font_measurement.font_bounding_box_ascent().floor(), }) } @@ -384,7 +387,7 @@ impl CanvasBackend { self.canvas.context.fill_text( cell.symbol(), x as f64 * self.canvas.cell_width, - y as f64 * self.canvas.cell_height, + y as f64 * self.canvas.cell_height + self.canvas.cell_ascent, )?; index += 1; @@ -454,7 +457,7 @@ impl CanvasBackend { self.canvas.context.fill_text( "_", pos.x as f64 * self.canvas.cell_width, - pos.y as f64 * self.canvas.cell_height, + pos.y as f64 * self.canvas.cell_height + self.canvas.cell_ascent, )?; self.canvas.context.restore(); From 8a20ce1de36cd6d520a0c221e1f09004c1ecafdf Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 09:53:09 -0600 Subject: [PATCH 06/35] Add custom font support to canvas backend --- src/backend/canvas.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 6069a18c..80e0677b 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -40,6 +40,8 @@ pub struct CanvasBackendOptions { /// this option may cause some performance issues when dealing with large /// numbers of simultaneous changes. always_clip_cells: bool, + /// An optional string which sets a custom font for the canvas + font_str: Option, } impl CanvasBackendOptions { @@ -74,6 +76,8 @@ struct Canvas { context: web_sys::CanvasRenderingContext2d, /// Background color. background_color: Color, + /// An optional string which sets a custom font for the canvas + font_str: Option, /// Width of a single cell. /// /// This will be used for multiplying the cell's x position to get the actual pixel @@ -90,6 +94,7 @@ struct Canvas { fn init_ctx( canvas: &web_sys::HtmlCanvasElement, + font_str: Option<&str>, ) -> Result { let context_options = Map::new(); context_options.set(&JsValue::from_str("alpha"), &Boolean::from(JsValue::TRUE)); @@ -103,7 +108,7 @@ fn init_ctx( .ok_or_else(|| Error::UnableToRetrieveCanvasContext)? .dyn_into::() .expect("Unable to cast canvas context"); - context.set_font("16px monospace"); + context.set_font(font_str.unwrap_or("16px monospace")); context.set_text_baseline("top"); Ok(context) } @@ -115,6 +120,7 @@ impl Canvas { width: u32, height: u32, background_color: Color, + font_str: Option, ) -> Result { let canvas = create_canvas_in_element(&parent_element, width, height)?; @@ -130,7 +136,7 @@ impl Canvas { .set_onresize(Some(closure.as_ref().unchecked_ref())); closure.forget(); - let context = init_ctx(&canvas)?; + let context = init_ctx(&canvas, font_str.as_deref())?; let font_measurement = context.measure_text("█")?; @@ -140,6 +146,7 @@ impl Canvas { context, inner: canvas, background_color, + font_str, cell_width: font_measurement.width().floor(), cell_height: (font_measurement.font_bounding_box_ascent().abs() + font_measurement.font_bounding_box_descent().abs()) @@ -156,7 +163,7 @@ impl Canvas { } fn re_init_ctx(&mut self) -> Result<(), Error> { - self.context = init_ctx(&self.inner)?; + self.context = init_ctx(&self.inner, self.font_str.as_deref())?; Ok(()) } } @@ -197,7 +204,7 @@ impl CanvasBackend { } /// Constructs a new [`CanvasBackend`] with the given options. - pub fn new_with_options(options: CanvasBackendOptions) -> Result { + pub fn new_with_options(mut options: CanvasBackendOptions) -> Result { // Parent element of canvas (uses unless specified) let parent = get_element_by_id_or_body(options.grid_id.as_ref())?; @@ -205,7 +212,7 @@ impl CanvasBackend { .size .unwrap_or_else(|| (parent.client_width() as u32, parent.client_height() as u32)); - let canvas = Canvas::new(parent, width, height, Color::Black)?; + let canvas = Canvas::new(parent, width, height, Color::Black, options.font_str.take())?; let buffer = get_sized_buffer_from_canvas(&canvas.inner, canvas.font_metrics()); let changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { From 97d5e440399744e816dda1eaa1d1dea202726090 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 09:54:16 -0600 Subject: [PATCH 07/35] Add custom font support to canvas backend options --- src/backend/canvas.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 80e0677b..33066e67 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -61,6 +61,12 @@ impl CanvasBackendOptions { self.size = Some(size); self } + + /// Sets the font that the canvas will use + pub fn font(mut self, font: Option) -> Self { + self.font_str = font; + self + } } /// Canvas renderer. From da35ead8db4a224814f56eb6f86202e7ed261975 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 09:55:11 -0600 Subject: [PATCH 08/35] Add custom font support to canvas backend options --- src/backend/canvas.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 33066e67..04e47987 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -63,8 +63,8 @@ impl CanvasBackendOptions { } /// Sets the font that the canvas will use - pub fn font(mut self, font: Option) -> Self { - self.font_str = font; + pub fn font(mut self, font: String) -> Self { + self.font_str = Some(font); self } } From 407c831571adca111be76475296bb0f876b72820 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 10:57:41 -0600 Subject: [PATCH 09/35] Demonstrate usage of custom font in demo --- examples/demo/Cargo.lock | 1 + examples/demo/Cargo.toml | 4 ++++ examples/demo/index.html | 12 +++++++++++- examples/demo/src/{main.rs => lib.rs} | 18 +++++++++++------- 4 files changed, 27 insertions(+), 8 deletions(-) rename examples/demo/src/{main.rs => lib.rs} (85%) diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 78a73c85..25575cd6 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -292,6 +292,7 @@ dependencies = [ "rand", "ratzilla", "tachyonfx", + "wasm-bindgen", "web-time", ] diff --git a/examples/demo/Cargo.toml b/examples/demo/Cargo.toml index 2d1dd5fb..c99f2e7e 100644 --- a/examples/demo/Cargo.toml +++ b/examples/demo/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" publish = false +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] clap = { version = "4.5.23", features = ["derive"] } rand = { version = "0.8.5", features = ["small_rng"], default-features = false } @@ -11,5 +14,6 @@ ratzilla = { path = "../../" } tachyonfx = { version = "0.15.0", default-features = false, features = ["web-time"] } web-time = "1.1.0" console_error_panic_hook = "0.1.7" +wasm-bindgen = "0.2.100" # tui-big-text = "0.6.1" diff --git a/examples/demo/index.html b/examples/demo/index.html index 91a9d9b1..c269f415 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -34,6 +34,16 @@ - + + + \ No newline at end of file diff --git a/examples/demo/src/main.rs b/examples/demo/src/lib.rs similarity index 85% rename from examples/demo/src/main.rs rename to examples/demo/src/lib.rs index e120f56d..638c5acc 100644 --- a/examples/demo/src/main.rs +++ b/examples/demo/src/lib.rs @@ -10,10 +10,12 @@ use std::{cell::RefCell, io::Result, rc::Rc}; use app::App; use clap::Parser; +use ratzilla::backend::canvas::{CanvasBackend, CanvasBackendOptions}; use ratzilla::event::KeyCode; use ratzilla::ratatui::style::Color; use ratzilla::ratatui::Terminal; -use ratzilla::{CanvasBackend, WebRenderer}; +use ratzilla::WebRenderer; +use wasm_bindgen::prelude::*; mod app; @@ -32,15 +34,19 @@ struct Cli { unicode: bool, } -fn main() -> Result<()> { +#[wasm_bindgen] +pub fn main() { console_error_panic_hook::set_once(); let app_state = Rc::new(RefCell::new(App::new("Demo", false))); - // let mut backend = CanvasBackend::new_with_size(1600, 900)?; - let mut backend = CanvasBackend::new()?; + // let mut backend = CanvasBackend::new_with_size(1600, 900).unwrap(); + let mut backend = CanvasBackend::new_with_options( + CanvasBackendOptions::new().font(String::from("16px Fira Code")), + ) + .unwrap(); // backend.set_debug_mode(Some("red")); backend.set_background_color(Color::Rgb(18, 18, 18)); - let terminal = Terminal::new(backend)?; + let terminal = Terminal::new(backend).unwrap(); terminal.on_key_event({ let app_state_cloned = app_state.clone(); move |event| { @@ -72,6 +78,4 @@ fn main() -> Result<()> { let elapsed = app_state.on_tick(); ui::draw(elapsed, f, &mut app_state); }); - - Ok(()) } From 7770e54b844eef9cd06ff0a303edf7be20b0b352 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 10:58:30 -0600 Subject: [PATCH 10/35] Demonstrate usage of custom font in demo --- examples/demo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demo/src/lib.rs b/examples/demo/src/lib.rs index 638c5acc..f8dc82f7 100644 --- a/examples/demo/src/lib.rs +++ b/examples/demo/src/lib.rs @@ -6,7 +6,7 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{cell::RefCell, io::Result, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use app::App; use clap::Parser; From 6cec98392fd09fb20c228356c9e6a7c53bb8db0c Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 11:06:28 -0600 Subject: [PATCH 11/35] Demonstrate usage of custom font in demo --- examples/demo/index.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/demo/index.html b/examples/demo/index.html index c269f415..98b93903 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -38,10 +38,13 @@ From 979c71b609961550fcdcfd6b0a3775a6b83fe921 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 17:40:58 -0600 Subject: [PATCH 12/35] Use sledgehammer_bindgen to buffer canvas calls --- Cargo.lock | 37 ++++ Cargo.toml | 2 + examples/demo/Cargo.lock | 37 ++++ src/backend/canvas.rs | 390 ++++++++++++++++++++++++----------- src/backend/canvas_import.js | 1 + 5 files changed, 344 insertions(+), 123 deletions(-) create mode 100644 src/backend/canvas_import.js diff --git a/Cargo.lock b/Cargo.lock index 9d1ce321..db8d48a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -379,10 +379,18 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -415,6 +423,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 96d283b8..d4c2a32b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,5 @@ console_error_panic_hook = "0.1.7" thiserror = "2.0.12" bitvec = { version = "1.0.1", default-features = false, features = ["alloc", "std"] } beamterm-renderer = "0.1.1" +sledgehammer_bindgen = { version = "0.6.0", features = ["web"] } +sledgehammer_utils = "0.3.1" diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 25575cd6..a1beaf71 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -556,10 +556,18 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -598,6 +606,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 04e47987..ee29fe05 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -1,5 +1,6 @@ use bitvec::{bitvec, prelude::BitVec}; use ratatui::layout::Rect; +use sledgehammer_bindgen::bindgen; use std::{ io::Result as IoResult, rc::Rc, @@ -23,8 +24,12 @@ use ratatui::{ style::{Color, Modifier}, }; use web_sys::{ - js_sys::{Boolean, Map}, - wasm_bindgen::{prelude::Closure, JsCast, JsValue}, + js_sys::Uint16Array, + wasm_bindgen::{ + self, + prelude::{wasm_bindgen, Closure}, + JsCast, + }, Element, }; @@ -69,17 +74,173 @@ impl CanvasBackendOptions { } } +#[wasm_bindgen(inline_js = r#" +export class RatzillaCanvas { + constructor() {} + + measure_text(text) { + return this.ctx.measureText(text); + } + + get_canvas() { + return this.canvas; + } + + reinit_canvas() { + this.canvas.width = this.parent.clientWidth; + this.canvas.height = this.parent.clientHeight; + return new Uint16Array([this.canvas.width, this.canvas.height]); + } +} +"#)] +extern "C" { + #[wasm_bindgen] + /// External JS class for managing the actual HTML canvas, context, + /// and parent element. + pub type RatzillaCanvas; + + #[wasm_bindgen(method)] + fn measure_text(this: &RatzillaCanvas, text: &str) -> web_sys::TextMetrics; + + #[wasm_bindgen(method)] + fn get_canvas(this: &RatzillaCanvas) -> web_sys::HtmlCanvasElement; + + #[wasm_bindgen(method)] + fn reinit_canvas(this: &RatzillaCanvas) -> Uint16Array; +} + +#[bindgen] +mod js { + #[extends(RatzillaCanvas)] + /// Responsible for buffering the calls to the canvas and + /// canvas context + struct Buffer; + + const BASE: &str = r#"src/backend/canvas_import.js"#; + + fn clear_rect() { + r#" + this.ctx.clearRect( + 0, + 0, + this.canvas.clientWidth, + this.canvas.clientHeight, + ); + "# + } + + fn translate(x: u16, y: u16) { + r#" + this.ctx.translate($x$, $y$); + "# + } + + fn translate_neg(x: u16, y: u16) { + r#" + this.ctx.translate(-$x$, -$y$); + "# + } + + fn save() { + r#" + this.ctx.save(); + "# + } + + fn restore() { + r#" + this.ctx.restore(); + "# + } + + fn begin_path() { + r#" + this.ctx.beginPath(); + "# + } + + fn clip() { + r#" + this.ctx.clip(); + "# + } + + fn rect(x: u16, y: u16, w: u16, h: u16) { + r#" + this.ctx.rect($x$, $y$, $w$, $h$) + "# + } + + fn fill_rect(x: u16, y: u16, w: u16, h: u16) { + r#" + this.ctx.fillRect($x$, $y$, $w$, $h$) + "# + } + + fn stroke_rect(x: u16, y: u16, w: u16, h: u16) { + r#" + this.ctx.strokeRect($x$, $y$, $w$, $h$) + "# + } + + fn fill_text(text: &str, x: u16, y: u16) { + r#" + this.ctx.fillText($text$, $x$, $y$); + "# + } + + fn set_fill_style_str(style: &str) { + r#" + this.ctx.fillStyle = $style$; + "# + } + + fn set_stroke_style_str(style: &str) { + r#" + this.ctx.strokeStyle = $style$; + "# + } + + fn create_canvas_in_element(parent: &str) { + r#" + this.parent = document.getElementById($parent$); + if (this.parent == null) { + this.parent = document.body; + } + this.canvas = document.createElement("canvas"); + this.parent.appendChild(this.canvas); + "# + } + + fn reinit_canvas() { + r#" + this.canvas.width = this.parent.clientWidth; + this.canvas.height = this.parent.clientHeight; + "# + } + + fn init_ctx(font_str: &str) { + r#" + this.ctx = this.canvas.getContext("2d", { + alpha: true, + desynchronized: true + }); + this.ctx.font = $font_str$; + this.ctx.textBaseline = "top"; + "# + } +} + /// Canvas renderer. -#[derive(Debug)] struct Canvas { - /// The canvas's parent element - parent: Element, + /// The buffer of draw calls to the canvas + buffer: Buffer, /// Whether the canvas has been initialized. initialized: Rc, - /// Canvas element. + /// The inner HTML canvas element + /// + /// Use **only** for implementing `WebBackend` inner: web_sys::HtmlCanvasElement, - /// Rendering context. - context: web_sys::CanvasRenderingContext2d, /// Background color. background_color: Color, /// An optional string which sets a custom font for the canvas @@ -98,38 +259,13 @@ struct Canvas { cell_ascent: f64, } -fn init_ctx( - canvas: &web_sys::HtmlCanvasElement, - font_str: Option<&str>, -) -> Result { - 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::() - .expect("Unable to cast canvas context"); - context.set_font(font_str.unwrap_or("16px monospace")); - context.set_text_baseline("top"); - Ok(context) -} - impl Canvas { /// Constructs a new [`Canvas`]. fn new( - parent_element: web_sys::Element, - width: u32, - height: u32, + parent_element: &str, background_color: Color, font_str: Option, ) -> Result { - let canvas = create_canvas_in_element(&parent_element, width, height)?; - let initialized: Rc = Rc::new(false.into()); let closure = Closure::::new({ let initialized = Rc::clone(&initialized); @@ -142,23 +278,35 @@ impl Canvas { .set_onresize(Some(closure.as_ref().unchecked_ref())); closure.forget(); - let context = init_ctx(&canvas, font_str.as_deref())?; - - let font_measurement = context.measure_text("█")?; + let mut buffer = Buffer::default(); + buffer.create_canvas_in_element(parent_element); - Ok(Self { - parent: parent_element, + let mut canvas = Self { + inner: { + let ratzilla_canvas: &RatzillaCanvas = buffer.js_channel().as_ref(); + ratzilla_canvas.get_canvas() + }, + buffer, initialized, - context, - inner: canvas, background_color, font_str, - cell_width: font_measurement.width().floor(), - cell_height: (font_measurement.font_bounding_box_ascent().abs() - + font_measurement.font_bounding_box_descent().abs()) - .floor(), - cell_ascent: font_measurement.font_bounding_box_ascent().floor(), - }) + cell_width: 0.0, + cell_height: 0.0, + cell_ascent: 0.0, + }; + + canvas.init_ctx(); + canvas.buffer.flush(); + + let ratzilla_canvas: &RatzillaCanvas = canvas.buffer.js_channel().as_ref(); + let font_measurement = ratzilla_canvas.measure_text("█"); + canvas.cell_width = font_measurement.width().floor(); + canvas.cell_height = (font_measurement.font_bounding_box_ascent().abs() + + font_measurement.font_bounding_box_descent().abs()) + .floor(); + canvas.cell_ascent = font_measurement.font_bounding_box_ascent().floor(); + + Ok(canvas) } fn font_metrics(&self) -> Size { @@ -168,16 +316,15 @@ impl Canvas { } } - fn re_init_ctx(&mut self) -> Result<(), Error> { - self.context = init_ctx(&self.inner, self.font_str.as_deref())?; - Ok(()) + fn init_ctx(&mut self) { + self.buffer + .init_ctx(self.font_str.as_deref().unwrap_or("16px monospace")); } } /// Canvas backend. /// /// This backend renders the buffer onto a HTML canvas element. -#[derive(Debug)] pub struct CanvasBackend { /// The options passed to the backend upon instantiation options: CanvasBackendOptions, @@ -212,14 +359,12 @@ impl CanvasBackend { /// Constructs a new [`CanvasBackend`] with the given options. pub fn new_with_options(mut options: CanvasBackendOptions) -> Result { // Parent element of canvas (uses unless specified) - 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 parent = options.grid_id.as_deref().unwrap_or_default(); - let canvas = Canvas::new(parent, width, height, Color::Black, options.font_str.take())?; - let buffer = get_sized_buffer_from_canvas(&canvas.inner, canvas.font_metrics()); + let canvas = Canvas::new(parent, Color::Black, options.font_str.take())?; + let ratzilla_canvas: &RatzillaCanvas = canvas.buffer.js_channel().as_ref(); + let buffer = + get_sized_buffer_from_canvas(&ratzilla_canvas.get_canvas(), canvas.font_metrics()); let changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { options, @@ -237,16 +382,11 @@ impl CanvasBackend { } fn initialize(&mut self) -> Result<(), Error> { - let (width, height) = self.options.size.unwrap_or_else(|| { - ( - self.canvas.parent.client_width() as u32, - self.canvas.parent.client_height() as u32, - ) - }); - - self.canvas.inner.set_width(width); - self.canvas.inner.set_height(height); - self.canvas.re_init_ctx()?; + let ratzilla_canvas: &RatzillaCanvas = self.canvas.buffer.js_channel().as_ref(); + let canvas_size = ratzilla_canvas.reinit_canvas(); + // TODO: Find a way to not use a Javascript array + let (width, height) = (canvas_size.get_index(0), canvas_size.get_index(1)); + self.canvas.init_ctx(); let new_buffer_size = size_to_buffer_size( Size { @@ -311,13 +451,8 @@ impl CanvasBackend { // If `force_redraw` is `true`, the entire canvas will be cleared and redrawn. 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.initialize()?; + self.canvas.buffer.clear_rect(); // NOTE: The draw_* functions each traverse the buffer once, instead of // traversing it once per cell; this is done to reduce the number of // WASM calls per cell. @@ -325,7 +460,9 @@ impl CanvasBackend { } let left_margin = (self.canvas.cell_width / 2.0).floor(); let top_margin = (self.canvas.cell_height / 2.0).floor(); - self.canvas.context.translate(left_margin, top_margin)?; + self.canvas + .buffer + .translate(left_margin as _, top_margin as _); self.draw_background()?; self.draw_symbols()?; @@ -334,7 +471,10 @@ impl CanvasBackend { self.draw_debug()?; } - self.canvas.context.translate(-left_margin, -top_margin)?; + self.canvas + .buffer + .translate_neg(left_margin as _, top_margin as _); + self.canvas.buffer.flush(); self.changed_cells.set_elements(0x00); Ok(()) } @@ -357,7 +497,7 @@ impl CanvasBackend { let changed_cells = &self.changed_cells; let mut index = 0; - self.canvas.context.save(); + self.canvas.buffer.save(); let mut last_color = None; for (y, line) in self.buffer.iter().enumerate() { for (x, cell) in line.iter().enumerate() { @@ -372,41 +512,41 @@ impl CanvasBackend { // 1. When we need to create a clipping path (for potentially problematic glyphs) // 2. When the text color changes if self.options.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 * self.canvas.cell_width, - y as f64 * self.canvas.cell_height, - self.canvas.cell_width, - self.canvas.cell_height, + self.canvas.buffer.restore(); + self.canvas.buffer.save(); + + self.canvas.buffer.begin_path(); + self.canvas.buffer.rect( + (x as f64 * self.canvas.cell_width) as _, + (y as f64 * self.canvas.cell_height) as _, + self.canvas.cell_width as _, + self.canvas.cell_height as _, ); - self.canvas.context.clip(); + self.canvas.buffer.clip(); last_color = None; // reset last color to avoid clipping let color = get_canvas_color(color, Color::White); - self.canvas.context.set_fill_style_str(&color); + self.canvas.buffer.set_fill_style_str(&color); } else if last_color != Some(color) { - self.canvas.context.restore(); - self.canvas.context.save(); + self.canvas.buffer.restore(); + self.canvas.buffer.save(); last_color = Some(color); let color = get_canvas_color(color, Color::White); - self.canvas.context.set_fill_style_str(&color); + self.canvas.buffer.set_fill_style_str(&color); } - self.canvas.context.fill_text( + self.canvas.buffer.fill_text( cell.symbol(), - x as f64 * self.canvas.cell_width, - y as f64 * self.canvas.cell_height + self.canvas.cell_ascent, - )?; + (x as f64 * self.canvas.cell_width) as _, + (y as f64 * self.canvas.cell_height + self.canvas.cell_ascent) as _, + ); index += 1; } } - self.canvas.context.restore(); + self.canvas.buffer.restore(); Ok(()) } @@ -420,17 +560,17 @@ impl CanvasBackend { /// color, and then it draws the accumulated rectangle. fn draw_background(&mut self) -> Result<(), Error> { let changed_cells = &self.changed_cells; - self.canvas.context.save(); + self.canvas.buffer.save(); - let draw_region = |(rect, color): (Rect, Color)| { - let color = get_canvas_color(color, self.canvas.background_color); + let draw_region = |(rect, color, canvas): (Rect, Color, &mut Canvas)| { + let color = get_canvas_color(color, canvas.background_color); - self.canvas.context.set_fill_style_str(&color); - self.canvas.context.fill_rect( - rect.x as f64 * self.canvas.cell_width, - rect.y as f64 * self.canvas.cell_height, - rect.width as f64 * self.canvas.cell_width, - rect.height as f64 * self.canvas.cell_height, + canvas.buffer.set_fill_style_str(&color); + canvas.buffer.fill_rect( + (rect.x as f64 * canvas.cell_width) as _, + (rect.y as f64 * canvas.cell_height) as _, + (rect.width as f64 * canvas.cell_width) as _, + (rect.height as f64 * canvas.cell_height) as _, ); }; @@ -442,19 +582,23 @@ impl CanvasBackend { // Only calls `draw_region` if the color is different from the previous one row_renderer .process_color((x, y), actual_bg_color(cell)) - .map(draw_region); + .map(|(rect, color)| draw_region((rect, color, &mut self.canvas))); } else { // Cell is unchanged so we must flush any held region // to avoid clearing the foreground (symbol) of the cell - row_renderer.flush().map(draw_region); + row_renderer + .flush() + .map(|(rect, color)| draw_region((rect, color, &mut self.canvas))); } index += 1; } // Flush the remaining region after traversing the row - row_renderer.flush().map(draw_region); + row_renderer + .flush() + .map(|(rect, color)| draw_region((rect, color, &mut self.canvas))); } - self.canvas.context.restore(); + self.canvas.buffer.restore(); Ok(()) } @@ -465,15 +609,15 @@ impl CanvasBackend { let cell = &self.buffer[pos.y as usize][pos.x as usize]; if cell.modifier.contains(Modifier::UNDERLINED) { - self.canvas.context.save(); + self.canvas.buffer.save(); - self.canvas.context.fill_text( + self.canvas.buffer.fill_text( "_", - pos.x as f64 * self.canvas.cell_width, - pos.y as f64 * self.canvas.cell_height + self.canvas.cell_ascent, - )?; + (pos.x as f64 * self.canvas.cell_width) as _, + (pos.y as f64 * self.canvas.cell_height + self.canvas.cell_ascent) as _, + ); - self.canvas.context.restore(); + self.canvas.buffer.restore(); } } @@ -482,22 +626,22 @@ impl CanvasBackend { /// Draws cell boundaries for debugging. fn draw_debug(&mut self) -> Result<(), Error> { - self.canvas.context.save(); + self.canvas.buffer.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 * self.canvas.cell_width, - y as f64 * self.canvas.cell_height, - self.canvas.cell_width, - self.canvas.cell_height, + self.canvas.buffer.set_stroke_style_str(color); + self.canvas.buffer.stroke_rect( + (x as f64 * self.canvas.cell_width) as _, + (y as f64 * self.canvas.cell_height) as _, + self.canvas.cell_width as _, + self.canvas.cell_height as _, ); } } - self.canvas.context.restore(); + self.canvas.buffer.restore(); Ok(()) } diff --git a/src/backend/canvas_import.js b/src/backend/canvas_import.js new file mode 100644 index 00000000..2d81b932 --- /dev/null +++ b/src/backend/canvas_import.js @@ -0,0 +1 @@ +import { RatzillaCanvas } from './inline0.js'; From 890695cca486b3484fec2d18a62763d5c9e7d262 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sat, 19 Jul 2025 18:36:10 -0600 Subject: [PATCH 13/35] Use sledgehammer bindgen as Dioxus does --- src/backend/canvas.rs | 47 +++++++++++++----------------------- src/backend/canvas_import.js | 18 +++++++++++++- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index ee29fe05..fe79f23b 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -74,25 +74,8 @@ impl CanvasBackendOptions { } } -#[wasm_bindgen(inline_js = r#" -export class RatzillaCanvas { - constructor() {} - - measure_text(text) { - return this.ctx.measureText(text); - } - - get_canvas() { - return this.canvas; - } - - reinit_canvas() { - this.canvas.width = this.parent.clientWidth; - this.canvas.height = this.parent.clientHeight; - return new Uint16Array([this.canvas.width, this.canvas.height]); - } -} -"#)] +// Mirrors usage in https://github.com/DioxusLabs/dioxus/blob/main/packages/interpreter/src/unified_bindings.rs +#[wasm_bindgen] extern "C" { #[wasm_bindgen] /// External JS class for managing the actual HTML canvas, context, @@ -109,6 +92,14 @@ extern "C" { fn reinit_canvas(this: &RatzillaCanvas) -> Uint16Array; } +impl Buffer { + /// Converts the buffer to its baseclass + pub fn ratzilla_canvas(&self) -> &RatzillaCanvas { + use wasm_bindgen::prelude::JsCast; + self.js_channel().unchecked_ref() + } +} + #[bindgen] mod js { #[extends(RatzillaCanvas)] @@ -282,10 +273,7 @@ impl Canvas { buffer.create_canvas_in_element(parent_element); let mut canvas = Self { - inner: { - let ratzilla_canvas: &RatzillaCanvas = buffer.js_channel().as_ref(); - ratzilla_canvas.get_canvas() - }, + inner: buffer.ratzilla_canvas().get_canvas(), buffer, initialized, background_color, @@ -298,8 +286,7 @@ impl Canvas { canvas.init_ctx(); canvas.buffer.flush(); - let ratzilla_canvas: &RatzillaCanvas = canvas.buffer.js_channel().as_ref(); - let font_measurement = ratzilla_canvas.measure_text("█"); + let font_measurement = canvas.buffer.ratzilla_canvas().measure_text("█"); canvas.cell_width = font_measurement.width().floor(); canvas.cell_height = (font_measurement.font_bounding_box_ascent().abs() + font_measurement.font_bounding_box_descent().abs()) @@ -362,9 +349,10 @@ impl CanvasBackend { let parent = options.grid_id.as_deref().unwrap_or_default(); let canvas = Canvas::new(parent, Color::Black, options.font_str.take())?; - let ratzilla_canvas: &RatzillaCanvas = canvas.buffer.js_channel().as_ref(); - let buffer = - get_sized_buffer_from_canvas(&ratzilla_canvas.get_canvas(), canvas.font_metrics()); + let buffer = get_sized_buffer_from_canvas( + &canvas.buffer.ratzilla_canvas().get_canvas(), + canvas.font_metrics(), + ); let changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { options, @@ -382,8 +370,7 @@ impl CanvasBackend { } fn initialize(&mut self) -> Result<(), Error> { - let ratzilla_canvas: &RatzillaCanvas = self.canvas.buffer.js_channel().as_ref(); - let canvas_size = ratzilla_canvas.reinit_canvas(); + let canvas_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); // TODO: Find a way to not use a Javascript array let (width, height) = (canvas_size.get_index(0), canvas_size.get_index(1)); self.canvas.init_ctx(); diff --git a/src/backend/canvas_import.js b/src/backend/canvas_import.js index 2d81b932..e19f3a6c 100644 --- a/src/backend/canvas_import.js +++ b/src/backend/canvas_import.js @@ -1 +1,17 @@ -import { RatzillaCanvas } from './inline0.js'; +export class RatzillaCanvas { + constructor() {} + + measure_text(text) { + return this.ctx.measureText(text); + } + + get_canvas() { + return this.canvas; + } + + reinit_canvas() { + this.canvas.width = this.parent.clientWidth; + this.canvas.height = this.parent.clientHeight; + return new Uint16Array([this.canvas.width, this.canvas.height]); + } +} From 45efbc5aaeccdb9d82725a4aa6b9fe22dd01506f Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sun, 20 Jul 2025 09:56:06 -0600 Subject: [PATCH 14/35] Move a comment and fix initial canvas initialization --- src/backend/canvas.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index fe79f23b..e02503e3 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -283,6 +283,7 @@ impl Canvas { cell_ascent: 0.0, }; + canvas.buffer.reinit_canvas(); canvas.init_ctx(); canvas.buffer.flush(); @@ -436,13 +437,14 @@ impl CanvasBackend { // accordingly. // // If `force_redraw` is `true`, the entire canvas will be cleared and redrawn. + // + // NOTE: The draw_* functions each traverse the buffer once, instead of + // traversing it once per cell; this is done to reduce the number of + // WASM calls per cell. fn update_grid(&mut self, force_redraw: bool) -> Result<(), Error> { if force_redraw { self.initialize()?; self.canvas.buffer.clear_rect(); - // NOTE: The draw_* functions each traverse the buffer once, instead of - // traversing it once per cell; this is done to reduce the number of - // WASM calls per cell. self.changed_cells.set_elements(usize::MAX); } let left_margin = (self.canvas.cell_width / 2.0).floor(); From c9c632d9195b6b4aa37577987697268378f40896 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sun, 20 Jul 2025 11:43:42 -0600 Subject: [PATCH 15/35] Move resizing code to JavaScript --- examples/demo/index.html | 5 -- src/backend/canvas.rs | 113 ++++++++--------------------------- src/backend/canvas_import.js | 45 ++++++++++++-- 3 files changed, 65 insertions(+), 98 deletions(-) diff --git a/examples/demo/index.html b/examples/demo/index.html index 98b93903..20b6328e 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -26,11 +26,6 @@ font-size: 16px; margin: 0px; } - - canvas { - width: 100%; - height: 100%; - } diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index e02503e3..e10ffc4c 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -24,7 +24,7 @@ use ratatui::{ style::{Color, Modifier}, }; use web_sys::{ - js_sys::Uint16Array, + js_sys::{Float64Array, Uint16Array}, wasm_bindgen::{ self, prelude::{wasm_bindgen, Closure}, @@ -38,8 +38,6 @@ use web_sys::{ pub struct CanvasBackendOptions { /// The element ID. grid_id: Option, - /// Override the automatically detected size. - size: Option<(u32, u32)>, /// 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 @@ -61,12 +59,6 @@ impl CanvasBackendOptions { self } - /// Sets the size of the canvas, in pixels. - pub fn size(mut self, size: (u32, u32)) -> Self { - self.size = Some(size); - self - } - /// Sets the font that the canvas will use pub fn font(mut self, font: String) -> Self { self.font_str = Some(font); @@ -83,12 +75,14 @@ extern "C" { pub type RatzillaCanvas; #[wasm_bindgen(method)] - fn measure_text(this: &RatzillaCanvas, text: &str) -> web_sys::TextMetrics; + /// Returns the cell width, cell height, and cell ascent in that order + fn measure_text(this: &RatzillaCanvas, text: &str) -> Float64Array; #[wasm_bindgen(method)] fn get_canvas(this: &RatzillaCanvas) -> web_sys::HtmlCanvasElement; #[wasm_bindgen(method)] + /// Returns the new number of cells in width and height in that order fn reinit_canvas(this: &RatzillaCanvas) -> Uint16Array; } @@ -109,29 +103,6 @@ mod js { const BASE: &str = r#"src/backend/canvas_import.js"#; - fn clear_rect() { - r#" - this.ctx.clearRect( - 0, - 0, - this.canvas.clientWidth, - this.canvas.clientHeight, - ); - "# - } - - fn translate(x: u16, y: u16) { - r#" - this.ctx.translate($x$, $y$); - "# - } - - fn translate_neg(x: u16, y: u16) { - r#" - this.ctx.translate(-$x$, -$y$); - "# - } - fn save() { r#" this.ctx.save(); @@ -199,25 +170,16 @@ mod js { this.parent = document.body; } this.canvas = document.createElement("canvas"); - this.parent.appendChild(this.canvas); - "# - } - - fn reinit_canvas() { - r#" this.canvas.width = this.parent.clientWidth; this.canvas.height = this.parent.clientHeight; + this.parent.appendChild(this.canvas); "# } fn init_ctx(font_str: &str) { r#" - this.ctx = this.canvas.getContext("2d", { - alpha: true, - desynchronized: true - }); - this.ctx.font = $font_str$; - this.ctx.textBaseline = "top"; + this.font_str = $font_str$; + super.init_ctx(); "# } } @@ -283,16 +245,13 @@ impl Canvas { cell_ascent: 0.0, }; - canvas.buffer.reinit_canvas(); canvas.init_ctx(); canvas.buffer.flush(); let font_measurement = canvas.buffer.ratzilla_canvas().measure_text("█"); - canvas.cell_width = font_measurement.width().floor(); - canvas.cell_height = (font_measurement.font_bounding_box_ascent().abs() - + font_measurement.font_bounding_box_descent().abs()) - .floor(); - canvas.cell_ascent = font_measurement.font_bounding_box_ascent().floor(); + canvas.cell_width = font_measurement.get_index(0); + canvas.cell_height = font_measurement.get_index(1); + canvas.cell_ascent = font_measurement.get_index(2); Ok(canvas) } @@ -314,8 +273,11 @@ impl Canvas { /// /// This backend renders the buffer onto a HTML canvas element. pub struct CanvasBackend { - /// The options passed to the backend upon instantiation - options: CanvasBackendOptions, + /// 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, /// Current buffer. buffer: Vec>, /// Changed buffer cells @@ -336,14 +298,6 @@ impl CanvasBackend { Self::new_with_options(CanvasBackendOptions::default()) } - /// Constructs a new [`CanvasBackend`] with the given size. - pub fn new_with_size(width: u32, height: u32) -> Result { - Self::new_with_options(CanvasBackendOptions { - size: Some((width, height)), - ..Default::default() - }) - } - /// Constructs a new [`CanvasBackend`] with the given options. pub fn new_with_options(mut options: CanvasBackendOptions) -> Result { // Parent element of canvas (uses unless specified) @@ -356,7 +310,7 @@ impl CanvasBackend { ); let changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { - options, + always_clip_cells: options.always_clip_cells, buffer, changed_cells, canvas, @@ -371,18 +325,13 @@ impl CanvasBackend { } fn initialize(&mut self) -> Result<(), Error> { - let canvas_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); // TODO: Find a way to not use a Javascript array - let (width, height) = (canvas_size.get_index(0), canvas_size.get_index(1)); - self.canvas.init_ctx(); - - let new_buffer_size = size_to_buffer_size( - Size { - width: width as u16, - height: height as u16, - }, - self.canvas.font_metrics(), - ); + let new_buffer_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); + + let new_buffer_size = Size { + width: new_buffer_size.get_index(0), + height: new_buffer_size.get_index(1), + }; if self.buffer_size() != new_buffer_size { for line in &mut self.buffer { line.resize_with(new_buffer_size.width as usize, || Cell::default()); @@ -444,14 +393,7 @@ impl CanvasBackend { fn update_grid(&mut self, force_redraw: bool) -> Result<(), Error> { if force_redraw { self.initialize()?; - self.canvas.buffer.clear_rect(); - self.changed_cells.set_elements(usize::MAX); } - let left_margin = (self.canvas.cell_width / 2.0).floor(); - let top_margin = (self.canvas.cell_height / 2.0).floor(); - self.canvas - .buffer - .translate(left_margin as _, top_margin as _); self.draw_background()?; self.draw_symbols()?; @@ -460,9 +402,6 @@ impl CanvasBackend { self.draw_debug()?; } - self.canvas - .buffer - .translate_neg(left_margin as _, top_margin as _); self.canvas.buffer.flush(); self.changed_cells.set_elements(0x00); Ok(()) @@ -500,7 +439,7 @@ impl CanvasBackend { // We need to reset the canvas context state in two scenarios: // 1. When we need to create a clipping path (for potentially problematic glyphs) // 2. When the text color changes - if self.options.always_clip_cells || !cell.symbol().is_ascii() { + if self.always_clip_cells || !cell.symbol().is_ascii() { self.canvas.buffer.restore(); self.canvas.buffer.save(); @@ -719,11 +658,7 @@ impl Backend for CanvasBackend { } fn size(&self) -> IoResult { - let size = self.buffer_size(); - Ok(Size { - width: size.width.saturating_sub(1), - height: size.height.saturating_sub(1), - }) + Ok(self.buffer_size()) } fn window_size(&mut self) -> IoResult { diff --git a/src/backend/canvas_import.js b/src/backend/canvas_import.js index e19f3a6c..7e7dbd7f 100644 --- a/src/backend/canvas_import.js +++ b/src/backend/canvas_import.js @@ -2,7 +2,20 @@ export class RatzillaCanvas { constructor() {} measure_text(text) { - return this.ctx.measureText(text); + let metrics = this.ctx.measureText(text); + this.cellWidth = Math.floor(metrics.width); + this.cellHeight = Math.floor(Math.abs(metrics.fontBoundingBoxAscent) + Math.abs(metrics.fontBoundingBoxDescent)); + this.cellAscent = Math.floor(metrics.fontBoundingBoxAscent); + return new Float64Array([this.cellWidth, this.cellHeight, this.cellAscent]); + } + + init_ctx() { + this.ctx = this.canvas.getContext("2d", { + alpha: true, + desynchronized: true + }); + this.ctx.font = this.font_str; + this.ctx.textBaseline = "top"; } get_canvas() { @@ -10,8 +23,32 @@ export class RatzillaCanvas { } reinit_canvas() { - this.canvas.width = this.parent.clientWidth; - this.canvas.height = this.parent.clientHeight; - return new Uint16Array([this.canvas.width, this.canvas.height]); + let sourceW = Math.floor(this.parent.clientWidth / this.cellWidth); + let sourceH = Math.floor(this.parent.clientHeight / this.cellHeight); + + let canvasW = sourceW * this.cellWidth; + let canvasH = sourceH * this.cellHeight; + + if (this.canvas.width != canvasW || this.canvas.height != canvasH) { + let dummyCanvas = new OffscreenCanvas(this.canvas.width, this.canvas.height); + let dummyCtx = dummyCanvas.getContext('2d', { + alpha: true + }); + + dummyCtx.drawImage(this.canvas, 0, 0); + + this.canvas.width = canvasW; + this.canvas.height = canvasH; + this.init_ctx(); + + this.ctx.drawImage(dummyCanvas, + 0, 0, + canvasW, canvasH, + 0, 0, + canvasW, canvasH, + ); + } + + return new Uint16Array([sourceW, sourceH]); } } From 79f110638d853712dbd02bb49e311766adcdcfda Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sun, 20 Jul 2025 12:54:24 -0600 Subject: [PATCH 16/35] Fix resizing commit placement --- src/backend/canvas.rs | 71 ++++++++----------- .../{canvas_import.js => ratzilla_canvas.js} | 11 +++ 2 files changed, 41 insertions(+), 41 deletions(-) rename src/backend/{canvas_import.js => ratzilla_canvas.js} (82%) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index e10ffc4c..30cc89c4 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -74,6 +74,13 @@ extern "C" { /// and parent element. pub type RatzillaCanvas; + #[wasm_bindgen(method)] + /// Does the initial construction of the RatzillaCanvas class + /// + /// `sledgehammer_bindgen` only lets you have an empty constructor, + /// so we must initialize the class after construction + fn create_canvas_in_element(this: &RatzillaCanvas, parent: &str, font_str: &str); + #[wasm_bindgen(method)] /// Returns the cell width, cell height, and cell ascent in that order fn measure_text(this: &RatzillaCanvas, text: &str) -> Float64Array; @@ -101,7 +108,15 @@ mod js { /// canvas context struct Buffer; - const BASE: &str = r#"src/backend/canvas_import.js"#; + const BASE: &str = r#"src/backend/ratzilla_canvas.js"#; + + fn clear_rect() { + r#" + this.ctx.clearRect( + 0, 0, this.canvas.width, this.canvas.height + ); + "# + } fn save() { r#" @@ -162,26 +177,6 @@ mod js { this.ctx.strokeStyle = $style$; "# } - - fn create_canvas_in_element(parent: &str) { - r#" - this.parent = document.getElementById($parent$); - if (this.parent == null) { - this.parent = document.body; - } - this.canvas = document.createElement("canvas"); - this.canvas.width = this.parent.clientWidth; - this.canvas.height = this.parent.clientHeight; - this.parent.appendChild(this.canvas); - "# - } - - fn init_ctx(font_str: &str) { - r#" - this.font_str = $font_str$; - super.init_ctx(); - "# - } } /// Canvas renderer. @@ -196,8 +191,6 @@ struct Canvas { inner: web_sys::HtmlCanvasElement, /// Background color. background_color: Color, - /// An optional string which sets a custom font for the canvas - font_str: Option, /// Width of a single cell. /// /// This will be used for multiplying the cell's x position to get the actual pixel @@ -231,23 +224,22 @@ impl Canvas { .set_onresize(Some(closure.as_ref().unchecked_ref())); closure.forget(); - let mut buffer = Buffer::default(); - buffer.create_canvas_in_element(parent_element); + let buffer = Buffer::default(); + buffer.ratzilla_canvas().create_canvas_in_element( + parent_element, + font_str.as_deref().unwrap_or("16px monospace"), + ); let mut canvas = Self { inner: buffer.ratzilla_canvas().get_canvas(), buffer, initialized, background_color, - font_str, cell_width: 0.0, cell_height: 0.0, cell_ascent: 0.0, }; - canvas.init_ctx(); - canvas.buffer.flush(); - let font_measurement = canvas.buffer.ratzilla_canvas().measure_text("█"); canvas.cell_width = font_measurement.get_index(0); canvas.cell_height = font_measurement.get_index(1); @@ -262,11 +254,6 @@ impl Canvas { height: self.cell_height as u16, } } - - fn init_ctx(&mut self) { - self.buffer - .init_ctx(self.font_str.as_deref().unwrap_or("16px monospace")); - } } /// Canvas backend. @@ -390,11 +377,8 @@ impl CanvasBackend { // NOTE: The draw_* functions each traverse the buffer once, instead of // traversing it once per cell; this is done to reduce the number of // WASM calls per cell. - fn update_grid(&mut self, force_redraw: bool) -> Result<(), Error> { - if force_redraw { - self.initialize()?; - } - + fn update_grid(&mut self) -> Result<(), Error> { + // self.canvas.buffer.clear_rect(); self.draw_background()?; self.draw_symbols()?; self.draw_cursor()?; @@ -615,9 +599,14 @@ impl Backend for CanvasBackend { /// This function is called after the [`CanvasBackend::draw`] function to /// actually render the content to the screen. fn flush(&mut self) -> IoResult<()> { + if self.changed_cells.any() { + self.update_grid()?; + } + let initialized = self.canvas.initialized.swap(true, Ordering::Relaxed); - if self.changed_cells.any() || !initialized { - self.update_grid(!initialized)?; + + if !initialized { + self.initialize()?; } Ok(()) diff --git a/src/backend/canvas_import.js b/src/backend/ratzilla_canvas.js similarity index 82% rename from src/backend/canvas_import.js rename to src/backend/ratzilla_canvas.js index 7e7dbd7f..fc653453 100644 --- a/src/backend/canvas_import.js +++ b/src/backend/ratzilla_canvas.js @@ -1,6 +1,17 @@ export class RatzillaCanvas { constructor() {} + create_canvas_in_element(parent, font_str) { + this.parent = document.getElementById(parent); + if (this.parent == null) { + this.parent = document.body; + } + this.canvas = document.createElement("canvas"); + this.parent.appendChild(this.canvas); + this.font_str = font_str; + this.init_ctx(); + } + measure_text(text) { let metrics = this.ctx.measureText(text); this.cellWidth = Math.floor(metrics.width); From 3a27bbf042958ee9b9b2f87908a2149fe2bee1e0 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sun, 20 Jul 2025 15:32:03 -0600 Subject: [PATCH 17/35] Remove buffers --- src/backend/canvas.rs | 402 +++++++++++++-------------------- src/backend/ratzilla_canvas.js | 4 +- 2 files changed, 156 insertions(+), 250 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 30cc89c4..8fb6d796 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -1,4 +1,3 @@ -use bitvec::{bitvec, prelude::BitVec}; use ratatui::layout::Rect; use sledgehammer_bindgen::bindgen; use std::{ @@ -144,19 +143,19 @@ mod js { fn rect(x: u16, y: u16, w: u16, h: u16) { r#" - this.ctx.rect($x$, $y$, $w$, $h$) + this.ctx.rect($x$, $y$, $w$, $h$); "# } fn fill_rect(x: u16, y: u16, w: u16, h: u16) { r#" - this.ctx.fillRect($x$, $y$, $w$, $h$) + this.ctx.fillRect($x$, $y$, $w$, $h$); "# } fn stroke_rect(x: u16, y: u16, w: u16, h: u16) { r#" - this.ctx.strokeRect($x$, $y$, $w$, $h$) + this.ctx.strokeRect($x$, $y$, $w$, $h$); "# } @@ -247,13 +246,6 @@ impl Canvas { Ok(canvas) } - - fn font_metrics(&self) -> Size { - Size { - width: self.cell_width as u16, - height: self.cell_height as u16, - } - } } /// Canvas backend. @@ -265,10 +257,8 @@ pub struct CanvasBackend { /// this option may cause some performance issues when dealing with large /// numbers of simultaneous changes. always_clip_cells: bool, - /// Current buffer. - buffer: Vec>, - /// Changed buffer cells - changed_cells: BitVec, + /// The size of the current screen in cells + buffer_size: Size, /// Canvas. canvas: Canvas, /// Cursor position. @@ -291,44 +281,24 @@ impl CanvasBackend { let parent = options.grid_id.as_deref().unwrap_or_default(); let canvas = Canvas::new(parent, Color::Black, options.font_str.take())?; - let buffer = get_sized_buffer_from_canvas( - &canvas.buffer.ratzilla_canvas().get_canvas(), - canvas.font_metrics(), - ); - let changed_cells = bitvec![1; buffer.len() * buffer[0].len()]; Ok(Self { always_clip_cells: options.always_clip_cells, - buffer, - changed_cells, canvas, + buffer_size: Size::ZERO, cursor_position: None, cursor_shape: CursorShape::SteadyBlock, debug_mode: None, }) } - fn buffer_size(&self) -> Size { - Size::new(self.buffer[0].len() as u16, self.buffer.len() as u16) - } - fn initialize(&mut self) -> Result<(), Error> { // TODO: Find a way to not use a Javascript array let new_buffer_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); - let new_buffer_size = Size { + self.buffer_size = Size { width: new_buffer_size.get_index(0), height: new_buffer_size.get_index(1), }; - if self.buffer_size() != new_buffer_size { - for line in &mut self.buffer { - line.resize_with(new_buffer_size.width as usize, || Cell::default()); - } - self.buffer - .resize_with(new_buffer_size.height as usize, || { - vec![Cell::default(); new_buffer_size.width as usize] - }); - self.changed_cells = bitvec![usize::MAX; self.buffer.len() * self.buffer[0].len()]; - } Ok(()) } @@ -369,112 +339,49 @@ impl CanvasBackend { self.debug_mode = color.map(Into::into); } - // Compare the current buffer to the previous buffer and updates the canvas - // accordingly. - // - // If `force_redraw` is `true`, the entire canvas will be cleared and redrawn. - // - // NOTE: The draw_* functions each traverse the buffer once, instead of - // traversing it once per cell; this is done to reduce the number of - // WASM calls per cell. - fn update_grid(&mut self) -> Result<(), Error> { - // self.canvas.buffer.clear_rect(); - self.draw_background()?; - self.draw_symbols()?; - self.draw_cursor()?; - if self.debug_mode.is_some() { - self.draw_debug()?; - } - - self.canvas.buffer.flush(); - self.changed_cells.set_elements(0x00); - Ok(()) - } - - /// Draws the text symbols on the canvas. - /// - /// This method renders the textual content of each cell in the buffer, optimizing canvas operations - /// by minimizing state changes across the WebAssembly boundary. - /// - /// # Optimization Strategy - /// - /// Rather than saving/restoring the canvas context for every cell (which would be expensive), - /// this implementation: - /// - /// 1. Only processes cells that have changed since the last render. - /// 2. Tracks the last foreground color used to avoid unnecessary style changes - /// 3. Only creates clipping paths for potentially problematic glyphs (non-ASCII) - /// or when `always_clip_cells` is enabled. - fn draw_symbols(&mut self) -> Result<(), Error> { - let changed_cells = &self.changed_cells; - let mut index = 0; - + /// Draws cell boundaries for debugging. + fn draw_debug(&mut self) -> Result<(), Error> { self.canvas.buffer.save(); - let mut last_color = None; - for (y, line) in self.buffer.iter().enumerate() { - for (x, cell) in line.iter().enumerate() { - // Skip empty cells - if !changed_cells.get(index).map(|c| *c).unwrap_or(true) || cell.symbol() == " " { - index += 1; - continue; - } - let color = actual_fg_color(cell); - - // We need to reset the canvas context state in two scenarios: - // 1. When we need to create a clipping path (for potentially problematic glyphs) - // 2. When the text color changes - if self.always_clip_cells || !cell.symbol().is_ascii() { - self.canvas.buffer.restore(); - self.canvas.buffer.save(); - - self.canvas.buffer.begin_path(); - self.canvas.buffer.rect( - (x as f64 * self.canvas.cell_width) as _, - (y as f64 * self.canvas.cell_height) as _, - self.canvas.cell_width as _, - self.canvas.cell_height as _, - ); - self.canvas.buffer.clip(); - - last_color = None; // reset last color to avoid clipping - let color = get_canvas_color(color, Color::White); - self.canvas.buffer.set_fill_style_str(&color); - } else if last_color != Some(color) { - self.canvas.buffer.restore(); - self.canvas.buffer.save(); - last_color = Some(color); - - let color = get_canvas_color(color, Color::White); - self.canvas.buffer.set_fill_style_str(&color); - } - - self.canvas.buffer.fill_text( - cell.symbol(), + let color = self.debug_mode.as_ref().unwrap(); + for y in 0..self.buffer_size.height { + for x in 0..self.buffer_size.width { + self.canvas.buffer.set_stroke_style_str(color); + self.canvas.buffer.stroke_rect( (x as f64 * self.canvas.cell_width) as _, - (y as f64 * self.canvas.cell_height + self.canvas.cell_ascent) as _, + (y as f64 * self.canvas.cell_height) as _, + self.canvas.cell_width as _, + self.canvas.cell_height as _, ); - - index += 1; } } + self.canvas.buffer.restore(); Ok(()) } +} + +impl Backend for CanvasBackend { + // Populates the buffer with the given content. + fn draw<'a, I>(&mut self, content: I) -> IoResult<()> + where + I: Iterator, + { + let initialized = self.canvas.initialized.swap(true, Ordering::Relaxed); + + if !initialized { + self.initialize()?; + } - /// Draws the background of the cells. - /// - /// This function uses [`RowColorOptimizer`] to optimize the drawing of the background - /// colors by batching adjacent cells with the same color into a single rectangle. - /// - /// In other words, it accumulates "what to draw" until it finds a different - /// color, and then it draws the accumulated rectangle. - fn draw_background(&mut self) -> Result<(), Error> { - let changed_cells = &self.changed_cells; self.canvas.buffer.save(); - let draw_region = |(rect, color, canvas): (Rect, Color, &mut Canvas)| { + let draw_region = |(rect, color, canvas, cell_buffer): ( + Rect, + Color, + &mut Canvas, + &mut Vec<(u16, u16, &Cell)>, + )| { let color = get_canvas_color(color, canvas.background_color); canvas.buffer.set_fill_style_str(&color); @@ -484,112 +391,107 @@ impl CanvasBackend { (rect.width as f64 * canvas.cell_width) as _, (rect.height as f64 * canvas.cell_height) as _, ); - }; - 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.get(index).map(|c| *c).unwrap_or(true) { - // Only calls `draw_region` if the color is different from the previous one - row_renderer - .process_color((x, y), actual_bg_color(cell)) - .map(|(rect, color)| draw_region((rect, color, &mut self.canvas))); - } else { - // Cell is unchanged so we must flush any held region - // to avoid clearing the foreground (symbol) of the cell - row_renderer - .flush() - .map(|(rect, color)| draw_region((rect, color, &mut self.canvas))); - } - index += 1; - } - // Flush the remaining region after traversing the row - row_renderer - .flush() - .map(|(rect, color)| draw_region((rect, color, &mut self.canvas))); - } + // Draws the text symbols on the canvas. + // + // This method renders the textual content of each cell in the buffer, optimizing canvas operations + // by minimizing state changes across the WebAssembly boundary. + // + // # Optimization Strategy + // + // Rather than saving/restoring the canvas context for every cell (which would be expensive), + // this implementation: + // + // 1. Only processes cells that have changed since the last render. + // 2. Tracks the last foreground color used to avoid unnecessary style changes + // 3. Only creates clipping paths for potentially problematic glyphs (non-ASCII) + // or when `always_clip_cells` is enabled + let mut last_color = None; + canvas.buffer.save(); + for (x, y, cell) in cell_buffer.drain(..) { + let color = actual_fg_color(cell); - self.canvas.buffer.restore(); + // We need to reset the canvas context state in two scenarios: + // 1. When we need to create a clipping path (for potentially problematic glyphs) + // 2. When the text color changes + if self.always_clip_cells || !cell.symbol().is_ascii() { + canvas.buffer.restore(); + canvas.buffer.save(); + + canvas.buffer.begin_path(); + canvas.buffer.rect( + (x as f64 * canvas.cell_width) as _, + (y as f64 * canvas.cell_height) as _, + canvas.cell_width as _, + canvas.cell_height as _, + ); + canvas.buffer.clip(); - Ok(()) - } + last_color = None; // reset last color to avoid clipping + let color = get_canvas_color(color, Color::White); + canvas.buffer.set_fill_style_str(&color); + } else if last_color != Some(color) { + canvas.buffer.restore(); + canvas.buffer.save(); - /// Draws the cursor on the canvas. - 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]; + last_color = Some(color); - if cell.modifier.contains(Modifier::UNDERLINED) { - self.canvas.buffer.save(); + let color = get_canvas_color(color, Color::White); + canvas.buffer.set_fill_style_str(&color); + } - self.canvas.buffer.fill_text( - "_", - (pos.x as f64 * self.canvas.cell_width) as _, - (pos.y as f64 * self.canvas.cell_height + self.canvas.cell_ascent) as _, + canvas.buffer.fill_text( + cell.symbol(), + (x as f64 * canvas.cell_width) as _, + (y as f64 * canvas.cell_height + canvas.cell_ascent) as _, ); - - self.canvas.buffer.restore(); + if cell.modifier.contains(Modifier::UNDERLINED) { + canvas.buffer.fill_text( + "_", + (x as f64 * canvas.cell_width) as _, + (y as f64 * canvas.cell_height + canvas.cell_ascent) as _, + ); + } + if cell.modifier.contains(Modifier::CROSSED_OUT) { + canvas.buffer.fill_text( + "—", + (x as f64 * canvas.cell_width) as _, + (y as f64 * canvas.cell_height + canvas.cell_ascent) as _, + ); + } } - } - - Ok(()) - } + canvas.buffer.restore(); + }; - /// Draws cell boundaries for debugging. - fn draw_debug(&mut self) -> Result<(), Error> { - self.canvas.buffer.save(); + let mut row_renderer = RowColorOptimizer::new(); + let mut cell_buffer = Vec::new(); + for (x, y, cell) in content { + // Draws the background of the cells. + // + // This function uses [`RowColorOptimizer`] to optimize the drawing of the background + // colors by batching adjacent cells with the same color into a single rectangle. + // + // In other words, it accumulates "what to draw" until it finds a different + // color, and then it draws the accumulated rectangle. + // + // Only calls `draw_region` if the color is different from the + // previous one, or if we have advanced past the last y position, + // or if we have advanced more than one x position + row_renderer + .process_color((x, y), actual_bg_color(cell)) + .map(|(rect, color)| { + draw_region((rect, color, &mut self.canvas, &mut cell_buffer)) + }); - let color = self.debug_mode.as_ref().unwrap(); - for (y, line) in self.buffer.iter().enumerate() { - for (x, _) in line.iter().enumerate() { - self.canvas.buffer.set_stroke_style_str(color); - self.canvas.buffer.stroke_rect( - (x as f64 * self.canvas.cell_width) as _, - (y as f64 * self.canvas.cell_height) as _, - self.canvas.cell_width as _, - self.canvas.cell_height as _, - ); - } + cell_buffer.push((x, y, cell)); } - self.canvas.buffer.restore(); - - Ok(()) - } -} + // Flush the remaining region after traversing the changed cells + row_renderer + .flush() + .map(|(rect, color)| draw_region((rect, color, &mut self.canvas, &mut cell_buffer))); -impl Backend for CanvasBackend { - // Populates the buffer with the given content. - fn draw<'a, I>(&mut self, content: I) -> IoResult<()> - where - I: Iterator, - { - for (x, y, cell) in content { - let y = y as usize; - let x = x as usize; - if let Some(line) = self.buffer.get_mut(y) { - line.get_mut(x).map(|c| *c = cell.clone()); - if let Some(mut cell) = self.changed_cells.get_mut((y * line.len()) + x) { - cell.set(true); - } - } - } - - // Draw the cursor if set - if let Some(pos) = self.cursor_position { - let y = pos.y as usize; - let x = pos.x as usize; - if let Some(line) = self.buffer.get_mut(y) { - if x < line.len() { - let cursor_style = self.cursor_shape.show(line[x].style()); - line.get_mut(x).map(|c| c.set_style(cursor_style)); - if let Some(mut cell) = self.changed_cells.get_mut((y * line.len()) + x) { - cell.set(true); - } - } - } - } + self.canvas.buffer.restore(); Ok(()) } @@ -599,15 +501,11 @@ impl Backend for CanvasBackend { /// This function is called after the [`CanvasBackend::draw`] function to /// actually render the content to the screen. fn flush(&mut self) -> IoResult<()> { - if self.changed_cells.any() { - self.update_grid()?; + if self.debug_mode.is_some() { + self.draw_debug()?; } - let initialized = self.canvas.initialized.swap(true, Ordering::Relaxed); - - if !initialized { - self.initialize()?; - } + self.canvas.buffer.flush(); Ok(()) } @@ -616,11 +514,11 @@ impl Backend for CanvasBackend { 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); - } + // 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(()) @@ -639,15 +537,21 @@ impl Backend for CanvasBackend { } fn clear(&mut self) -> IoResult<()> { - self.buffer - .iter_mut() - .flatten() - .for_each(|c| *c = Cell::default()); + self.canvas.buffer.clear_rect(); Ok(()) } fn size(&self) -> IoResult { - Ok(self.buffer_size()) + if self.canvas.initialized.load(Ordering::Relaxed) { + Ok(self.buffer_size) + } else { + let new_buffer_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); + let new_buffer_size = Size { + width: new_buffer_size.get_index(0), + height: new_buffer_size.get_index(1), + }; + Ok(new_buffer_size) + } } fn window_size(&mut self) -> IoResult { @@ -666,11 +570,11 @@ impl Backend for CanvasBackend { 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); - } + // 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(()) @@ -702,22 +606,22 @@ impl RowColorOptimizer { } /// Processes a cell with the given position and color. - fn process_color(&mut self, pos: (usize, usize), color: Color) -> Option<(Rect, Color)> { + fn process_color(&mut self, pos: (u16, u16), color: Color) -> Option<(Rect, Color)> { if let Some((active_rect, active_color)) = self.pending_region.as_mut() { - if active_color == &color { + if active_color == &color && pos.0 == active_rect.right() && pos.1 == active_rect.y { // Same color: extend the rectangle active_rect.width += 1; } else { // Different color: flush the previous region and start a new one let region = *active_rect; let region_color = *active_color; - *active_rect = Rect::new(pos.0 as _, pos.1 as _, 1, 1); + *active_rect = Rect::new(pos.0, pos.1, 1, 1); *active_color = color; return Some((region, region_color)); } } else { // First color: create a new rectangle - let rect = Rect::new(pos.0 as _, pos.1 as _, 1, 1); + let rect = Rect::new(pos.0, pos.1, 1, 1); self.pending_region = Some((rect, color)); } diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index fc653453..f99f8c83 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -1,5 +1,7 @@ export class RatzillaCanvas { - constructor() {} + constructor() { + this.logging = true; + } create_canvas_in_element(parent, font_str) { this.parent = document.getElementById(parent); From b4af1c2638223c7f119b8c063d11defea1c71ac2 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sun, 20 Jul 2025 19:28:07 -0600 Subject: [PATCH 18/35] Add support for underlines, cross outs, and bolds --- Cargo.toml | 1 - examples/demo/src/lib.rs | 6 +- src/backend/canvas.rs | 130 +++++++++++++++++++++++++++------ src/backend/ratzilla_canvas.js | 28 ++++--- 4 files changed, 126 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4c2a32b..b64990a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ web-sys = { version = "0.3.77", features = [ 'Node', 'Performance', 'Screen', - 'TextMetrics', 'WebGl2RenderingContext', 'WebGlBuffer', 'WebGlProgram', diff --git a/examples/demo/src/lib.rs b/examples/demo/src/lib.rs index f8dc82f7..3dcda130 100644 --- a/examples/demo/src/lib.rs +++ b/examples/demo/src/lib.rs @@ -12,7 +12,7 @@ use app::App; use clap::Parser; use ratzilla::backend::canvas::{CanvasBackend, CanvasBackendOptions}; use ratzilla::event::KeyCode; -use ratzilla::ratatui::style::Color; +use ratzilla::ratatui::style::{Color, Modifier}; use ratzilla::ratatui::Terminal; use ratzilla::WebRenderer; use wasm_bindgen::prelude::*; @@ -41,7 +41,9 @@ pub fn main() { let app_state = Rc::new(RefCell::new(App::new("Demo", false))); // let mut backend = CanvasBackend::new_with_size(1600, 900).unwrap(); let mut backend = CanvasBackend::new_with_options( - CanvasBackendOptions::new().font(String::from("16px Fira Code")), + CanvasBackendOptions::new() + .font(String::from("16px Fira Code")) + .disable_modifiers(Modifier::ITALIC), ) .unwrap(); // backend.set_debug_mode(Some("red")); diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 8fb6d796..82debb6f 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -33,7 +33,7 @@ use web_sys::{ }; /// Options for the [`CanvasBackend`]. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct CanvasBackendOptions { /// The element ID. grid_id: Option, @@ -42,10 +42,29 @@ pub struct CanvasBackendOptions { /// this option may cause some performance issues when dealing with large /// numbers of simultaneous changes. always_clip_cells: bool, + /// Modifiers which may be used in rendering. Allows for the disabling + /// of things like italics, which my not be available in some fonts + /// like Fira Code + enabled_modifiers: Modifier, /// An optional string which sets a custom font for the canvas font_str: Option, } +impl Default for CanvasBackendOptions { + fn default() -> Self { + Self { + grid_id: None, + always_clip_cells: false, + enabled_modifiers: Modifier::BOLD + | Modifier::ITALIC + | Modifier::UNDERLINED + | Modifier::REVERSED + | Modifier::CROSSED_OUT, + font_str: None, + } + } +} + impl CanvasBackendOptions { /// Constructs a new [`CanvasBackendOptions`]. pub fn new() -> Self { @@ -63,6 +82,21 @@ impl CanvasBackendOptions { self.font_str = Some(font); self } + + /// Enable modifiers for rendering, all modifiers that are supported + /// are enabled by default + pub fn enable_modifiers(mut self, modifiers: Modifier) -> Self { + self.enabled_modifiers |= modifiers; + self + } + + /// Disable modifiers in rendering, allows for things like + /// italics to be disabled if your chosen font doesn't support + /// them + pub fn disable_modifiers(mut self, modifiers: Modifier) -> Self { + self.enabled_modifiers ^= modifiers; + self + } } // Mirrors usage in https://github.com/DioxusLabs/dioxus/blob/main/packages/interpreter/src/unified_bindings.rs @@ -81,8 +115,8 @@ extern "C" { fn create_canvas_in_element(this: &RatzillaCanvas, parent: &str, font_str: &str); #[wasm_bindgen(method)] - /// Returns the cell width, cell height, and cell ascent in that order - fn measure_text(this: &RatzillaCanvas, text: &str) -> Float64Array; + /// Returns the cell width, cell height, and cell baseline in that order + fn measure_text(this: &RatzillaCanvas) -> Float64Array; #[wasm_bindgen(method)] fn get_canvas(this: &RatzillaCanvas) -> web_sys::HtmlCanvasElement; @@ -141,6 +175,24 @@ mod js { "# } + fn bold() { + r#" + this.ctx.font = "bold " + this.ctx.font; + "# + } + + fn italic() { + r#" + this.ctx.font = "italic " + this.ctx.font; + "# + } + + fn reset_font() { + r#" + this.ctx.font = this.font_str; + "# + } + fn rect(x: u16, y: u16, w: u16, h: u16) { r#" this.ctx.rect($x$, $y$, $w$, $h$); @@ -184,6 +236,10 @@ struct Canvas { buffer: Buffer, /// Whether the canvas has been initialized. initialized: Rc, + /// Modifiers which may be used in rendering. Allows for the disabling + /// of things like italics, which my not be available in some fonts + /// like Fira Code + enabled_modifiers: Modifier, /// The inner HTML canvas element /// /// Use **only** for implementing `WebBackend` @@ -200,8 +256,10 @@ struct Canvas { /// This will be used for multiplying the cell's y position to get the actual pixel /// position on the canvas. cell_height: f64, - /// The font ascent of the `|` character as measured by the canvas - cell_ascent: f64, + /// The font descent as measured by the canvas + cell_baseline: f64, + /// The font descent as measured by the canvas + underline_pos: f64, } impl Canvas { @@ -210,6 +268,7 @@ impl Canvas { parent_element: &str, background_color: Color, font_str: Option, + enabled_modifiers: Modifier, ) -> Result { let initialized: Rc = Rc::new(false.into()); let closure = Closure::::new({ @@ -233,16 +292,19 @@ impl Canvas { inner: buffer.ratzilla_canvas().get_canvas(), buffer, initialized, + enabled_modifiers, background_color, cell_width: 0.0, cell_height: 0.0, - cell_ascent: 0.0, + cell_baseline: 0.0, + underline_pos: 0.0, }; - let font_measurement = canvas.buffer.ratzilla_canvas().measure_text("█"); + let font_measurement = canvas.buffer.ratzilla_canvas().measure_text(); canvas.cell_width = font_measurement.get_index(0); canvas.cell_height = font_measurement.get_index(1); - canvas.cell_ascent = font_measurement.get_index(2); + canvas.cell_baseline = font_measurement.get_index(2); + canvas.underline_pos = font_measurement.get_index(3); Ok(canvas) } @@ -280,7 +342,12 @@ impl CanvasBackend { // Parent element of canvas (uses unless specified) let parent = options.grid_id.as_deref().unwrap_or_default(); - let canvas = Canvas::new(parent, Color::Black, options.font_str.take())?; + let canvas = Canvas::new( + parent, + Color::Black, + options.font_str.take(), + options.enabled_modifiers, + )?; Ok(Self { always_clip_cells: options.always_clip_cells, canvas, @@ -440,25 +507,40 @@ impl Backend for CanvasBackend { canvas.buffer.set_fill_style_str(&color); } + for modifier in cell.modifier & canvas.enabled_modifiers { + match modifier { + Modifier::BOLD => canvas.buffer.bold(), + Modifier::ITALIC => canvas.buffer.italic(), + Modifier::UNDERLINED => { + canvas.buffer.fill_rect( + (x as f64 * canvas.cell_width) as _, + (y as f64 * canvas.cell_height + canvas.underline_pos) as _, + canvas.cell_width as _, + 1, + ); + } + Modifier::CROSSED_OUT => { + canvas.buffer.fill_text( + "─", + (x as f64 * canvas.cell_width) as _, + (y as f64 * canvas.cell_height + canvas.cell_height + - canvas.cell_baseline) as _, + ); + } + _ => {} + } + } + + // Very useful symbol positioning formulas from here + // https://github.com/ghostty-org/ghostty/blob/a88689ca754a6eb7dce6015b85ccb1416b5363d8/src/Surface.zig#L1589C5-L1589C10 canvas.buffer.fill_text( cell.symbol(), (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height + canvas.cell_ascent) as _, + (y as f64 * canvas.cell_height + canvas.cell_height - canvas.cell_baseline) + as _, ); - if cell.modifier.contains(Modifier::UNDERLINED) { - canvas.buffer.fill_text( - "_", - (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height + canvas.cell_ascent) as _, - ); - } - if cell.modifier.contains(Modifier::CROSSED_OUT) { - canvas.buffer.fill_text( - "—", - (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height + canvas.cell_ascent) as _, - ); - } + + canvas.buffer.reset_font(); } canvas.buffer.restore(); }; diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index f99f8c83..b4e30239 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -1,7 +1,5 @@ export class RatzillaCanvas { - constructor() { - this.logging = true; - } + constructor() {} create_canvas_in_element(parent, font_str) { this.parent = document.getElementById(parent); @@ -14,12 +12,19 @@ export class RatzillaCanvas { this.init_ctx(); } - measure_text(text) { - let metrics = this.ctx.measureText(text); - this.cellWidth = Math.floor(metrics.width); - this.cellHeight = Math.floor(Math.abs(metrics.fontBoundingBoxAscent) + Math.abs(metrics.fontBoundingBoxDescent)); - this.cellAscent = Math.floor(metrics.fontBoundingBoxAscent); - return new Float64Array([this.cellWidth, this.cellHeight, this.cellAscent]); + // Very useful code from here https://github.com/ghostty-org/ghostty/blob/a88689ca754a6eb7dce6015b85ccb1416b5363d8/src/font/face/web_canvas.zig#L242 + measure_text() { + // A character with max width, max height, and max bottom + let metrics = this.ctx.measureText("█"); + if (metrics.actualBoundingBoxRight > 0) { + this.cellWidth = Math.floor(metrics.actualBoundingBoxRight); + } else { + this.cellWidth = Math.floor(metrics.width); + } + this.cellHeight = Math.floor(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); + this.cellBaseline = Math.floor(metrics.actualBoundingBoxDescent); + this.underlinePos = Math.floor(this.cellHeight - 1.0); + return new Float64Array([this.cellWidth, this.cellHeight, this.cellBaseline, this.underlinePos]); } init_ctx() { @@ -28,7 +33,6 @@ export class RatzillaCanvas { desynchronized: true }); this.ctx.font = this.font_str; - this.ctx.textBaseline = "top"; } get_canvas() { @@ -36,8 +40,8 @@ export class RatzillaCanvas { } reinit_canvas() { - let sourceW = Math.floor(this.parent.clientWidth / this.cellWidth); - let sourceH = Math.floor(this.parent.clientHeight / this.cellHeight); + let sourceW = Math.ceil(this.parent.clientWidth / this.cellWidth); + let sourceH = Math.ceil(this.parent.clientHeight / this.cellHeight); let canvasW = sourceW * this.cellWidth; let canvasH = sourceH * this.cellHeight; From 92f82ec3904cc616ee28c91feaba3df623492108 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Sun, 20 Jul 2025 22:23:54 -0600 Subject: [PATCH 19/35] Re-add support for cursor --- examples/demo/src/lib.rs | 1 + src/backend/canvas.rs | 207 +++++++++++++++++++-------------- src/backend/color.rs | 42 +++++-- src/backend/cursor.rs | 14 +-- src/backend/dom.rs | 12 +- src/backend/ratzilla_canvas.js | 2 +- src/backend/utils.rs | 24 +--- src/backend/webgl2.rs | 12 +- 8 files changed, 177 insertions(+), 137 deletions(-) diff --git a/examples/demo/src/lib.rs b/examples/demo/src/lib.rs index 3dcda130..a43614d1 100644 --- a/examples/demo/src/lib.rs +++ b/examples/demo/src/lib.rs @@ -43,6 +43,7 @@ pub fn main() { let mut backend = CanvasBackend::new_with_options( CanvasBackendOptions::new() .font(String::from("16px Fira Code")) + // Fira Code does not have an italic variation .disable_modifiers(Modifier::ITALIC), ) .unwrap(); diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 82debb6f..068c39a0 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -23,7 +23,7 @@ use ratatui::{ style::{Color, Modifier}, }; use web_sys::{ - js_sys::{Float64Array, Uint16Array}, + js_sys::Uint16Array, wasm_bindgen::{ self, prelude::{wasm_bindgen, Closure}, @@ -116,7 +116,7 @@ extern "C" { #[wasm_bindgen(method)] /// Returns the cell width, cell height, and cell baseline in that order - fn measure_text(this: &RatzillaCanvas) -> Float64Array; + fn measure_text(this: &RatzillaCanvas) -> Uint16Array; #[wasm_bindgen(method)] fn get_canvas(this: &RatzillaCanvas) -> web_sys::HtmlCanvasElement; @@ -187,12 +187,6 @@ mod js { "# } - fn reset_font() { - r#" - this.ctx.font = this.font_str; - "# - } - fn rect(x: u16, y: u16, w: u16, h: u16) { r#" this.ctx.rect($x$, $y$, $w$, $h$); @@ -250,16 +244,16 @@ struct Canvas { /// /// This will be used for multiplying the cell's x position to get the actual pixel /// position on the canvas. - cell_width: f64, + cell_width: u16, /// Height of a single cell. /// /// This will be used for multiplying the cell's y position to get the actual pixel /// position on the canvas. - cell_height: f64, + cell_height: u16, /// The font descent as measured by the canvas - cell_baseline: f64, + cell_baseline: u16, /// The font descent as measured by the canvas - underline_pos: f64, + underline_pos: u16, } impl Canvas { @@ -294,10 +288,10 @@ impl Canvas { initialized, enabled_modifiers, background_color, - cell_width: 0.0, - cell_height: 0.0, - cell_baseline: 0.0, - underline_pos: 0.0, + cell_width: 0, + cell_height: 0, + cell_baseline: 0, + underline_pos: 0, }; let font_measurement = canvas.buffer.ratzilla_canvas().measure_text(); @@ -320,7 +314,7 @@ pub struct CanvasBackend { /// numbers of simultaneous changes. always_clip_cells: bool, /// The size of the current screen in cells - buffer_size: Size, + buffer: Vec>, /// Canvas. canvas: Canvas, /// Cursor position. @@ -351,22 +345,41 @@ impl CanvasBackend { Ok(Self { always_clip_cells: options.always_clip_cells, canvas, - buffer_size: Size::ZERO, + buffer: Vec::new(), cursor_position: None, cursor_shape: CursorShape::SteadyBlock, debug_mode: None, }) } + fn buffer_size(&self) -> Size { + Size { + width: self.buffer.get(0).map(|b| b.len()).unwrap_or(0) as u16, + height: self.buffer.len() as u16, + } + } + fn initialize(&mut self) -> Result<(), Error> { // TODO: Find a way to not use a Javascript array let new_buffer_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); - self.buffer_size = Size { + let new_buffer_size = Size { width: new_buffer_size.get_index(0), height: new_buffer_size.get_index(1), }; + if self.buffer_size() != new_buffer_size { + let new_buffer_width = new_buffer_size.width as usize; + let new_buffer_height = new_buffer_size.height as usize; + + for line in &mut self.buffer { + line.resize_with(new_buffer_width, || Cell::default()); + } + self.buffer.resize_with(new_buffer_height, || { + vec![Cell::default(); new_buffer_width] + }); + } + Ok(()) } @@ -411,14 +424,15 @@ impl CanvasBackend { self.canvas.buffer.save(); let color = self.debug_mode.as_ref().unwrap(); - for y in 0..self.buffer_size.height { - for x in 0..self.buffer_size.width { + let buffer_size = self.buffer_size(); + for y in 0..buffer_size.height { + for x in 0..buffer_size.width { self.canvas.buffer.set_stroke_style_str(color); self.canvas.buffer.stroke_rect( - (x as f64 * self.canvas.cell_width) as _, - (y as f64 * self.canvas.cell_height) as _, - self.canvas.cell_width as _, - self.canvas.cell_height as _, + x * self.canvas.cell_width, + y * self.canvas.cell_height, + self.canvas.cell_width, + self.canvas.cell_height, ); } } @@ -447,16 +461,16 @@ impl Backend for CanvasBackend { Rect, Color, &mut Canvas, - &mut Vec<(u16, u16, &Cell)>, + &mut Vec<(u16, u16, &Cell, Modifier)>, )| { - let color = get_canvas_color(color, canvas.background_color); + let color = get_canvas_color(color); canvas.buffer.set_fill_style_str(&color); canvas.buffer.fill_rect( - (rect.x as f64 * canvas.cell_width) as _, - (rect.y as f64 * canvas.cell_height) as _, - (rect.width as f64 * canvas.cell_width) as _, - (rect.height as f64 * canvas.cell_height) as _, + rect.x * canvas.cell_width, + rect.y * canvas.cell_height, + rect.width * canvas.cell_width, + rect.height * canvas.cell_height, ); // Draws the text symbols on the canvas. @@ -475,8 +489,8 @@ impl Backend for CanvasBackend { // or when `always_clip_cells` is enabled let mut last_color = None; canvas.buffer.save(); - for (x, y, cell) in cell_buffer.drain(..) { - let color = actual_fg_color(cell); + for (x, y, cell, modifiers) in cell_buffer.drain(..) { + let color = actual_fg_color(cell, modifiers, Color::White, canvas.background_color); // We need to reset the canvas context state in two scenarios: // 1. When we need to create a clipping path (for potentially problematic glyphs) @@ -487,15 +501,15 @@ impl Backend for CanvasBackend { canvas.buffer.begin_path(); canvas.buffer.rect( - (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height) as _, - canvas.cell_width as _, - canvas.cell_height as _, + x * canvas.cell_width, + y * canvas.cell_height, + canvas.cell_width, + canvas.cell_height, ); canvas.buffer.clip(); last_color = None; // reset last color to avoid clipping - let color = get_canvas_color(color, Color::White); + let color = get_canvas_color(color); canvas.buffer.set_fill_style_str(&color); } else if last_color != Some(color) { canvas.buffer.restore(); @@ -503,28 +517,27 @@ impl Backend for CanvasBackend { last_color = Some(color); - let color = get_canvas_color(color, Color::White); + let color = get_canvas_color(color); canvas.buffer.set_fill_style_str(&color); } - for modifier in cell.modifier & canvas.enabled_modifiers { + for modifier in modifiers { match modifier { Modifier::BOLD => canvas.buffer.bold(), Modifier::ITALIC => canvas.buffer.italic(), Modifier::UNDERLINED => { canvas.buffer.fill_rect( - (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height + canvas.underline_pos) as _, - canvas.cell_width as _, + x * canvas.cell_width, + y * canvas.cell_height + canvas.underline_pos, + canvas.cell_width, 1, ); } Modifier::CROSSED_OUT => { canvas.buffer.fill_text( "─", - (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height + canvas.cell_height - - canvas.cell_baseline) as _, + x * canvas.cell_width, + y * canvas.cell_height + canvas.cell_height - canvas.cell_baseline, ); } _ => {} @@ -535,12 +548,9 @@ impl Backend for CanvasBackend { // https://github.com/ghostty-org/ghostty/blob/a88689ca754a6eb7dce6015b85ccb1416b5363d8/src/Surface.zig#L1589C5-L1589C10 canvas.buffer.fill_text( cell.symbol(), - (x as f64 * canvas.cell_width) as _, - (y as f64 * canvas.cell_height + canvas.cell_height - canvas.cell_baseline) - as _, + x * canvas.cell_width, + y * canvas.cell_height + canvas.cell_height - canvas.cell_baseline, ); - - canvas.buffer.reset_font(); } canvas.buffer.restore(); }; @@ -548,6 +558,26 @@ impl Backend for CanvasBackend { let mut row_renderer = RowColorOptimizer::new(); let mut cell_buffer = Vec::new(); for (x, y, cell) in content { + let mut modifiers = cell.modifier; + { + let x = x as usize; + let y = y as usize; + if let Some(line) = self.buffer.get_mut(y) { + line.get_mut(x).map(|c| *c = cell.clone()); + } + } + + if self + .cursor_position + .map(|pos| pos.x == x && pos.y == y) + .unwrap_or_default() + { + let cursor_modifiers = self.cursor_shape.show(modifiers); + modifiers = cursor_modifiers; + } + + modifiers &= self.canvas.enabled_modifiers; + // Draws the background of the cells. // // This function uses [`RowColorOptimizer`] to optimize the drawing of the background @@ -560,12 +590,15 @@ impl Backend for CanvasBackend { // previous one, or if we have advanced past the last y position, // or if we have advanced more than one x position row_renderer - .process_color((x, y), actual_bg_color(cell)) + .process_color( + (x, y), + actual_bg_color(cell, modifiers, Color::White, self.canvas.background_color), + ) .map(|(rect, color)| { draw_region((rect, color, &mut self.canvas, &mut cell_buffer)) }); - cell_buffer.push((x, y, cell)); + cell_buffer.push((x, y, cell, modifiers)); } // Flush the remaining region after traversing the changed cells @@ -593,39 +626,61 @@ impl Backend for CanvasBackend { } fn hide_cursor(&mut self) -> IoResult<()> { - if let Some(pos) = self.cursor_position { - let y = pos.y as usize; + // Redraw the cell under the cursor, but without + // the cursor style + if let Some(pos) = self.cursor_position.take() { 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); - // } + let y = pos.y as usize; + if let Some(line) = self.buffer.get(y) { + if let Some(cell) = line.get(x).cloned() { + self.draw([(pos.x, pos.y, &cell)].into_iter())?; + } + } } - self.cursor_position = None; Ok(()) } fn show_cursor(&mut self) -> IoResult<()> { + // Redraw the new cell under the cursor, but with + // the cursor style + if let Some(pos) = self.cursor_position { + let x = pos.x as usize; + let y = pos.y as usize; + if let Some(line) = self.buffer.get(y) { + if let Some(cell) = line.get(x).cloned() { + self.draw([(pos.x, pos.y, &cell)].into_iter())?; + } + } + } Ok(()) } - fn get_cursor(&mut self) -> IoResult<(u16, u16)> { - Ok((0, 0)) + fn get_cursor_position(&mut self) -> IoResult { + match self.cursor_position { + None => Ok((0, 0).into()), + Some(position) => Ok(position), + } } - fn set_cursor(&mut self, _x: u16, _y: u16) -> IoResult<()> { + fn set_cursor_position>(&mut self, position: P) -> IoResult<()> { + self.hide_cursor()?; + self.cursor_position = Some(position.into()); + self.show_cursor()?; Ok(()) } fn clear(&mut self) -> IoResult<()> { self.canvas.buffer.clear_rect(); + self.buffer + .iter_mut() + .flatten() + .for_each(|c| *c = Cell::default()); Ok(()) } fn size(&self) -> IoResult { if self.canvas.initialized.load(Ordering::Relaxed) { - Ok(self.buffer_size) + Ok(self.buffer_size()) } else { let new_buffer_size = self.canvas.buffer.ratzilla_canvas().reinit_canvas(); let new_buffer_size = Size { @@ -639,28 +694,6 @@ impl Backend for CanvasBackend { fn window_size(&mut self) -> IoResult { unimplemented!() } - - fn get_cursor_position(&mut self) -> IoResult { - match self.cursor_position { - None => Ok((0, 0).into()), - Some(position) => Ok(position), - } - } - - fn set_cursor_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(()) - } } impl WebBackend for CanvasBackend { diff --git a/src/backend/color.rs b/src/backend/color.rs index e7b3ad67..fb84f756 100644 --- a/src/backend/color.rs +++ b/src/backend/color.rs @@ -39,20 +39,46 @@ pub(super) fn ansi_to_rgb(color: Color) -> Option<(u8, u8, u8)> { } /// Returns the actual foreground color of a cell, considering the `REVERSED` modifier. -pub(super) fn actual_fg_color(cell: &Cell) -> Color { - if cell.modifier.contains(Modifier::REVERSED) { - cell.bg +pub(super) fn actual_fg_color( + cell: &Cell, + modifiers: Modifier, + fg_fallback: Color, + bg_fallback: Color, +) -> Color { + if modifiers.contains(Modifier::REVERSED) { + if cell.bg == Color::Reset { + bg_fallback + } else { + cell.bg + } } else { - cell.fg + if cell.fg == Color::Reset { + fg_fallback + } else { + cell.fg + } } } /// Returns the actual background color of a cell, considering the `REVERSED` modifier. -pub(super) fn actual_bg_color(cell: &Cell) -> Color { - if cell.modifier.contains(Modifier::REVERSED) { - cell.fg +pub(super) fn actual_bg_color( + cell: &Cell, + modifiers: Modifier, + fg_fallback: Color, + bg_fallback: Color, +) -> Color { + if modifiers.contains(Modifier::REVERSED) { + if cell.fg == Color::Reset { + fg_fallback + } else { + cell.fg + } } else { - cell.bg + if cell.bg == Color::Reset { + bg_fallback + } else { + cell.bg + } } } diff --git a/src/backend/cursor.rs b/src/backend/cursor.rs index d44f84f2..8419df60 100644 --- a/src/backend/cursor.rs +++ b/src/backend/cursor.rs @@ -1,4 +1,4 @@ -use ratatui::style::{Style, Stylize}; +use ratatui::style::Modifier; /// Supported cursor shapes. #[derive(Debug, Default)] @@ -12,18 +12,18 @@ pub enum CursorShape { impl CursorShape { /// Transforms the given style to hide the cursor. - pub fn hide(&self, style: Style) -> Style { + pub fn hide(&self, style: Modifier) -> Modifier { match self { - CursorShape::SteadyBlock => style.not_reversed(), - CursorShape::SteadyUnderScore => style.not_underlined(), + CursorShape::SteadyBlock => style ^ Modifier::REVERSED, + CursorShape::SteadyUnderScore => style ^ Modifier::UNDERLINED, } } /// Transforms the given style to show the cursor. - pub fn show(&self, style: Style) -> Style { + pub fn show(&self, style: Modifier) -> Modifier { match self { - CursorShape::SteadyBlock => style.reversed(), - CursorShape::SteadyUnderScore => style.underlined(), + CursorShape::SteadyBlock => style | Modifier::REVERSED, + CursorShape::SteadyUnderScore => style | Modifier::UNDERLINED, } } } diff --git a/src/backend/dom.rs b/src/backend/dom.rs index c66235c5..0b2d2599 100644 --- a/src/backend/dom.rs +++ b/src/backend/dom.rs @@ -249,8 +249,8 @@ impl Backend for DomBackend { let x = pos.x as usize; let line = &mut self.buffer[y]; if x < line.len() { - let cursor_style = self.options.cursor_shape().show(line[x].style()); - line[x].set_style(cursor_style); + let cursor_modifier = self.options.cursor_shape().show(line[x].modifier); + line[x].modifier = cursor_modifier; } } @@ -285,8 +285,8 @@ impl Backend for DomBackend { let x = pos.x as usize; let line = &mut self.buffer[y]; if x < line.len() { - let style = self.options.cursor_shape.hide(line[x].style()); - line[x].set_style(style); + let modifier = self.options.cursor_shape.hide(line[x].modifier); + line[x].modifier = modifier; } } self.cursor_position = None; @@ -335,8 +335,8 @@ impl Backend for DomBackend { let x = old_pos.x as usize; let line = &mut self.buffer[y]; if x < line.len() && old_pos != new_pos { - let style = self.options.cursor_shape.hide(line[x].style()); - line[x].set_style(style); + let modifier = self.options.cursor_shape.hide(line[x].modifier); + line[x].modifier = modifier; } } self.cursor_position = Some(new_pos); diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index b4e30239..b10c0064 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -24,7 +24,7 @@ export class RatzillaCanvas { this.cellHeight = Math.floor(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); this.cellBaseline = Math.floor(metrics.actualBoundingBoxDescent); this.underlinePos = Math.floor(this.cellHeight - 1.0); - return new Float64Array([this.cellWidth, this.cellHeight, this.cellBaseline, this.underlinePos]); + return new Uint16Array([this.cellWidth, this.cellHeight, this.cellBaseline, this.underlinePos]); } init_ctx() { diff --git a/src/backend/utils.rs b/src/backend/utils.rs index fc1c1963..e4a97fb8 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -6,7 +6,6 @@ use crate::{ use compact_str::{format_compact, CompactString}; use ratatui::{ buffer::Cell, - layout::Size, style::{Color, Modifier}, }; use web_sys::{ @@ -89,8 +88,8 @@ pub(crate) fn get_cell_style_as_css(cell: &Cell) -> String { } /// Converts a Color to a CSS style. -pub(crate) fn get_canvas_color(color: Color, fallback_color: Color) -> CompactString { - let color = ansi_to_rgb(color).unwrap_or_else(|| ansi_to_rgb(fallback_color).unwrap()); +pub(crate) fn get_canvas_color(color: Color) -> CompactString { + let color = ansi_to_rgb(color).unwrap(); format_compact!("rgb({}, {}, {})", color.0, color.1, color.2) } @@ -127,25 +126,6 @@ pub(crate) fn get_sized_buffer() -> Vec> { vec![vec![Cell::default(); size.width as usize]; size.height as usize] } -pub(crate) fn size_to_buffer_size(size: Size, font_metrics: Size) -> Size { - Size { - width: size.width / font_metrics.width, - height: size.height / font_metrics.height, - } -} - -/// Returns a buffer based on the canvas size. -pub(crate) fn get_sized_buffer_from_canvas( - canvas: &HtmlCanvasElement, - font_metrics: Size, -) -> Vec> { - let width = canvas.client_width() as u16; - let height = canvas.client_height() as u16; - - let size = size_to_buffer_size(Size { width, height }, font_metrics); - vec![vec![Cell::default(); size.width as usize]; size.height as usize] -} - /// Returns the document object from the window. pub(crate) fn get_document() -> Result { get_window()? diff --git a/src/backend/webgl2.rs b/src/backend/webgl2.rs index 98ab7c31..0076e231 100644 --- a/src/backend/webgl2.rs +++ b/src/backend/webgl2.rs @@ -289,8 +289,8 @@ impl WebGl2Backend { let idx = pos.y as usize * w + pos.x as usize; if idx < self.buffer.len() { - let cursor_style = self.cursor_shape.show(self.buffer[idx].style()); - self.buffer[idx].set_style(cursor_style); + let cursor_modifier = self.cursor_shape.show(self.buffer[idx].modifier); + self.buffer[idx].modifier = cursor_modifier; } Ok(()) @@ -367,8 +367,8 @@ impl Backend for WebGl2Backend { let w = self.context.terminal_grid.terminal_size().0 as usize; if let Some(cell) = self.buffer.get_mut(y * w + x) { - let style = self.cursor_shape.hide(cell.style()); - cell.set_style(style); + let modifier = self.cursor_shape.hide(cell.modifier); + cell.modifier = modifier; } } @@ -421,8 +421,8 @@ impl Backend for WebGl2Backend { let old_idx = y * w + x; if let Some(old_cell) = self.buffer.get_mut(old_idx) { - let style = self.cursor_shape.hide(old_cell.style()); - old_cell.set_style(style); + let modifier = self.cursor_shape.hide(old_cell.modifier); + old_cell.modifier = modifier; } } self.cursor_position = Some(new_pos); From c7185eaa9dadf078af65a58b65451f5ab8226e43 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 21 Jul 2025 11:48:50 -0600 Subject: [PATCH 20/35] Optimize rendering --- examples/demo/index.html | 3 +- src/backend/canvas.rs | 158 ++++++++++++++++++++++++--------- src/backend/ratzilla_canvas.js | 11 ++- src/backend/utils.rs | 13 +-- src/lib.rs | 1 + src/render.rs | 2 + 6 files changed, 132 insertions(+), 56 deletions(-) diff --git a/examples/demo/index.html b/examples/demo/index.html index c805c2b1..40424d2f 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -12,8 +12,7 @@ body { margin: 20px; - width: 1600px; - height: 900px; + height: calc(100% - 40px); display: flex; flex-direction: column; justify-content: center; diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 068c39a0..24bdbb23 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -7,10 +7,7 @@ use std::{ }; use crate::{ - backend::{ - color::{actual_bg_color, actual_fg_color}, - utils::*, - }, + backend::color::{actual_bg_color, actual_fg_color, to_rgb}, error::Error, render::WebBackend, CursorShape, @@ -159,6 +156,8 @@ mod js { fn restore() { r#" + this.bold = false; + this.italic = false; this.ctx.restore(); "# } @@ -177,13 +176,51 @@ mod js { fn bold() { r#" - this.ctx.font = "bold " + this.ctx.font; + this.bold = true; + this.init_font(); "# } fn italic() { r#" - this.ctx.font = "italic " + this.ctx.font; + this.italic = true; + this.init_font(); + "# + } + + fn bolditalic() { + r#" + this.bold = true; + this.italic = true; + this.init_font(); + "# + } + + fn unbold() { + r#" + this.bold = false; + this.init_font(); + "# + } + + fn unitalic() { + r#" + this.italic = false; + this.init_font(); + "# + } + + fn unbolditalic() { + r#" + this.bold = false; + this.italic = false; + this.init_font(); + "# + } + + fn reset_font() { + r#" + this.init_font(); "# } @@ -193,6 +230,18 @@ mod js { "# } + fn fill() { + r#" + this.ctx.fill(); + "# + } + + fn stroke() { + r#" + this.ctx.stroke(); + "# + } + fn fill_rect(x: u16, y: u16, w: u16, h: u16) { r#" this.ctx.fillRect($x$, $y$, $w$, $h$); @@ -217,11 +266,23 @@ mod js { "# } + fn set_fill_style(style: u32) { + r#" + this.ctx.fillStyle = `#\${$style$.toString(16).padStart(6, '0')}`; + "# + } + fn set_stroke_style_str(style: &str) { r#" this.ctx.strokeStyle = $style$; "# } + + fn set_stroke_style(style: u32) { + r#" + this.ctx.strokeStyle = `#\${$style$.toString(16).padStart(6, '0')}`; + "# + } } /// Canvas renderer. @@ -455,23 +516,27 @@ impl Backend for CanvasBackend { self.initialize()?; } - self.canvas.buffer.save(); - let draw_region = |(rect, color, canvas, cell_buffer): ( Rect, Color, &mut Canvas, &mut Vec<(u16, u16, &Cell, Modifier)>, )| { - let color = get_canvas_color(color); + canvas.buffer.save(); + let color = to_rgb(color, 0x000000); - canvas.buffer.set_fill_style_str(&color); - canvas.buffer.fill_rect( + canvas.buffer.set_fill_style(color); + // canvas.buffer.set_stroke_style(0xFF0000); + canvas.buffer.begin_path(); + canvas.buffer.rect( rect.x * canvas.cell_width, rect.y * canvas.cell_height, rect.width * canvas.cell_width, rect.height * canvas.cell_height, ); + canvas.buffer.clip(); + canvas.buffer.fill(); + // canvas.buffer.stroke(); // Draws the text symbols on the canvas. // @@ -485,17 +550,13 @@ impl Backend for CanvasBackend { // // 1. Only processes cells that have changed since the last render. // 2. Tracks the last foreground color used to avoid unnecessary style changes - // 3. Only creates clipping paths for potentially problematic glyphs (non-ASCII) - // or when `always_clip_cells` is enabled + // 3. Only creates cell-level clipping paths when `always_clip_cells` is enabled let mut last_color = None; - canvas.buffer.save(); + let mut last_modifier = Modifier::empty(); for (x, y, cell, modifiers) in cell_buffer.drain(..) { let color = actual_fg_color(cell, modifiers, Color::White, canvas.background_color); - // We need to reset the canvas context state in two scenarios: - // 1. When we need to create a clipping path (for potentially problematic glyphs) - // 2. When the text color changes - if self.always_clip_cells || !cell.symbol().is_ascii() { + if self.always_clip_cells { canvas.buffer.restore(); canvas.buffer.save(); @@ -508,23 +569,19 @@ impl Backend for CanvasBackend { ); canvas.buffer.clip(); - last_color = None; // reset last color to avoid clipping - let color = get_canvas_color(color); - canvas.buffer.set_fill_style_str(&color); - } else if last_color != Some(color) { - canvas.buffer.restore(); - canvas.buffer.save(); + last_color = None; + last_modifier = Modifier::empty(); + } + if last_color != Some(color) { last_color = Some(color); - let color = get_canvas_color(color); - canvas.buffer.set_fill_style_str(&color); + let color = to_rgb(color, 0xFFFFFF); + canvas.buffer.set_fill_style(color); } for modifier in modifiers { match modifier { - Modifier::BOLD => canvas.buffer.bold(), - Modifier::ITALIC => canvas.buffer.italic(), Modifier::UNDERLINED => { canvas.buffer.fill_rect( x * canvas.cell_width, @@ -534,23 +591,46 @@ impl Backend for CanvasBackend { ); } Modifier::CROSSED_OUT => { - canvas.buffer.fill_text( - "─", + canvas.buffer.fill_rect( x * canvas.cell_width, - y * canvas.cell_height + canvas.cell_height - canvas.cell_baseline, + y * canvas.cell_height + canvas.cell_height / 2, + canvas.cell_width, + 1, ); } _ => {} } } - // Very useful symbol positioning formulas from here - // https://github.com/ghostty-org/ghostty/blob/a88689ca754a6eb7dce6015b85ccb1416b5363d8/src/Surface.zig#L1589C5-L1589C10 - canvas.buffer.fill_text( - cell.symbol(), - x * canvas.cell_width, - y * canvas.cell_height + canvas.cell_height - canvas.cell_baseline, - ); + let removed_modifiers = last_modifier - modifiers; + + match removed_modifiers & (Modifier::BOLD | Modifier::ITALIC) { + Modifier::BOLD => canvas.buffer.unbold(), + Modifier::ITALIC => canvas.buffer.unitalic(), + modifier if modifier.is_empty() => {} + _ => canvas.buffer.unbolditalic(), + } + + let added_modifiers = modifiers - last_modifier; + + match added_modifiers & (Modifier::BOLD | Modifier::ITALIC) { + Modifier::BOLD => canvas.buffer.bold(), + Modifier::ITALIC => canvas.buffer.italic(), + modifier if modifier.is_empty() => {} + _ => canvas.buffer.bolditalic(), + } + + last_modifier = modifiers; + + if cell.symbol() != " " { + // Very useful symbol positioning formulas from here + // https://github.com/ghostty-org/ghostty/blob/a88689ca754a6eb7dce6015b85ccb1416b5363d8/src/Surface.zig#L1589C5-L1589C10 + canvas.buffer.fill_text( + cell.symbol(), + x * canvas.cell_width, + y * canvas.cell_height + canvas.cell_height - canvas.cell_baseline, + ); + } } canvas.buffer.restore(); }; @@ -606,8 +686,6 @@ impl Backend for CanvasBackend { .flush() .map(|(rect, color)| draw_region((rect, color, &mut self.canvas, &mut cell_buffer))); - self.canvas.buffer.restore(); - Ok(()) } diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index b10c0064..c91ef2f8 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -1,5 +1,8 @@ export class RatzillaCanvas { - constructor() {} + constructor() { + this.bold = false; + this.italic = false; + } create_canvas_in_element(parent, font_str) { this.parent = document.getElementById(parent); @@ -32,7 +35,11 @@ export class RatzillaCanvas { alpha: true, desynchronized: true }); - this.ctx.font = this.font_str; + this.init_font(); + } + + init_font() { + this.ctx.font = `${this.bold ? 'bold' : ''} ${this.italic ? 'italic' : ''} ${this.font_str}`; } get_canvas() { diff --git a/src/backend/utils.rs b/src/backend/utils.rs index e4a97fb8..23b67833 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -3,11 +3,7 @@ use crate::{ error::Error, utils::{get_screen_size, get_window_size, is_mobile}, }; -use compact_str::{format_compact, CompactString}; -use ratatui::{ - buffer::Cell, - style::{Color, Modifier}, -}; +use ratatui::{buffer::Cell, style::Modifier}; use web_sys::{ wasm_bindgen::{JsCast, JsValue}, window, Document, Element, HtmlCanvasElement, Window, @@ -87,13 +83,6 @@ pub(crate) fn get_cell_style_as_css(cell: &Cell) -> String { format!("{fg_style} {bg_style} {modifier_style}") } -/// Converts a Color to a CSS style. -pub(crate) fn get_canvas_color(color: Color) -> CompactString { - let color = ansi_to_rgb(color).unwrap(); - - format_compact!("rgb({}, {}, {})", color.0, color.1, color.2) -} - /// Calculates the number of pixels that can fit in the window. pub(crate) fn get_raw_window_size() -> (u16, u16) { fn js_val_to_int>(val: JsValue) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 2c922a35..b4c3f4a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,4 +28,5 @@ pub use web_sys; pub use backend::{ canvas::CanvasBackend, cursor::CursorShape, dom::DomBackend, webgl2::WebGl2Backend, }; +pub use render::WebBackend; pub use render::WebRenderer; diff --git a/src/render.rs b/src/render.rs index 8468ef0a..6679ed2f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -4,6 +4,8 @@ use web_sys::{wasm_bindgen::prelude::*, window, Element}; use crate::event::{KeyEvent, MouseEvent}; +/// Trait for providing backend-specific shared functionality +/// for each backend pub trait WebBackend { /// This is the element that event listeners will be added /// to to capture mouse and keyboard events From 8fc093ff13c1f22f8158840b1223d1bda8e64c25 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 21 Jul 2025 12:36:34 -0600 Subject: [PATCH 21/35] Fix background in demo --- examples/demo/src/effects.rs | 1 + src/backend/canvas.rs | 123 ++++++++++++++++++++++------------- 2 files changed, 78 insertions(+), 46 deletions(-) diff --git a/examples/demo/src/effects.rs b/examples/demo/src/effects.rs index 09b418b7..ca4746b5 100644 --- a/examples/demo/src/effects.rs +++ b/examples/demo/src/effects.rs @@ -10,6 +10,7 @@ pub fn startup() -> Effect { parallel(&[ parallel(&[ + style_all_cells(), sweep_in(Motion::LeftToRight, 100, 20, Color::Black, timer), sweep_in(Motion::UpToDown, 100, 20, Color::Black, timer), ]), diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 24bdbb23..9dc2404b 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -363,6 +363,42 @@ impl Canvas { Ok(canvas) } + + fn draw_rect_modifiers(&mut self, region: Rect, modifiers: Modifier) { + for modifier in modifiers { + match modifier { + Modifier::UNDERLINED => { + // self.buffer.stroke_rect( + // region.x * self.cell_width, + // region.y * self.cell_height, + // region.width * self.cell_width, + // self.cell_height, + // ); + self.buffer.fill_rect( + region.x * self.cell_width, + region.y * self.cell_height + self.underline_pos, + region.width * self.cell_width, + 1, + ); + } + Modifier::CROSSED_OUT => { + // self.buffer.stroke_rect( + // region.x * self.cell_width, + // region.y * self.cell_height, + // region.width * self.cell_width, + // self.cell_height, + // ); + self.buffer.fill_rect( + region.x * self.cell_width, + region.y * self.cell_height + self.cell_height / 2, + region.width * self.cell_width, + 1, + ); + } + _ => {} + } + } + } } /// Canvas backend. @@ -553,6 +589,7 @@ impl Backend for CanvasBackend { // 3. Only creates cell-level clipping paths when `always_clip_cells` is enabled let mut last_color = None; let mut last_modifier = Modifier::empty(); + let mut underline_optimizer = RowOptimizer::new(); for (x, y, cell, modifiers) in cell_buffer.drain(..) { let color = actual_fg_color(cell, modifiers, Color::White, canvas.background_color); @@ -576,30 +613,18 @@ impl Backend for CanvasBackend { if last_color != Some(color) { last_color = Some(color); + if let Some((region, modifiers)) = underline_optimizer.flush() { + canvas.draw_rect_modifiers(region, modifiers); + } let color = to_rgb(color, 0xFFFFFF); canvas.buffer.set_fill_style(color); } - for modifier in modifiers { - match modifier { - Modifier::UNDERLINED => { - canvas.buffer.fill_rect( - x * canvas.cell_width, - y * canvas.cell_height + canvas.underline_pos, - canvas.cell_width, - 1, - ); - } - Modifier::CROSSED_OUT => { - canvas.buffer.fill_rect( - x * canvas.cell_width, - y * canvas.cell_height + canvas.cell_height / 2, - canvas.cell_width, - 1, - ); - } - _ => {} - } + if let Some((region, modifiers)) = underline_optimizer.process( + (x, y), + modifiers & (Modifier::UNDERLINED | Modifier::CROSSED_OUT), + ) { + canvas.draw_rect_modifiers(region, modifiers); } let removed_modifiers = last_modifier - modifiers; @@ -632,10 +657,14 @@ impl Backend for CanvasBackend { ); } } + + if let Some((region, modifiers)) = underline_optimizer.flush() { + canvas.draw_rect_modifiers(region, modifiers); + } canvas.buffer.restore(); }; - let mut row_renderer = RowColorOptimizer::new(); + let mut bg_optimizer = RowOptimizer::new(); let mut cell_buffer = Vec::new(); for (x, y, cell) in content { let mut modifiers = cell.modifier; @@ -669,8 +698,8 @@ impl Backend for CanvasBackend { // Only calls `draw_region` if the color is different from the // previous one, or if we have advanced past the last y position, // or if we have advanced more than one x position - row_renderer - .process_color( + bg_optimizer + .process( (x, y), actual_bg_color(cell, modifiers, Color::White, self.canvas.background_color), ) @@ -682,7 +711,7 @@ impl Backend for CanvasBackend { } // Flush the remaining region after traversing the changed cells - row_renderer + bg_optimizer .flush() .map(|(rect, color)| draw_region((rect, color, &mut self.canvas, &mut cell_buffer))); @@ -780,17 +809,17 @@ impl WebBackend for CanvasBackend { } } -/// Optimizes canvas rendering by batching adjacent cells with the same color into a single rectangle. +/// Optimizes canvas rendering by batching adjacent cells with the same data into a single rectangle. /// /// This reduces the number of draw calls to the canvas API by coalescing adjacent cells -/// with identical colors into larger rectangles, which is particularly beneficial for +/// with identical data into larger rectangles, which is particularly beneficial for /// WASM where calls are quiteexpensive. -struct RowColorOptimizer { - /// The currently accumulating region and its color - pending_region: Option<(Rect, Color)>, +struct RowOptimizer { + /// The currently accumulating region and its data + pending_region: Option<(Rect, T)>, } -impl RowColorOptimizer { +impl RowOptimizer { /// Creates a new empty optimizer with no pending region. fn new() -> Self { Self { @@ -798,31 +827,33 @@ impl RowColorOptimizer { } } - /// Processes a cell with the given position and color. - fn process_color(&mut self, pos: (u16, u16), color: Color) -> Option<(Rect, Color)> { - if let Some((active_rect, active_color)) = self.pending_region.as_mut() { - if active_color == &color && pos.0 == active_rect.right() && pos.1 == active_rect.y { - // Same color: extend the rectangle + /// Finalizes and returns the current pending region, if any. + fn flush(&mut self) -> Option<(Rect, T)> { + self.pending_region.take() + } +} + +impl RowOptimizer { + /// Processes a cell with the given position and data. + fn process(&mut self, pos: (u16, u16), data: T) -> Option<(Rect, T)> { + if let Some((active_rect, active_data)) = self.pending_region.as_mut() { + if active_data == &data && pos.0 == active_rect.right() && pos.1 == active_rect.y { + // Same data: extend the rectangle active_rect.width += 1; } else { - // Different color: flush the previous region and start a new one + // Different data: flush the previous region and start a new one let region = *active_rect; - let region_color = *active_color; + let region_data = *active_data; *active_rect = Rect::new(pos.0, pos.1, 1, 1); - *active_color = color; - return Some((region, region_color)); + *active_data = data; + return Some((region, region_data)); } } else { - // First color: create a new rectangle + // First data: create a new rectangle let rect = Rect::new(pos.0, pos.1, 1, 1); - self.pending_region = Some((rect, color)); + self.pending_region = Some((rect, data)); } None } - - /// Finalizes and returns the current pending region, if any. - fn flush(&mut self) -> Option<(Rect, Color)> { - self.pending_region.take() - } } From f0452f64b469885b99216b22cd409a695f958200 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 21 Jul 2025 13:19:12 -0600 Subject: [PATCH 22/35] Properly handle double width characters --- Cargo.lock | 21 +++++++++++---------- Cargo.toml | 1 + examples/demo/Cargo.lock | 1 + src/backend/canvas.rs | 13 ++++++++++--- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db8d48a8..536fca0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "cassowary" @@ -70,9 +70,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] @@ -226,9 +226,9 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "instability" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", @@ -264,9 +264,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "log" @@ -382,6 +382,7 @@ dependencies = [ "sledgehammer_bindgen", "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] @@ -488,9 +489,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b64990a3..b6833623 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,4 @@ bitvec = { version = "1.0.1", default-features = false, features = ["alloc", "st beamterm-renderer = "0.1.1" sledgehammer_bindgen = { version = "0.6.0", features = ["web"] } sledgehammer_utils = "0.3.1" +unicode-width = "0.2.0" diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 651682f7..19fcad65 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -569,6 +569,7 @@ dependencies = [ "sledgehammer_bindgen", "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 9dc2404b..8097d62f 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -5,6 +5,7 @@ use std::{ rc::Rc, sync::atomic::{AtomicBool, Ordering}, }; +use unicode_width::UnicodeWidthStr; use crate::{ backend::color::{actual_bg_color, actual_fg_color, to_rgb}, @@ -91,7 +92,7 @@ impl CanvasBackendOptions { /// italics to be disabled if your chosen font doesn't support /// them pub fn disable_modifiers(mut self, modifiers: Modifier) -> Self { - self.enabled_modifiers ^= modifiers; + self.enabled_modifiers &= !modifiers; self } } @@ -561,13 +562,18 @@ impl Backend for CanvasBackend { canvas.buffer.save(); let color = to_rgb(color, 0x000000); + let width: u16 = cell_buffer + .iter() + .map(|(_, _, c, _)| c.symbol().width() as u16) + .sum(); + canvas.buffer.set_fill_style(color); // canvas.buffer.set_stroke_style(0xFF0000); canvas.buffer.begin_path(); canvas.buffer.rect( rect.x * canvas.cell_width, rect.y * canvas.cell_height, - rect.width * canvas.cell_width, + width * canvas.cell_width, rect.height * canvas.cell_height, ); canvas.buffer.clip(); @@ -597,11 +603,12 @@ impl Backend for CanvasBackend { canvas.buffer.restore(); canvas.buffer.save(); + let symbol_width = cell.symbol().width() as u16; canvas.buffer.begin_path(); canvas.buffer.rect( x * canvas.cell_width, y * canvas.cell_height, - canvas.cell_width, + symbol_width * canvas.cell_width, canvas.cell_height, ); canvas.buffer.clip(); From 600dfc3c5cb1a4e83e75eacd009bbb6d6ca30d8e Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 21 Jul 2025 16:02:02 -0600 Subject: [PATCH 23/35] Further optimize rendering --- examples/animations/Cargo.lock | 38 ++++++ examples/canvas_stress_test/Cargo.lock | 38 ++++++ examples/canvas_waves/Cargo.lock | 38 ++++++ examples/pong/Cargo.lock | 38 ++++++ src/backend/canvas.rs | 153 +++++++++++++++++++++---- src/backend/ratzilla_canvas.js | 10 +- 6 files changed, 285 insertions(+), 30 deletions(-) diff --git a/examples/animations/Cargo.lock b/examples/animations/Cargo.lock index 584772bc..c6f5a61c 100644 --- a/examples/animations/Cargo.lock +++ b/examples/animations/Cargo.lock @@ -440,10 +440,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -482,6 +491,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/canvas_stress_test/Cargo.lock b/examples/canvas_stress_test/Cargo.lock index b903a200..a916ba19 100644 --- a/examples/canvas_stress_test/Cargo.lock +++ b/examples/canvas_stress_test/Cargo.lock @@ -400,10 +400,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -436,6 +445,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/canvas_waves/Cargo.lock b/examples/canvas_waves/Cargo.lock index 462482d6..4e15a1e0 100644 --- a/examples/canvas_waves/Cargo.lock +++ b/examples/canvas_waves/Cargo.lock @@ -441,10 +441,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -483,6 +492,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/pong/Cargo.lock b/examples/pong/Cargo.lock index 949a3af7..177b7c43 100644 --- a/examples/pong/Cargo.lock +++ b/examples/pong/Cargo.lock @@ -398,10 +398,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -434,6 +443,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 8097d62f..8ff7e0e8 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -110,7 +110,12 @@ extern "C" { /// /// `sledgehammer_bindgen` only lets you have an empty constructor, /// so we must initialize the class after construction - fn create_canvas_in_element(this: &RatzillaCanvas, parent: &str, font_str: &str); + fn create_canvas_in_element( + this: &RatzillaCanvas, + parent: &str, + font_str: &str, + background_color: u32, + ); #[wasm_bindgen(method)] /// Returns the cell width, cell height, and cell baseline in that order @@ -143,7 +148,8 @@ mod js { fn clear_rect() { r#" - this.ctx.clearRect( + this.ctx.fillStyle = this.backgroundColor; + this.ctx.fillRect( 0, 0, this.canvas.width, this.canvas.height ); "# @@ -316,6 +322,15 @@ struct Canvas { cell_baseline: u16, /// The font descent as measured by the canvas underline_pos: u16, + /// Whether an actual change has been committed to the canvas + /// aside from the background + begun_drawing: bool, + /// The fill style that the canvas should be set to, given drawing + /// has begun + fill_style: Color, + /// The font modifiers that the canvas should be set to, given drawing + /// has begun + modifier: Modifier, } impl Canvas { @@ -342,6 +357,7 @@ impl Canvas { buffer.ratzilla_canvas().create_canvas_in_element( parent_element, font_str.as_deref().unwrap_or("16px monospace"), + to_rgb(background_color, 0x000000), ); let mut canvas = Self { @@ -354,6 +370,9 @@ impl Canvas { cell_height: 0, cell_baseline: 0, underline_pos: 0, + begun_drawing: false, + modifier: Modifier::empty(), + fill_style: Color::default(), }; let font_measurement = canvas.buffer.ratzilla_canvas().measure_text(); @@ -400,6 +419,81 @@ impl Canvas { } } } + + fn begin_drawing(&mut self) { + if !self.begun_drawing { + self.buffer.save(); + self.buffer.clip(); + self.begun_drawing = true; + let color = to_rgb(self.fill_style, 0xFFFFFF); + self.buffer.set_fill_style(color); + match self.modifier & (Modifier::BOLD | Modifier::ITALIC) { + Modifier::BOLD => self.buffer.bold(), + Modifier::ITALIC => self.buffer.italic(), + modifier if modifier.is_empty() => {} + _ => self.buffer.bolditalic(), + } + } + } + + fn end_drawing(&mut self) { + if self.begun_drawing { + self.buffer.restore(); + self.begun_drawing = false; + self.fill_style = Color::default(); + self.modifier = Modifier::empty(); + } + } + + fn bold(&mut self) { + self.modifier |= Modifier::BOLD; + if self.begun_drawing { + self.buffer.bold(); + } + } + + fn italic(&mut self) { + self.modifier |= Modifier::ITALIC; + if self.begun_drawing { + self.buffer.italic(); + } + } + + fn bolditalic(&mut self) { + self.modifier |= Modifier::ITALIC | Modifier::BOLD; + if self.begun_drawing { + self.buffer.bolditalic(); + } + } + + fn unbold(&mut self) { + self.modifier &= !Modifier::BOLD; + if self.begun_drawing { + self.buffer.unbold(); + } + } + + fn unitalic(&mut self) { + self.modifier &= !Modifier::ITALIC; + if self.begun_drawing { + self.buffer.unitalic(); + } + } + + fn unbolditalic(&mut self) { + self.modifier &= !(Modifier::ITALIC | Modifier::BOLD); + if self.begun_drawing { + self.buffer.unbolditalic(); + } + } + + fn set_lazy_fill_style(&mut self, color: Color) { + self.fill_style = color; + if self.begun_drawing { + let color = to_rgb(self.fill_style, 0xFFFFFF); + self.buffer.set_fill_style(color); + } + } } /// Canvas backend. @@ -553,21 +647,20 @@ impl Backend for CanvasBackend { self.initialize()?; } - let draw_region = |(rect, color, canvas, cell_buffer): ( + // self.canvas.buffer.clear_rect(); + + let draw_region = |(rect, bg_color, canvas, cell_buffer): ( Rect, Color, &mut Canvas, &mut Vec<(u16, u16, &Cell, Modifier)>, )| { - canvas.buffer.save(); - let color = to_rgb(color, 0x000000); - let width: u16 = cell_buffer .iter() .map(|(_, _, c, _)| c.symbol().width() as u16) .sum(); - canvas.buffer.set_fill_style(color); + canvas.buffer.set_fill_style(to_rgb(bg_color, 0x000000)); // canvas.buffer.set_stroke_style(0xFF0000); canvas.buffer.begin_path(); canvas.buffer.rect( @@ -576,7 +669,6 @@ impl Backend for CanvasBackend { width * canvas.cell_width, rect.height * canvas.cell_height, ); - canvas.buffer.clip(); canvas.buffer.fill(); // canvas.buffer.stroke(); @@ -595,11 +687,13 @@ impl Backend for CanvasBackend { // 3. Only creates cell-level clipping paths when `always_clip_cells` is enabled let mut last_color = None; let mut last_modifier = Modifier::empty(); - let mut underline_optimizer = RowOptimizer::new(); + let mut underline_optimizer: RowOptimizer = RowOptimizer::new(); for (x, y, cell, modifiers) in cell_buffer.drain(..) { - let color = actual_fg_color(cell, modifiers, Color::White, canvas.background_color); + let fg_color = + actual_fg_color(cell, modifiers, Color::White, canvas.background_color); if self.always_clip_cells { + canvas.begin_drawing(); canvas.buffer.restore(); canvas.buffer.save(); @@ -617,46 +711,52 @@ impl Backend for CanvasBackend { last_modifier = Modifier::empty(); } - if last_color != Some(color) { - last_color = Some(color); + if last_color != Some(fg_color) { + last_color = Some(fg_color); if let Some((region, modifiers)) = underline_optimizer.flush() { - canvas.draw_rect_modifiers(region, modifiers); + if !modifiers.is_empty() { + canvas.begin_drawing(); + canvas.draw_rect_modifiers(region, modifiers); + } } - let color = to_rgb(color, 0xFFFFFF); - canvas.buffer.set_fill_style(color); + canvas.set_lazy_fill_style(fg_color); } if let Some((region, modifiers)) = underline_optimizer.process( (x, y), modifiers & (Modifier::UNDERLINED | Modifier::CROSSED_OUT), ) { - canvas.draw_rect_modifiers(region, modifiers); + if !modifiers.is_empty() { + canvas.begin_drawing(); + canvas.draw_rect_modifiers(region, modifiers); + } } let removed_modifiers = last_modifier - modifiers; match removed_modifiers & (Modifier::BOLD | Modifier::ITALIC) { - Modifier::BOLD => canvas.buffer.unbold(), - Modifier::ITALIC => canvas.buffer.unitalic(), + Modifier::BOLD => canvas.unbold(), + Modifier::ITALIC => canvas.unitalic(), modifier if modifier.is_empty() => {} - _ => canvas.buffer.unbolditalic(), + _ => canvas.unbolditalic(), } let added_modifiers = modifiers - last_modifier; match added_modifiers & (Modifier::BOLD | Modifier::ITALIC) { - Modifier::BOLD => canvas.buffer.bold(), - Modifier::ITALIC => canvas.buffer.italic(), + Modifier::BOLD => canvas.bold(), + Modifier::ITALIC => canvas.italic(), modifier if modifier.is_empty() => {} - _ => canvas.buffer.bolditalic(), + _ => canvas.bolditalic(), } last_modifier = modifiers; - if cell.symbol() != " " { + if fg_color != bg_color && cell.symbol() != " " { // Very useful symbol positioning formulas from here // https://github.com/ghostty-org/ghostty/blob/a88689ca754a6eb7dce6015b85ccb1416b5363d8/src/Surface.zig#L1589C5-L1589C10 + canvas.begin_drawing(); canvas.buffer.fill_text( cell.symbol(), x * canvas.cell_width, @@ -666,9 +766,12 @@ impl Backend for CanvasBackend { } if let Some((region, modifiers)) = underline_optimizer.flush() { - canvas.draw_rect_modifiers(region, modifiers); + if !modifiers.is_empty() { + canvas.begin_drawing(); + canvas.draw_rect_modifiers(region, modifiers); + } } - canvas.buffer.restore(); + canvas.end_drawing(); }; let mut bg_optimizer = RowOptimizer::new(); diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index c91ef2f8..c2932cb0 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -4,7 +4,7 @@ export class RatzillaCanvas { this.italic = false; } - create_canvas_in_element(parent, font_str) { + create_canvas_in_element(parent, font_str, backgroundColor) { this.parent = document.getElementById(parent); if (this.parent == null) { this.parent = document.body; @@ -12,6 +12,7 @@ export class RatzillaCanvas { this.canvas = document.createElement("canvas"); this.parent.appendChild(this.canvas); this.font_str = font_str; + this.backgroundColor = `#${backgroundColor.toString(16).padStart(6, '0')}`; this.init_ctx(); } @@ -32,10 +33,11 @@ export class RatzillaCanvas { init_ctx() { this.ctx = this.canvas.getContext("2d", { - alpha: true, desynchronized: true }); this.init_font(); + this.ctx.fillStyle = this.backgroundColor; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } init_font() { @@ -55,9 +57,7 @@ export class RatzillaCanvas { if (this.canvas.width != canvasW || this.canvas.height != canvasH) { let dummyCanvas = new OffscreenCanvas(this.canvas.width, this.canvas.height); - let dummyCtx = dummyCanvas.getContext('2d', { - alpha: true - }); + let dummyCtx = dummyCanvas.getContext('2d', {}); dummyCtx.drawImage(this.canvas, 0, 0); From b1eb0f8cdf0d3c3f805c66aaee0fe975f182cbb1 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 21 Jul 2025 16:30:13 -0600 Subject: [PATCH 24/35] Run examples --- examples/clipboard/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ examples/colors_rgb/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ examples/demo2/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ examples/minimal/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ examples/text_area/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ examples/user_input/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ examples/world_map/Cargo.lock | 38 ++++++++++++++++++++++++++++++++++ 7 files changed, 266 insertions(+) diff --git a/examples/clipboard/Cargo.lock b/examples/clipboard/Cargo.lock index 1298b4b5..ddbe0be9 100644 --- a/examples/clipboard/Cargo.lock +++ b/examples/clipboard/Cargo.lock @@ -509,7 +509,10 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] @@ -519,6 +522,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -560,6 +569,35 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/colors_rgb/Cargo.lock b/examples/colors_rgb/Cargo.lock index 26b8083b..b095eb91 100644 --- a/examples/colors_rgb/Cargo.lock +++ b/examples/colors_rgb/Cargo.lock @@ -516,10 +516,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -558,6 +567,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/demo2/Cargo.lock b/examples/demo2/Cargo.lock index d07aa7ae..bb14edc9 100644 --- a/examples/demo2/Cargo.lock +++ b/examples/demo2/Cargo.lock @@ -546,10 +546,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -588,6 +597,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/minimal/Cargo.lock b/examples/minimal/Cargo.lock index cc624254..e7c191e4 100644 --- a/examples/minimal/Cargo.lock +++ b/examples/minimal/Cargo.lock @@ -397,10 +397,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -433,6 +442,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/text_area/Cargo.lock b/examples/text_area/Cargo.lock index 3e223f78..d4dfe3d6 100644 --- a/examples/text_area/Cargo.lock +++ b/examples/text_area/Cargo.lock @@ -389,10 +389,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -425,6 +434,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/user_input/Cargo.lock b/examples/user_input/Cargo.lock index caf4ab92..fdf18d21 100644 --- a/examples/user_input/Cargo.lock +++ b/examples/user_input/Cargo.lock @@ -389,10 +389,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -425,6 +434,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/examples/world_map/Cargo.lock b/examples/world_map/Cargo.lock index 23035598..09f8ba36 100644 --- a/examples/world_map/Cargo.lock +++ b/examples/world_map/Cargo.lock @@ -389,10 +389,19 @@ dependencies = [ "compact_str 0.9.0", "console_error_panic_hook", "ratatui", + "sledgehammer_bindgen", + "sledgehammer_utils", "thiserror", + "unicode-width 0.2.0", "web-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.21" @@ -425,6 +434,35 @@ dependencies = [ "syn", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash", +] + [[package]] name = "static_assertions" version = "1.1.0" From 96a321f519c9c637b8568e40193c1f89c3efe4a0 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Mon, 21 Jul 2025 22:31:19 -0600 Subject: [PATCH 25/35] HiDPI support --- src/backend/canvas.rs | 75 +++++++--------------------------- src/backend/ratzilla_canvas.js | 23 ++++------- 2 files changed, 22 insertions(+), 76 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 8ff7e0e8..4a7ea0b4 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -181,52 +181,10 @@ mod js { "# } - fn bold() { - r#" - this.bold = true; - this.init_font(); - "# - } - - fn italic() { - r#" - this.italic = true; - this.init_font(); - "# - } - - fn bolditalic() { - r#" - this.bold = true; - this.italic = true; - this.init_font(); - "# - } - - fn unbold() { - r#" - this.bold = false; - this.init_font(); - "# - } - - fn unitalic() { - r#" - this.italic = false; - this.init_font(); - "# - } - - fn unbolditalic() { - r#" - this.bold = false; - this.italic = false; - this.init_font(); - "# - } - - fn reset_font() { + fn font_modifiers(modifiers: u16) { r#" + this.bold = (modifiers & 0b0000_0000_0001) != 0; + this.italic = (modifiers & 0b0000_0000_0100) != 0; this.init_font(); "# } @@ -427,67 +385,62 @@ impl Canvas { self.begun_drawing = true; let color = to_rgb(self.fill_style, 0xFFFFFF); self.buffer.set_fill_style(color); - match self.modifier & (Modifier::BOLD | Modifier::ITALIC) { - Modifier::BOLD => self.buffer.bold(), - Modifier::ITALIC => self.buffer.italic(), - modifier if modifier.is_empty() => {} - _ => self.buffer.bolditalic(), - } + self.buffer.font_modifiers(self.modifier.bits()); } } fn end_drawing(&mut self) { + self.fill_style = Color::default(); + self.modifier = Modifier::empty(); if self.begun_drawing { self.buffer.restore(); self.begun_drawing = false; - self.fill_style = Color::default(); - self.modifier = Modifier::empty(); } } fn bold(&mut self) { self.modifier |= Modifier::BOLD; if self.begun_drawing { - self.buffer.bold(); + self.buffer.font_modifiers(self.modifier.bits()); } } fn italic(&mut self) { self.modifier |= Modifier::ITALIC; if self.begun_drawing { - self.buffer.italic(); + self.buffer.font_modifiers(self.modifier.bits()); } } fn bolditalic(&mut self) { self.modifier |= Modifier::ITALIC | Modifier::BOLD; if self.begun_drawing { - self.buffer.bolditalic(); + self.buffer.font_modifiers(self.modifier.bits()); } } fn unbold(&mut self) { self.modifier &= !Modifier::BOLD; if self.begun_drawing { - self.buffer.unbold(); + self.buffer.font_modifiers(self.modifier.bits()); } } fn unitalic(&mut self) { self.modifier &= !Modifier::ITALIC; if self.begun_drawing { - self.buffer.unitalic(); + self.buffer.font_modifiers(self.modifier.bits()); } } fn unbolditalic(&mut self) { self.modifier &= !(Modifier::ITALIC | Modifier::BOLD); if self.begun_drawing { - self.buffer.unbolditalic(); + self.buffer.font_modifiers(self.modifier.bits()); } } - fn set_lazy_fill_style(&mut self, color: Color) { + fn set_fill_style_lazy(&mut self, color: Color) { self.fill_style = color; if self.begun_drawing { let color = to_rgb(self.fill_style, 0xFFFFFF); @@ -720,7 +673,7 @@ impl Backend for CanvasBackend { canvas.draw_rect_modifiers(region, modifiers); } } - canvas.set_lazy_fill_style(fg_color); + canvas.set_fill_style_lazy(fg_color); } if let Some((region, modifiers)) = underline_optimizer.process( diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index c2932cb0..d7493478 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -32,10 +32,12 @@ export class RatzillaCanvas { } init_ctx() { + const ratio = window.devicePixelRatio; this.ctx = this.canvas.getContext("2d", { desynchronized: true }); this.init_font(); + this.ctx.scale(ratio, ratio); this.ctx.fillStyle = this.backgroundColor; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } @@ -49,28 +51,19 @@ export class RatzillaCanvas { } reinit_canvas() { + const ratio = window.devicePixelRatio; let sourceW = Math.ceil(this.parent.clientWidth / this.cellWidth); let sourceH = Math.ceil(this.parent.clientHeight / this.cellHeight); let canvasW = sourceW * this.cellWidth; let canvasH = sourceH * this.cellHeight; - if (this.canvas.width != canvasW || this.canvas.height != canvasH) { - let dummyCanvas = new OffscreenCanvas(this.canvas.width, this.canvas.height); - let dummyCtx = dummyCanvas.getContext('2d', {}); - - dummyCtx.drawImage(this.canvas, 0, 0); - - this.canvas.width = canvasW; - this.canvas.height = canvasH; + if (this.canvas.width != canvasW * ratio || this.canvas.height != canvasH * ratio) { + this.canvas.width = canvasW * ratio; + this.canvas.height = canvasH * ratio; + this.canvas.style.width = canvasW + "px"; + this.canvas.style.height = canvasH + "px"; this.init_ctx(); - - this.ctx.drawImage(dummyCanvas, - 0, 0, - canvasW, canvasH, - 0, 0, - canvasW, canvasH, - ); } return new Uint16Array([sourceW, sourceH]); From 0e7b339721a9e2df36d6c001972ddd92b3b45fb0 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Tue, 22 Jul 2025 10:45:45 -0600 Subject: [PATCH 26/35] Optimize cursor logic --- src/backend/canvas.rs | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 4a7ea0b4..d7327d4b 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -464,6 +464,8 @@ pub struct CanvasBackend { canvas: Canvas, /// Cursor position. cursor_position: Option, + /// Whether the cursor has been drawn to the screen yet + cursor_shown: bool, /// The cursor shape. cursor_shape: CursorShape, /// Draw cell boundaries with specified color. @@ -492,6 +494,7 @@ impl CanvasBackend { canvas, buffer: Vec::new(), cursor_position: None, + cursor_shown: false, cursor_shape: CursorShape::SteadyBlock, debug_mode: None, }) @@ -798,13 +801,16 @@ impl Backend for CanvasBackend { fn hide_cursor(&mut self) -> IoResult<()> { // Redraw the cell under the cursor, but without // the cursor style - if let Some(pos) = self.cursor_position.take() { - let x = pos.x as usize; - let y = pos.y as usize; - if let Some(line) = self.buffer.get(y) { - if let Some(cell) = line.get(x).cloned() { - self.draw([(pos.x, pos.y, &cell)].into_iter())?; + if self.cursor_shown { + if let Some(pos) = self.cursor_position.take() { + let x = pos.x as usize; + let y = pos.y as usize; + if let Some(line) = self.buffer.get(y) { + if let Some(cell) = line.get(x).cloned() { + self.draw([(pos.x, pos.y, &cell)].into_iter())?; + } } + self.cursor_shown = false; } } Ok(()) @@ -813,14 +819,17 @@ impl Backend for CanvasBackend { fn show_cursor(&mut self) -> IoResult<()> { // Redraw the new cell under the cursor, but with // the cursor style - if let Some(pos) = self.cursor_position { - let x = pos.x as usize; - let y = pos.y as usize; - if let Some(line) = self.buffer.get(y) { - if let Some(cell) = line.get(x).cloned() { - self.draw([(pos.x, pos.y, &cell)].into_iter())?; + if !self.cursor_shown { + if let Some(pos) = self.cursor_position { + let x = pos.x as usize; + let y = pos.y as usize; + if let Some(line) = self.buffer.get(y) { + if let Some(cell) = line.get(x).cloned() { + self.draw([(pos.x, pos.y, &cell)].into_iter())?; + } } } + self.cursor_shown = true; } Ok(()) } @@ -833,9 +842,12 @@ impl Backend for CanvasBackend { } fn set_cursor_position>(&mut self, position: P) -> IoResult<()> { - self.hide_cursor()?; - self.cursor_position = Some(position.into()); - self.show_cursor()?; + let position = position.into(); + if Some(position) != self.cursor_position { + self.hide_cursor()?; + self.cursor_position = Some(position.into()); + self.show_cursor()?; + } Ok(()) } From f1a086aa3ffa0c6e2a04f9306b77e560c8e224c9 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Tue, 22 Jul 2025 12:57:16 -0600 Subject: [PATCH 27/35] Optimize cursor logic --- src/backend/canvas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index d7327d4b..42eac9d7 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -810,8 +810,8 @@ impl Backend for CanvasBackend { self.draw([(pos.x, pos.y, &cell)].into_iter())?; } } - self.cursor_shown = false; } + self.cursor_shown = false; } Ok(()) } From ac54ca9588a3847c1539aeec78ebb00811212f94 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 08:50:55 -0600 Subject: [PATCH 28/35] Add input focus element --- Cargo.toml | 1 + src/backend/canvas.rs | 10 ++++------ src/backend/ratzilla_canvas.js | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b6833623..76945240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ web-sys = { version = "0.3.77", features = [ 'Element', 'HtmlCanvasElement', 'HtmlElement', + 'HtmlInputElement', 'KeyboardEvent', 'Location', 'MouseEvent', diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 42eac9d7..2cda37fa 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -122,7 +122,7 @@ extern "C" { fn measure_text(this: &RatzillaCanvas) -> Uint16Array; #[wasm_bindgen(method)] - fn get_canvas(this: &RatzillaCanvas) -> web_sys::HtmlCanvasElement; + fn get_input_element(this: &RatzillaCanvas) -> web_sys::HtmlInputElement; #[wasm_bindgen(method)] /// Returns the new number of cells in width and height in that order @@ -260,10 +260,8 @@ struct Canvas { /// of things like italics, which my not be available in some fonts /// like Fira Code enabled_modifiers: Modifier, - /// The inner HTML canvas element - /// - /// Use **only** for implementing `WebBackend` - inner: web_sys::HtmlCanvasElement, + /// The element that keyboard input should be listened on + inner: web_sys::HtmlInputElement, /// Background color. background_color: Color, /// Width of a single cell. @@ -319,7 +317,7 @@ impl Canvas { ); let mut canvas = Self { - inner: buffer.ratzilla_canvas().get_canvas(), + inner: buffer.ratzilla_canvas().get_input_element(), buffer, initialized, enabled_modifiers, diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index d7493478..4e813208 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -9,7 +9,37 @@ export class RatzillaCanvas { if (this.parent == null) { this.parent = document.body; } + // Uses input hack from https://github.com/emilk/egui/blob/fdcaff8465eac8db8cc1ebbcbb9b97e0791a8363/crates/eframe/src/web/text_agent.rs#L18 + this.inputElement = document.createElement("input"); + this.inputElement.autofocus = true; + this.inputElement.type = "text"; + this.inputElement.autocapitalize = "off"; + this.inputElement.style.backgroundColor = "transparent"; + this.inputElement.style.border = "none"; + this.inputElement.style.outline = "none"; + this.inputElement.style.width = "1px"; + this.inputElement.style.height = "1px"; + this.inputElement.style.caretColor = "transparent"; + this.inputElement.style.position= "absolute"; + this.inputElement.style.top = "0"; + this.inputElement.style.left = "0"; + this.inputElement.addEventListener("input", (event) => { + if (!event.isComposing) { + this.inputElement.blur(); + this.inputElement.focus(); + } + + if (this.inputElement.value.length === 0 && !event.isComposing) { + this.inputElement.value = "" + } + }); this.canvas = document.createElement("canvas"); + this.canvas.tabIndex = 0; + this.canvas.style.outline = "none"; + this.canvas.addEventListener("focus", () => { + this.inputElement.focus(); + }); + this.parent.appendChild(this.inputElement); this.parent.appendChild(this.canvas); this.font_str = font_str; this.backgroundColor = `#${backgroundColor.toString(16).padStart(6, '0')}`; @@ -46,8 +76,8 @@ export class RatzillaCanvas { this.ctx.font = `${this.bold ? 'bold' : ''} ${this.italic ? 'italic' : ''} ${this.font_str}`; } - get_canvas() { - return this.canvas; + get_input_element() { + return this.textArea; } reinit_canvas() { From 1e4d7cc6491a41278e6835696d9fe794b462e4e7 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 08:53:41 -0600 Subject: [PATCH 29/35] Try adding mobile keyboard to canvas --- src/backend/ratzilla_canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index 4e813208..0897c505 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -29,7 +29,7 @@ export class RatzillaCanvas { this.inputElement.focus(); } - if (this.inputElement.value.length === 0 && !event.isComposing) { + if (!(this.inputElement.value.length === 0) && !event.isComposing) { this.inputElement.value = "" } }); From 782758189e42c3bea7f72dd2d55d801c4a5748c1 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 09:07:29 -0600 Subject: [PATCH 30/35] Add input focus element --- src/backend/canvas.rs | 24 ++++++++++++++++++++++++ src/backend/ratzilla_canvas.js | 20 +++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 2cda37fa..e6d36bc8 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -248,6 +248,27 @@ mod js { this.ctx.strokeStyle = `#\${$style$.toString(16).padStart(6, '0')}`; "# } + + fn set_cursor_pos(x: u16, y: u16) { + r#" + this.inputElement.style.left = ($x$) * this.cellWidth; + this.inputElement.style.top = ($y$) * this.cellHeight; + "# + } + + fn show_cursor() { + r#" + this.cursorShown = true; + this.inputElement.focus(); + "# + } + + fn hide_cursor() { + r#" + this.cursorShown = false; + this.inputElement.blur(); + "# + } } /// Canvas renderer. @@ -809,6 +830,7 @@ impl Backend for CanvasBackend { } } } + self.canvas.buffer.hide_cursor(); self.cursor_shown = false; } Ok(()) @@ -827,6 +849,7 @@ impl Backend for CanvasBackend { } } } + self.canvas.buffer.show_cursor(); self.cursor_shown = true; } Ok(()) @@ -844,6 +867,7 @@ impl Backend for CanvasBackend { if Some(position) != self.cursor_position { self.hide_cursor()?; self.cursor_position = Some(position.into()); + self.canvas.buffer.set_cursor_pos(position.x, position.y); self.show_cursor()?; } Ok(()) diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index 0897c505..5f3cfcc7 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -2,6 +2,7 @@ export class RatzillaCanvas { constructor() { this.bold = false; this.italic = false; + this.cursorShown = false; } create_canvas_in_element(parent, font_str, backgroundColor) { @@ -9,9 +10,10 @@ export class RatzillaCanvas { if (this.parent == null) { this.parent = document.body; } + this.parentDiv = document.createElement("div"); + this.parentDiv.style.position = "relative"; // Uses input hack from https://github.com/emilk/egui/blob/fdcaff8465eac8db8cc1ebbcbb9b97e0791a8363/crates/eframe/src/web/text_agent.rs#L18 this.inputElement = document.createElement("input"); - this.inputElement.autofocus = true; this.inputElement.type = "text"; this.inputElement.autocapitalize = "off"; this.inputElement.style.backgroundColor = "transparent"; @@ -20,7 +22,7 @@ export class RatzillaCanvas { this.inputElement.style.width = "1px"; this.inputElement.style.height = "1px"; this.inputElement.style.caretColor = "transparent"; - this.inputElement.style.position= "absolute"; + this.inputElement.style.position = "absolute"; this.inputElement.style.top = "0"; this.inputElement.style.left = "0"; this.inputElement.addEventListener("input", (event) => { @@ -36,11 +38,17 @@ export class RatzillaCanvas { this.canvas = document.createElement("canvas"); this.canvas.tabIndex = 0; this.canvas.style.outline = "none"; + this.canvas.style.position = "absolute"; + this.canvas.style.top = "0"; + this.canvas.style.left = "0"; this.canvas.addEventListener("focus", () => { - this.inputElement.focus(); + if (this.cursorShown) { + this.inputElement.focus(); + } }); - this.parent.appendChild(this.inputElement); - this.parent.appendChild(this.canvas); + this.parentDiv.appendChild(this.canvas); + this.parentDiv.appendChild(this.inputElement); + this.parent.appendChild(this.parentDiv); this.font_str = font_str; this.backgroundColor = `#${backgroundColor.toString(16).padStart(6, '0')}`; this.init_ctx(); @@ -93,6 +101,8 @@ export class RatzillaCanvas { this.canvas.height = canvasH * ratio; this.canvas.style.width = canvasW + "px"; this.canvas.style.height = canvasH + "px"; + this.parentDiv.style.width = canvasW + "px"; + this.parentDiv.style.height = canvasH + "px"; this.init_ctx(); } From 9747c151bc39fe940f3bf82a46775c7f50b71d32 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 09:08:48 -0600 Subject: [PATCH 31/35] Add input focus element --- src/backend/canvas.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index e6d36bc8..62b42e62 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -259,7 +259,6 @@ mod js { fn show_cursor() { r#" this.cursorShown = true; - this.inputElement.focus(); "# } From bdb9a4eee983055bd765d0b674468e8cd217af9d Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 09:31:24 -0600 Subject: [PATCH 32/35] Add input focus element --- src/backend/canvas.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 62b42e62..bbcc91b9 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -251,8 +251,8 @@ mod js { fn set_cursor_pos(x: u16, y: u16) { r#" - this.inputElement.style.left = ($x$) * this.cellWidth; - this.inputElement.style.top = ($y$) * this.cellHeight; + this.inputElement.style.left = (x * this.cellWidth) + "px"; + this.inputElement.style.top = (y * this.cellHeight) + "px"; "# } @@ -265,7 +265,14 @@ mod js { fn hide_cursor() { r#" this.cursorShown = false; - this.inputElement.blur(); + "# + } + + fn resolve_cursor() { + r#" + if (!this.cursorShown) { + this.inputElement.blur(); + } "# } } @@ -811,6 +818,7 @@ impl Backend for CanvasBackend { self.draw_debug()?; } + self.canvas.buffer.resolve_cursor(); self.canvas.buffer.flush(); Ok(()) @@ -863,10 +871,10 @@ impl Backend for CanvasBackend { fn set_cursor_position>(&mut self, position: P) -> IoResult<()> { let position = position.into(); + self.canvas.buffer.set_cursor_pos(position.x, position.y); if Some(position) != self.cursor_position { self.hide_cursor()?; self.cursor_position = Some(position.into()); - self.canvas.buffer.set_cursor_pos(position.x, position.y); self.show_cursor()?; } Ok(()) From 2ca98b271cdc29f3960c2bdf0fd1675043068670 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 09:49:34 -0600 Subject: [PATCH 33/35] Add input focus element --- src/backend/ratzilla_canvas.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index 5f3cfcc7..780b854e 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -90,22 +90,25 @@ export class RatzillaCanvas { reinit_canvas() { const ratio = window.devicePixelRatio; - let sourceW = Math.ceil(this.parent.clientWidth / this.cellWidth); - let sourceH = Math.ceil(this.parent.clientHeight / this.cellHeight); - let canvasW = sourceW * this.cellWidth; - let canvasH = sourceH * this.cellHeight; + if (ratio != 0) { + let sourceW = Math.ceil(this.parent.clientWidth / this.cellWidth); + let sourceH = Math.ceil(this.parent.clientHeight / this.cellHeight); - if (this.canvas.width != canvasW * ratio || this.canvas.height != canvasH * ratio) { - this.canvas.width = canvasW * ratio; - this.canvas.height = canvasH * ratio; - this.canvas.style.width = canvasW + "px"; - this.canvas.style.height = canvasH + "px"; - this.parentDiv.style.width = canvasW + "px"; - this.parentDiv.style.height = canvasH + "px"; - this.init_ctx(); - } + let canvasW = sourceW * this.cellWidth; + let canvasH = sourceH * this.cellHeight; + + if (this.canvas.width != canvasW * ratio || this.canvas.height != canvasH * ratio) { + this.canvas.width = canvasW * ratio; + this.canvas.height = canvasH * ratio; + this.canvas.style.width = canvasW + "px"; + this.canvas.style.height = canvasH + "px"; + this.parentDiv.style.width = canvasW + "px"; + this.parentDiv.style.height = canvasH + "px"; + this.init_ctx(); + } - return new Uint16Array([sourceW, sourceH]); + return new Uint16Array([sourceW, sourceH]); + } } } From c355f46766247a2a2c8914886ffa1182fdfbf18b Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 09:54:28 -0600 Subject: [PATCH 34/35] Add input focus element --- src/backend/ratzilla_canvas.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index 780b854e..5f3cfcc7 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -90,25 +90,22 @@ export class RatzillaCanvas { reinit_canvas() { const ratio = window.devicePixelRatio; + let sourceW = Math.ceil(this.parent.clientWidth / this.cellWidth); + let sourceH = Math.ceil(this.parent.clientHeight / this.cellHeight); - if (ratio != 0) { - let sourceW = Math.ceil(this.parent.clientWidth / this.cellWidth); - let sourceH = Math.ceil(this.parent.clientHeight / this.cellHeight); + let canvasW = sourceW * this.cellWidth; + let canvasH = sourceH * this.cellHeight; - let canvasW = sourceW * this.cellWidth; - let canvasH = sourceH * this.cellHeight; - - if (this.canvas.width != canvasW * ratio || this.canvas.height != canvasH * ratio) { - this.canvas.width = canvasW * ratio; - this.canvas.height = canvasH * ratio; - this.canvas.style.width = canvasW + "px"; - this.canvas.style.height = canvasH + "px"; - this.parentDiv.style.width = canvasW + "px"; - this.parentDiv.style.height = canvasH + "px"; - this.init_ctx(); - } - - return new Uint16Array([sourceW, sourceH]); + if (this.canvas.width != canvasW * ratio || this.canvas.height != canvasH * ratio) { + this.canvas.width = canvasW * ratio; + this.canvas.height = canvasH * ratio; + this.canvas.style.width = canvasW + "px"; + this.canvas.style.height = canvasH + "px"; + this.parentDiv.style.width = canvasW + "px"; + this.parentDiv.style.height = canvasH + "px"; + this.init_ctx(); } + + return new Uint16Array([sourceW, sourceH]); } } From 64c36c277e1a7d801db6d6c89d006103912706b0 Mon Sep 17 00:00:00 2001 From: Isaac Mills Date: Wed, 23 Jul 2025 10:16:48 -0600 Subject: [PATCH 35/35] Try adding mobile keyboard to canvas --- Cargo.toml | 1 - src/backend/canvas.rs | 6 +++--- src/backend/ratzilla_canvas.js | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76945240..b6833623 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ web-sys = { version = "0.3.77", features = [ 'Element', 'HtmlCanvasElement', 'HtmlElement', - 'HtmlInputElement', 'KeyboardEvent', 'Location', 'MouseEvent', diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index bbcc91b9..9872cf62 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -122,7 +122,7 @@ extern "C" { fn measure_text(this: &RatzillaCanvas) -> Uint16Array; #[wasm_bindgen(method)] - fn get_input_element(this: &RatzillaCanvas) -> web_sys::HtmlInputElement; + fn get_canvas(this: &RatzillaCanvas) -> web_sys::HtmlCanvasElement; #[wasm_bindgen(method)] /// Returns the new number of cells in width and height in that order @@ -288,7 +288,7 @@ struct Canvas { /// like Fira Code enabled_modifiers: Modifier, /// The element that keyboard input should be listened on - inner: web_sys::HtmlInputElement, + inner: web_sys::HtmlCanvasElement, /// Background color. background_color: Color, /// Width of a single cell. @@ -344,7 +344,7 @@ impl Canvas { ); let mut canvas = Self { - inner: buffer.ratzilla_canvas().get_input_element(), + inner: buffer.ratzilla_canvas().get_canvas(), buffer, initialized, enabled_modifiers, diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js index 5f3cfcc7..0325cb9a 100644 --- a/src/backend/ratzilla_canvas.js +++ b/src/backend/ratzilla_canvas.js @@ -84,8 +84,8 @@ export class RatzillaCanvas { this.ctx.font = `${this.bold ? 'bold' : ''} ${this.italic ? 'italic' : ''} ${this.font_str}`; } - get_input_element() { - return this.textArea; + get_canvas() { + return this.canvas; } reinit_canvas() {