Rust has been a language of interest and trepadation to me for some time, with it’s functional programming and strict rules (coming from python). The one thing I have identified as the most difficult part in creating my projects is… programming a GUI (graphical user interface).
There are a host of competing frameworks and libraries, each with it’s own trade-offs and complexity. There’s even a website to track to track the state of these, humorously called are we gui yet?
Suffice to say each application is it’s own challange and deserves forethough as to what to use and how.
With all that said, my personal projects are much smaller in scope and definately not great lookers. So I have decided to buld my applications for the web and leverage browsers to do the heavy lifting.
After several days and not insignificant trial, error and ChatGPT voodoo… I decided to write a blog post about how to do it so that others may come across it and have a more seamless experience.
0. Prerequisites
0.0. Knowledge
This guide expects familiarity with (but provides links to read on) the following topics:
- Rust’s project structure
- Struct’s and Trait’s
- Cursory knowledge on: ** Borrow Checker (ownership rules) ** Datatypes ** HTML and CSS syntax (for the gui) ** preludes ** closures ** match statements
- What macros are
- functions and methods
- Know how to do things in the terminal
Knowing these apriori is nice, but you can skip this reading and refer to this section if you bump into the terms later on.
0.1. Software
It goes without saying that you need rust installed.
And that cargo installed binaries are available under your system’s PATH variable so we can invoke them from the terminal.
To build our webapp and to run a local development server for it we will use trunk.
|
|
We also need to add the webAssembly (WASM) as a build target so we can build our application.
|
|
1. Setting up our project
1.0. New binary crate
Let’s create a new project called yew-simplified and go into it’s directory.
|
|
It’s good practice to verify our environment works before we start to tinker. Which we can do simply by running:
|
|
That should have printed out a ‘Hello World!’, confirming everything works.
1.1. Adding dependencies
Next we need to edit our cargo.toml file to add two new dependencies.
|
|
Yew is our main web-framework so we can build our GUI. It is worth noting at this time that there are multiple ways of making a GUI with Yew, but the method I am presenting is, as of this writing, not explained enough that I understood it immediately.
Stylist on the other hand is one of the easier ways of adding CSS styles to our GUI to make it look pretty. It also features compile-time checking for valid CSS so it helps during development as well!
1.2. Making an empty Landing Page
trunk expects a minimal index.html file to act as our webapp’s landing page and will not build without it.
Let’s create it then! It’s very important to put it at the crate’s root (where cargo.toml is) and not inside the src/ directory.
Then fill it in with some basic HTML like so:
|
|
And that’s done, now we have our project set up we can write our program.
Yew will be filling in the <body> tag with HTML during runtime to facilitate re-drawing our application.
We can test that our application works fine by running it with trunk:
|
|
This will build are (unoptimized) application, downloading and building dependancies along the way.
It will then launch a web-server and using the --open flag will open the locally hosted webapp in our system’s default browser.
NOTE: So long as the trunk server is running, any changes to the project’s files will prompt an automatic build and refresh on the opened page, so we do not need to do anything to see our changes take effect during development.
Additionally, you can use trunk build to build the website without hosting it.
2. Defining our application’s datastructure
The rest of the logic and GUI we will code directly to our main source file under src/main.rs.
Later we can always refactor our code to separate out more complex segments into their own individual modules when and if needed.
So far the file should just hold the main function that the application executes whenever it is run.
|
|
2.1. Defining the GUI emitted messages
Message passing is the primary way the interface will communicate with our Rust code to determine what to do. If there are no messages, our code should do nothing at all, conserving system resources.
Rust offers a convenient and efficient way of defining messages like this called an enum.
Let’s create a message that will be emitted when a button is pressed. We’ll add a little bit of complexity by identifying each button with an ‘id’ number. This demonstrates how to the GUI can pass some meaningful data back to our Rust code.
|
|
Note: the message ButtonPressed(usize) means that the message ButtonPressed holds a value with type usize.
While we can substitute usize with any other type or struct, in this case we use it because the usize represents a pointer for our architecture (32bit, 64bit etc..) and arrays require this exact pointer type when using it to access an element ( for example: MyArray[elementId]).
2.2. Defining our application state struct
Now we need to actually define a struct to hold all of our application’s state information, that is all of the persistent data our application needs to hold.
In this we’ll just hold some text to display to the user which we will edit and change to show the GUI reacting. We’ll also store an array of texts which we’ll use to place appropriately labelled buttons onto the screen.
|
|
Note: for button_texts we used the type Vec<String>, which is just an array that hold values of a single type, String type in this case.
This is of course a simplification but for our use case holds true.
2.3. Implementing defaults for the application state data
So far we just defined what kind of data our application will store, but when we first load the application we need to initialize them with some default values.
We can accomplish this by implementing the Default trait for our App struct.
|
|
Note: string literals in Rust are of type &str (string slice) and need to be converted into String types, you can read as to why with this link.
You can see two ways to accomplish this conversions in the code above.
3. Using Yew and Stylist to draw our application to the screen
To reiterate once more, this is just one way of using Yew v0.20.0 to draw a web-based GUI. This just so happens to be the way that I found works best, is understandable (by me) and I personally use.
Please refer to the Yew documentation to learn more approaches.
3.0. Adding our dependencies
So far we’ve not needed to add any dependencies to our src/main.rs file.
Now however, we will be going deep into making the web GUI with Yew and then making it pretty with Stylist to embed CSS directly in our application.
Add the following to the top of src/main.rs :
|
|
Note: stylist has many ways of applying CSS styles, including some Yew integration, it is well worth reading up on it inside it’s documentation.
Using the css! macro is just one I have found to be simple and understandable.
3.1. Implementing the Yew Component trait on our App struct
Now to actually integrate our App struct with Yew we will need to implement the Component trait and it’s 3 required methods:
createfor initialization (we added default values in step 2.3.)updatefor reacting to GUI sent messages and running our codeviewfor actually building the raw HTML that will be displayed as our GUI
Let’s do that now without much functionality.
|
|
Note: the type keyword is a type alias.
In this case we are not using Properties so we alias that to the ‘unit’ type.
3.2. Starting the Yew renderer
The final thing to do is to actually call Yew to draw our application in the main() function.
This has changed drastically in Yew v0.20.0 compared to previous versions, mostly to support new functionality.
To draw our simple app, edit the main function as such:
|
|
If all went well we now have ‘Hello World!’ plastered on a blank page!
3.3. Using Yew and Stylist together
Note: Once more, there are many ways to do this.
It is just one of the simplest ways I am describing here.
For more information read the Yew and Stylist documentation.
With that said, let’s use the css! macro to create a style and apply it to our ‘Hello World!’ paragraph!
Edit the view method as such:
|
|
Take note how we capture the output of the css! macro into a variable and then use it inside Yew’s html! macro, by putting it in curly braces {}.
With Yew we can run code while building the page’s HTML and embed the results as valid HTML text.
Also note how we added the style by the paragraphs’ class= attribute.
This is because Stylist creates unique style classes within the <head> of our page, then simply assigns the appropriate classes to our elements.
4. Adding interactivity to our application
4.1. A single button with a callback
The simplest form of interaction is clicking buttons, without much fanfare let’s add one now!
Modify our view method as such:
|
|
Here we created a button event with ctx.link().callback(...) , a callback is a function that will be called when a specific event happens, storing the resulting callback in a variable.
This helps separate out the code we write outside of the html! so it is easier to read.
In this case the callback will invoke an anonymouse (unnamed) function we defined in a closure and return an AppMessage which we can later use to identify what event has fired.
The closure itself takes in a Yew even type (MouseEvent in this case) which we do not use, so we ignore it by assigning it to _ .
Note: to read more on Yew context’s see the documentation.
But in this guide, we essentially just use it as a way to pass messages back to our App struct.
4.2. Making the application display state data
So far the application displayed just a static, unchanging interface. Now let’s add some much needed interactivity.
Firstly, we are going to use our App struct’s displayed_text to put text on the screen, instead of the standard hello world so far.
Let’s modify our view method once more as follows:
|
|
Here we replaced our "Hello World!" with a reference to our desired data inside App struct.
And just like that we are halfway done!
Note: We needed to clone the data we use from App as the html! macro consumes it, taking ownership.
Because we only have a borrowed reference to it (read-only) we needed to create a clone of it for the macro to consume.
4.3. Updating view based on callback AppMessages
Finally we can focus on running application code now we have event callbacks sending messages to run code to!
In this case, let’s conditionally run code only when a message comes in that we have a defined behavior for.
Modify the update method as follows:
|
|
Note: in this case we return true from the update method when we successfully match against a message.
This signals to Yew that it needs to call the view method again to re-draw our screen.
Returning false here (which match does for us) will signal Yew that there is no need to re-draw our application.
4.4. Dynamically creating many buttons
Lastly, let’s add a bit of complexity to our view method to dynamically populate the buttons, based on the strings inside App.button_texts .
This will demonstrate how to programmatically add elements to our HTML website.
Modify the view function as follows:
|
|
Here we replaced the old button handling logic with the button_list variable, that holds a computed HTML section.
We iterate through our App.button_texts array, calling the html! every time to create a button with a set style, including the in-line closure to generate the callback message.
We also need to call collect on it to squash the array into a single HTML definition so we can replace the old button definition within our return HTML document.
5. Conclusion
While in no way exhaustive, we saw one way how to make a simple web application with Rust.
From here on out it’s just a case of changing the view method to make our application prettier and render more things, changing the update method to run application code on event messages and adding more of them in the AppMessage struct.
We could refactor our code to separate out the logic from update or even view , but that is left as an excersise to the reader.
One final note here is that the App struct we have made here is just one, monolithic application.
You could implement more structs as components and have them more self-contained, reducing the amount of re-rendering your browser needs to do to only those elements.
This is beyond the scope of this guide however.
I hope that it was coherent and helpful! For those who didn’t follow along, here is our finished src/main.rs file in its entierty:
|
|