diff --git a/cursive-core/Cargo.toml b/cursive-core/Cargo.toml index 08038af2..577d0616 100644 --- a/cursive-core/Cargo.toml +++ b/cursive-core/Cargo.toml @@ -73,6 +73,22 @@ optional = true version = "0.2" features = ["js"] + +[dependencies.web-sys] +optional = true +version = "0.3.64" +features = [ + "Window", +] + +[dependencies.wasm-bindgen] +optional = true +version = "0.2.87" + +[dependencies.wasm-bindgen-futures] +optional = true +version = "0.4.37" + [features] default = ["wasm"] doc-cfg = [] @@ -80,7 +96,7 @@ builder = ["inventory", "cursive-macros/builder"] markdown = ["pulldown-cmark"] ansi = ["ansi-parser"] unstable_scroll = [] # Deprecated feature, remove in next version -wasm = ["js-sys", "getrandom"] +wasm = ["js-sys", "getrandom", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"] [lib] name = "cursive_core" diff --git a/cursive-core/src/cursive.rs b/cursive-core/src/cursive.rs index e15fe29f..8226a173 100644 --- a/cursive-core/src/cursive.rs +++ b/cursive-core/src/cursive.rs @@ -878,6 +878,17 @@ impl Cursive { self.try_run_with::<(), _>(|| Ok(backend_init())).unwrap(); } + /// Initialize the backend and runs the event loop. + /// + /// Used for infallible backend initializers. + #[cfg(feature = "wasm")] + pub async fn run_with_async(&mut self, backend_init: F) + where + F: FnOnce() -> Box, + { + self.try_run_with_async::<(), _>(|| Ok(backend_init())).await.unwrap(); + } + /// Initialize the backend and runs the event loop. /// /// Returns an error if initializing the backend fails. @@ -892,6 +903,19 @@ impl Cursive { Ok(()) } + /// try run with async + #[cfg(feature = "wasm")] + pub async fn try_run_with_async(&mut self, backend_init: F) -> Result<(), E> + where + F: FnOnce() -> Result, E>, + { + let mut runner = self.runner(backend_init()?); + + runner.run_async().await; + + Ok(()) + } + /// Stops the event loop. pub fn quit(&mut self) { self.running = false; diff --git a/cursive-core/src/cursive_run.rs b/cursive-core/src/cursive_run.rs index 10137aaf..eb95fe41 100644 --- a/cursive-core/src/cursive_run.rs +++ b/cursive-core/src/cursive_run.rs @@ -1,6 +1,5 @@ use crate::{backend, event::Event, theme, Cursive, Vec2}; use std::borrow::{Borrow, BorrowMut}; -#[cfg(not(feature = "wasm"))] use std::time::Duration; #[cfg(feature = "wasm")] @@ -184,18 +183,59 @@ where } } - #[cfg(not(feature = "wasm"))] + /// post_events asynchronously + #[cfg(feature = "wasm")] + pub async fn post_events_async(&mut self, received_something: bool) { + let boring = !received_something; + // How many times should we try if it's still boring? + // Total duration will be INPUT_POLL_DELAY_MS * repeats + // So effectively fps = 1000 / INPUT_POLL_DELAY_MS / repeats + if !boring + || self + .fps() + .map(|fps| 1000 / INPUT_POLL_DELAY_MS as u32 / fps.get()) + .map(|repeats| self.boring_frame_count >= repeats) + .unwrap_or(false) + { + // We deserve to draw something! + + if boring { + // We're only here because of a timeout. + self.on_event(Event::Refresh); + self.process_pending_backend_calls(); + } + + self.refresh(); + } + + if boring { + self.sleep_async().await; + self.boring_frame_count += 1; + } + } + fn sleep(&self) { std::thread::sleep(Duration::from_millis(INPUT_POLL_DELAY_MS)); } #[cfg(feature = "wasm")] - fn sleep(&self) { - let start = Date::now(); - let mut now = start; - while (now - start) < INPUT_POLL_DELAY_MS as f64 { - now = Date::now(); - } + async fn sleep_async(&self) { + use wasm_bindgen::prelude::*; + let promise = js_sys::Promise::new(&mut |resolve, _| { + let closure = Closure::new(move || { + resolve.call0(&JsValue::null()).unwrap(); + }) as Closure; + web_sys::window() + .expect("window is None for sleep") + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + INPUT_POLL_DELAY_MS as i32, + ) + .expect("should register timeout for sleep"); + closure.forget(); + }); + let js_future = wasm_bindgen_futures::JsFuture::from(promise); + js_future.await.expect("should await sleep"); } /// Refresh the screen with the current view tree state. @@ -235,6 +275,14 @@ where received_something } + /// step asynchronously + #[cfg(feature = "wasm")] + pub async fn step_async(&mut self) -> bool { + let received_something = self.process_events(); + self.post_events_async(received_something).await; + received_something + } + /// Runs the event loop. /// /// It will wait for user input (key presses) @@ -256,4 +304,15 @@ where self.step(); } } + + /// Runs the event loop asynchronously. + #[cfg(feature = "wasm")] + pub async fn run_async(&mut self) { + self.refresh(); + + // And the big event loop begins! + while self.is_running() { + self.step_async().await; + } + } } diff --git a/cursive/Cargo.toml b/cursive/Cargo.toml index 737ee9bd..1f5dd391 100644 --- a/cursive/Cargo.toml +++ b/cursive/Cargo.toml @@ -69,6 +69,10 @@ optional = true version = "0.2" features = ["js"] +[dependencies.serde-wasm-bindgen] +optional = true +version = "0.5.0" + [features] doc-cfg = ["cursive_core/doc-cfg"] # Enable doc_cfg, a nightly-only doc feature. @@ -83,7 +87,7 @@ markdown = ["cursive_core/markdown"] ansi = ["cursive_core/ansi"] unstable_scroll = [] # Deprecated feature, remove in next version toml = ["cursive_core/toml"] -wasm-backend = ["wasm-bindgen", "web-sys", "cursive_core/wasm", "getrandom"] +wasm-backend = ["wasm-bindgen", "web-sys", "cursive_core/wasm", "getrandom", "serde-wasm-bindgen"] [lib] name = "cursive" diff --git a/cursive/src/backends/canvas.js b/cursive/src/backends/canvas.js new file mode 100644 index 00000000..f6a5002a --- /dev/null +++ b/cursive/src/backends/canvas.js @@ -0,0 +1,48 @@ +const fontWidth = 12; +const fontHeight = fontWidth * 2; +const textColorPairSize = 12; + +export function paint(buffer) { + const data = new Uint8Array(buffer); + const canvas = document.getElementById('cursive-wasm-canvas'); + const context = canvas.getContext('2d'); + const backBuffer = new Map(); + const frontBuffer = new Map(); + context.font = `${fontHeight}px monospace`; + for (let x = 0; x < 1000; x++) { + for (let y = 0; y < 1000; y++) { + const n = 1000 * y + x; + const textColorPair = data.slice(n * textColorPairSize, (n + 1) * textColorPairSize); + const text = String.fromCharCode(textColorPair[0] + (2**8) *textColorPair[1] + (2**16)* textColorPair[2] + (2 ** 24) + textColorPair[3]); + const front = byte_to_hex_string(textColorPair.slice(4, 7)); + const back = byte_to_hex_string(textColorPair.slice(7, 10)); + if (text != ' ') { + const buffer = frontBuffer.get(front) || []; + buffer.push({ x, y, text }); + frontBuffer.set(front, buffer); + } + const buffer = backBuffer.get(back) || []; + buffer.push({ x, y }); + backBuffer.set(back, buffer); + } + } + backBuffer.forEach((buffer, back) => { + context.fillStyle = back; + buffer.forEach(value => { + context.fillRect(value.x * fontWidth, value.y * fontHeight, fontWidth, fontHeight); + }); + }); + frontBuffer.forEach((buffer, front) => { + context.fillStyle = front; + buffer.forEach(value => { + context.fillText(value.text, value.x * fontWidth, (value.y + 0.8) * fontHeight); + }); + }); +} + +function byte_to_hex_string(bytes) { + const red = bytes[0].toString(16).padStart(2, '0'); + const green = bytes[1].toString(16).padStart(2, '0'); + const blue = bytes[2].toString(16).padStart(2, '0'); + return `#${red}${green}${blue}`; +} \ No newline at end of file diff --git a/cursive/src/backends/wasm.rs b/cursive/src/backends/wasm.rs index fb63c31b..6ac63716 100644 --- a/cursive/src/backends/wasm.rs +++ b/cursive/src/backends/wasm.rs @@ -8,22 +8,57 @@ use cursive_core::{ use std::collections::VecDeque; use std::rc::Rc; use std::cell::RefCell; -use web_sys::{ - HtmlCanvasElement, - CanvasRenderingContext2d, -}; +use web_sys::HtmlCanvasElement; use wasm_bindgen::prelude::*; use crate::backend; +#[wasm_bindgen] +#[derive(Debug, PartialEq)] +#[repr(C)] +struct TextColorPair { + text: char, + color: ColorPair, +} + +impl TextColorPair { + pub fn new(text: char, color: ColorPair) -> Self { + Self { + text, + color, + } + } +} + +fn text_color_pairs_to_bytes(buffer: &Vec) -> &[u8] { + unsafe { + std::slice::from_raw_parts( + buffer.as_ptr() as *const u8, + buffer.len() * std::mem::size_of::(), + ) + } +} + +impl Clone for TextColorPair { + fn clone(&self) -> Self { + Self { + text: self.text, + color: self.color.clone(), + } + } +} + + +#[wasm_bindgen(module = "/src/backends/canvas.js")] +extern "C" { + fn paint(buffer: &[u8]); +} /// Backend using wasm. pub struct Backend { canvas: HtmlCanvasElement, - ctx: CanvasRenderingContext2d, color: RefCell, - font_height: usize, - font_width: usize, events: Rc>>, + buffer: RefCell>, } impl Backend { /// Creates a new Cursive root using a wasm backend. @@ -38,10 +73,10 @@ impl Backend { std::io::ErrorKind::Other, "Failed to get document", ))?; - let canvas = document.create_element("canvas") - .map_err(|_| std::io::Error::new( + let canvas = document.get_element_by_id("cursive-wasm-canvas") + .ok_or(std::io::Error::new( std::io::ErrorKind::Other, - "Failed to create canvas", + "Failed to get window", ))? .dyn_into::() .map_err(|_| std::io::Error::new( @@ -51,28 +86,10 @@ impl Backend { canvas.set_width(1000); canvas.set_height(1000); - let font_width = 12; - let font_height = font_width * 2; - let ctx: CanvasRenderingContext2d = canvas.get_context("2d") - .map_err(|_| std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to get canvas context", - ))? - .ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to get canvas context", - ))? - .dyn_into::() - .map_err(|_| std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to cast canvas context", - ))?; - ctx.set_font(&format!("{}px monospace", font_height)); - - let color = RefCell::new(cursive_to_color_pair(theme::ColorPair { - front: theme::Color::Light(theme::BaseColor::White), - back:theme::Color::Dark(theme::BaseColor::Black), - })); + let color = cursive_to_color_pair(theme::ColorPair { + front: theme::Color::Light(theme::BaseColor::Black), + back:theme::Color::Dark(theme::BaseColor::Green), + }); let events = Rc::new(RefCell::new(VecDeque::new())); let cloned = events.clone(); @@ -88,13 +105,13 @@ impl Backend { ))?; closure.forget(); - let c = Backend { + let buffer = vec![TextColorPair::new(' ', color.clone()); 1_000_000]; + + let c = Backend { canvas, - ctx, - color, - font_height, - font_width, + color: RefCell::new(color), events, + buffer: RefCell::new(buffer), }; Ok(Box::new(c)) } @@ -109,7 +126,12 @@ impl cursive_core::backend::Backend for Backend { self.canvas.set_title(&title); } - fn refresh(self: &mut Backend) {} + fn refresh(self: &mut Backend) { + web_sys::console::time_with_label("refresh"); + let data = self.buffer.borrow().clone(); + paint(text_color_pairs_to_bytes(&data)); + web_sys::console::time_end_with_label("refresh"); + } fn has_colors(self: &Backend) -> bool { true @@ -120,15 +142,15 @@ impl cursive_core::backend::Backend for Backend { } fn print_at(self: &Backend, pos: Vec2, text: &str) { - let color = self.color.borrow(); - self.ctx.set_fill_style(&JsValue::from_str(&color.back)); - self.ctx.fill_rect((pos.x * self.font_width) as f64, (pos.y * self.font_height) as f64, (self.font_width * text.len()) as f64, self.font_height as f64); - self.ctx.set_fill_style(&JsValue::from_str(&color.front)); - self.ctx.fill_text(text, (pos.x * self.font_width) as f64, (pos.y * self.font_height + self.font_height * 3/4) as f64).unwrap(); + let color = (*self.color.borrow()).clone(); + let mut buffer = self.buffer.borrow_mut(); + for (i, c) in text.chars().enumerate() { + let x = pos.x + i; + buffer[1000 * pos.y + x] = TextColorPair::new(c, color.clone()); + } } - fn clear(self: &Backend, color: cursive_core::theme::Color) { - self.ctx.set_fill_style(&JsValue::from_str(&cursive_to_color(color))); + fn clear(self: &Backend, _color: cursive_core::theme::Color) { } fn set_color(self: &Backend, color_pair: cursive_core::theme::ColorPair) -> cursive_core::theme::ColorPair { @@ -149,10 +171,28 @@ impl cursive_core::backend::Backend for Backend { } -/// Type of hex color which starts with #. -pub type Color = String; +/// Type of hex color which is r,g,b +#[wasm_bindgen] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Color { + red: u8, + green: u8, + blue: u8 +} + +impl Color { + /// Creates a new `Color` with the given red, green, and blue values. + pub fn new(red: u8, green: u8, blue: u8) -> Self { + Self { + red, + green, + blue, + } + } +} -/// Type of color pair. +/// Type of color pair. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ColorPair { /// Foreground text color. pub front: Color, @@ -163,25 +203,25 @@ pub struct ColorPair { /// Convert cursive color to hex color. pub fn cursive_to_color(color: theme::Color) -> Color { match color { - theme::Color::Dark(theme::BaseColor::Black) => "#000000".to_string(), - theme::Color::Dark(theme::BaseColor::Red) => "#800000".to_string(), - theme::Color::Dark(theme::BaseColor::Green) => "#008000".to_string(), - theme::Color::Dark(theme::BaseColor::Yellow) => "#808000".to_string(), - theme::Color::Dark(theme::BaseColor::Blue) => "#000080".to_string(), - theme::Color::Dark(theme::BaseColor::Magenta) => "#800080".to_string(), - theme::Color::Dark(theme::BaseColor::Cyan) => "#008080".to_string(), - theme::Color::Dark(theme::BaseColor::White) => "#c0c0c0".to_string(), - theme::Color::Light(theme::BaseColor::Black) => "#808080".to_string(), - theme::Color::Light(theme::BaseColor::Red) => "#ff0000".to_string(), - theme::Color::Light(theme::BaseColor::Green) => "#00ff00".to_string(), - theme::Color::Light(theme::BaseColor::Yellow) => "#ffff00".to_string(), - theme::Color::Light(theme::BaseColor::Blue) => "#0000ff".to_string(), - theme::Color::Light(theme::BaseColor::Magenta) => "#ff00ff".to_string(), - theme::Color::Light(theme::BaseColor::Cyan) => "#00ffff".to_string(), - theme::Color::Light(theme::BaseColor::White) => "#ffffff".to_string(), - theme::Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b).to_string(), - theme::Color::RgbLowRes(r,g ,b ) => format!("#{:01x}{:01x}{:01x}", r, g, b).to_string(), - theme::Color::TerminalDefault => "#00ff00".to_string(), + theme::Color::Dark(theme::BaseColor::Black) => Color::new(0,0,0), + theme::Color::Dark(theme::BaseColor::Red) => Color::new(128,0,0), + theme::Color::Dark(theme::BaseColor::Green) => Color::new(0,128,0), + theme::Color::Dark(theme::BaseColor::Yellow) => Color::new(128,128,0), + theme::Color::Dark(theme::BaseColor::Blue) => Color::new(0,0,128), + theme::Color::Dark(theme::BaseColor::Magenta) => Color::new(128,0,128), + theme::Color::Dark(theme::BaseColor::Cyan) => Color::new(0,128,128), + theme::Color::Dark(theme::BaseColor::White) => Color::new(182,182,182), + theme::Color::Light(theme::BaseColor::Black) => Color::new(128,128,128), + theme::Color::Light(theme::BaseColor::Red) => Color::new(255,0,0), + theme::Color::Light(theme::BaseColor::Green) => Color::new(0,0,255), + theme::Color::Light(theme::BaseColor::Yellow) => Color::new(255,255,0), + theme::Color::Light(theme::BaseColor::Blue) => Color::new(0,0,255), + theme::Color::Light(theme::BaseColor::Magenta) => Color::new(255,0,255), + theme::Color::Light(theme::BaseColor::Cyan) => Color::new(0,255,255), + theme::Color::Light(theme::BaseColor::White) => Color::new(255,255,255), + theme::Color::Rgb(r, g, b) => Color::new(r,g,b), + theme::Color::RgbLowRes(r,g ,b ) => Color::new(r,g,b), + theme::Color::TerminalDefault => Color::new(0,255,0), } } diff --git a/cursive/src/cursive_runnable.rs b/cursive/src/cursive_runnable.rs index 1dd8bd1c..c5637d5f 100644 --- a/cursive/src/cursive_runnable.rs +++ b/cursive/src/cursive_runnable.rs @@ -93,6 +93,13 @@ impl CursiveRunnable { self.siv.try_run_with(&mut self.backend_init) } + + /// try_run_ asynchronously + #[cfg(feature = "wasm-backend")] + pub async fn try_run_async(&mut self) -> Result<(), Box> { + self.siv.try_run_with_async(&mut self.backend_init).await + } + /// Gets a runner with the registered backend. /// /// Used to manually control the event loop. In most cases, running