💾 Archived View for gemini.ctrl-c.club › ~fte368 › 2023 › 2023-03-18_tauri.gmi captured on 2023-03-20 at 19:35:54. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
There are already some choices for developing GUI programs with Rust. I don't want to rely on a C library for doing this, so a web based solution is a natural choice. Tauri provides the infrastructure to create a webview based application in Rust (something similar to Electron).
(work in progress)
The progress can be monitored in the public repository linked below. Also the final contents of all files are visible here.
Public repository of this project on BitBucket
I want to create a simple application which demonstrates the most central concepts for a Tauri application. It has to show some GUI and has to demonstrate at least the use of Tauri commands, possibly also Tauri events.
I chose a stopwatch. The watch can include start/stop buttons which control the backend and it could use Tauri events which trigger the update of the timer display.
This is the secod try - the first one is described here:
I chose Yew because some days ago there was a new version of create-tauri-app released which is able to generate a working app using Yew.
Components used in this project:
Tauri, portable GUI programs with a Rust backend
Yew - another Rust web framework
Trunk - WASM application bundler for Rust
I will not describe here how to setup Rust. You can look this up on the Rust website. Rust is able to generate code in WASM, another way to let programs run inside the browser. To connect the Javascript world and WASM generated from Rust there is the wasm-bindgen library in Rust.
Module bundling in Yew is done by "Trunk".
First you need to install Node.js and npm because create-tauri-app needs them.
When you have Node.js installed you can install the create-tauri-app command:
npm install -g create-tauri-app
Npm is the Node.js package manager. The -g switch to the install command makes the command globally available on your computer.
Because Yew uses trunk as its module bundler it has to be installed also - this time using cargo, the Rust package manager:
cargo install --locked trunk
The installation takes some time because cargo has to update the crates index, download all necessary components and compile them. Don't be impatient!
Important for Apple users using a Mac with an M1 processor: Until wasm-bindgen has pre-built binaries for Apple M1, M1 users will need to install wasm-bindgen manually:
cargo install --locked wasm-bindgen-cli
Now it's time to create the example project. Tauri's create-tauri-app command supports a template for Yew, so with the following command you can create a working Tauri app:
create-tauri-app -> Project name: stopwatch -> Language: Rust -> UI template: Yew # then do: cd stopwatch cargo tauri dev
This will again take some time because Cargo has to install and compile a lot of packages (crates). After some time you will get a nice looking application window where you can enter your name and click on the button named "Greet". You will then get a greeting back from Rust - this means the frontend programmed in Yew sent your name to the Tauri backend, the backend created the greeting and sent it back to the frontend. Yew got the message and displayed the greeting in the user interface. A fully working Tauri application!
The user interface will show a seconds counter and three buttons: reset, start and stop. Reset should set the seconds to zero, start will start the counting of seconds and stop will halt the counting of seconds, keeping the just reached seconds value in the display.
Tauri's implementation approach is a sharp separation between the presentation logic in the webview on the one hand and the algorithmic solution and access to local computer resources/network resources at the other hand. This means that most of the computational work and the application data model has to be located in the backend.
Tauri provides procedure calls and events for communication between the webview and the backend. The plan for the stopwatch is the following:
Tauri names backend procedures which can be called from the frontend "commands". For now we will prepare the backend to receive the procedure calls for the button presses. For this we will introduce only one command which gets the name of the button as parameter.
First we change the backend. This is located in the file src-tauri/src/main.rs in the project. The template already contains a command called greet:
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) }
In this form the command receives a name as parameter and returns a greeting containing the name as result.
The buttons will not request to get any string as result when they get pressed - they should simply change the state of the backend process. So the command will return nothing. To see that the backend in fact gets the information about a pressed button we simply print out a string when this happens (for now). The changed command looks like this:
#[tauri::command] fn button_press(name: &str) { println!("Button pressed: {}", name); }
The main function does register the commands. Because the name of the only command is changed now the main function has to be adapted:
fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![button_press]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
That's it for the backend! Now the changes for the frontend - they happen in the file src/app.rs in the project.
First we delete the following blocks within the app function:
let greet_input_ref = ... and let name = use_state... and let greet_msg = ... and { let greet_msg = greet_msg.clone(); let name = name.clone(); ... up to the line before the html! macro.
These are all lines handling the name and greeting response. Within the html macro the last parts can be deleted (the form and the greeting text):
<form class="row" ... up to <p><b>{ &*greet_msg }</b></p>
Now the GreetArgs struct at the top of the file is unused. We change it to the name "ButtonArgs" which we will use later:
#[derive(Serialize, Deserialize)] struct ButtonArgs<'a> { name: &'a str, }
We also need a state of the seconds value in the frontend and an enum for the Button values. The enum is not strictly needed but it makes the coding of the buttons inside the callback more formal and better readable. The definitions got after the "ButtonArgs" definition an before the app function.
struct SecondsState { seconds: u64, } enum ButtonName { Reset, Start, Stop }
For the button clicks we use a callback function which gets defined outside the app function:
fn button_click(name: ButtonName ) { let namestr: &str; match name { ButtonName::Reset => namestr = "Reset", ButtonName::Start => namestr = "Start", ButtonName::Stop => namestr = "Stop", } spawn_local(async move { let args = to_value(&ButtonArgs { name: namestr }).unwrap(); invoke("button_press", args).await; }); }
Here the ButtonName enum is used as parameter. The match statement sets the variable namestr according to the value of the parameter. A default path for the match statement is not necessary because all values of the enum are handled.
After setting the button name as string the interesting part comes. The spawn_local and async move create an asynchronous environment for calling the Tauri command. The frontend running as WASM application calls a Javascript function which in turn calls the Tauri backend command. That's why the parameter has to be converted to a Javascript dictionary which contains the named parameter "name". Also the Tauri commands return a Future, which translates to a Javascript Promise. That's why the async environment is necessary and also the "await" for the result of the "invoke" function. The result gets ignored then because we are not interested in it.
Now the app function gets changed a bit. First we need to hold the frontend model for the seconds in a "state" to make it immune against competing access from multiple threads. So within the app function the follwing line is used for this:
let seconds_state = use_state(|| SecondsState{seconds: 2453});
We can assign a value here already. Normally 0 would be used but with another value it's easier to see if the HTML generation works.
Inside the html! macro (directly before the closing </main> the following is inserted:
<p><b>{"Seconds: "}{(&seconds_state).seconds}</b></p> <p> <button onclick={Callback::from(|_| button_click(ButtonName::Reset))}>{"Reset"}</button> <button onclick={Callback::from(|_| button_click(ButtonName::Start))}>{"Start"}</button> <button onclick={Callback::from(|_| button_click(ButtonName::Stop))}>{"Stop"}</button> </p>
When you enter the command "cargo tauri dev" the application should compile without error. The application window should open showing the seconds counter and the three buttons. When you press one of the buttons it's name should be printed at the console where you entered the cargo commmand.
All changes described in this paragraph can be seen in the repository in commit cf530b4. It is linked below.
Commit with all changes of this paragraph
to be continued ...