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 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions cursive-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ lazy_static = "1"
ahash = "0.8"
serde_json = "1.0.85"
serde_yaml = "0.9.11"
futures = "0.3.28"

[dependencies.cursive-macros]
path = "../cursive-macros"
Expand Down Expand Up @@ -64,13 +65,34 @@ 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", "async"]
async = []

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

/// run_dummy with async feature
#[cfg(feature = "async")]
pub fn run_dummy(&mut self) {
futures::executor::block_on(self.run_with(backend::Dummy::init))
}

/// Returns a new runner on the given backend.
///
/// Used to manually control the event loop. In most cases, running
Expand Down Expand Up @@ -871,16 +878,29 @@ impl Cursive {
/// Initialize the backend and runs the event loop.
///
/// Used for infallible backend initializers.
#[cfg(not(feature = "async"))]
pub fn run_with<F>(&mut self, backend_init: F)
where
F: FnOnce() -> Box<dyn backend::Backend>,
{
self.try_run_with::<(), _>(|| Ok(backend_init())).unwrap();
}

/// Initialize the backend and runs the event loop.
///
/// Used for infallible backend initializers.
#[cfg(feature = "async")]
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())).await.unwrap();
}

/// Initialize the backend and runs the event loop.
///
/// Returns an error if initializing the backend fails.
#[cfg(not(feature = "async"))]
pub fn try_run_with<E, F>(&mut self, backend_init: F) -> Result<(), E>
where
F: FnOnce() -> Result<Box<dyn backend::Backend>, E>,
Expand All @@ -892,6 +912,19 @@ impl Cursive {
Ok(())
}

/// try run with async
#[cfg(feature = "async")]
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().await;

Ok(())
}

/// Stops the event loop.
pub fn quit(&mut self) {
self.running = false;
Expand Down
81 changes: 80 additions & 1 deletion 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,6 +152,7 @@ where
/// [1]: CursiveRunner::run()
/// [2]: CursiveRunner::step()
/// [3]: CursiveRunner::process_events()
#[cfg(not(feature = "async"))]
pub fn post_events(&mut self, received_something: bool) {
let boring = !received_something;
// How many times should we try if it's still boring?
Expand All @@ -175,11 +177,67 @@ where
}

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

/// post_events asynchronously
#[cfg(feature = "async")]
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
// 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().await;
self.boring_frame_count += 1;
}
}

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

#[cfg(feature = "async")]
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,12 +269,21 @@ where
/// during this step, and `false` otherwise.
///
/// [`run(&mut self)`]: #method.run
#[cfg(not(feature = "async"))]
pub fn step(&mut self) -> bool {
let received_something = self.process_events();
self.post_events(received_something);
received_something
}

/// step asynchronously
#[cfg(feature = "async")]
pub async fn step(&mut self) -> bool {
let received_something = self.process_events();
self.post_events(received_something).await;
received_something
}

/// Runs the event loop.
///
/// It will wait for user input (key presses)
Expand All @@ -230,6 +297,7 @@ where
///
/// [`step(&mut self)`]: #method.step
/// [`quit(&mut self)`]: #method.quit
#[cfg(not(feature = "async"))]
pub fn run(&mut self) {
self.refresh();

Expand All @@ -238,4 +306,15 @@ where
self.step();
}
}

/// Runs the event loop asynchronously.
#[cfg(feature = "async")]
pub async fn run(&mut self) {
self.refresh();

// And the big event loop begins!
while self.is_running() {
self.step().await;
}
}
}
19 changes: 19 additions & 0 deletions cursive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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 +78,8 @@ 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", "async"]
async = ["cursive_core/async"]

[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, width, height) {
// console.time('paint');
genieCS marked this conversation as resolved.
Show resolved Hide resolved
const data = new Uint8Array(buffer);
const canvas = document.getElementById('cursive-wasm-canvas');
const context = canvas.getContext('2d');
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);
}
}
}
// console.timeEnd('paint');
genieCS marked this conversation as resolved.
Show resolved Hide resolved
}

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