Intro to Iced
Iced is a Rust GUI library that uses the Elm architecture. It’s the GUI Library powering Sxitch and Rustcast.
Iced is one of the best GUI libraries available and we’ll take a look at the pros and cons of using it for a MacOS App that requires interacting with platform specific APIs.
Precursor
The Elm architecture is a way of structuring apps.
An oversimplification is only making the GUI update when a Message is sent via either the UI (e.g. Buttons / Text Input) or a Subscription (Background task so to speak) is called.
The update interacts and modifies the app state, which in turn updates the view.
Example app
Lets go through the process of making a counter app.
I will be skipping through imports, boilerplate (Like
fn main) and organisation of code.
App State
The first step is to define an App State which stores any relevant data, in memory which will be important for making background apps, where your App state should be as few fields as possible.
Since this is just a counter app, we’ll just be storing the counters value.
struct AppState {
count: i32,
}
The message enum
Iced uses “messages” to communicate what kind of action you want to take. You can use any type you want, but my personal preference is enums.
pub enum Message {
Increment,
Decrement,
}
This signifies the “actions” the app can take. Be it either from the UI or from Subscriptions (We’ll cover both of these in a second).
Initialising State
The first thing our app needs to do is load up the state using a “new” function.
This function returns a tuple with our AppState, and a iced::Task with a
generic for our “Message” type.
See rustcasts code for new state creation here
impl AppState {
pub fn new() -> (AppState, Task<Message>) {
let state = AppState {
count: 0,
};
let task = Task::none(); // Do nothing, or you can run stuff on first window creation
(state, task)
}
}
Handling updates
Before we do the UI, we need to handle the Messages and the changes to the App State. The way we do this is by taking a mutable reference to the state, and a message (of type Message).
Additionally the function should return a task so that you can do things like:
- Window moving, resizing etc.
- Trigger / Batch / Chain multiple Messages after a few messages.
- Return
Task::none()which basically means no other tasks to run.
impl AppState {
pub fn update(&mut self, msg: Message) -> Task<Message> {
match msg {
Message::Increment => {
self.count += 1;
Task::none()
},
Message::Decrement => {
self.count -= 1;
Task::none()
},
}
}
}
I recommend putting the
Task::none()inside the match arms since you’ll undoubtedly end up withTask::done(msg)in a more professional app
The View (AKA UI)
We’re handling updates, and initialising, so the next step is to add a way to display the current UI.
The core of Iced runs on the concept of widgets. Text, buttons, Text Inputs, are
all widgets. Iced v0.14 onwards, there are sufficient widgets to get a 100%
working decent app up. Both Sxitch and Rustcast (apps i’ve built using Iced)
don’t use widgets outside of those that are available in iced::widget.
impl AppState {
pub fn view(&self) -> Element<'_, Message> {
row![
Button::new("+").on_press(Message::Increment),
Text::new(self.count),
Button::new("-").on_press(Message::Decrement),
].into() // convert the Row to an Element
}
}
You can apply styling using the
.style()method which takes a closure that returns the respective widget’s style. The closure will take a theme struct which we’ll explore later
Starting the app
There are 2 entry points for our app. One is the main function, and one is starting the iced application. We’ll skip the main function since its simply calling the start_app function defined below.
Starting an app is really simple. We need to pass in the functions we’ve written above into a run function defined in Iced.
There are 2 iced functions you might want to use.
iced::applicationiced::daemon
The difference is quite obvious in the naming. application means the app shuts
down when you create a window::close(wid) task, or when the window gets closed
by the user.
The daemon function on the other hand only quits when you explicitly terminate
the app process (Panics, exit, or killed via the PID)
I’ll use the daemon because that makes more sense for MacOS apps that have a toggleable window.
fn start_app() {
iced::daemon( // pass in the functions as arguments, not calling them
AppState::new,
AppState::update,
AppState::view
)
.run()
.unwrap();
}
Closure
This article has run on a bit too much, so I’ll go into MacOS Specifics in my next article.
For now, explore iced, and take a look at Rustcast’s Code
Until then, have fun coding!