Iced for Desktop Development in Rust
Tags: iced, rust
Published: 2023-09-28 14:06:37

---
title: Iced for Desktop Development in Rust
published: true
date: 2023-09-25 00:00:00 UTC
tags: ["iced", "rust"]
canonical_url: https://webbureaucrat.gitlab.io/articles/iced-for-desktop-development-in-rust/
---

As someone who loves Elm and has recently fallen in love with Rust, I was delighted to learn there are not just one but _two_ Rust frameworks modeled on the Elm Architecture. If you're not familiar, the Elm Architecture is a pain-free way of developing fast, responsive, asynchronous-by-default front-ends. Let's see it in action by developing a simple "Hello, world" app.

_Note: I'm using `iced` version `0.10.0`. Iced is currently under rapid development with frequent breaking changes. It's possible these directions won't work as expected in a future release._ As always, if something breaks, [open an issue](https://gitlab.com/webbureaucrat/webbureaucrat.gitlab.io/-/issues) or submit a pull request.

### Setup

Let's start with a fresh application:

```bash
cargo init hello-iced
cd hello-iced
cargo add iced
cargo build
```

If the build succeeds, we're off to a good start.

### Writing a State Model for Iced in Rust

The easiest part of the Elm Architecture to understand is the model state, so let's start there. The model state represents the whole state of the application--everything you would need to know in order to draw any given application screen. As you can imagine, state models are often quite large, but our application will need only one piece of information: whom to greet (by default: the world). That's easy.

```rust
struct Hello{ 
    addressee: String
}
```

### Writing a Message Enum for Iced in Rust

Next, we need our Message. A message is an enum that represents all the ways that our model can _update_ according to the logic of our application. For example, you might have a counter that can increment or a counter that can either increment and decrement, or a counter that can increment, decrement, or reset, all depending on the logic of your application.

In our case, we only have one field in our model `struct`, and it can only be updated through a text input, so let's write an enum to that effect:

```rust
#[derive(Clone, Debug)]
enum HelloMessage { 
    TextBoxChange(String)
}
```

We can see that our enum takes a `String`. This represents the new string to which we will update our model. Also note that it derives `Clone` and `Debug`. This is required by the framework.

### Implementing `iced::Application`

Now the real work begins: turning our model into an Iced `Application`. Let's begin:

```rust
struct Hello { 
    addressee: String,
} 

#[derive(Clone, Debug)]
enum HelloMessage { 
    TextBoxChange(String)
}

impl iced::Application for Hello {}
```

We need four types for our application.

- `Executor` - The only type provided by the framework is `Default`, which will suffice for our purposes.
- `Flags` - represents any data we want to initialize our application. We have no flags to pass, so the unit type will suffice.
- `Message` - this is the `HelloMessage` we defined earlier.
- `Theme` - the theme of the application.

```rust
struct Hello {
    addressee: String,
}
 
#[derive(Clone, Debug)]
enum HelloMessage {
    TextBoxChange(String)
}

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

}

```

Next we need a function that initializes our model. Our model will default to greeting, well, the world.

Start by adding `use iced::Command;` at the top. A command is an asynchronous action that the framework will take on next. We don't need to start any commands on startup, so we'll initialize with `Command::none()`.

```rust
impl iced::Application for Hello {
    type Executor = iced::executor::Default;
    type Flags = ();
    type Message = HelloMessage;
    type Theme = iced::Theme;

    fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
        ( Hello { addressee: String::from("world"), }, Command::none() )
    }

```

We also need a function which takes the `&self` state model and returns a title for the title bar at the top of the application.

```rust
impl iced::Application for Hello {
    type Executor = iced::executor::Default;
    type Flags = ();
    type Message = HelloMessage;
    type Theme = iced::Theme;

    fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
        ( Hello { addressee: String::from("world"), }, Command::none() )
    }

    fn title(&self) -> String {
        String::from("Greet the World")
    }
}
```

Now we're getting to the real meat of the application: the `update` and `view`functions.

The `update` function takes our state model and mutates it based on the given message. It also gives us the opportunity to start another `Command` after mutating our state. For example, a common use case for a submit action message is to change part of the state to represent that the state is "Loading..." and then to start a command to fetch some data based on the submission. We only have one kind of message, so our update function will be very simple.

```rust
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
	match message {
	    HelloMessage::TextBoxChange(string) => {
		self.addressee = string;
		Command::none()
	    }
	}
}

```

Finally, we can write our view. Let's start by `use`ing some relevant parts.

```rust
use iced::{Command, Element, Length, Renderer, Settings};
use iced::widget::{Column, Row, Text};
```

We build out our view programmatically using the`push` method, and then convert the final result to an `Element` using`into()`.

```rust
fn view(&self) -> Element<Self::Message> {
	let text = Text::new(format!("Hello, {0}.", self.addressee))
            .width(Length::Fill)
            .horizontal_alignment(iced::alignment::Horizontal::Center);
	let row1 = Row::new().push(text);
	let text_input: iced::widget::TextInput<'_, HelloMessage, Renderer> =
	    iced::widget::text_input("world", self.addressee.as_str())
	    .on_input(HelloMessage::TextBoxChange);
	let row2 = Row::new().push(text_input);
	Column::new().push(row1).push(row2).into()
    }

```

### Writing a `main` function to `run` an Iced `Application`

Finally, we need to `run` our application from the `main` function. Make sure to `use` `iced::Application` to bring the `run` function into scope.

```rust
use iced::{Application, Command, Element, Length, Renderer, Settings};
use iced::widget::{Column, Row, Text};

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

### Wrapping up

In conclusion, I hope this has been a helpful introduction to the Iced framework and the Elm Architecture. The architecture has a learning curve, so I figure the more learning resources out there, the better.

For reference, this is the full source code:

_src/main.rs_

```rust
use iced::{Application, Command, Element, Length, Renderer, Settings};
use iced::widget::{Column, Row, Text};

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

struct Hello {
    addressee: String,
}
 
#[derive(Clone, Debug)]
enum HelloMessage {
    TextBoxChange(String)
}

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

    fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
        ( Hello { addressee: String::from("world"), }, Command::none() )
    }

    fn title(&self) -> String {
        String::from("Greet the World")
    }

    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
	match message {
	    HelloMessage::TextBoxChange(string) => {
		self.addressee = string;
		Command::none()
	    }
	}
    }
    
    fn view(&self) -> Element<Self::Message> {
	let text = Text::new(format!("Hello, {0}.", self.addressee))
            .width(Length::Fill)
            .horizontal_alignment(iced::alignment::Horizontal::Center);
	let row1 = Row::new().push(text);
	let text_input: iced::widget::TextInput<'_, HelloMessage, Renderer> =
	    iced::widget::text_input("world", self.addressee.as_str())
	    .on_input(HelloMessage::TextBoxChange);
	let row2 = Row::new().push(text_input);
	Column::new().push(row1).push(row2).into()
    }
}

```