First Steps

But enough with the theory. It’s about time we start writing some code!

iced embraces The Elm Architecture as the most natural approach for architecting interactive applications. Therefore, when using iced, we will be dealing with the four main ideas we introduced in the previous chapter: state, messages, update logic, and view logic.

In the previous chapter, we dissected and studied the classical counter interface. Let’s try to build it in Rust while leveraging The Elm Architecture.

A classical counter interface

State

Let’s start with the state—the underlying data of the application.

In Rust, given the ownership and borrowing rules, it is extremely important to think carefully about the data model of your application.

I encourage you to always start by pondering about the data of your application and its different states—not only those that are possible, but also those that must be impossible. Then try to leverage the type system as much as you can to Make Impossible States Impossible.

For our counter interface, all we need is a counter value. Since we have both increment and decrement interactions, the number could potentially be negative. This means we need a signed integer.

Also, we know some users are crazy and they may want to count a lot of things. Let’s give them 64 bits to play with:

struct Counter {
    value: i64,
}

If a crazy user counted 1000 things every second, it would take them ~300 million years to run out of numbers. Let’s hope that’s enough.

Messages

Next, we need to define our messages—the interactions of the application.

Our counter interface has two interactions: increment and decrement. Technically, we could use a simple boolean to encode these interactions: true for increment and false for decrement, for instance.

But… we can do better in Rust! Interactions are mutually exclusive—when we have an interaction, what we really have is one value of a possible set of values. It turns out that Rust has the perfect data type for modeling this kind of idea: the enum.

Thus, we can define our messages like this:

enum Message {
    Increment,
    Decrement,
}

Simple enough! This also sets us up for the long-term. If we ever wanted to add additional interactions to our application—like a Reset interaction, for instance—we could just introduce additional variants to this type. Enums are very powerful and convenient.

Update Logic

Now, it’s time for our update logic—how messages change the state of the application.

Basically, we need to write some logic that given any message can update any state of the application accordingly. The simplest and most idiomatic way to express this logic in Rust is by defining a method named update in our application state.

For our counter interface, we only need to properly increment or decrement the value of our Counter struct based on the Message we just defined:

impl Counter {
    fn update(&mut self, message: Message) {
        match message {
            Message::Increment => {
                self.value += 1;
            }
            Message::Decrement => {
                self.value -= 1;
            }
        }
    }
}

Great! Now we are ready to process user interactions. For instance, imagine we initialized our counter like this:

let mut counter = Counter { value: 0 };

And let’s say we wanted to simulate a user playing with our interface for a bit—pressing the increment button twice and then the decrement button once. We could easily compute the final state of our counter with our update logic:

counter.update(Message::Increment);
counter.update(Message::Increment);
counter.update(Message::Decrement);

This would cause our Counter to end up with a value of 1:

assert_eq!(counter.value, 1);

In fact, we have just written a simple test for our application logic:

#[test]
fn it_counts_properly() {
    let mut counter = Counter { value: 0 };

    counter.update(Message::Increment);
    counter.update(Message::Increment);
    counter.update(Message::Decrement);

    assert_eq!(counter.value, 1);
}

Notice how easy this was to write! So far, we are just leveraging very simple Rust concepts. No dependencies in sight! You may even be wondering… “Where is the GUI code?!”

This is one of the main advantages of The Elm Architecture. As we discovered in the previous chapter, widgets are the only fundamental idea of an interface that is reusable in nature. All the parts we have defined so far are application-specific and, therefore, do not need to know about the UI library at all!

The Elm Architecture properly embraces the different nature of each part of a user interface—decoupling state, messages, and update logic from widgets and view logic.

View Logic

Finally, the only part left for us to define is our view logic—how state dictates the widgets of the application.

Here is where the magic happens! In view logic, we bring together the state of the application and its possible interactions to produce a visual representation of the user interface that must be displayed to the user.

A classical counter interface

As we have already learned, this visual representation is made of widgets—the visibly distinct units of an interface. Most widgets are not application-specific and they can be abstracted and packaged into reusable libraries. These libraries are normally called widget toolkits, GUI frameworks, or simply GUI libraries.

And this is where iced comes in—finally! iced is a cross-platform GUI library for Rust. It packages a fair collection of ready-to-use widgets; buttons and numbers included. Exactly what we need for our counter.

The Buttons

Our counter interface has two buttons. Let’s see how we can define them using iced.

In iced, widgets are independent values. The same way you can have an integer in a variable, you can have a widget as well. These values are normally created using a helper function from the widget module.

For our buttons, we can use the button helper:

use iced::widget::button;

let increment = button("+");
let decrement = button("-");

That’s quite simple, isn’t it? For now, we have just defined a couple of variables for our buttons.

As we can see, widget helpers may take arguments for configuring parts of the widgets to our liking. In this case, the button function takes a single argument used to describe the contents of the button.

The Number

We have our buttons sitting nicely in our increment and decrement variables. How about we do the same for our counter value?

While iced does not really have a number widget, it does have a more generic text widget that can be used to display any kind of text—numbers included:

use iced::widget::text;

let counter = text(15);

Sweet! Like button, text also takes an argument used to describe its contents. Since we are just getting started, let’s simply hardcode 15 for now.

The Layout

Alright! We have our two buttons in increment and decrement, and our counter value in counter. That should be everything, right?

Not so fast! The widgets in our counter interface are displayed in a specific order. Given our three widgets, there is a total of six different ways to order them. However, the order we want is: increment, counter, and decrement.

A very simple way of describing this order is to create a list with our widgets:

let interface = vec![increment, counter, decrement];

But we are still missing something! It’s not only the order that is specific, our interface also has a specific visual layout.

The widgets are positioned on top of each other, but they could very well be positioned from left to right instead. There is nothing in our description so far that talks about the layout of our widgets.

In iced, layout is described using… well, more widgets! That’s right. Not all widgets produce visual results directly; some may simply manage the position of existing widgets. And since widgets are just values, they can be nested and composed nicely.

The kind of vertical layout that we need for our counter can be achieved with the column widget:

use iced::widget::column;

let interface = column![increment, counter, decrement];

This is very similar to our previous snippet. iced provides a column! macro for creating a column out of some widgets in a particular order—analogous to vec!.

The Interactions

At this point, we have in our interface variable a column representing our counter interface. But if we actually tried to run it, we would quickly find out that something is wrong.

Our buttons would be completely disabled. Of course! We have not defined any interactions for them. Notice that we have yet to use our Message enum in our view logic. How is our user interface supposed to produce messages if we don’t specify them? Let’s do that now.

In iced, every widget has a specific type that enables further configuration using simple builder methods. The button helper returns an instance of the Button type, which has an on_press method we can use to define the message it must produce when a user presses the button:

use iced::widget::button;

let increment = button("+").on_press(Message::Increment);
let decrement = button("-").on_press(Message::Decrement);

Awesome! Our interactions are wired up. But there is still a small detail left. A button can be pressed multiple times. Therefore, the same button may need to produce multiple instances of the same Message. As a result, we need our Message type to be cloneable.

We can easily derive the Clone trait—as well as Debug and Copy for good measure:

#[derive(Debug, Clone, Copy)]
enum Message {
    Increment,
    Decrement,
}

In The Elm Architecture, messages represent events that have occurred—made of pure data. As a consequence, it should always be easy to derive Debug and Clone for our Message type.

The View

We are almost there! There is only one thing left to do: connecting our application state to the view logic.

Let’s bring together all the view logic we have written so far:

use iced::widget::{button, column, text};

// The buttons
let increment = button("+").on_press(Message::Increment);
let decrement = button("-").on_press(Message::Decrement);

// The number
let counter = text(15);

// The layout
let interface = column![increment, counter, decrement];

If we ran this view logic, we would now be able to press the buttons. However, nothing would happen as a result. The counter would be stuck—always showing the number 15. Our interface is completely stateless!

Obviously, the issue here is that our counter variable contains a text widget with a hardcoded 15. Instead, what we want is to actually display the value field of our Counter state. This way, when a button is pressed and our update logic is triggered, the text widget will display the new value.

We can easily do this by running our view logic in a method of our Counter—just like we did with our update logic:

use iced::widget::{button, column, text};

impl Counter {
    fn view(&self) {
        // The buttons
        let increment = button("+").on_press(Message::Increment);
        let decrement = button("-").on_press(Message::Decrement);

        // The number
        let counter = text(self.value);

        // The layout
        let interface = column![increment, counter, decrement];
    }
}

Our counter variable now will always have a text widget with the current value of our Counter. Great!

However, and as you may have noticed, this view method is completely useless—it constructs an interface, but then… It does nothing with it and throws it away!

In iced, constructing and configuring widgets has no side effects. There is no “global context” you need to worry about in your view code.

Instead of throwing the interface away, we need to return it. Remember, the purpose of our view logic is to dictate the widgets of our user interface; and the content of the interface variable is precisely the description of the interface we want:

use iced::widget::{button, column, text, Column};

impl Counter {
    fn view(&self) -> Column<Message> {
        // The buttons
        let increment = button("+").on_press(Message::Increment);
        let decrement = button("-").on_press(Message::Decrement);

        // The number
        let counter = text(self.value);

        // The layout
        let interface = column![increment, counter, decrement];

        interface
    }
}

Tada! Notice how the view method needs a return type now. The returned type is Column because the column! macro produces a widget of this type—just like button produces a widget of the Button type.

You may also have noticed that this Column type has a generic type parameter. This type parameter simply specifies the type of messages the widget may produce. In this case, it takes our Message because the increment and decrement buttons inside the column produce messages of this type.

iced has a strong focus on type safety—leveraging the type system and compile-time guarantees to minimize runtime errors as much as possible.

And well… That’s it! Our view logic is done! But wait… It’s a bit verbose right now. Since it’s such a simple interface, let’s just inline everything:

A classical counter interface
use iced::widget::{button, column, text, Column};

impl Counter {
    fn view(&self) -> Column<Message> {
        column![
            button("+").on_press(Message::Increment),
            text(self.value),
            button("-").on_press(Message::Decrement),
        ]
    }
}

That’s much more concise. It even resembles the actual interface! Since creating widgets just yields values with no side effects; we can move things around in our view logic without worrying about breaking other stuff. No spooky action at a distance!

And that’s all there is to our counter interface. I am sure you can’t wait to run it. Shall we?