Iced v0.12 Tutorial - Asynchronous actions with Commands

In the recent past, I wrote a tutorial covering the basics of creating an app using the Iced library. While this was a good starting point there are several issues that can arise when creating more complex apps. One problem you might notice is that when performing time-consuming or blocking tasks your application freezes unless handled properly, leaving your user a poor experience while using the app.

But there is a solution to this: "Commands". Commands allow you to run actions asynchronously, so you don't have to block the user's interactions. This is useful for HTTP requests, long-running algorithms, IO operations, and other blocking tasks.

In this tutorial, I'll cover how to prevent blocking your app with commands. You'll learn how to create one, and when they're useful. I'll also go over some useful commands that iced already has available:

  • Loading font files into your application.
  • Copying and pasting from the clipboard.
  • Scrolling to a certain offset from a scrollable widget.

Prerequisites

Some knowledge of using the Iced library is expected. This time, I won't cover how widgets or messages work. Neither will I explain how to run your application. My beginner tutorial on Iced 0.12 covers all of that in great detail.

Just like in my beginners tutorial, you're also expected to be proficient enough with Rust. Skipping this step might lead to a lot of unnecessary confusion and frustration. So read the Official Rust Book if you aren't comfortable with Rust.

Counting Primes

First, we'll demonstrate the reason why we need asynchronous actions in the first place. We're going to start a new project. We want this project to give us an exhaustive list of every prime number up to the number we pass into the text input.

If you don't care to follow along, just skip to the end of the section where you'll find the complete code.

Note

In case you've forgotten from school, a prime number is a number that can only be exactly divided by any whole number other than itself or 1.

Let's create a new Iced project with the following as the Cargo.toml:

[package]
name = "Grocery-List-Application"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = {version = "0.12.1"}

We'll first import all the library items we need from Iced at the top of our main.rs file.

use iced::{widget::{button, column, row, scrollable, text, text_input}, Element, Sandbox, Settings};

We'll create the following struct which holds our main application state:

struct PrimeNumbersApp {
result: Vec<usize>,
text_value: String
}

The attribute result with the value Vec<usize> will be the results of our prime numbers. The other attribute text_value holds the value for our upcoming text_input.

We also need to create the following Message enum to reflect the anticipated user input.

#[derive(Clone, Debug)]
enum Message {
ButtonPress,
TextInputted(String)
}

The variants ButtonPress and TextInputted(String) handle the submit button and the text_input widget interactions, respectively.

Now we're going to implement the Sandbox trait into our PrimeNumbersApp struct.

impl Sandbox for PrimeNumbersApp {
type Message = Message;
fn new() -> Self {
Self {
result: Vec::new(),
text_value: String::new(),
}
}
fn title(&self) -> String {
String::from("Get all the prime numbers up to the passed number")
}
fn update(&mut self, message: Self::Message) {
match message {
Message::ButtonPress => {
let number: Result<usize, _> = self.text_value.parse();
if let Ok(number) = number {
self.result = list_of_primes(number)
}
}
Message::TextInputted(input) => self.text_value = input,
}
}
fn view(&self) -> Element<'_, Self::Message> {
let text_items: Vec<Element<Message>> = prime_numbers_items(&self.result);
column!(
row!(
button(text("Calculate Primes")).on_press(Message::ButtonPress),
text_input("Numbers only", &self.text_value).on_input(|value| Message::TextInputted(value))
)
.spacing(50)
.padding(50),
scrollable(column(text_items)).width(200)
)
.align_items(iced::Alignment::Center)
.into()
}
}

We're doing a lot here, so I'll explain what's going on.

Like in my last tutorial, we're defining the Message our application uses as our previously defined enum also named Message, we have a method new which initializes the state of our application, and we're setting the title of the application to a string with the title method. None of this should be new to you.

In the update method, we're processing the variants of the Message enum, like usual. On ButtonPress if the value passed can be converted into a number, we create a list of prime numbers up to the number passed. We save that value to result.

Finally, in our view method we take the list of prime numbers and pass them into a scrollable widget. We also added a button that sends the ButtonPress message, and a text input sends a TextInputted message.

Now we're only missing a few core functions for everything to work:

fn is_prime(n: usize) -> bool {
if n <= 1 {
return false;
}
for possible_multiple in 2..n {
if n % possible_multiple == 0 {
return false;
}
}
true
}
fn list_of_primes(n: usize) -> Vec<usize> {
let mut prime_numbers = Vec::new();
for i in 2..n {
if is_prime(i) {
prime_numbers.push(i);
}
}
prime_numbers
}
fn prime_numbers_items(items: &Vec<usize>) -> Vec<Element<Message>> {
let mut element_items: Vec<Element<Message>> = Vec::new();
for i in items {
element_items.push(text(i).into());
}
element_items
}

We have an is_prime function which returns true or false depending on whether the passed number is a prime number or not. This function is used by the list_of_primes function, which returns a vector of prime numbers up to the number passed. Lastly, we have the prime_numbers_items function which takes a vector of numbers (presumably prime numbers) and returns a vector of text widgets.

Note

An astute reader might notice that the list_of_primes and is_prime functions can be optimized. Good job noticing! However, finding an optimal algorithm is not the point of this tutorial.

Before we forget, we must add the main function so that we can run this application.

pub fn main() -> iced::Result {
PrimeNumbersApp::run(Settings::default())
}

With that, we're done! The final result should look like this:

use iced::{
widget::{button, column, row, scrollable, text, text_input},
Element, Sandbox, Settings,
};
fn is_prime(n: usize) -> bool {
if n <= 1 {
return false;
}
for possible_multiple in 2..n {
if n % possible_multiple == 0 {
return false;
}
}
true
}
fn list_of_primes(n: usize) -> Vec<usize> {
let mut prime_numbers = Vec::new();
for i in 2..n {
if is_prime(i) {
prime_numbers.push(i);
}
}
prime_numbers
}
fn prime_numbers_items(items: &Vec<usize>) -> Vec<Element<Message>> {
let mut element_items: Vec<Element<Message>> = Vec::new();
for i in items {
element_items.push(text(i).into());
}
element_items
}
struct PrimeNumbersApp {
result: Vec<usize>,
text_value: String,
}
#[derive(Clone, Debug)]
enum Message {
ButtonPress,
TextInputted(String),
}
impl Sandbox for PrimeNumbersApp {
type Message = Message;
fn new() -> Self {
Self {
result: Vec::new(),
text_value: String::new(),
}
}
fn title(&self) -> String {
String::from("Get all the prime numbers up to the passed number")
}
fn update(&mut self, message: Self::Message) {
match message {
Message::ButtonPress => {
let number: Result<usize, _> = self.text_value.parse();
if let Ok(number) = number {
self.result = list_of_primes(number)
}
}
Message::TextInputted(input) => self.text_value = input,
}
}
fn view(&self) -> Element<'_, Self::Message> {
let text_items: Vec<Element<Message>> = prime_numbers_items(&self.result);
column!(
row!(
button(text("Calculate Primes")).on_press(Message::ButtonPress),
text_input("Numbers only", &self.text_value).on_input(|value| Message::TextInputted(value))
)
.spacing(50)
.padding(50),
scrollable(column(text_items)).width(200)
)
.align_items(iced::Alignment::Center)
.into()
}
}
pub fn main() -> iced::Result {
PrimeNumbersApp::run(Settings::default())
}

Your Cargo.toml should be unchanged, but it should look like this:

[package]
name = "Grocery-List-Application"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = {version = "0.12.1"}

When you run your app, you should see this when you input the number 100 and click the button:

An Iced application displaying all prime numbers before 100.

Great our application works well... or does it?

Submit the number a hundred, the application runs fine. The same for a thousand. At ten thousand, you might notice a slight lag. At a hundred-thousand is where you might notice some problems. Our app gets stuck trying to calculate our prime numbers. You'll no longer be able to click on buttons or type in the text input. On my machine, if the program is blocked for long enough, my operating system will report that the app has frozen.

The problem is that our application runs the calculations synchronously, waiting for the result of our calculation of list_of_primes before letting any other updates occur. This gives our users a horrendous user experience. As you might already know, this problem is not unique to long-running algorithms, HTTP requests, reading files: anything that blocks the application will completely freeze the application. Luckily for us, there's a way to run operations asynchronously.

Commands

Asynchronous operations won't block the application while giving us our desired result. In Iced, to run asynchronous tasks, we create a command. This is a core aspect of how Iced works.

Commands are actions that run asynchronously. We can create a custom command that returns a Message, this message can then be used to change the state of our application. Commands are hidden from developers when you use the Sandbox trait, so we'll need to use the Application trait.

Let's modify our main.rs file. Like always, we'll start with our imports.

use iced::{
advanced::graphics::futures::backend::default::Executor,
widget::{button, column, row, scrollable, text, text_input},
Application, Element, Settings, Theme, Command
};

We're going to add a new variant to our Message enum.

#[derive(Clone, Debug)]
enum Message {
ButtonPress,
TextInputted(String),
ListOfPrimes(Vec<usize>),
}

ListOfPrimes will contain the list of prime numbers returned from the command we'll create later.

We're going to remove our implementation of the Sandbox trait and replace it with an implementation of a more robust trait, Application.

impl Application for CommandsMain {
type Message = Message;
type Executor = Executor;
type Theme = Theme;
type Flags = ();
fn new(_: ()) -> (Self, Command<Message>) {
(
Self {
result: Vec::new(),
text_value: String::new(),
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Using commands to calculate prime numbers.")
}
fn update(&mut self, message: Self::Message) -> Command<Message> {
match message {
Message::ButtonPress => {
let number: Result<usize, _> = self.text_value.parse();
if let Ok(number) = number {
return Command::perform(
async move { list_of_primes(number) },
|value| Message::ListOfPrimes(value),
);
}
Command::none()
}
Message::TextInputted(input) => {
self.text_value = input;
Command::none()
}
Message::ListOfPrimes(primes) => {
self.result = primes;
Command::none()
}
}
}
fn view(&self) -> Element<'_, Self::Message> {
let text_items: Vec<Element<Message>> = prime_numbers(&self.result);
column!(
row!(
button(text("Calculate Primes")).on_press(Message::ButtonPress),
text_input("Numbers only", &self.text_value).on_input(Message::TextInputted)
)
.spacing(50)
.padding(50),
scrollable(column(text_items)).width(200)
)
.align_items(iced::Alignment::Center)
.into()
}
}

You'll notice that the Application trait is almost identical to the Sandbox trait, although there are a few differences.

At the beginning of our implementation, we define 4 types. Message, Theme, Executor, and Flags. For now, this is not important to us, so we'll ignore this.

The methods title and view are mostly untouched, but our new method has a significant change though. Previously, our method returned Self. It still does, but the method also returns a command. It's so that we can define a command that we might want to run once the application first starts. This will be useful later. Since we don't need to run a command on the app's start time for now, we return Command::none, which is an empty command (one that doesn't do anything).

Our update method has also changed, now it returns a command too. We take advantage of this, in the ButtonPress variant of our message enum, instead of running our list_of_primes function directly, we create a command that runs it asynchronously.

We can create commands by running Command::perform. Command::perform takes two parameters. One is an asynchronous function, this is where we pass our list_of_primes function so that it doesn't block our app, the other parameter is a callback function, it'll take the result of our asynchronous function and return a Message. Just like our button and text_input widgets, this Message will be sent to the update method to be processed. We create a ListOfPrimes message which we use to save the result of the command.

Every variant processed in Message, other than the ButtonPressed, returns Command::none.

The final code should look like this:

use iced::{
advanced::graphics::futures::backend::default::Executor,
widget::{button, column, row, scrollable, text, text_input},
Application, Element, Settings, Theme, Command
};
fn is_prime(n: usize) -> bool {
if n <= 1 {
return false;
}
for possible_multiple in 2..n {
if n % possible_multiple == 0 {
return false;
}
}
true
}
fn list_of_primes(n: usize) -> Vec<usize> {
let mut prime_numbers = Vec::new();
for i in 2..n {
if is_prime(i) {
prime_numbers.push(i);
}
}
prime_numbers
}
fn prime_numbers(items: &Vec<usize>) -> Vec<Element<Message>> {
let mut element_items: Vec<Element<Message>> = Vec::new();
for i in items {
element_items.push(text(i).into());
}
element_items
}
struct CommandsMain {
result: Vec<usize>,
text_value: String,
}
#[derive(Clone, Debug)]
enum Message {
ButtonPress,
TextInputted(String),
ListOfPrimes(Vec<usize>),
}
impl Application for CommandsMain {
type Message = Message;
type Executor = Executor;
type Theme = Theme;
type Flags = ();
fn new(_: ()) -> (Self, Command<Message>) {
(
Self {
result: Vec::new(),
text_value: String::new(),
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Using commands to calculate prime numbers.")
}
fn update(&mut self, message: Self::Message) -> Command<Message> {
match message {
Message::ButtonPress => {
let number: Result<usize, _> = self.text_value.parse();
if let Ok(number) = number {
return Command::perform(
async move { list_of_primes(number) },
|value| Message::ListOfPrimes(value),
);
}
Command::none()
}
Message::TextInputted(input) => {
self.text_value = input;
Command::none()
}
Message::ListOfPrimes(primes) => {
self.result = primes;
Command::none()
}
}
}
fn view(&self) -> Element<'_, Self::Message> {
let text_items: Vec<Element<Message>> = prime_numbers(&self.result);
column!(
row!(
button(text("Calculate Primes")).on_press(Message::ButtonPress),
text_input("Numbers only", &self.text_value).on_input(Message::TextInputted)
)
.spacing(50)
.padding(50),
scrollable(column(text_items)).width(200)
)
.align_items(iced::Alignment::Center)
.into()
}
}
pub fn main() -> iced::Result {
CommandsMain::run(Settings::default())
}

If you run the application it'll look exactly the same as before, but you should notice it provides a much better experience. When you pass bigger numbers you can still highlight the button by hovering over it, and you're able to type into the text_input. Great!

In this example, we used a command to run an algorithm in the background. But when you're sending other asynchronous actions, such as HTTP requests, commands are essential. However, regardless of the asynchronous block or function, the process should be the same: Create a command with Command:perform, pass the asynchronous function or block you want to run, and pass the returned value into a message so that it can be saved.

Note

Because in the update method, we can create commands that create messages, it's possible to create an infinite loop of updates and messages. This can be accomplished by create a message which creates a message of the same type. Avoid doing this by accident.

Predefined commands

You know how to create your own commands now, but what about the commands that Iced already provides? I'll give a list of the commands I find most important. Each example is a stand alone project, and I won't dive deeply into every concept of the project, so you aren't expected copy and run each project if you don't want to.

Loading a font

Let's build an application that loads a font, and when a button is pressed, it'll toggle between bold and regular font weight.

Let's say we want to use one of my favorite fonts, the Poppins font, in our application. First, we would download the font and place it in the root of our directory. I set them up like this:

<root_of_project>/fonts/poppins/Poppins-Regular.ttf
<root_of_project>fonts/poppins/Poppins-Black.ttf

We can now include these fonts like this:

use iced::{advanced::graphics::futures::backend::default::Executor, font::Family, widget::{button, column, text}, Application, Command, Element, Font, Pixels, Settings, Theme};
struct FontLoadingApp {
state: AppState,
should_be_bold: bool,
}
#[derive(Clone, Debug)]
enum Message {
FontLoaded( Result<(), iced::font::Error>),
ToggleBold
}
enum AppState {
FontLoaded,
Loading,
Error,
}
const POPPINS_REGULAR: Font = Font {
family: Family::Name("Poppins"),
weight: iced::font::Weight::Normal,
stretch: iced::font::Stretch::Normal,
style: iced::font::Style::Normal,
};
const POPPINS_BOLD: Font = Font {
family: Family::Name("Poppins"),
weight: iced::font::Weight::Bold,
stretch: iced::font::Stretch::Normal,
style: iced::font::Style::Normal,
};
impl Application for FontLoadingApp {
type Executor = Executor;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_: ()) -> (Self, iced::Command<Self::Message>) {
let poppins_regular = iced::font::load(include_bytes!("../fonts/Poppins/Poppins-Regular.ttf")
.as_slice()).map(Message::FontLoaded);
let poppins_bold = iced::font::load(include_bytes!("../fonts/Poppins/Poppins-Bold.ttf")
.as_slice()).map(Message::FontLoaded);
let commands = vec![poppins_regular, poppins_bold];
(Self {state: AppState::Loading, should_be_bold: false}, Command::batch(commands))
}
fn title(&self) -> String {
String::from("A tutorial on how to use commands")
}
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
match message {
Message::FontLoaded(result) => {
match result {
Ok(_value) => self.state = AppState::FontLoaded,
Err(_e) => self.state = AppState::Error
}
self.state = AppState::FontLoaded;
}
Message::ToggleBold => {
self.should_be_bold = !self.should_be_bold;
},
}
Command::none()
}
fn view(&self) -> Element<'_, Self::Message, Self::Theme, iced::Renderer> {
let font = match self.should_be_bold {
true => POPPINS_BOLD,
false => POPPINS_REGULAR
};
match self.state {
AppState::FontLoaded => {
column!(
text("Whereas disregard and contempt for human rights have resulted")
.size(Pixels::from(60))
.font(font),
button("Toggle bold").on_press(Message::ToggleBold)
)
.align_items(iced::Alignment::Center)
.into()
},
AppState::Loading => text("Loading").into(),
AppState::Error => text("Error").into(),
}
}
}
pub fn main() -> iced::Result {
FontLoadingApp::run(Settings::default())
}

There are a few interesting points in this code.

The first is that we are running a command on startup. In the new method we'll run the font::load command that iced provides. font::load allows you to pass the bytes of the font you want and it'll include this font into the available fonts for Iced.

Another interesting aspect of the new method is instead of returning a single Command we take two commands and create a batch of them. By running Command::batch we can take two commands and merge them into one, this is useful for methods that only allow a single command to be returned (which is all of them).

We also create Font structs, it tells our text widgets which font we want to render. The only difference between the POPPINS_REGULAR and the POPPINS_BOLD fonts is their weight attributes. POPPINS_REGULAR and POPPINS_BOLD have Normal and Bold weights, respectively. It's very important that the family attribute matches the family name of the font that you just loaded. If the family names don't match, Iced will simply render the text with a default font and give no error.

Note

You should find the family name of the font on the site you downloaded. There are also tools like fc-scan to figure out the family name of your font by the font file.

Scroll command

The scrollable widget can be scrolled manually by the user, but there's also a way to scroll to a specific offset with a command. I'll demonstrate that with another example.

use iced::{
advanced::graphics::futures::backend::default::Executor,
widget::{self, button, column, scrollable, scrollable::AbsoluteOffset, text},
Application, Element, Length, Settings, Theme,
};
struct Scroll {
items: Vec<String>,
}
#[derive(Clone, Debug)]
enum Message {
ButtonPress,
}
impl Application for Scroll {
type Executor = Executor;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_: ()) -> (Self, iced::Command<Self::Message>) {
let mut items = Vec::new();
for i in 0..1000 {
items.push(i.to_string())
}
(Self { items }, iced::Command::none())
}
fn title(&self) -> String {
String::from("A tutorial on how to use commands")
}
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
match message {
Message::ButtonPress => iced::widget::scrollable::scroll_to(
widget::scrollable::Id::new("1"),
AbsoluteOffset {
x: 0.0,
y: f32::MAX,
},
),
}
}
fn view(&self) -> Element<'_, Self::Message, Self::Theme, iced::Renderer> {
let mut widgets: Vec<Element<Message>> = vec![];
for i in &self.items {
widgets.push(
text(i)
.horizontal_alignment(iced::alignment::Horizontal::Center)
.width(Length::Fill)
.into(),
);
}
column!(
button("Scroll").on_press(Message::ButtonPress),
scrollable(column(widgets))
.id(scrollable::Id::new("1"))
.width(Length::Fill)
)
.align_items(iced::Alignment::Center)
.into()
}
}
pub fn main() -> iced::Result {
Scroll::run(Settings::default())
}

Unlike in previous usages of the scrollable widget, we call the method id which accepts an Id. This informs the Iced library which scrollable widget we are referring to when we call the method widget::scrollable::scroll_to.

We call the widget::scrollable::scroll_to in the update method. When the user presses the button, we can scroll the scrollable widget by setting the offset to whatever we want. Here we set the offset to the highest possible number f32 can handle so that we always scroll to the bottom.

Copy and Pasting

Iced allows you to copy selected text and paste while using text_input. But what if you want full access to the clipboard? There are commands that allow you to do this.

use iced::{
advanced::graphics::futures::backend::default::Executor,
widget::{button, row, text_input},
Application, Command, Element, Settings, Theme,
};
// use rfd;
struct CopyPaste {
input_value: String,
}
#[derive(Clone, Debug)]
enum Message {
Cut,
Paste,
TextInputted(String),
}
impl Application for CopyPaste {
type Executor = Executor;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_: ()) -> (Self, iced::Command<Self::Message>) {
(
Self {
input_value: String::new(),
},
iced::Command::none(),
)
}
fn title(&self) -> String {
String::from("A tutorial on how to use commands")
}
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
match message {
Message::Cut => {
let command = iced::clipboard::write::<Message>(self.input_value.clone());
self.input_value = String::new();
command
}
Message::Paste => iced::clipboard::read(|value| {
if let Some(value) = value {
Message::TextInputted(value)
} else {
Message::TextInputted(String::new())
}
}),
Message::TextInputted(value) => {
self.input_value = value;
Command::none()
}
}
}
fn view(&self) -> Element<'_, Self::Message, Self::Theme, iced::Renderer> {
row!(
text_input("", &self.input_value).on_input(|value| Message::TextInputted(value)),
button("Cut").on_press(Message::Cut),
button("Paste").on_press(Message::Paste)
)
.spacing(10)
.padding(50)
.into()
}
}
pub fn main() -> iced::Result {
CopyPaste::run(Settings::default())
}

Here we have the copy and paste controlled by the user's buttons. We copy the text inside the text_input and clear the text input when the "Cut" button is pressed, and we paste whatever text is in the clipboard with the "Paste" button.

The update method is the main point of interest in this example. When the Cut variant of the Message enum is received, we create and use a command to write and replace the value in the clipboard. With the clipboard::write function, we pass in a string to be inside the clipboard. In this instance, we use the string that was in our text_input.

To paste the text into our input, we run the clipboard::read function that also returns a command. We pass into the clipboard::read a callback that contains the string of the clipboard and returns our desired message when the paste action is performed. We make use of the Message variant TextInputted which is used by the text_input to also paste the current value from the clipboard.

Final notes

Commands are a good way to allow you to handle asynchronous events caused by the user, but what about a constant stream of events? Whether the event happens in regular intervals or a less predictable manner, you would need to use the Iced library "subscription". This allows you to create commands and messages based on a stream of events for instance: web sockets or time intervals. Eventually, I plan to also cover subscriptions. Let me know if subscriptions are a particular point of interest for you on my socials.

I decided to cover the commands I found most useful in my projects, if there are any commands that you want to know more about, contact me on my socials and I'll update this tutorial.