Skip to content

Commit

Permalink
add wasm-backend using HTML canvas element(#1)
Browse files Browse the repository at this point in the history
* add wasm-backend

* add wasm configuration and replace sleep

* add wasm-backend
  • Loading branch information
genieCS committed Jul 13, 2023
1 parent f80e20d commit e219ed8
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 3 deletions.
12 changes: 11 additions & 1 deletion cursive-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,23 @@ default-features = false
optional = true
version = "0.9"

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

[dependencies.getrandom]
optional = true
version = "0.2"
features = ["js"]

[features]
default = []
default = ["wasm"]
doc-cfg = []
builder = ["inventory", "cursive-macros/builder"]
markdown = ["pulldown-cmark"]
ansi = ["ansi-parser"]
unstable_scroll = [] # Deprecated feature, remove in next version
wasm = ["js-sys", "getrandom"]

[lib]
name = "cursive_core"
20 changes: 19 additions & 1 deletion cursive-core/src/cursive_run.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::{backend, event::Event, theme, Cursive, Vec2};
use std::borrow::{Borrow, BorrowMut};
#[cfg(not(feature = "wasm"))]
use std::time::Duration;

#[cfg(feature = "wasm")]
use js_sys::Date;

// How long we wait between two empty input polls
const INPUT_POLL_DELAY_MS: u64 = 30;

Expand Down Expand Up @@ -175,11 +179,25 @@ where
}

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

#[cfg(not(feature = "wasm"))]
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();
}
}

/// Refresh the screen with the current view tree state.
pub fn refresh(&mut self) {
self.boring_frame_count = 0;
Expand Down
25 changes: 24 additions & 1 deletion cursive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,33 @@ 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",
]

[dependencies.getrandom]
optional = true
version = "0.2"
features = ["js"]


[features]
doc-cfg = ["cursive_core/doc-cfg"] # Enable doc_cfg, a nightly-only doc feature.
builder = ["cursive_core/builder"]
blt-backend = ["bear-lib-terminal"]
default = ["ncurses-backend"]
default = ["wasm-backend"]
ncurses-backend = ["ncurses", "maplit"]
pancurses-backend = ["pancurses", "maplit"]
termion-backend = ["termion"]
Expand All @@ -61,6 +83,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"]

[lib]
name = "cursive"
Expand Down
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
194 changes: 194 additions & 0 deletions cursive/src/backends/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#![cfg(feature = "wasm-backend")]

use cursive_core::{
event::Event,
Vec2,
theme,
};
use std::collections::VecDeque;
use std::rc::Rc;
use std::cell::RefCell;
use web_sys::{
HtmlCanvasElement,
CanvasRenderingContext2d,
};
use wasm_bindgen::prelude::*;
use crate::backend;


/// Backend using wasm.
pub struct Backend {
canvas: HtmlCanvasElement,
ctx: CanvasRenderingContext2d,
color: RefCell<ColorPair>,
font_height: usize,
font_width: usize,
events: Rc<RefCell<VecDeque<Event>>>,
}
impl Backend {
/// Creates a new Cursive root using a wasm backend.
pub fn init() -> std::io::Result<Box<dyn backend::Backend>> {
let document = web_sys::window()
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to get window",
))?
.document()
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to get document",
))?;
let canvas = document.create_element("canvas")
.map_err(|_| std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to create canvas",
))?
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to cast canvas",
))?;
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::<CanvasRenderingContext2d>()
.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 events = Rc::new(RefCell::new(VecDeque::new()));
let cloned = events.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
for c in event.key().chars() {
cloned.borrow_mut().push_back(Event::Char(c));
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
.map_err(|_| std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to add event listener",
))?;
closure.forget();

let c = Backend {
canvas,
ctx,
color,
font_height,
font_width,
events,
};
Ok(Box::new(c))
}
}

impl cursive_core::backend::Backend for Backend {
fn poll_event(self: &mut Backend) -> Option<Event> {
self.events.borrow_mut().pop_front()
}

fn set_title(self: &mut Backend, title: String) {
self.canvas.set_title(&title);
}

fn refresh(self: &mut Backend) {}

fn has_colors(self: &Backend) -> bool {
true
}

fn screen_size(self: &Backend) -> Vec2 {
Vec2::new(self.canvas.width() as usize, self.canvas.height() as usize)
}

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();
}

fn clear(self: &Backend, color: cursive_core::theme::Color) {
self.ctx.set_fill_style(&JsValue::from_str(&cursive_to_color(color)));
}

fn set_color(self: &Backend, color_pair: cursive_core::theme::ColorPair) -> cursive_core::theme::ColorPair {
let mut color = self.color.borrow_mut();
*color = cursive_to_color_pair(color_pair);
color_pair
}

fn set_effect(self: &Backend, _: cursive_core::theme::Effect) {
}

fn unset_effect(self: &Backend, _: cursive_core::theme::Effect) {
}

fn name(&self) -> &str {
"cursive-wasm-backend"
}
}


/// Type of hex color which starts with #.
pub type Color = String;

/// Type of color pair.
pub struct ColorPair {
/// Foreground text color.
pub front: Color,
/// Background color.
pub back: Color,
}

/// 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(),
}
}

/// Convert cursive color pair to hex color pair.
pub fn cursive_to_color_pair(c: theme::ColorPair) -> ColorPair {
ColorPair {
front: cursive_to_color(c.front),
back: cursive_to_color(c.back),
}
}
13 changes: 13 additions & 0 deletions cursive/src/cursive_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
/// siv.run_crossterm().unwrap();
/// #[cfg(feature = "blt-backend")]
/// siv.run_blt();
/// #[cfg(feature="wasm-backend")]
/// siv.run_wasm().unwrap();
/// ```
pub trait CursiveExt {
/// Tries to use one of the enabled backends.
Expand Down Expand Up @@ -59,6 +61,10 @@ pub trait CursiveExt {
#[cfg(feature = "blt-backend")]
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "blt-backend")))]
fn run_blt(&mut self);

/// Creates a new Cursive root using a wasm backend.
#[cfg(feature = "wasm-backend")]
fn run_wasm(&mut self) -> std::io::Result<()>;
}

impl CursiveExt for cursive_core::Cursive {
Expand All @@ -74,6 +80,8 @@ impl CursiveExt for cursive_core::Cursive {
self.run_pancurses().unwrap()
} else if #[cfg(feature = "ncurses-backend")] {
self.run_ncurses().unwrap()
} else if #[cfg(feature = "wasm-backend")] {
self.run_wasm().unwrap()
} else {
log::warn!("No built-it backend, falling back to Cursive::dummy().");
self.run_dummy()
Expand Down Expand Up @@ -110,4 +118,9 @@ impl CursiveExt for cursive_core::Cursive {
fn run_blt(&mut self) {
self.run_with(crate::backends::blt::Backend::init)
}

#[cfg(feature = "wasm-backend")]
fn run_wasm(&mut self) -> std::io::Result<()> {
self.try_run_with(crate::backends::wasm::Backend::init)
}
}
8 changes: 8 additions & 0 deletions cursive/src/cursive_runnable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,12 @@ impl CursiveRunnable {
pub fn blt() -> Self {
Self::new::<std::convert::Infallible, _>(|| Ok(backends::blt::Backend::init()))
}

/// Creates a new Cursive wrapper using the wasm backend.
///
/// _Requires the `wasm-backend` feature._
#[cfg(feature = "wasm-backend")]
pub fn wasm() -> Self {
Self::new(backends::wasm::Backend::init)
}
}
6 changes: 6 additions & 0 deletions cursive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ pub fn blt() -> CursiveRunnable {
CursiveRunnable::blt()
}

/// Creates a new Cursive root using a crossterm backend.
#[cfg(feature = "wasm-backend")]
pub fn wasm() -> CursiveRunnable {
CursiveRunnable::wasm()
}

/// Creates a new Cursive root using a dummy backend.
///
/// Nothing will be output. This is mostly here for tests.
Expand Down

0 comments on commit e219ed8

Please sign in to comment.