Building GUI Rust Apps With The Iced Toolkit
Tuesday, December 20, 2022
With the explosive popularity of the Rust programming language, many toolkits have popped up to offer Graphical User Interface (GUI) libraries. One such toolkit is called Iced. It's a cross platform toolkit that is written entirely in Rust, and offers extensive flexibility for creating GUI applications.
But why are we taking a look at this instead of another toolkit? Well, if you follow the desktop Linux world you may already know. Makers of the PopOS Linux distribution System 76 have embarked on the journey of creating their own desktop environment. In this environment, they have announced the intention of using Iced as a base to create their user interface. This means that if you like PopOS enough to want to write apps specifically for their distribution, you should probably write it with the Iced toolkit. You can also write apps for Windows, Mac, even WebApps with Iced, which is why it has become such an interesting toolkit.
With that in mind, we can learn a bit more about Iced, then get into creating a simple app in Iced. Iced is based on the ELM architecture. This simply means that the toolkit is based on a user feedback loop model. Information is presented to the user, the user interacts with it, and the information is then updated and presented again. To keep track of this, there are four main components to an Iced application:
- The State, or what information you will want to present and keep track of.
- The Messages, or the user interactions you care about and want to keep a watch on.
- The View Logic, which is how you will display the state and watch for messages.
- The Update Logic, which is how your app will react to messages and update the state.
To demonstrate this, we will be creating a small application. This app will show X and Y coordinates, and provide four buttons to move the coordinates around. It's simple, and will show just how Iced is supposed to work.
To get started, create a new rust project wherever you store your projects. I use my desktop for in progress works, but any folder will work. I name mine iced-demo
but you can name your project whatever works for you. In the newly created project, open the Cargo.toml
file and add the following line under the line that says [dependencies]
.
[dependencies]
iced = "0.5.2"
This will tell the rust compiler that we want to pull in the Iced toolkit when we are building the app. Next is to open up the src/main.rs
file. We will be adding library imports at the very top of this file, so that we can use the Iced code in the main file. We'll be using quite a few pieces of the library, so add the following to the top line of your main file, just above the fn main() {
line.
using iced::widget::{button, column, row, text};
using iced::executor;
using iced::{Alignment, Application, Command, Element, Theme, Settings};
This seems like a lot, but it really isn't much. A few things we pulled in were done so that we don't have to type iced::Command
every time we want to use the Command
keyword.
Now we are ready to begin tackling our four components of Iced apps. The State. For our application, there are two pieces of information we really care about. The X coordinate and Y coordinate of our position. To keep track of those, we will add the following code to the end of our main file.
struct Position {
x_cord: i32,
y_cord: i32,
}
The keyword struct
is a way for us to group variables and functions under one roof. In the snipit above, we grouped two integers under the Position
keyword. Now that we have our state, we can move on to making our Message. Add the below snipit to the bottom of our program.
#[derive(Debug, Clone, Copy)]
enum Message {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
}
The enumeration above is how we will name our button presses. we could look for other events, like the app closing, in this enumeration. However we only need to watch for button presses for now. Since we will need more extended functionality that the standard rust offers, we included the #[derive(Debug, Clone, Copy)]
feature flags so we can use methods from Debug
, Clone
, and Copy
.
Next we will cover our aliasing and overwrites. Start by adding the following code block to the end of our program:
impl Application for Position {
}
This code block will allow us to tap into the Application
class of Iced and overwrite functions that we will need to have a working app. In some programming concepts this is also called subclassing. But first we're gonna start with aliasing some types.
In rust, we can alias out variable types so we can write our app however we want. An example is if we take an integer type, i32
and alias it to the work Integer
by using the line type Integer = i32;
we can then use the full word in the type's place. So we could say let x: Integer = 3;
instead of let x: i32 = 3;
.
Now when we are subclassing the Application
class for our Iced app, we need to alias some types for the back end Iced code to work. Add the following to out subclass code block:
type Executor = executor::Default;
type Flags = ();
type Messages = Messages;
type Theme = Theme;
Next, we need to define what happens when our app is first run. We will do this by adding the new
function in our subclass block. Add the following:
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Self {
x_cord: 0,
y_cord: 0,
},
Command::none()
)
}
This code bit tells us that when we create a new Position
, we will start at the coordinates 0, 0. There is some other technical components in that code bit, however we don't need to worry about them for this demo.
After that we need to define the Application title. We do so by adding the following to our subclass block:
fn title(&self) -> String {
String::from("Position Tracker")
}
Now we're ready to move on to adding our Update Logic. There's some more technical components involved, but all we will be doing is providing instructions on what to do to our coordinates when we press any of our buttons. We'll add the following to the subclass block:
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::MoveUp => self.y_cord += 1,
Message::MoveDown => self.y_cord -= 1,
Message::MoveLeft => self.x_cord -= 1,
Message::MoveRight => self.x_cord += 1,
}
Command::none()
}
Next we can add our View Logic. This will tell how the application will look when we run it. Add the following to the subclass code block:
fn view(&self) -> Element<Message> {
column![
text(format!("X: {}, Y: {}", self.x_cord, self.y_cord)),
row![
button("Left").on_press(Message::MoveLeft),
button("Down").on_press(Message::MoveDown),
button("Up").on_press(Message::MoveUp),
button("Right").on_press(Message::MoveRight),
]
.spacing(6)
]
.padding(12)
.spacing(12)
.align_items(Alignment::Center)
.into()
}
The view logic itself is pretty self explanitory. One column, two rows. One row for a text view that shows the coordinates, and one row for all the buttons that will move our position around.
Finally we need to initialize our struct in the main
function. We will be changing the function a little, as below:
fn main() -> iced::Result {
Position::run(Settings::default())
}
What we did is made the main
function return iced::Result
so that if the program fails, we can view a small log of what happened. Since we subclassed our Position
as an Application
, we used run
to actually run the app.
Now all that's left is to run the application. In the project root, run the command cargo run
and after a long time of compiling you should be greeted with a window that looks something like this:
And that is the basics of creating a desktop application with Rust and the Iced toolkit. As you can see it's not that difficut to master, and is a very extensible tool. What do you think of Iced? Love it? Hate it? Let me know over at Mastodon or via email.
Thanks, Jacob