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(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1
}
iced::Task::none()
}
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 button
s. We can declare our UI as follows,
fn view(&self) {
let ui = column![
button("+").on_press(Message::IncrementCount),
text(self.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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
3. Implementing the Counter
To create a window, we implement the update
function for our Counter
.
Initial state
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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
Tip: Use
count.saturating_add(1)
orcount.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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
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 typeElement<'_, 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
To run the counter we first create a application with the application
function.
With that function we define our Window Title and the update and view functions.
This illicitly defines out state type and the message type.
Then we run the counter with an initial state and a start task that does nothing.
use iced::widget;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
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.
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;
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 our Counter
impl Counter {
fn new() -> Self {
// initialize the counter struct
// with count value as 0.
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
// handle emitted messages
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message> {
// create the View Logic (UI)
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text(self.count),
widget::button("+").on_press(Message::IncrementCount)
];
widget::container(row)
.center_x(iced::widget::Fill)
.center_y(iced::widget::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
fn main() -> Result<(), iced::Error> {
// run the app from main function
iced::application("Counter Example", Counter::update, Counter::view).run_with(|| (Counter::new(), iced::Task::none()))
}
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.
This is how they would align in the center.
In code, if you want to set the Alignment
you can call the align_x
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_x(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)
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
andheight
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 Tasks
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 Task
and Subscription
.
A task will run until it is finished, while a subscription runs as long as the application says so.
In this chapter, we will look into both of them and how we can use them.
Tasks
Note: in the past
Task
s where namedCommand
s
A task is "A set of asynchronous actions to be performed by some runtime".
Basically, a task is just a Stream
that returns messages.
You can create custom tasks, 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 task.
A task will run until it has finished and can return multiple messages during its execution.
Executing a Task
In your App, you can execute a task by returning it from the update function of your application.
Batch multiple tasks
Sometimes you want to return more than one task. For that, you can use the Task::batch function to batch a few of them together like this:
return Task::batch(vec![task1, task2, task3]);
Custom Task 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 task.
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.13.1", features = ["tokio"]}
reqwest = "0.11.24"
Making the api request
At first, we define what our task 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::Task;
#[derive(Debug, Clone)]
enum Message {
Refetch,
CurrentIp(String),
}
struct App {
ip: String,
}
impl App {
fn new() -> (Self, iced::Task<Message>) {
(App { ip: String::new() }, Task::none())
}
fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text(&self.ip),
iced::widget::button("Start task").on_press(Message::Refetch)
]
.into()
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
println!("update");
match message {
Message::Refetch => return Task::perform(fetch_ip(), Message::CurrentIp),
Message::CurrentIp(text) => {
self.ip = text;
}
}
Task::none()
}
}
async fn fetch_ip() -> String {
println!("fetch_ip");
reqwest::get("https://api.seeip.org")
.await
.unwrap()
.text()
.await
.unwrap()
}
fn main() {
iced::run("Custom Task Example", App::update, App::view).unwrap();
}
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 task or subscription to run a closure on a thread where blocking is acceptable.
Starting/Creating the task
In the update function we return Task::none()
or our custom task depending on the message.
If the Message is Message::CurrentIp
we change our state, if it is Message::Refetch
we return our task.
use iced::Task;
#[derive(Debug, Clone)]
enum Message {
Refetch,
CurrentIp(String),
}
struct App {
ip: String,
}
impl App {
fn new() -> (Self, iced::Task<Message>) {
(App { ip: String::new() }, Task::none())
}
fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text(&self.ip),
iced::widget::button("Start task").on_press(Message::Refetch)
]
.into()
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
println!("update");
match message {
Message::Refetch => return Task::perform(fetch_ip(), Message::CurrentIp),
Message::CurrentIp(text) => {
self.ip = text;
}
}
Task::none()
}
}
async fn fetch_ip() -> String {
println!("fetch_ip");
reqwest::get("https://api.seeip.org")
.await
.unwrap()
.text()
.await
.unwrap()
}
fn main() {
iced::run("Custom Task Example", App::update, App::view).unwrap();
}
To create our custom task, we use the Task::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::Task;
#[derive(Debug, Clone)]
enum Message {
Refetch,
CurrentIp(String),
}
struct App {
ip: String,
}
impl App {
fn new() -> (Self, iced::Task<Message>) {
(App { ip: String::new() }, Task::none())
}
fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text(&self.ip),
iced::widget::button("Start task").on_press(Message::Refetch)
]
.into()
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
println!("update");
match message {
Message::Refetch => return Task::perform(fetch_ip(), Message::CurrentIp),
Message::CurrentIp(text) => {
self.ip = text;
}
}
Task::none()
}
}
async fn fetch_ip() -> String {
println!("fetch_ip");
reqwest::get("https://api.seeip.org")
.await
.unwrap()
.text()
.await
.unwrap()
}
fn main() {
iced::run("Custom Task Example", App::update, App::view).unwrap();
}
Note:
fetch_ip()
produces the future
Note:
Message::CurrentIp
is a shorthand for|x| Message::CurrentIp(x)
Full Code
use iced::Task;
#[derive(Debug, Clone)]
enum Message {
Refetch,
CurrentIp(String),
}
struct App {
ip: String,
}
impl App {
fn new() -> (Self, iced::Task<Message>) {
(App { ip: String::new() }, Task::none())
}
fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text(&self.ip),
iced::widget::button("Start task").on_press(Message::Refetch)
]
.into()
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
println!("update");
match message {
Message::Refetch => return Task::perform(fetch_ip(), Message::CurrentIp),
Message::CurrentIp(text) => {
self.ip = text;
}
}
Task::none()
}
}
async fn fetch_ip() -> String {
println!("fetch_ip");
reqwest::get("https://api.seeip.org")
.await
.unwrap()
.text()
.await
.unwrap()
}
fn main() {
iced::run("Custom Task Example", App::update, App::view).unwrap();
}
Task From Stream
Imagine you want a Task
that produces more than one Message.
One solution is to use Task::run
and pass a Stream to it.
A Stream
is basically an async iterator.
To create a Stream
we can use iced::stream::channel
.
With that function, we can convert a Future
to a Stream
. In the Future
we can emit messages via a given Sender
.
Subscriptions
A subscription is similar to a task. It runs in the background. Subscriptions are often used to listen to external events. It can produce one or more values. One key difference is that we control how long a subscription runs. That leads to the "issue" that the subscription itself can never end by itself, even after finishing its work.
Warning: I am not that familiar with the iced internals so the following might be incorrect. It is only how I understood it.
A Subscription
runs as long as we return it from the closure provided by the subscription
function.
The runtime calls that method after each update and checks if a new or old subscription is provided.
Every Subscription
has an ID. If you use Subscription::run_with_id
you specify the ID. If you use Subscription::run
the function pointer is used, which could cause bugs, from what I have heard.
If a new subscription is provided, the runtime will start it. If an old one that already runs is provided, nothing happens. If a subscription runs that is not provided by the subscription function, the running subscription is terminated.
Create a Stream
To create a Stream
we can use iced::stream::channel
.
With that function, we can convert a Future
to a Stream
. In the future, we can emit messages via a given Sender
.
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 = { version = "0.13.1", 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,
ChangeContent(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::ChangeContent(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::ChangeContent),
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::ChangeContent(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,
ChangeContent(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::ChangeContent(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::ChangeContent),
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::ChangeContent(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,
ChangeContent(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::ChangeContent(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::ChangeContent),
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::ChangeContent(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.
Creating a Hyperlink Example
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.
When run on the web, iced uses a canvas to render the application to it.
Previously, there was the iced_web
repository that used another approach, but since it was hard to maintain, support was dropped. As of today, iced_web
is out of date and not maintained.
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.