Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add wasm-backend using HTML canvas element #741

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cursive-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,33 @@ default-features = false
optional = true
version = "0.9"

[dependencies.js-sys]
version = "0.3.64"
optional = true

[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 = []
doc-cfg = []
builder = ["inventory", "cursive-macros/builder"]
markdown = ["pulldown-cmark"]
ansi = ["ansi-parser"]
unstable_scroll = [] # Deprecated feature, remove in next version
wasm = ["js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"]

[lib]
name = "cursive_core"
12 changes: 6 additions & 6 deletions cursive-core/src/cursive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,8 @@ impl Cursive {
/// Runs a dummy event loop.
///
/// Initializes a dummy backend for the event loop.
pub fn run_dummy(&mut self) {
self.run_with(backend::Dummy::init)
pub async fn run_dummy(&mut self) {
self.run_with(backend::Dummy::init).await
}

/// Returns a new runner on the given backend.
Expand Down Expand Up @@ -871,23 +871,23 @@ impl Cursive {
/// Initialize the backend and runs the event loop.
///
/// Used for infallible backend initializers.
pub fn run_with<F>(&mut self, backend_init: F)
pub async fn run_with<F>(&mut self, backend_init: F)
where
F: FnOnce() -> Box<dyn backend::Backend>,
{
self.try_run_with::<(), _>(|| Ok(backend_init())).unwrap();
self.try_run_with::<(), _>(|| Ok(backend_init())).await.unwrap();
}

/// Initialize the backend and runs the event loop.
///
/// Returns an error if initializing the backend fails.
pub fn try_run_with<E, F>(&mut self, backend_init: F) -> Result<(), E>
pub async fn try_run_with<E, F>(&mut self, backend_init: F) -> Result<(), E>
where
F: FnOnce() -> Result<Box<dyn backend::Backend>, E>,
{
let mut runner = self.runner(backend_init()?);

runner.run();
runner.run().await;

Ok(())
}
Expand Down
40 changes: 33 additions & 7 deletions cursive-core/src/cursive_run.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{backend, event::Event, theme, Cursive, Vec2};
use std::borrow::{Borrow, BorrowMut};
#[cfg(not(feature = "async"))]
use std::time::Duration;

// How long we wait between two empty input polls
Expand Down Expand Up @@ -151,7 +152,7 @@ where
/// [1]: CursiveRunner::run()
/// [2]: CursiveRunner::step()
/// [3]: CursiveRunner::process_events()
pub fn post_events(&mut self, received_something: bool) {
pub async fn post_events(&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
Expand All @@ -175,11 +176,36 @@ where
}

if boring {
std::thread::sleep(Duration::from_millis(INPUT_POLL_DELAY_MS));
self.sleep().await;
self.boring_frame_count += 1;
}
}

#[cfg(not(feature = "wasm"))]
async fn sleep(&self) {
std::thread::sleep(Duration::from_millis(INPUT_POLL_DELAY_MS));
}

#[cfg(feature = "wasm")]
async fn sleep(&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<dyn FnMut()>;
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.
pub fn refresh(&mut self) {
self.boring_frame_count = 0;
Expand Down Expand Up @@ -211,9 +237,9 @@ where
/// during this step, and `false` otherwise.
///
/// [`run(&mut self)`]: #method.run
pub fn step(&mut self) -> bool {
pub async fn step(&mut self) -> bool {
let received_something = self.process_events();
self.post_events(received_something);
self.post_events(received_something).await;
received_something
}

Expand All @@ -230,12 +256,12 @@ where
///
/// [`step(&mut self)`]: #method.step
/// [`quit(&mut self)`]: #method.quit
pub fn run(&mut self) {
pub async fn run(&mut self) {
self.refresh();

// And the big event loop begins!
while self.is_running() {
self.step();
self.step().await;
}
}
}
}
19 changes: 19 additions & 0 deletions cursive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ libc = "0.2"
maplit = { version = "1.0", optional = true }
log = "0.4"
ahash = "0.8"
async-trait = "0.1.73"

[dependencies.bear-lib-terminal]
optional = true
Expand All @@ -48,6 +49,23 @@ version = "2"
optional = true
version = "0.26"

[dependencies.wasm-bindgen]
optional = true
version = "0.2.63"

[dependencies.web-sys]
optional = true
version = "0.3.64"
features = [
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"KeyboardEvent",
"TextMetrics",
"Window",
"Document",
"console",
]

[features]
doc-cfg = ["cursive_core/doc-cfg"] # Enable doc_cfg, a nightly-only doc feature.
builder = ["cursive_core/builder"]
Expand All @@ -61,6 +79,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"]

[lib]
name = "cursive"
Expand Down
34 changes: 34 additions & 0 deletions cursive/src/backends/canvas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 WIDTH = 100;
const HEIGHT = 100;
context.font = `${fontHeight - 2}px monospace`;
for (let x = 0; x < WIDTH; x++) {
for (let y = 0; y < HEIGHT; y++) {
const n = WIDTH * 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));
context.fillStyle = back;
context.fillRect(x * fontWidth, y * fontHeight, fontWidth, fontHeight);
if (text != ' ') {
context.fillStyle = front;
context.fillText(text, x * fontWidth, (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}`;
}
5 changes: 5 additions & 0 deletions cursive/src/backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod crossterm;
pub mod curses;
pub mod puppet;
pub mod termion;
/// Provides a backend using the `wasm`.
pub mod wasm;

#[allow(dead_code)]
fn boxed(e: impl std::error::Error + 'static) -> Box<dyn std::error::Error> {
Expand All @@ -30,6 +32,7 @@ fn boxed(e: impl std::error::Error + 'static) -> Box<dyn std::error::Error> {
/// * Crossterm
/// * Pancurses
/// * Ncurses
/// * wasm
/// * Dummy
pub fn try_default() -> Result<Box<dyn cursive_core::backend::Backend>, Box<dyn std::error::Error>>
{
Expand All @@ -44,6 +47,8 @@ pub fn try_default() -> Result<Box<dyn cursive_core::backend::Backend>, Box<dyn
curses::pan::Backend::init().map_err(boxed)
} else if #[cfg(feature = "ncurses-backend")] {
curses::n::Backend::init().map_err(boxed)
} else if #[cfg(feature = "wasm-backend")] {
wasm::Backend::init().map_err(boxed)
} else {
log::warn!("No built-it backend, falling back to Dummy backend.");
Ok(cursive_core::backend::Dummy::init())
Expand Down
Loading