Welcome to the unofficial iced-rs guide

Iced is an Open Source GUI library written in rust to create beautiful and minimal cross platform applications. It leverages the power of Google's Skia (via tiny-skia) and wGPU to render beautiful UI while maintaining clean and maintainable code by using the ELM (or MVU) architecture.

Iced focuses on Simplicity and type-safety so that you can concenterate on your implementation without any framework specific distractions.

This guide tries to explain the basics of the Iced GUI library for the Rust programming language.

Disclaimer

To make it clear at the beginning. This is not an official guide. It is not approved by the iced-rs team! If you search for the official documentation or iced book take a look at the Iced website.

Contribution

If you want to contribute to this guide, you can open an issue on GitHub and make a pull request. For large changes, it is preferred that you open an issue first to discuss the changes. For any small changes, spelling, grammar and formatting fixes directly opening a pull request should not be a problem.

© Héctor Ramón (hecrj) for the iced logo.

Quickstart

In this section, you will learn in detail about how iced uses the ELM architecture for it's application design. We also, learn how to write a simple counter application which increments or decrements counts with buttons.

Architecture

The architecture of iced is inspired by the elm architecture. This architecture splits your code into 4 main parts:

  • Messages
  • State
  • Update Logic
  • View Logic

 

State

The state contains all the data that your program wants to store throughout its lifespan. This is implemented using a struct. For example, in case of a simple counter app, which increments or decrements the current count value, the state would be like this,

struct Counter {
    count: i32
}

In the above snippet, all we need is a count value for a simple counter application. hence the state.

Message

The message defines any events or interactions that your program will care about. In iced, it will be implemented using the rust enum. For example, let's take a simple counter app, the Messages / Events that might occur are stored in the Message enum, For example,

enum Message {
    IncrementCount,
    DecrementCount
}

Update Logic

The update logic is called every time a message is emitted and can operate based on this message. This logic is the only one that can change the state of your application. A rough example of update logic with respect to the previous counter example is below,

fn update(message: Message) {
    match message {
        Message::IncrementCount => count += 1,
        Message::DecrementCount => count -= 1
    }
}

View Logic

The view logic generates the view, elements/widgets, and layout based on the current state. The view logic is called every time after the update logic is called. So for a simple counter app, all we need is a text view and two buttons. We can declare our UI as follows,

fn view() {
    let ui = column![
        button("+").on_press(Message::IncrementCount),
        text(count),
        button("-").on_press(Message::DecrementCount)
    ]
}

Note: The snippets shown above are just for example purposes and will not compile.

Now that we got a basic understanding of the ELM architecture, we can deep dive into Iced and create a simple counter app.

A minimal Application - Counter

Our goal is to create a simple counter where we have a number displayed that we can increase or decrease with two buttons.

Creating a new Project

First of all, make sure Rust is installed in your system. If not head over to Rust Installation Page.

After installing rust, create a new binary crate by executing,

$ cargo new counter-app-iced
$ cd counter-app-iced

Add Iced crate by executing,

$ cargo add iced

Now, build the app using

$ cargo run

On successful build, you can see the text Hello World is printed on console. Now we are ready to create our beautiful GUIs using Iced.

1. Defining the State

For the state, we define a struct. For the counter, we need to store the current value of the counter.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

2. Defining the Message

For our counter, we have two major events that matter to us. Increasing and decreasing the counter.

The message is represented as an enum with two variants, IncrementCounter and DecrementCounter.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

3. Implementing the Sandbox for Counter

To create a window, we implement the Sandbox trait for our Counter. There are two different windowing implementations. One is Sandbox and the other is Application. The difference between both is that Sandbox trait provides a much simpler interface to work with if you are just getting started.

It's not harder to switch your app from Sandbox to Application. We will use the Sandbox trait throughout this section.

The Sandbox trait implments the following,

pub trait Sandbox {
    type Message: Debug + Send;

    // Required methods
    fn new() -> Self;
    fn title(&self) -> String;
    fn update(&mut self, message: Self::Message);
    fn view(&self) -> Element<'_, Self::Message>;

    // Provided methods
    fn theme(&self) -> Theme { ... }
    fn style(&self) -> Application { ... }
    fn scale_factor(&self) -> f64 { ... }
    fn run(settings: Settings<()>) -> Result<(), Error>
       where Self: 'static + Sized { ... }
}

Message

The Sandbox trait expects us to define our own Message type. So we should map our Message enum as a type alias for Message in the Sandbox implementation.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

Initial state and Title

Next, we need to set out initial value of our state, which is the count value. The new() function helps us to do exactly that. The state of the Counter is returned with it's initial count value as 0.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

In, the title() function, we set the title of the app. This will be the title of the current instance of the Application.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

Update logic

Now we have to handle the messages that are emitted by the View Logic. The update() function does exactly that. The update() function get called every time when the View Logic emits a message. We use the rust's powerful match expression to handle messages. Here we use the match expression to increase the count when IncrementCount is emitted and decrease the count when DecrementCount is emitted.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

Tip: Use count.saturating_add(1) or count.saturating_sub(1) for more error proof and optimized code.

View logic

The only thing left is to define our View (a.k.a UI). Define your View Logic in the view() function. In iced, all UI components are called widgets. For a counter, we need two button widgets (one for incrementing and another for decrementing) and a text widget. They need to be aligned one after another in a horizontal manner. So, we use row widget macro to align our widgets in a horizontal manner.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

In the above code, we can see that the button's on_press function accepts the message type to be emitted.

Note: By default, the view() function returns the type Element<'_, Self::Message>. So, we use .into() for conversion purpose.

That's pretty much everything for a simple counter app. Now, let's run it.

4. Running the Counter

Now we can run our simple Counter App by calling Counter::run() in the main() function. The run() method is a default implementation of the Sandbox trait.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

Note: The main function should have a return type of Result<(), iced::Error>.

5. Full Code

Now that we completed our simple counter application, the complete code will look like this.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

 

In the next section, you will see about common layouting techniques that iced offers.

Layout

In this section, you will learn about different layouting widgets and techniques that iced offers us to align and place widgets. In addition, you will learn some of the basic layouting practices. These layout techniques are used to maintain best UI structure and maintain responsiveness.

Element.explain

When styling a GUI, often the result does not look exactly what you wanted it to look like. Therefore, you debug your layout.

In web development, you can use the inspection tool of your browser to show the layout and borders of your elements.

In iced, we don't have an inspection tool. But we have the Element.explain function that we can apply to any Element. This function will draw a line around the element and all of its children. With that, you can debug how spacing and sizing are applied by the renderer.

Here is a short snipped that uses .explain:

iced::Element::new(your_widget).explain(iced::Color::BLACK)

Length

Length is used to fill space in a specific dimension. The Length enum also has capablities of being responsive.

The Length enum has the following variants:

  • Length::Fill
  • Length::FillPortion(u16)
  • Length::Shrink
  • Length::Fixed(f32)

1. Length::Fill enum

Length::Fill is used to set a widget's width or height to fill the viewport. For example, setting a container's width property to Length::Fill will set the container's width to fill the available sapce.

let ui = container(...)
    .width(Length::Fill)
    .height(50.0);

This will result in the following,

2. Length::FillPortion(u16) enum

Length::FillPortion(u16) is used to set width or height to a collection of widgets in a specified ratio. This enum is mostly used in collection widgets such as row or column.

Let’s say we have two elements: one with FillPortion(3) and one with FillPortion(2). The first will get 2 portions of the available space, while the second one would get 3. So basically, the two elements will get it's portions in the ratio of 3:2.

let col = column![
    container(...)
        .width(Length::FillPortion(2)),
    container(...)
        .width(Length::FillPortion(3)),
].height(50.0)

This will result in the following,

Shrink

Length::Shrink is used to set the minimum length that an element needs to be properly displayed. This length will be independent from anything else. For example, a container will take the length of it's child element.

Fixed

Length::Fixed(f32) is used to set a fixed length. This length will be independent from anything else.

Length::Fixed(15.0)
Length::from(15.0)
Length::from(15)    

Columns and Rows

The two most important structs for laying out widgets are Column and Row.

Both lay out their children in one direction. The column organizes the widgets vertically and the row horizontally.

Column and Row Example

By default, they align the items in the top left corner of their space.

A convenient way to create columns and rows is with the column! and row! macros.

We saw one of them in the Minimal Application - Counter.

use iced::{widget, Sandbox};

struct Counter {
    // This will be our state of the counter app
    // a.k.a the current count value
    count: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    // Emitted when the increment ("+") button is pressed
    IncrementCount,
    // Emitted when decrement ("-") button is pressed
    DecrementCount,
}

// Implement Sandbox for our Counter
impl Sandbox for Counter {

    // alias our Message enum with the
    // Sandbox's Message type
    type Message = Message;

    fn new() -> Self {
        // initialize the counter struct
        // with count value as 0.
        Self { count: 0 }
    }

    fn title(&self) -> String {
        //define the title for our app
        String::from("Counter App")
    }

    fn update(&mut self, message: Self::Message) {
        // handle emitted messages
        match message {
            Message::IncreaseCounter => self.count += 1,
            Message::DecreaseCounter => self.count -= 1,
        }
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        // create the View Logic (UI)
        let rw = widget::row![
            widget::button("-").on_press(Message::DecrementCount),
            widget::text(self.count),
            widget::button("+").on_press(Message::IncrementCount)
        ];
        widget::container(rw)
            .center_x()
            .center_y()
            .width(iced::Length::Fill)
            .height(iced::Length::Fill)
            .into()
    }
}

fn main() -> Result<(), iced::Error> {
    // run the app from main function
    Counter::run(iced::Settings::default())
}

There, we created a Column with three children inside. One text and two buttons. The syntax for rows is the same.

You can put any Element inside a Column or Row.

Alignment

Of course, we can change the horizontal alignment for columns and the vertical alignment for rows.

Column and Row Example

This is how they would align in the center.

In code, if you want to set the Alignment you can call the align_items method on your column/row. It will return itself with the new alignment.

let some_column = iced::widget::column![
    iced::widget::text("Hello World!"),
    iced::widget::text("Another Hello World!")
].align_items(iced::Alignment::Center)

Spacing

Since you can not set a margin in iced and often want to have a spacing between elements.

Columns and rows have the spacing method to set the spacing.

Here is an example of how to use spacing on a column:

let some_column = iced::widget::column![
    iced::widget::text("Hello World!"),
    iced::widget::text("Another Hello World!")
].spacing(20)

Spacing Image

Container

The Container is useful when aligning items. A Container has one child element (could be a button, text, column, row, etc.).

use iced::{widget, Length};
use iced::alignment::{Horizontal, Vertical};

let stuff_centered = widget::Container::new(widget::text("Some Text"))
    .align_x(Horizontal::Center)
    .align_y(Vertical::Center)
    .width(Length::Fill)
    .height(Length::Fill);

Note:

We use width and height to maximize the size of the Container. This makes space for centering.

This it not exactly necessary as long as the container is large enough. If there is no space you will not see a difference between applied alignment and none.

Both align_x and align_y methods are available for alignment purposes on Container.

Subscriptions and Commands

Sometimes you have a task that takes a bit and should run in the background. If you just put it in your update function, the GUI will be locked and unresponsive until the task is finished. This might be a web request or listening to external events.

Iced has two solutions to this issue. The Command and Subscription.

A command will run until it is finished, while a subscription runs as long as the application says so.

These can only be used in an Application and not in a Sandbox.

In this chapter, we will look into both of them and how we can use them.

Commands / Tasks

Note: In the past releases of Iced there were Commands. In the current master branch and probably the next release, they are renamed to Task. Since this change this Guide uses sometimes Command and sometimes Task but meant the same thing.

A command is "A set of asynchronous actions to be performed by some runtime".

Basically, a command is just a Stream that returns messages.

You can create custom commands, but often you get them by some function and just want to execute it. For example, minimizing and maximizing a window requires executing a given command.

A command will run until it has finished and can return multiple messages during its execution.

Executing a Command

In your App, you can execute a command by returning it from the update function of your Application.

Note: This is not possible when you use Sandbox instead of Application

Batch multiple commands

Sometimes you want to return more than one command. For that, you can use the Command::batch function to batch a few of them together like this:

return Command::batch(vec![command1, command2, command3]);

Custom Command using perform

In this example, we will create an app that fetches your current IP address by making an API call with our own custom command..

Dependencies

As you see, we have two dependencies in our project.

One of them is reqwest. We use reqwest to make the API call.

The other one is iced. Since this is a guide for iced, that should not wonder you. But as you see, we added the tokio feature. This lets iced use tokio as part of the runtime as needed for reqwest.

[dependencies]
iced = {version="0.12.1", features = ["tokio"]}
reqwest = "0.11.24"

Making the api request

At first, we define what our command will do.

For that, we are creating an async function that makes an async get request to an API that provides the public IP.

use iced::{Application, Command};

#[derive(Debug, Clone)]
enum Message {
    Refetch,
    CurrentIp(String),
}

struct App {
    ip: String,
}

impl Application for App {
    type Message = Message;
    type Theme = iced::Theme;
    type Flags = ();
    type Executor = iced::executor::Default;

    fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) {
        (App { ip: String::new() }, Command::none())
    }

    fn title(&self) -> String {
        String::new()
    }

    fn view(&self) -> iced::Element<'_, Self::Message, Self::Theme, iced::Renderer> {
        iced::widget::column![
            iced::widget::text(&self.ip),
            iced::widget::button("Start Command").on_press(Message::Refetch)
        ]
        .into()
    }

    fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
        println!("update");
        match message {
            Message::Refetch => return Command::perform(fetch_ip(), Message::CurrentIp),
            Message::CurrentIp(text) => {
                self.ip = text;
            }
        }
        Command::none()
    }
}

async fn fetch_ip() -> String {
    println!("fetch_ip");
    reqwest::get("https://api.seeip.org")
        .await
        .unwrap()
        .text()
        .await
        .unwrap()
}

fn main() {
    App::run(iced::Settings::default()).expect("Application raised an error");
}

Tip: If you have something that is not async but synchronous and will block your application like a heavy computation, you can use tokio::spawn_blocking in a command or subscription to run a closure on a thread where blocking is acceptable.

Starting/Creating the command

In the update function we return Command::none() or our custom command depending on the message.

If the Message is Message::CurrentIp we change our state, if it is Message::Refetch we return our command.

use iced::{Application, Command};

#[derive(Debug, Clone)]
enum Message {
    Refetch,
    CurrentIp(String),
}

struct App {
    ip: String,
}

impl Application for App {
    type Message = Message;
    type Theme = iced::Theme;
    type Flags = ();
    type Executor = iced::executor::Default;

    fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) {
        (App { ip: String::new() }, Command::none())
    }

    fn title(&self) -> String {
        String::new()
    }

    fn view(&self) -> iced::Element<'_, Self::Message, Self::Theme, iced::Renderer> {
        iced::widget::column![
            iced::widget::text(&self.ip),
            iced::widget::button("Start Command").on_press(Message::Refetch)
        ]
        .into()
    }

    fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
        println!("update");
        match message {
            Message::Refetch => return Command::perform(fetch_ip(), Message::CurrentIp),
            Message::CurrentIp(text) => {
                self.ip = text;
            }
        }
        Command::none()
    }
}

async fn fetch_ip() -> String {
    println!("fetch_ip");
    reqwest::get("https://api.seeip.org")
        .await
        .unwrap()
        .text()
        .await
        .unwrap()
}

fn main() {
    App::run(iced::Settings::default()).expect("Application raised an error");
}

To create our custom command, we use the Command::perform function. It takes a future, in this case our fetch_ip function, and a closure that converts the returned value of the future into a massage.

use iced::{Application, Command};

#[derive(Debug, Clone)]
enum Message {
    Refetch,
    CurrentIp(String),
}

struct App {
    ip: String,
}

impl Application for App {
    type Message = Message;
    type Theme = iced::Theme;
    type Flags = ();
    type Executor = iced::executor::Default;

    fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) {
        (App { ip: String::new() }, Command::none())
    }

    fn title(&self) -> String {
        String::new()
    }

    fn view(&self) -> iced::Element<'_, Self::Message, Self::Theme, iced::Renderer> {
        iced::widget::column![
            iced::widget::text(&self.ip),
            iced::widget::button("Start Command").on_press(Message::Refetch)
        ]
        .into()
    }

    fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
        println!("update");
        match message {
            Message::Refetch => return Command::perform(fetch_ip(), Message::CurrentIp),
            Message::CurrentIp(text) => {
                self.ip = text;
            }
        }
        Command::none()
    }
}

async fn fetch_ip() -> String {
    println!("fetch_ip");
    reqwest::get("https://api.seeip.org")
        .await
        .unwrap()
        .text()
        .await
        .unwrap()
}

fn main() {
    App::run(iced::Settings::default()).expect("Application raised an error");
}

Note: fetch_ip() produces the future

Note: Message::CurrentIp is a shorthand for |x| Message::CurrentIp(x)

Full Code

use iced::{Application, Command};

#[derive(Debug, Clone)]
enum Message {
    Refetch,
    CurrentIp(String),
}

struct App {
    ip: String,
}

impl Application for App {
    type Message = Message;
    type Theme = iced::Theme;
    type Flags = ();
    type Executor = iced::executor::Default;

    fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) {
        (App { ip: String::new() }, Command::none())
    }

    fn title(&self) -> String {
        String::new()
    }

    fn view(&self) -> iced::Element<'_, Self::Message, Self::Theme, iced::Renderer> {
        iced::widget::column![
            iced::widget::text(&self.ip),
            iced::widget::button("Start Command").on_press(Message::Refetch)
        ]
        .into()
    }

    fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
        println!("update");
        match message {
            Message::Refetch => return Command::perform(fetch_ip(), Message::CurrentIp),
            Message::CurrentIp(text) => {
                self.ip = text;
            }
        }
        Command::none()
    }
}

async fn fetch_ip() -> String {
    println!("fetch_ip");
    reqwest::get("https://api.seeip.org")
        .await
        .unwrap()
        .text()
        .await
        .unwrap()
}

fn main() {
    App::run(iced::Settings::default()).expect("Application raised an error");
}

Blocking Code

To run non async code / blocking in a Task or a Subscription we can use tokio::task::spawn_blocking

Note: This might only work on native and not on wasm

Example

Here is a small example that shows how to use tokio::task::spawn_blocking.

Cargo.toml

Because we want to use spawn_blocking from tokio we need to add the tokio feature to iced. This will lead to iced using tokio.

[dependencies]
iced = { git = "https://github.com/iced-rs/iced.git", rev = "978327f9e7f68d3e5bc280faa0617487d8eabc57", features = [
    "tokio",
] }
tokio = { version = "1.38.0", features = ["rt"] }

Actual Example

In the example, there will be a button and a text. A press onto the button will trigger a large computation to be started (in the example, we will just sleep a few seconds and return a number). If the computation finishes, the result will be shown in the text widget.

Our computation runs in a task, because we do not want to block our whole UI until it has finished.

Inside the task we call spawn_blocking with a closure of our computation. To get the returned value of the closure, we need to await the JoinHandle returned by spawn_blocking. That will give us the result of the heavy computation without blocking the UI.

#[derive(Debug, Clone)]
enum Message {
    CalculatedInformation(i32),
    StartCalculatingInformation,
}

#[derive(Default)]
struct App {
    hard_to_process_information: Option<i32>,
    calculation_in_progress: bool,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::CalculatedInformation(information) => {
                // Set the information
                self.hard_to_process_information = Some(information);
            }
            Message::StartCalculatingInformation => {
                // Change the state to indicate that the calculation is in progress
                self.calculation_in_progress = true;
                // Return a task that will calculate the information
                return iced::Task::future(async {
                    let information = tokio::task::spawn_blocking(|| {
                        println!("Calculating information...");
                        // Simulate a long computation
                        std::thread::sleep(std::time::Duration::from_secs(2));

                        // return some value
                        42
                    })
                    .await
                    .unwrap();

                    // Send the information back to the update function
                    Message::CalculatedInformation(information)
                });
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            // Display the information if it is available
            iced::widget::Text::new(self.hard_to_process_information.map_or(
                "Information will be ready in a second...".to_string(),
                |x| format!("Information: {}", x),
            )),
            // Display a button to start the calculation
            iced::widget::button("Start Calculation").on_press_maybe(
                if self.calculation_in_progress {
                    None
                } else {
                    Some(Message::StartCalculatingInformation)
                }
            )
        ]
        .into()
    }
}

fn main() {
    iced::run("Task Example", App::update, App::view).unwrap();
}

Full Code

#[derive(Debug, Clone)]
enum Message {
    CalculatedInformation(i32),
    StartCalculatingInformation,
}

#[derive(Default)]
struct App {
    hard_to_process_information: Option<i32>,
    calculation_in_progress: bool,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::CalculatedInformation(information) => {
                // Set the information
                self.hard_to_process_information = Some(information);
            }
            Message::StartCalculatingInformation => {
                // Change the state to indicate that the calculation is in progress
                self.calculation_in_progress = true;
                // Return a task that will calculate the information
                return iced::Task::future(async {
                    let information = tokio::task::spawn_blocking(|| {
                        println!("Calculating information...");
                        // Simulate a long computation
                        std::thread::sleep(std::time::Duration::from_secs(2));

                        // return some value
                        42
                    })
                    .await
                    .unwrap();

                    // Send the information back to the update function
                    Message::CalculatedInformation(information)
                });
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            // Display the information if it is available
            iced::widget::Text::new(self.hard_to_process_information.map_or(
                "Information will be ready in a second...".to_string(),
                |x| format!("Information: {}", x),
            )),
            // Display a button to start the calculation
            iced::widget::button("Start Calculation").on_press_maybe(
                if self.calculation_in_progress {
                    None
                } else {
                    Some(Message::StartCalculatingInformation)
                }
            )
        ]
        .into()
    }
}

fn main() {
    iced::run("Task Example", App::update, App::view).unwrap();
}

Structuring Apps

When you create larger iced apps, you might want to have reusable components and views. For that, there is a common approach that is also used in this showcase from the founder of iced.

In this architecture a view/component is independent of the application and is split into three parts, message, the state and the action.

The state will have a normal view function that returns a iced::Element<Message>.

The update function will differ a bit. Instead of a Task like our main application, it will return an action enum. Common variants for this enum can be None, Task(iced::Task<Message>) and Submit(String). You can think of this action like a message that is sent from the view to the parent application that should handle it.

This kind of architecture will enhance composability.

Initial State

The view should also have a function that creates a new instance of that view. If some task needs to be done for the initial state of the view (i.e. fetching data), something like (Self, iced::Task<Message>) should be returned by that function. The task would be executed, and the data is sent via a message to the view.

To differ between a loaded and not loaded state you can use something like this pattern for your state:

enum State {
    NotLoaded,
    Loaded {
        field1: String,
        field2: i32
    }
}

Mapping

If you want to compose your UI of subparts that have their own functionality, it makes sense to give them their own messages.

But to use a iced::Element<ViewMessage> in your app that requires a iced::Element<AppMessage> you have to map them.

For that, you can simply use iced::Element<ViewMessage>.map(AppMessage::ViewMessage).

Note: this is a shortcut for iced::Element<ViewMessage>.map(|view_message| AppMessage::ViewMessage(view_message))

The .map function takes a closure that takes the message and converts it into another message. This often requires having a dedicated message variant on the application level that contains the view message.

Map functions like this are available on Task::map, Subscription::map, Element::map.

Example

The example shows an unfinished joke listing app with a view for creating jokes. When the user tries to create a new joke, a default joke, fetched from an API, is provided. The user can get a random joke from the API on a button click as well. All Jokes are listed in the main/default view.

This is how the view for new jokes looks like using this design:

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewMessage(new::Message),
    New,
}

#[derive(Debug, Default)]
enum View {
    #[default]
    Default,
    Edit(new::NewView),
}

#[derive(Debug, Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewMessage(view_message) => {
                if let View::Edit(edit) = &mut self.view {
                    // Call the update method of the edit view 
                    // and handle the returned action
                    match edit.update(view_message) {
                        new::Action::None => {}
                        // If the action is a task, map the
                        // task to a message, the higher level message
                        new::Action::Task(task) => return task.map(Message::NewMessage),
                        new::Action::Submitted(content) => {
                            self.view = View::Default;
                            self.items.push(content);
                        }
                    }
                }
            }
            Message::New => {
                // Create a new view
                let (view, task) = new::NewView::new();
                self.view = View::Edit(view);
                // Run the task and map it to the higher level message
                return task.map(Message::NewMessage);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::Default => {
                let items = self
                    .items
                    .iter()
                    .map(|item| iced::widget::text(item).into())
                    .collect();
                iced::widget::column![
                    iced::widget::button("Edit").on_press(Message::New),
                    iced::widget::Column::from_vec(items)
                ]
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::Edit(edit) => edit.view().map(Message::NewMessage),
        }
    }
}

mod new {
    pub enum Action {
        None,
        Task(iced::Task<Message>),
        Submitted(String),
    }

    #[derive(Debug, Clone)]
    pub enum Message {
        Submit,
        SetChanged(String),
        RandomJoke,
    }

    #[derive(Debug, Default)]
    pub struct NewView {
        content: String,
    }

    impl NewView {
        pub fn new() -> (Self, iced::Task<Message>) {
            (Self::default(), Self::random_joke_task())
        }

        pub fn update(&mut self, message: Message) -> Action {
            match message {
                Message::Submit => Action::Submitted(self.content.clone()),
                Message::SetChanged(content) => {
                    self.content = content;
                    Action::None
                }
                Message::RandomJoke => Action::Task(Self::random_joke_task()),
            }
        }

        pub fn view(&self) -> iced::Element<Message> {
            iced::widget::column![
                iced::widget::text_input("Content", &self.content)
                    .on_input(Message::SetChanged),
                iced::widget::button("Random Joke").on_press(Message::RandomJoke),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .into()
        }

        fn random_joke_task() -> iced::Task<Message> {
            iced::Task::future(async {
                // Fetch a joke from the internet
                let client = reqwest::Client::new();
                let response: serde_json::Value = client
                    .get("https://icanhazdadjoke.com")
                    .header("Accept", "application/json")
                    .send()
                    .await
                    .unwrap()
                    .json()
                    .await
                    .unwrap();

                // Parse the response
                let joke = response["joke"].as_str().unwrap();

                // Return the joke as a message
                Message::Initialize(joke.to_owned())
            })
        }
    }
}

As you see, the update function produces this action, that the parent of the view can handle:

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewMessage(new::Message),
    New,
}

#[derive(Debug, Default)]
enum View {
    #[default]
    Default,
    Edit(new::NewView),
}

#[derive(Debug, Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewMessage(view_message) => {
                if let View::Edit(edit) = &mut self.view {
                    // Call the update method of the edit view 
                    // and handle the returned action
                    match edit.update(view_message) {
                        new::Action::None => {}
                        // If the action is a task, map the
                        // task to a message, the higher level message
                        new::Action::Task(task) => return task.map(Message::NewMessage),
                        new::Action::Submitted(content) => {
                            self.view = View::Default;
                            self.items.push(content);
                        }
                    }
                }
            }
            Message::New => {
                // Create a new view
                let (view, task) = new::NewView::new();
                self.view = View::Edit(view);
                // Run the task and map it to the higher level message
                return task.map(Message::NewMessage);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::Default => {
                let items = self
                    .items
                    .iter()
                    .map(|item| iced::widget::text(item).into())
                    .collect();
                iced::widget::column![
                    iced::widget::button("Edit").on_press(Message::New),
                    iced::widget::Column::from_vec(items)
                ]
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::Edit(edit) => edit.view().map(Message::NewMessage),
        }
    }
}

mod new {
    pub enum Action {
        None,
        Task(iced::Task<Message>),
        Submitted(String),
    }

    #[derive(Debug, Clone)]
    pub enum Message {
        Submit,
        SetChanged(String),
        RandomJoke,
    }

    #[derive(Debug, Default)]
    pub struct NewView {
        content: String,
    }

    impl NewView {
        pub fn new() -> (Self, iced::Task<Message>) {
            (Self::default(), Self::random_joke_task())
        }

        pub fn update(&mut self, message: Message) -> Action {
            match message {
                Message::Submit => Action::Submitted(self.content.clone()),
                Message::SetChanged(content) => {
                    self.content = content;
                    Action::None
                }
                Message::RandomJoke => Action::Task(Self::random_joke_task()),
            }
        }

        pub fn view(&self) -> iced::Element<Message> {
            iced::widget::column![
                iced::widget::text_input("Content", &self.content)
                    .on_input(Message::SetChanged),
                iced::widget::button("Random Joke").on_press(Message::RandomJoke),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .into()
        }

        fn random_joke_task() -> iced::Task<Message> {
            iced::Task::future(async {
                // Fetch a joke from the internet
                let client = reqwest::Client::new();
                let response: serde_json::Value = client
                    .get("https://icanhazdadjoke.com")
                    .header("Accept", "application/json")
                    .send()
                    .await
                    .unwrap()
                    .json()
                    .await
                    .unwrap();

                // Parse the response
                let joke = response["joke"].as_str().unwrap();

                // Return the joke as a message
                Message::Initialize(joke.to_owned())
            })
        }
    }
}

The application that would host the view could look like this:

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewMessage(new::Message),
    New,
}

#[derive(Debug, Default)]
enum View {
    #[default]
    Default,
    Edit(new::NewView),
}

#[derive(Debug, Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewMessage(view_message) => {
                if let View::Edit(edit) = &mut self.view {
                    // Call the update method of the edit view 
                    // and handle the returned action
                    match edit.update(view_message) {
                        new::Action::None => {}
                        // If the action is a task, map the
                        // task to a message, the higher level message
                        new::Action::Task(task) => return task.map(Message::NewMessage),
                        new::Action::Submitted(content) => {
                            self.view = View::Default;
                            self.items.push(content);
                        }
                    }
                }
            }
            Message::New => {
                // Create a new view
                let (view, task) = new::NewView::new();
                self.view = View::Edit(view);
                // Run the task and map it to the higher level message
                return task.map(Message::NewMessage);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::Default => {
                let items = self
                    .items
                    .iter()
                    .map(|item| iced::widget::text(item).into())
                    .collect();
                iced::widget::column![
                    iced::widget::button("Edit").on_press(Message::New),
                    iced::widget::Column::from_vec(items)
                ]
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::Edit(edit) => edit.view().map(Message::NewMessage),
        }
    }
}

mod new {
    pub enum Action {
        None,
        Task(iced::Task<Message>),
        Submitted(String),
    }

    #[derive(Debug, Clone)]
    pub enum Message {
        Submit,
        SetChanged(String),
        RandomJoke,
    }

    #[derive(Debug, Default)]
    pub struct NewView {
        content: String,
    }

    impl NewView {
        pub fn new() -> (Self, iced::Task<Message>) {
            (Self::default(), Self::random_joke_task())
        }

        pub fn update(&mut self, message: Message) -> Action {
            match message {
                Message::Submit => Action::Submitted(self.content.clone()),
                Message::SetChanged(content) => {
                    self.content = content;
                    Action::None
                }
                Message::RandomJoke => Action::Task(Self::random_joke_task()),
            }
        }

        pub fn view(&self) -> iced::Element<Message> {
            iced::widget::column![
                iced::widget::text_input("Content", &self.content)
                    .on_input(Message::SetChanged),
                iced::widget::button("Random Joke").on_press(Message::RandomJoke),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .into()
        }

        fn random_joke_task() -> iced::Task<Message> {
            iced::Task::future(async {
                // Fetch a joke from the internet
                let client = reqwest::Client::new();
                let response: serde_json::Value = client
                    .get("https://icanhazdadjoke.com")
                    .header("Accept", "application/json")
                    .send()
                    .await
                    .unwrap()
                    .json()
                    .await
                    .unwrap();

                // Parse the response
                let joke = response["joke"].as_str().unwrap();

                // Return the joke as a message
                Message::Initialize(joke.to_owned())
            })
        }
    }
}

Component

Sometimes you want to create your own reusable custom components that you can reuse through your applications. That is where the Component trait comes in place. You can turn anything that implements this trait easily into an Element with the component function.

Components should only be used as reusable widgets and not for organizing code or splitting your applications into different parts!

Note: Component is only available on crate feature lazy.

The 3 parts of a Component

Each component is build out of three parts: the component itself, the state of the component and the internal message of the component.

These are similar to the 3 parts of your application with one difference. The internal state that can change based on events is represented as an extra a struct, not as the component struct itself.

Component itself

At first, we create the component struct itself:

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

As you see, it has one field link. Here, we store the link that will be displayed and opened when the hyperlink is clicked.

Message / Event

Now we need to create the message that will be used inside our component:

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

Here we have three events for our component. The Clicked event is called every time the user clicks onto the component. The MouseEnter and MouseExit events are called when the mouse enters over the component and leaves (in other words, hovering over the component).

State

In the state of our component, we store if the mouse hovers over the component.

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

Implementing the Component trait

Now we can implement the Component trait for the Hyperlink struct.

Full Implementation

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

Types

We define the types for our state and message/event:

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

View and Update logic

Every time an event is called, the update and view function gets called.

In the update function, we set the hovered field of the state or print "open link". Instead of printing something you could use crates like opener to open files and website, but that is beyond this example.

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

As you see, we return None in the update function. Instead of None we could return a Some(Message) that is propagated to the parent application.

We define in the view function how our component looks on the screen. In this case, we have a mouse area with a text inside.

The text color changes when the mouse is hovered over the component. If the mouse hovers above the component is determined by the state.hovered field that is hold up to date by our update function.

use iced::widget;
use iced::widget::Component;
pub struct Hyperlink {
    link: String,
}

impl Hyperlink {
    pub fn new(link: String) -> Self {
        Self { link }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum HyperlinkEvent {
    Clicked,
    MouseEnter,
    MouseExit,
}

#[derive(Default)]
pub struct HyperlinkState {
    hovered: bool,
}

impl<Message> Component<Message> for Hyperlink {
    type State = HyperlinkState;
    type Event = HyperlinkEvent;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
        match event {
            HyperlinkEvent::Clicked => println!("open link"),
            HyperlinkEvent::MouseEnter => state.hovered = true,
            HyperlinkEvent::MouseExit => state.hovered = false,
        }
        None
    }

    fn view(
        &self,
        state: &Self::State,
    ) -> iced::Element<'_, Self::Event, iced::Theme, iced::Renderer> {
        widget::container(
            widget::mouse_area(widget::text(&self.link).style(iced::theme::Text::Color(
                if state.hovered {
                    iced::Color::from_rgb(0.5, 0.5, 0.5)
                } else {
                    iced::Color::from_rgb(0.0, 0.0, 0.0)
                },
            )))
            .on_enter(HyperlinkEvent::MouseEnter)
            .on_exit(HyperlinkEvent::MouseExit)
            .on_press(HyperlinkEvent::Clicked),
        )
        .into()
    }
}

Render Backend

By default, Iced tries to use wgpu as the backend, and if that is not possible, tiny-skia.

If you want to specifically use tiny-skia as render backend, you can do that with an environment variable:

ICED_BACKEND=tiny-skia

Wasm / Running on Web

Iced has the ability to compile to web assembly (WASM). With that, you can run your iced app in the normal web browser.

Preparation

Before we start, we need to get the tools for compiling for the web.

Add Wasm Target

First, we need to add the wasm32-unknown-unknown target to our compiler so that we are able to compile to wasm.

rustup target add wasm32-unknown-unknown

If you want, you can compile to that target using

cargo build --target wasm32-unknown-unknown

Install Trunk

We will use trunk for building and serving the web page. For that, we need to install trunk via cargo:

cargo install --locked trunk

Create index.html

For trunk we need to create a index.html file that trunk will use as the base file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>NAME OF THE WEBSITE</title>
    <base data-trunk-public-url />
</head>
<body style="margin: 0; padding: 0; width:100%; height:100svh">
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="directoryname" />
</body>
</html>

Replace the directoryname with the name of your project.

Iced Features that are important for wasm

  • web-colors on the web, the colors of your app might not be correct or as intended, this feature fixes that.
  • webgl makes your app run in browsers that do not support wgpu (sadly, wgpu is not supported on all browsers and all platforms).

Running the App using Trunk

trunk serve

This will compile your project to wasm, build a web page, watch for changes and serve the app for you. You can access the web app at the URL given in the output. The generated site that is served is located in the dist directory.

Deploying the Web app

For now, we only used trunk serve to run our app. This works fine for development, but is not very good for production. We don't need to watch for changes in our files and rebuild/autoload them in production. Without that functionality, the served html can be way smaller.

Thankfully, we can use trunk build to build our app into a minimal result. If you look at the dist directory, you can see your build app (index.html as the starting point).

Disclaimer: If you try to open the index.html file without a web server that serves it, you will run into CORS issues.

You can serve these files on any web server. As a minimal example, you can use the built-in python web server and execute python3 -m http.server in the dist directory.

Debugging Wasm (ready)

If you encounter panics and errors in your browser console from your wasm builds, you might notice that these errors are nearly impossible to interpret.

To get a better error with a notice where exactly in your code the panic occurred, you have to change the panic hook to one that provides better exceptions in your browser.

For that, we can use the crate console_error_panic_hook.

To use that one you need to set the panic hook at the start of your program like here:

fn main() {
    std::panic::set_hook(Box::new(console_error_panic_hook::hook));

    // ...
}

With that in place, the errors in your web console should look a lot better.