Recreate Dev.to offline using RustWasm
Dev's offline page is fun. Can we do that with Rust and WebAssembly?
The answer is yes. Let us do it.
First, we will create a simple Rust and WebAssembly application with Webpack.
npm init rust-webpack dev-offline-canvas
The Rust and WebAssembly ecosystem provides web_sys
that provides the necessary binding over the Web APIs. Check it out here.
The sample application already has web_sys
dependency. The web_sys
crate includes all the available WebAPI bindings.
Including all the WebAPI bindings will increase the binding file size. It is very important to include only the APIs that we need.
We will remove the existing feature
features = [
'console'
]
and replace it with the following:
features = [
'CanvasRenderingContext2d',
'CssStyleDeclaration',
'Document',
'Element',
'EventTarget',
'HtmlCanvasElement',
'HtmlElement',
'MouseEvent',
'Node',
'Window',
]
The above list of features is the entire set of features that we will be using in this example.
Lets write some Rust
Open the src/lib.rs
.
replace the start()
function with the following:
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
Ok()
}
The #[wasm_bindgen(start)]
calls this function as soon as the WebAssembly Module is instantiated. Check out more about the start function in the spec here.
We will get the window
object in the Rust.
let window = web_sys::window().expect("should have a window in this context");
Then get the document from the window
object.
let document = window.document().expect("window should have a document");
Create a Canvas element and append it to the document.
let canvas = document
.create_element("canvas")?
.dyn_into::<web_sys::HtmlCanvasElement>()?;
document.body().unwrap().append_child(&canvas)?;
Set width, height, and the border for the canvas element.
canvas.set_width(640);
canvas.set_height(480);
canvas.style().set_property("border", "solid")?;
In the Rust, the memories are discarded once the execution goes out of context or when the method returns any value. But in JavaScript, the window
, document
is alive as long as the page is up and running.
So it is important to create a reference for the memory and make it live statically until the program is completely shut down.
Get the Canvas' rendering context and create a wrapper around it in order to preserve its lifetime.
RC
stands for Reference Counted
.
The type Rc
This reference is cloned and used for callback methods.
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()?;
let context = Rc::new(context);
Since we are going to capture the mouse events. We will create a boolean variable called pressed
. The pressed
will hold the current value of mouse click
.
let pressed = Rc::new(Cell::new(false));
Now we need to create a closure (call back function) for mouseDown
| mouseUp
| mouseMove
.
{ mouse_down(&context, &pressed, &canvas); }
{ mouse_move(&context, &pressed, &canvas); }
{ mouse_up(&context, &pressed, &canvas); }
We will define the actions that we need to do during those events as separate functions. These functions take the context of the Canvas element and pressed status.
fn mouse_up(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, pressed: &std::rc::Rc<std::cell::Cell<bool>>, canvas: &web_sys::HtmlCanvasElement) {
let context = context.clone();
let pressed = pressed.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
pressed.set(false);
context.line_to(event.offset_x() as f64, event.offset_y() as f64);
context.stroke();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
fn mouse_move(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, pressed: &std::rc::Rc<std::cell::Cell<bool>>, canvas: &web_sys::HtmlCanvasElement){
let context = context.clone();
let pressed = pressed.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
if pressed.get() {
context.line_to(event.offset_x() as f64, event.offset_y() as f64);
context.stroke();
context.begin_path();
context.move_to(event.offset_x() as f64, event.offset_y() as f64);
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
fn mouse_down(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, pressed: &std::rc::Rc<std::cell::Cell<bool>>, canvas: &web_sys::HtmlCanvasElement){
let context = context.clone();
let pressed = pressed.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
context.begin_path();
context.set_line_width(5.0);
context.move_to(event.offset_x() as f64, event.offset_y() as f64);
pressed.set(true);
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
They are very similar to how your JavaScript
API will look like but they are written in Rust.
Now we are all set. We can run the application and draw inside the canvas. :tada: :tada: :tada:
But we do not have any colours.
Lets add some colours.
To add the colour swatches. Create a list of divs and use them as a selector.
Define the list of colours that we need to add inside the start
program.
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
// ....... Some content
let colors = vec!["#F4908E", "#F2F097", "#88B0DC", "#F7B5D1", "#53C4AF", "#FDE38C"];
Ok()
}
Then run through the list and create a div for all the colours and append it to the document. For every div add an onClick
handler too to change the colour.
for c in colors {
let div = document
.create_element("div")?
.dyn_into::<web_sys::HtmlElement>()?;
div.set_class_name("color");
{
click(&context, &div, c.clone()); // On Click Closure.
}
div.style().set_property("background-color", c);
let div = div.dyn_into::<web_sys::Node>()?;
document.body().unwrap().append_child(&div)?;
}
The click hander is as follows:
fn click(context: &std::rc::Rc<web_sys::CanvasRenderingContext2d>, div: &web_sys::HtmlElement, c: &str) {
let context = context.clone();
let c = JsValue::from(String::from(c));
let closure = Closure::wrap(Box::new(move || {
context.set_stroke_style(&c);
}) as Box<dyn FnMut()>);
div.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget();
}
Now a little beautification. Open the static/index.html
and add the style for the colour div.
<style>
.color {
display: inline-block;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
margin: 10px;
}
</style>
That is it, we have created the application. :tada:
Check out the demo application available here.
I hope this gives you a motivation to start your awesome WebAssembly journey. If you have any questions/suggestions/feel that I missed something feel free to add a comment.
You can follow me on Twitter.
Inspired from the article.
Check out my more WebAssembly articles here.