Welcome! We notice you're using an outdated browser, which will result in a degraded experience on this site. Please consider a modern, fully supported browser.

webbureaucrat

The articles are just window-dressing for code snippets I want to keep.

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 or submit a pull request.

Setup

Let's start with a fresh application:

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.

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:

#[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:

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.
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().

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.

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.

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 useing some relevant parts.

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().

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.

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

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()
    }
}

I write to learn, so I welcome your constructive criticism. Report issues on GitLab.

← Home