Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/trevorwslee/wasm_calculator

Implement a Simple WASM Calculator in Rust Using Leptos, and with DumbCalculator
https://github.com/trevorwslee/wasm_calculator

Last synced: about 1 month ago
JSON representation

Implement a Simple WASM Calculator in Rust Using Leptos, and with DumbCalculator

Awesome Lists containing this project

README

        

---
title: 'Implement a Simple WASM Calculator in Rust Using Leptos, and with DumbCalculator'
published: true
tags: 'webassembly, rust, leptos'
id: 1811064
date: '2024-04-04T11:44:32Z'
---

# Implement a Simple WASM Calculator in Rust Using Leptos, and with DumbCalculator

![WASM Calculator](wasm_calculator.png)

## Development Environment

Here I will assume program development tools like
* Of course, the [Rust](https://www.rust-lang.org/tools/install) programming language itself.
* The popular [VSCode](https://code.visualstudio.com/download) program development editor / IDE, with the extension
[rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
* Preferably the popular source control tool [GIT](https://git-scm.com/downloads).

## Rust Crates Used

* [Leptos](https://leptos.dev/) -- a Rust framework to develop WASM app in Rust.
* [DumbCalculator](https://docs.rs/rusty_dumb_tools/0.1.7/rusty_dumb_tools/calculator/struct.DumbCalculator.html) of [rusty_dumb_tools](https://github.com/trevorwslee/rusty_dumb_tools) -- a simple Rust component that acts like a simple physical calculator.

## Preparation for WASM Development

WASM development in Rust can be enabled with the [Trunk](https://trunkrs.dev/) tool.
Indeed, Trunk is used here, and we install Trunk like

```
cargo install trunk
```

After installing Trunk, we will also need to add the Rust target `wasm32-unknown-unknown`, like
```
rustup target add wasm32-unknown-unknown
```

## Kick-starting `wasm_calculator`

To get kick-started, create a new Rust project `wasm_calculator` like
```
cargo new wasm_calculator
```

Open the just created folder `wasm_calculator` with VSCode like
```
cd wasm_calculator
code .
```

In VSCode, open and edit `Cargo.toml` adding the necessary dependencies, like
```
...
[dependencies]
leptos = { version = "0.6.5", features = ["csr"] }
rusty_dumb_tools = "0.1.8"
```

Add the Trunk config file `Trunk.toml` with content like
```
[build]
target = "trunk.html"
```

Add `trunk.html`, which is sort of the template for our final output `index.html`
```


```

Note that without the above mentioned `Trunk.toml` config file, Trunk will in fact look for `index.html` as the template instead.
(However, we would like to reserve `index.html` for other purposes, and hence would use `trunk.html` instead.)

Our WASM code will be "mounted" to `` of this `trunk.html`, let's see it working
```
trunk serve --open
```

This will run the Trunk server serving the `trunk.html` merged with whatever WASM code in `main.rs`

The server will keep running, and hot update the page whenever `trunk.html` or `main.rs` get changed

Say, change the `` of `trunk.html` to
```

— WASM Calculator —


```

See that the browser page is updated accordingly.

## The Basis of `wasm_calculator`

The initially generated `main.rs` is actually not WASM code to be "mounted" to ``.
To "mount" some simple WASM code (written in Rust), can change `main.rs` like
```
use leptos::*;
fn main() {
mount_to_body(move || {
view! {

Hello, World!

}
});
}
```

Again, our modification will be hot-deployed, and we should see that
```

Hello, World!

```
is "mounted" to ``, after `

— WASM Calculator —

`

Here is some little insights from the above code:
- `mount_to_body` is the function provided by Leptos to "mount" WASM code (written in Rust) to ``
- `mount_to_body` can accept a closure which accepts no argument and returns the result of calling the `view!` macro, which is of cause also provided by Leptos.
- Inside `view!`, we write "HTML" -- like `

Hello, World!
` -- which even looks like plain HTML, is in fact "legal" Rust code to be pre-processed by the macro `view!`.

In fact, normally, we will be coding our WASM code, in an `App()` function, and "mount" it like
```
use leptos::*;
fn main() {
mount_to_body(move || view! { });
}
fn App() -> impl IntoView {
view! {

Hello, World!

}
}
```

As mentioned above, things inside `view!` will be pre-processed to be transcribed to Rust code in compile time, hence, we should be able to include regular Rust code inside `view!` like
```
fn App() -> impl IntoView {
let color = "red";
let who = "World";
view! {

Hello, {who}!

}
}
```
As shown, we can enclose regular Rust code inside `{}` as above `{format!("color:{}", color)}` and `Hello, {who}!`

We can even "nest" `view!` like
```
fn App() -> impl IntoView {
let color = "red";
let who = "World";
view! {
{
view! {

Hello, {who}!

}
}
}
}
```
Why "nest" `view!`? Hopefully, it will become apparent in later sections.

Let's extract the styling of the `

` to live else-where. Indeed, we can put our CSS in `trunk.html` and use it in `App()` like

`trunk.html`
```

.test-class {
color: green;
}

...
```

`main.rs`
```
...
fn App() -> impl IntoView {
let who = "World";
view! {
{
view! {

Hello, {who}!

}
}
}
}
```

Now, let's add two buttons to it to make it interactive.

Note that Leptos will generate HTML once only -- like the above `App()` will only be called once to generate initial HTML code -- any updates are triggered with "signals", and rendered with closures (more about this later).

Therefore, to make it interactive, not only we will need to add some interactive HTML elements, like ``, we will also need to make use of "signals" as well.

First, let's add two ``s, and be able to log to the browser's console when any of the button is clicked:
```
use leptos::*;
use leptos::logging::log;
use web_sys::MouseEvent;
...
fn App() -> impl IntoView {
let who = "World";
let on_clicked = |ev: MouseEvent| {
let value = event_target_value(&ev);
log!("* clicked value [{}]", value);
};
view! {
{
view! {

Hello, {who}!

}
}
I am 1
I am 2
}
}
```

Notes:
* Two more dependencies
```
use leptos::logging::log;
use web_sys::MouseEvent;
```
* The closure defined by `on_clicked` will be called when any one of the button is clicked
```
I am 1
I am 2
```
Notice that the two ``s are placed in the same level as the "nested" `view!`
* Each button is associated with a value (e.g. `value="1"`), and when a button is clicked, the associated value is retrieved and printed out to the browser's console
```
let value = event_target_value(&ev);
log!("* clicked value [{}]", value);
```

Now, let's make use of "signal" to trigger update of the `

` content

```
fn App() -> impl IntoView {
let (clicked_value, set_clicked_value) = create_signal(String::from(""));
let on_clicked = move |ev: MouseEvent| {
let value = event_target_value(&ev);
log!("* clicked value [{}]", value);
set_clicked_value.set(value);
};
view! {
{
move || view! {

{
let value = clicked_value.get();
format!("Hello, [{}]!", value)
}

}
}
I am 1
I am 2
}
}
```

Notes:
* The "signal" is created like
```
let (clicked_value, set_clicked_value) = create_signal(String::from(""));
```
- such a "signal" is composed of a "getter" `clicked_value` and a "setter" `set_clicked_value`
- the type of the "signal" is `String`, and the "signal" is initialized to the empty `String`
* The "setter" `set_clicked_value` is called to set new value in the closure defined by `on_click`, which is called when any of the button is clicked
```
let on_clicked = move |ev: MouseEvent| {
...
set_clicked_value.set(value);
}
```
Notice that the closure captures variables, like `set_clicked_value`, moved; and this is the requirement of using "signal"
* The "user" of the "signal" is the `

` created by the nested `view!`
```
move || view! {
{
let value = clicked_value.get();
format!("Hello, [{}]!", value)
}

}
```
Notice:
- the "nested" `view` is now a "moved" closure, since it is using the "signal" to get the new value set
- the value set is returned by calling `clicked_value.get()`
- by using the "signal", Leptos knows that the closure need be called again to update the content of the nested `view!` when the "signal" is updated
* Here is what will happen when a button is clicked
- the closure `on_click` gets called
- the "signal" gets updated when `set_clicked_value` is called
- the closure of the "nested" `view!` gets called when the "signal" is updated, which will update the `
` generated by the "nested" `view!`

Let's change the buttons to ones that simulate the key presses for `1 + 2 =`

```
1
+
2
=
```

And also add `DumbCalculator` into the picture
```
...
use std::cell::RefCell;
use rusty_dumb_tools::calculator::*;
...
fn App() -> impl IntoView {
let calculator_ref = RefCell::new(DumbCalculator::new());
let (clicked_value, set_clicked_value) = create_signal(String::from(""));
let on_clicked = move |ev: MouseEvent| {
let value = event_target_value(&ev);
log!("* clicked value [{}]", value);
set_clicked_value.set(value);
};
view! {
{
move || view! {

{
let mut calculator = calculator_ref.borrow_mut();
let value = clicked_value.get();
if !value.is_empty() {
calculator.push(value.as_str()).unwrap();
}
let result_value = calculator.get_display_sized(10);
format!("[{}]", result_value)
}

}
}
1
+
2
=
}
}
```
Notes:
* An instance of `DumbCalculator` is created and stored in a `RefCell`, and assigned to `calculator_ref`; note that even `calculator_ref` is immutable, the instance of `DumbCalculator` can be retrieved mutable
```
let calculator_ref = RefCell::new(DumbCalculator::new());
```
Note that since `App()` will only be called one, only a single instance of `DumbCalculator` will ever be created, unless the browser page is refreshed.
* The "nested" `view!` code for the `
` is changed to something like
```
{
let mut calculator = calculator_ref.borrow_mut();
let value = clicked_value.get();
if !value.is_empty() {
calculator.push(value.as_str()).unwrap();
}
let result_value = calculator.get_display_sized(10);
format!("[{}]", result_value)
}

```
- The instance of `DumbCalculator` is "borrowed" mutable
```
let mut calculator = calculator_ref.borrow_mut();
```
- The button value, which is supposed to simulate calculator key press, is pushed to the `DumbCalculator` like
```
calculator.push(value.as_str()).unwrap();
```
- What the calculator display should look like is rendered in the content of the `
`
```
let result_value = calculator.get_display_sized(10);
format!("[{}]!", result_value)
```

Now, let's add two buttons for "AC" (*all cancel*) as well as "⬅" (*undo*)

```
fn App() -> impl IntoView {
let calculator_ref = RefCell::new(DumbCalculator::new());
let (clicked_value, set_clicked_value) = create_signal(String::from(""));
let on_clicked = move |ev: MouseEvent| {
let value = event_target_value(&ev);
log!("* clicked value [{}]", value);
set_clicked_value.set(value);
};
view! {
{
move || view! {

{
let mut calculator = calculator_ref.borrow_mut();
let value = clicked_value.get();
if value == "ac" {
calculator.reset();
} else if value == "<" {
calculator.undo();
} else if !value.is_empty() {
calculator.push(value.as_str()).unwrap();
}
let result_value = calculator.get_display_sized(10);
format!("[{}]", result_value)
}

}
}
1
+
2
=

AC
{"⬅"}

}
}
```

Notes:
* The two additional buttons are added below the `1 + 2 =` buttons like
```


AC
{"⬅"}

```
* Clicking of the two buttons are handled like
```
let value = clicked_value.get();
if value == "ac" {
calculator.reset();
} else if value == "<" {
calculator.undo();
} else if !value.is_empty() {
calculator.push(value.as_str()).unwrap();
}
```

We are getting closer and closer. Let's add to the code the capability of showing the history of the calculator.

For this, we will be using another `history` "signal".
```
fn App() -> impl IntoView {
let calculator_ref = RefCell::new(DumbCalculator::new());
let (clicked_value, set_clicked_value) = create_signal(String::from(""));
let (history, set_history) = create_signal(String::from(""));
let on_clicked = move |ev: MouseEvent| {
let value = event_target_value(&ev);
log!("* clicked value [{}]", value);
set_clicked_value.set(value);
};
view! {
{
move || view! {

{
let mut calculator = calculator_ref.borrow_mut();
let value = clicked_value.get();
if value == "ac" {
calculator.reset();
} else if value == "<" {
calculator.undo();
} else if !value.is_empty() {
calculator.push(value.as_str()).unwrap();
}
let history = calculator.get_history_string(true);
match &history {
Some(hist) => set_history.set(hist.to_string()),
None => set_history.set("".to_string()),
}
let result_value = calculator.get_display_sized(10);
format!("[{}]", result_value)
}

}
}
1
+
2
=

AC
{"⬅"}
{move || view! {
{ history.get() }
}}

}
}
```

Notes:
* The `history` "signal" is added like
```
let (history, set_history) = create_signal(String::from(""));
```
* The `history` if set after pushing value to the calculator
```
let history = calculator.get_history_string(true);
match &history {
Some(hist) => set_history.set(hist.to_string()),
None => set_history.set("".to_string()),
}
```
* The updated `history` is displayed next to the "⬅" button
```
{move || view! {
{ history.get() }
}}
```

## Finally

Finally, let's add all the whistles and bells and finish off our calculator ...

Simply ... please replace the corresponding file as listed here:

- `trunk.html` -- https://github.com/trevorwslee/wasm_calculator/blob/master/trunk.html
- `main.rs` -- https://github.com/trevorwslee/wasm_calculator/blob/master/src/main.rs

Or ... simply ... clone the GitHub repo https://github.com/trevorwslee/wasm_calculator

## Manually Deploy to GitHub Pages

Now that we have the final result WASM calculator, we may want to deploy it to GitHub Pages.

Assuming a GitHub account, say like mine -- `trevorwslee`, I can easily post some static pages to my GitHub Pages -- `https://trevorwslee.github.io/`

What I needed is a GitHub repository with the "same" name `trevorwslee.github.io` -- https://github.com/trevorwslee/trevorwslee.github.io

In order for GitHub Pages to host our WASM Calculator app
* With Trunk, build *release* version specifying the context to use in GitHub Pages, like mine
- The context is `WASMCalculator`
- Then the URL to the WASM app is https://trevorwslee.github.io/WASMCalculator/
* To build such *release*, run Trunk like
```
trunk build --release --public-url "WASMCalculator"
```
* After building the *release* with Trunk, the `dist` folder will contain the files
- `index.html`
- `wasm_calculator-XXX.wasm`
- `wasm_calculator-XXX.js`
to be checked in to `WASMCalculator` folder of the GitHub Pages repository, like mine -- https://github.com/trevorwslee/trevorwslee.github.io/tree/main/WASMCalculator
* After a few moments, the WASM Calculator should be ready to try out -- https://trevorwslee.github.io/WASMCalculator/

## Enjoy!

> Peace be with you!
> May God bless you!
> Jesus loves you!
> Amazing Grace!