diff --git a/Cargo.lock b/Cargo.lock index 9d1ce321..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" @@ -379,10 +379,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" @@ -415,6 +424,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" @@ -451,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 eba627e4..b6833623 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,6 @@ 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" +unicode-width = "0.2.0" 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/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/demo/Cargo.lock b/examples/demo/Cargo.lock index c60f96c8..19fcad65 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", "examples-shared", "rand", "ratzilla", @@ -565,10 +566,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" @@ -607,6 +617,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/demo/Cargo.toml b/examples/demo/Cargo.toml index bf58d614..c24b1c71 100644 --- a/examples/demo/Cargo.toml +++ b/examples/demo/Cargo.toml @@ -4,11 +4,15 @@ 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 } 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" examples-shared = { path = "../shared" } # tui-big-text = "0.6.1" diff --git a/examples/demo/index.html b/examples/demo/index.html index 1d67f957..40424d2f 100644 --- a/examples/demo/index.html +++ b/examples/demo/index.html @@ -1,25 +1,47 @@ - - - - Ratzilla Demo - - - - + + + + + Ratzilla Demo + + + + + + + + \ No newline at end of file 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/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/examples/demo/src/main.rs b/examples/demo/src/lib.rs similarity index 78% rename from examples/demo/src/main.rs rename to examples/demo/src/lib.rs index 226e55d1..45839500 100644 --- a/examples/demo/src/main.rs +++ b/examples/demo/src/lib.rs @@ -6,17 +6,16 @@ //! [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; +use examples_shared::backend::{BackendType, MultiBackendBuilder}; use ratzilla::event::KeyCode; +use ratzilla::ratatui::style::Modifier; +use ratzilla::web_sys::wasm_bindgen::{self, prelude::*}; use ratzilla::WebRenderer; -use examples_shared::backend::{BackendType, MultiBackendBuilder}; -use ratzilla::{ - backend::webgl2::WebGl2BackendOptions, - backend::canvas::CanvasBackendOptions, -}; +use ratzilla::{backend::canvas::CanvasBackendOptions, backend::webgl2::WebGl2BackendOptions}; mod app; @@ -35,13 +34,18 @@ 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))); - + // Create backend with explicit size like main branch (1600x900) let canvas_options = CanvasBackendOptions::new() - .size((1600, 900)); - + .font(String::from("16px Fira Code")) + // Fira Code does not have an italic variation + .disable_modifiers(Modifier::ITALIC); + let webgl2_options = WebGl2BackendOptions::new() .measure_performance(true) .size((1600, 900)); @@ -49,8 +53,9 @@ fn main() -> Result<()> { let terminal = MultiBackendBuilder::with_fallback(BackendType::WebGl2) .canvas_options(canvas_options) .webgl2_options(webgl2_options) - .build_terminal()?; - + .build_terminal() + .unwrap(); + terminal.on_key_event({ let app_state_cloned = app_state.clone(); move |event| { @@ -68,6 +73,9 @@ fn main() -> Result<()> { KeyCode::Down => { app_state.on_down(); } + KeyCode::Char(' ') => { + app_state.pause_unpause(); + } KeyCode::Char(c) => app_state.on_key(c), _ => {} } @@ -79,6 +87,4 @@ fn main() -> Result<()> { let elapsed = app_state.on_tick(); ui::draw(elapsed, f, &mut app_state); }); - - Ok(()) } 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/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/examples/shared/src/backend.rs b/examples/shared/src/backend.rs index 1b33ee04..79195c8c 100644 --- a/examples/shared/src/backend.rs +++ b/examples/shared/src/backend.rs @@ -1,15 +1,16 @@ -use std::io; -use std::fmt; -use std::convert::TryFrom; -use web_sys::{window, Url}; +use crate::fps; +use crate::utils::inject_backend_footer; use ratzilla::backend::canvas::CanvasBackendOptions; use ratzilla::backend::dom::DomBackendOptions; use ratzilla::backend::webgl2::WebGl2BackendOptions; -use ratzilla::{CanvasBackend, DomBackend, WebGl2Backend}; -use ratzilla::ratatui::{Terminal, TerminalOptions}; use ratzilla::ratatui::backend::Backend; -use crate::fps; -use crate::utils::inject_backend_footer; +use ratzilla::ratatui::{Terminal, TerminalOptions}; +use ratzilla::web_sys::Element; +use ratzilla::{CanvasBackend, DomBackend, WebBackend, WebGl2Backend}; +use std::convert::TryFrom; +use std::fmt; +use std::io; +use web_sys::{window, Url}; /// Available backend types #[derive(Debug, Clone, Copy, Default, PartialEq)] @@ -39,7 +40,9 @@ impl TryFrom for BackendType { "dom" => Ok(BackendType::Dom), "canvas" => Ok(BackendType::Canvas), "webgl2" => Ok(BackendType::WebGl2), - _ => Err(format!("Invalid backend type: '{s}'. Valid options are: dom, canvas, webgl2")), + _ => Err(format!( + "Invalid backend type: '{s}'. Valid options are: dom, canvas, webgl2" + )), } } } @@ -51,13 +54,13 @@ impl fmt::Display for BackendType { } /// Enum wrapper for different Ratzilla backends that implements the Ratatui Backend trait. -/// +/// /// This enum allows switching between different rendering backends at runtime while /// providing a unified interface. All backend operations are delegated to the wrapped /// backend implementation. -/// +/// /// # Backends -/// +/// /// - `Dom`: HTML DOM-based rendering with accessibility features /// - `Canvas`: Canvas 2D API rendering with full Unicode support /// - `WebGl2`: GPU-accelerated rendering using WebGL2 and beamterm-renderer @@ -78,6 +81,16 @@ impl RatzillaBackend { } } +impl WebBackend for RatzillaBackend { + fn listening_element(&self) -> &Element { + match self { + RatzillaBackend::Dom(dom) => dom.listening_element(), + RatzillaBackend::Canvas(canvas) => canvas.listening_element(), + RatzillaBackend::WebGl2(webgl) => webgl.listening_element(), + } + } +} + impl Backend for RatzillaBackend { fn draw<'a, I>(&mut self, content: I) -> io::Result<()> where @@ -177,7 +190,7 @@ pub struct FpsTrackingBackend { impl FpsTrackingBackend { /// Create a new FPS tracking backend that wraps the given backend. - /// + /// /// Frame timing will be recorded automatically on each successful flush operation. pub fn new(backend: RatzillaBackend) -> Self { Self { inner: backend } @@ -195,6 +208,12 @@ impl From for FpsTrackingBackend { } } +impl WebBackend for FpsTrackingBackend { + fn listening_element(&self) -> &Element { + self.inner.listening_element() + } +} + impl Backend for FpsTrackingBackend { fn draw<'a, I>(&mut self, content: I) -> io::Result<()> where @@ -383,7 +402,6 @@ impl MultiBackendBuilder { Ok(terminal) } - } impl From for MultiBackendBuilder { 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" diff --git a/src/backend/canvas.rs b/src/backend/canvas.rs index 33dea067..9872cf62 100644 --- a/src/backend/canvas.rs +++ b/src/backend/canvas.rs @@ -1,13 +1,16 @@ -use bitvec::{bitvec, prelude::BitVec}; use ratatui::layout::Rect; -use std::io::Result as IoResult; +use sledgehammer_bindgen::bindgen; +use std::{ + io::Result as IoResult, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; +use unicode_width::UnicodeWidthStr; 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, }; use ratatui::{ @@ -18,34 +21,46 @@ use ratatui::{ style::{Color, Modifier}, }; use web_sys::{ - js_sys::{Boolean, Map}, - wasm_bindgen::{JsCast, JsValue}, + js_sys::Uint16Array, + wasm_bindgen::{ + self, + prelude::{wasm_bindgen, Closure}, + JsCast, + }, + 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)] +#[derive(Debug)] 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 /// 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 { @@ -60,78 +75,422 @@ impl CanvasBackendOptions { self } - /// Sets the size of the canvas, in pixels. - pub fn size(mut self, size: (u32, u32)) -> Self { - self.size = Some(size); + /// Sets the font that the canvas will use + pub fn font(mut self, font: String) -> Self { + 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 +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + /// External JS class for managing the actual HTML canvas, context, + /// 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, + background_color: u32, + ); + + #[wasm_bindgen(method)] + /// Returns the cell width, cell height, and cell baseline in that order + fn measure_text(this: &RatzillaCanvas) -> Uint16Array; + + #[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; +} + +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)] + /// Responsible for buffering the calls to the canvas and + /// canvas context + struct Buffer; + + const BASE: &str = r#"src/backend/ratzilla_canvas.js"#; + + fn clear_rect() { + r#" + this.ctx.fillStyle = this.backgroundColor; + this.ctx.fillRect( + 0, 0, this.canvas.width, this.canvas.height + ); + "# + } + + fn save() { + r#" + this.ctx.save(); + "# + } + + fn restore() { + r#" + this.bold = false; + this.italic = false; + this.ctx.restore(); + "# + } + + fn begin_path() { + r#" + this.ctx.beginPath(); + "# + } + + fn clip() { + r#" + this.ctx.clip(); + "# + } + + fn font_modifiers(modifiers: u16) { + r#" + this.bold = (modifiers & 0b0000_0000_0001) != 0; + this.italic = (modifiers & 0b0000_0000_0100) != 0; + this.init_font(); + "# + } + + fn rect(x: u16, y: u16, w: u16, h: u16) { + r#" + this.ctx.rect($x$, $y$, $w$, $h$); + "# + } + + 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$); + "# + } + + 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_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')}`; + "# + } + + fn set_cursor_pos(x: u16, y: u16) { + r#" + this.inputElement.style.left = (x * this.cellWidth) + "px"; + this.inputElement.style.top = (y * this.cellHeight) + "px"; + "# + } + + fn show_cursor() { + r#" + this.cursorShown = true; + "# + } + + fn hide_cursor() { + r#" + this.cursorShown = false; + "# + } + + fn resolve_cursor() { + r#" + if (!this.cursorShown) { + this.inputElement.blur(); + } + "# + } } /// Canvas renderer. -#[derive(Debug)] struct Canvas { - /// Canvas element. + /// The buffer of draw calls to the 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 element that keyboard input should be listened on inner: web_sys::HtmlCanvasElement, - /// Rendering context. - 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: 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: u16, + /// The font descent as measured by the 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 { /// Constructs a new [`Canvas`]. fn new( - parent_element: web_sys::Element, - width: u32, - height: u32, + parent_element: &str, background_color: Color, + font_str: Option, + enabled_modifiers: Modifier, ) -> 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 initialized: Rc = Rc::new(false.into()); + 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 buffer = Buffer::default(); + buffer.ratzilla_canvas().create_canvas_in_element( + parent_element, + font_str.as_deref().unwrap_or("16px monospace"), + to_rgb(background_color, 0x000000), ); - 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(Self { - inner: canvas, - context, + let mut canvas = Self { + inner: buffer.ratzilla_canvas().get_canvas(), + buffer, + initialized, + enabled_modifiers, background_color, - }) + cell_width: 0, + 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(); + canvas.cell_width = font_measurement.get_index(0); + canvas.cell_height = font_measurement.get_index(1); + canvas.cell_baseline = font_measurement.get_index(2); + canvas.underline_pos = font_measurement.get_index(3); + + 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, + ); + } + _ => {} + } + } + } + + 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); + 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; + } + } + + fn bold(&mut self) { + self.modifier |= Modifier::BOLD; + if self.begun_drawing { + self.buffer.font_modifiers(self.modifier.bits()); + } + } + + fn italic(&mut self) { + self.modifier |= Modifier::ITALIC; + if self.begun_drawing { + self.buffer.font_modifiers(self.modifier.bits()); + } + } + + fn bolditalic(&mut self) { + self.modifier |= Modifier::ITALIC | Modifier::BOLD; + if self.begun_drawing { + self.buffer.font_modifiers(self.modifier.bits()); + } + } + + fn unbold(&mut self) { + self.modifier &= !Modifier::BOLD; + if self.begun_drawing { + self.buffer.font_modifiers(self.modifier.bits()); + } + } + + fn unitalic(&mut self) { + self.modifier &= !Modifier::ITALIC; + if self.begun_drawing { + self.buffer.font_modifiers(self.modifier.bits()); + } + } + + fn unbolditalic(&mut self) { + self.modifier &= !(Modifier::ITALIC | Modifier::BOLD); + if self.begun_drawing { + self.buffer.font_modifiers(self.modifier.bits()); + } + } + + 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); + self.buffer.set_fill_style(color); + } } } /// Canvas backend. /// /// 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, - /// Current buffer. + /// The size of the current screen in cells buffer: Vec>, - /// Previous buffer. - prev_buffer: Vec>, - /// Changed buffer cells - changed_cells: BitVec, /// Canvas. 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. @@ -141,43 +500,62 @@ 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()) - } - - /// 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() - }) + Self::new_with_options(CanvasBackendOptions::default()) } /// 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())?; - - let (width, height) = options - .size - .unwrap_or_else(|| (parent.client_width() as u32, parent.client_height() as u32)); - - let canvas = Canvas::new(parent, width, height, Color::Black)?; - let buffer = get_sized_buffer_from_canvas(&canvas.inner); - let changed_cells = bitvec![0; buffer.len() * buffer[0].len()]; + let parent = options.grid_id.as_deref().unwrap_or_default(); + + let canvas = Canvas::new( + parent, + Color::Black, + options.font_str.take(), + options.enabled_modifiers, + )?; Ok(Self { - prev_buffer: buffer.clone(), always_clip_cells: options.always_clip_cells, - buffer, - initialized: false, - changed_cells, canvas, + buffer: Vec::new(), cursor_position: None, + cursor_shown: false, 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(); + + 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(()) + } + /// Sets the background color of the canvas. pub fn set_background_color(&mut self, color: Color) { self.canvas.background_color = color; @@ -214,241 +592,220 @@ 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. - fn update_grid(&mut self, force_redraw: bool) -> Result<(), Error> { - if force_redraw { - self.canvas.context.clear_rect( - 0.0, - 0.0, - self.canvas.inner.client_width() as f64, - self.canvas.inner.client_height() as f64, - ); - } - self.canvas.context.translate(5_f64, 5_f64)?; - - // 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()?; - if self.debug_mode.is_some() { - self.draw_debug()?; - } - - self.canvas.context.translate(-5_f64, -5_f64)?; - Ok(()) - } + /// Draws cell boundaries for debugging. + fn draw_debug(&mut self) -> Result<(), Error> { + self.canvas.buffer.save(); - /// 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; + let color = self.debug_mode.as_ref().unwrap(); + 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 * self.canvas.cell_width, + y * self.canvas.cell_height, + self.canvas.cell_width, + self.canvas.cell_height, + ); } } - } - - /// 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; - - self.canvas.context.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[index] || 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.context.restore(); - self.canvas.context.save(); - - self.canvas.context.begin_path(); - self.canvas.context.rect( - x as f64 * CELL_WIDTH, - y as f64 * CELL_HEIGHT, - CELL_WIDTH, - CELL_HEIGHT, - ); - self.canvas.context.clip(); - - last_color = None; // reset last color to avoid clipping - let color = get_canvas_color(color, Color::White); - self.canvas.context.set_fill_style_str(&color); - } else if last_color != Some(color) { - self.canvas.context.restore(); - self.canvas.context.save(); - last_color = Some(color); + self.canvas.buffer.restore(); - let color = get_canvas_color(color, Color::White); - self.canvas.context.set_fill_style_str(&color); - } + Ok(()) + } +} - self.canvas.context.fill_text( - cell.symbol(), - x as f64 * CELL_WIDTH, - y as f64 * CELL_HEIGHT, - )?; +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); - index += 1; - } + if !initialized { + self.initialize()?; } - self.canvas.context.restore(); - - Ok(()) - } - /// 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.context.save(); - - let draw_region = |(rect, color): (Rect, Color)| { - let color = get_canvas_color(color, self.canvas.background_color); - - self.canvas.context.set_fill_style_str(&color); - self.canvas.context.fill_rect( - rect.x as f64 * CELL_WIDTH, - rect.y as f64 * CELL_HEIGHT, - rect.width as f64 * CELL_WIDTH, - rect.height as f64 * CELL_HEIGHT, + // self.canvas.buffer.clear_rect(); + + let draw_region = |(rect, bg_color, canvas, cell_buffer): ( + Rect, + Color, + &mut Canvas, + &mut Vec<(u16, u16, &Cell, Modifier)>, + )| { + let width: u16 = cell_buffer + .iter() + .map(|(_, _, c, _)| c.symbol().width() as u16) + .sum(); + + canvas.buffer.set_fill_style(to_rgb(bg_color, 0x000000)); + // canvas.buffer.set_stroke_style(0xFF0000); + canvas.buffer.begin_path(); + canvas.buffer.rect( + rect.x * canvas.cell_width, + rect.y * canvas.cell_height, + width * canvas.cell_width, + rect.height * canvas.cell_height, ); - }; + canvas.buffer.fill(); + // canvas.buffer.stroke(); + + // 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 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 = RowOptimizer::new(); + for (x, y, cell, modifiers) in cell_buffer.drain(..) { + 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(); + + let symbol_width = cell.symbol().width() as u16; + canvas.buffer.begin_path(); + canvas.buffer.rect( + x * canvas.cell_width, + y * canvas.cell_height, + symbol_width * canvas.cell_width, + canvas.cell_height, + ); + canvas.buffer.clip(); - let mut index = 0; - for (y, line) in self.buffer.iter().enumerate() { - let mut row_renderer = RowColorOptimizer::new(); - for (x, cell) in line.iter().enumerate() { - if changed_cells[index] { - // 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); - } 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); + last_color = None; + last_modifier = Modifier::empty(); } - index += 1; - } - // Flush the remaining region after traversing the row - row_renderer.flush().map(draw_region); - } - self.canvas.context.restore(); + if last_color != Some(fg_color) { + last_color = Some(fg_color); - Ok(()) - } + if let Some((region, modifiers)) = underline_optimizer.flush() { + if !modifiers.is_empty() { + canvas.begin_drawing(); + canvas.draw_rect_modifiers(region, modifiers); + } + } + canvas.set_fill_style_lazy(fg_color); + } - /// 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]; + if let Some((region, modifiers)) = underline_optimizer.process( + (x, y), + modifiers & (Modifier::UNDERLINED | Modifier::CROSSED_OUT), + ) { + if !modifiers.is_empty() { + canvas.begin_drawing(); + canvas.draw_rect_modifiers(region, modifiers); + } + } - if cell.modifier.contains(Modifier::UNDERLINED) { - self.canvas.context.save(); + let removed_modifiers = last_modifier - modifiers; - self.canvas.context.fill_text( - "_", - pos.x as f64 * CELL_WIDTH, - pos.y as f64 * CELL_HEIGHT, - )?; + match removed_modifiers & (Modifier::BOLD | Modifier::ITALIC) { + Modifier::BOLD => canvas.unbold(), + Modifier::ITALIC => canvas.unitalic(), + modifier if modifier.is_empty() => {} + _ => canvas.unbolditalic(), + } - self.canvas.context.restore(); - } - } + let added_modifiers = modifiers - last_modifier; - Ok(()) - } + match added_modifiers & (Modifier::BOLD | Modifier::ITALIC) { + Modifier::BOLD => canvas.bold(), + Modifier::ITALIC => canvas.italic(), + modifier if modifier.is_empty() => {} + _ => canvas.bolditalic(), + } - /// Draws cell boundaries for debugging. - fn draw_debug(&mut self) -> Result<(), Error> { - self.canvas.context.save(); + last_modifier = modifiers; - let color = self.debug_mode.as_ref().unwrap(); - for (y, line) in self.buffer.iter().enumerate() { - for (x, _) in line.iter().enumerate() { - self.canvas.context.set_stroke_style_str(color); - self.canvas.context.stroke_rect( - x as f64 * CELL_WIDTH, - y as f64 * CELL_HEIGHT, - CELL_WIDTH, - CELL_HEIGHT, - ); + 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, + y * canvas.cell_height + canvas.cell_height - canvas.cell_baseline, + ); + } } - } - - self.canvas.context.restore(); - Ok(()) - } -} + if let Some((region, modifiers)) = underline_optimizer.flush() { + if !modifiers.is_empty() { + canvas.begin_drawing(); + canvas.draw_rect_modifiers(region, modifiers); + } + } + canvas.end_drawing(); + }; -impl Backend for CanvasBackend { - // Populates the buffer with the given content. - fn draw<'a, I>(&mut self, content: I) -> IoResult<()> - where - I: Iterator, - { + let mut bg_optimizer = RowOptimizer::new(); + let mut cell_buffer = Vec::new(); 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(); - } + 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()); + } + } - // 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 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 + // 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 + bg_optimizer + .process( + (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, modifiers)); } + // Flush the remaining region after traversing the changed cells + bg_optimizer + .flush() + .map(|(rect, color)| draw_region((rect, color, &mut self.canvas, &mut cell_buffer))); + Ok(()) } @@ -457,99 +814,116 @@ 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)?; + if self.debug_mode.is_some() { + self.draw_debug()?; } - self.prev_buffer = self.buffer.clone(); + self.canvas.buffer.resolve_cursor(); + self.canvas.buffer.flush(); Ok(()) } fn hide_cursor(&mut self) -> IoResult<()> { - if let Some(pos) = self.cursor_position { - let y = pos.y as usize; - let x = pos.x as usize; - let line = &mut self.buffer[y]; - if x < line.len() { - let style = self.cursor_shape.hide(line[x].style()); - line[x].set_style(style); + // Redraw the cell under the cursor, but without + // the cursor style + 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.canvas.buffer.hide_cursor(); + self.cursor_shown = false; } - self.cursor_position = None; Ok(()) } fn show_cursor(&mut self) -> IoResult<()> { + // Redraw the new cell under the cursor, but with + // the cursor style + 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.canvas.buffer.show_cursor(); + self.cursor_shown = true; + } 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<()> { + 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.show_cursor()?; + } Ok(()) } fn clear(&mut self) -> IoResult<()> { - self.buffer = get_sized_buffer(); + self.canvas.buffer.clear_rect(); + 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, - )) + 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 { 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 { + fn listening_element(&self) -> &Element { + &self.canvas.inner } } -/// 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 { @@ -557,31 +931,33 @@ impl RowColorOptimizer { } } - /// Processes a cell with the given position and color. - fn process_color(&mut self, pos: (usize, usize), color: Color) -> Option<(Rect, Color)> { - if let Some((active_rect, active_color)) = self.pending_region.as_mut() { - if active_color == &color { - // 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; - *active_rect = Rect::new(pos.0 as _, pos.1 as _, 1, 1); - *active_color = color; - return Some((region, region_color)); + let region_data = *active_data; + *active_rect = Rect::new(pos.0, pos.1, 1, 1); + *active_data = data; + return Some((region, region_data)); } } else { - // First color: create a new rectangle - let rect = Rect::new(pos.0 as _, pos.1 as _, 1, 1); - self.pending_region = Some((rect, color)); + // First data: create a new rectangle + let rect = Rect::new(pos.0, pos.1, 1, 1); + 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() - } } 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 2ce52ebd..0b2d2599 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)] @@ -246,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; } } @@ -282,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; @@ -332,11 +335,17 @@ 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); Ok(()) } } + +impl WebBackend for DomBackend { + fn listening_element(&self) -> &Element { + &self.grid_parent + } +} diff --git a/src/backend/ratzilla_canvas.js b/src/backend/ratzilla_canvas.js new file mode 100644 index 00000000..0325cb9a --- /dev/null +++ b/src/backend/ratzilla_canvas.js @@ -0,0 +1,111 @@ +export class RatzillaCanvas { + constructor() { + this.bold = false; + this.italic = false; + this.cursorShown = false; + } + + create_canvas_in_element(parent, font_str, backgroundColor) { + this.parent = document.getElementById(parent); + 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.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.style.position = "absolute"; + this.canvas.style.top = "0"; + this.canvas.style.left = "0"; + this.canvas.addEventListener("focus", () => { + if (this.cursorShown) { + this.inputElement.focus(); + } + }); + 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(); + } + + // 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 Uint16Array([this.cellWidth, this.cellHeight, this.cellBaseline, this.underlinePos]); + } + + 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); + } + + init_font() { + this.ctx.font = `${this.bold ? 'bold' : ''} ${this.italic ? 'italic' : ''} ${this.font_str}`; + } + + get_canvas() { + return this.canvas; + } + + 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 * 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]); + } +} diff --git a/src/backend/utils.rs b/src/backend/utils.rs index 1596bf7f..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, fallback_color: Color) -> CompactString { - let color = ansi_to_rgb(color).unwrap_or_else(|| ansi_to_rgb(fallback_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 { @@ -126,13 +115,6 @@ pub(crate) fn get_sized_buffer() -> Vec> { vec![vec![Cell::default(); size.width as usize]; size.height as usize] } -/// 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] -} - /// 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 bc5e0267..0076e231 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}; @@ -288,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(()) @@ -366,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; } } @@ -420,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); @@ -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/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 bc559ff8..6679ed2f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,14 +1,22 @@ 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}; +/// 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 + 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 +61,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 +82,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