DEV Community

webbureaucrat
webbureaucrat

Posted on • Originally published at webbureaucrat.gitlab.io on

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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;

}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we're getting to the real meat of the application: the update and viewfunctions.

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

Enter fullscreen mode Exit fullscreen mode

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};
Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Top comments (0)