Your first app

For our first app, let's create something original: A simple counter app.

App screenshot dark

In this app we will have a counter which can be incremented and decremented by pressing the corresponding buttons.

The app we will write in this chapter is also available here. Run cargo run --example simple_manual from the example directory if you want to see the code in action.

The basic structure

Relm4 builds on the Elm programming model. This means there are three important data types you need to define:

  • The model type that stores your application state.
  • The message type that defines which messages can be sent to modify the model.
  • The widgets type that stores the GTK widgets (UI elements).

Let's see how we can implement those types for our counter app.

The model

Our app only needs to store the state of a counter, so a simple u8 is enough.

struct AppModel {
    counter: u8,
}

The message

Now we need to define what messages can be used to modify the model. The message can be represented by any data type, but most often, an enum is used. In our case, we just want to increment and decrement the counter.

enum AppMsg {
    Increment,
    Decrement,
}

The widgets

The widgets struct stores the widgets we need to build our user interface. For our app, we can use a window with an increment button, a decrement button and a label to display the counter value. Besides that, we need a box as a container to place our buttons and the label inside because a window can only have one child.

struct AppWidgets {
    window: gtk::ApplicationWindow,
    vbox: gtk::Box,
    inc_button: gtk::Button,
    dec_button: gtk::Button,
    label: gtk::Label,
}

The Model trait

With our data types in place, we can now implement the model trait. This trait associates a model with other types to reduce the amount of generic parameters in other trait implementations.

There are three types we need to include:

  • Msg: what message type do we use to update the model?
  • Widgets: which struct stores the widgets of our UI?
  • Components: which child components does our model use?

We don't care about components for now because we are just writing a simple app. Therefore, we can use () as placeholder.

impl Model for AppModel {
    type Msg = AppMsg;
    type Widgets = AppWidgets;
    type Components = ();
}

The update loop

As a next step, we want to make our app interactive. Relm4 has two important functions that update state and UI:

  • update: receives a message and modifies the model
  • view: receives the modified model and updates the UI accordingly

Before anything happens, a message must be sent through a channel. Theoretically, anything can send messages, but usually you send messages when a button is clicked or similar events occur. We will have a look at this later.

relm update loop

Data and widgets are separated from each other: the update function doesn't interact with the widgets and the view function doesn't modify the model.

The AppUpdate trait

Theory is nice, but let's see it in action.

Our update function is implemented with the AppUpdate trait.

impl AppUpdate for AppModel {
    fn update(&mut self, msg: AppMsg, _components: &(), _sender: Sender<AppMsg>) -> bool {
        match msg {
            AppMsg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            AppMsg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
        true
    }
}

wrapping_add(1) and wrapping_sub(1) are like +1 and -1 , but don't panic on overflows.

Whenever a new message is sent by our buttons, we process this message here and modify our counter accordingly.

Also, we return true because we don't want to quit our application. If our app should close, we can simply return false to close the application.

The Widgets trait

Our last step is implementing the widgets trait. It provides methods to initialize and update the UI.

Let's do this step by step. First, we'll have a look at beginning of the trait impl.

impl Widgets<AppModel, ()> for AppWidgets {
    type Root = gtk::ApplicationWindow;

You'll notice that

  • there are two generic parameters
  • a Root type

The two generic parameters are our model and the parent model. The parent model is only used in components which we will discuss later, so again we can simply use () as placeholder.

The Root type is the root widget of the app. Components can choose this type freely, but the main application must use a gtk::ApplicationWindow.

Next up, we want to initialize our UI.

    /// Initialize the UI.
    fn init_view(model: &AppModel, _parent_widgets: &(), sender: Sender<AppMsg>) -> Self {
        let window = gtk::ApplicationWindow::builder()
            .title("Simple app")
            .default_width(300)
            .default_height(100)
            .build();
        let vbox = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(5)
            .build();
        vbox.set_margin_all(5);

        let inc_button = gtk::Button::with_label("Increment");
        let dec_button = gtk::Button::with_label("Decrement");

        let label = gtk::Label::new(Some(&format!("Counter: {}", model.counter)));
        label.set_margin_all(5);

        // Connect the widgets
        window.set_child(Some(&vbox));
        vbox.append(&inc_button);
        vbox.append(&dec_button);
        vbox.append(&label);

        // Connect events
        let btn_sender = sender.clone();
        inc_button.connect_clicked(move |_| {
            send!(btn_sender, AppMsg::Increment);
        });

        dec_button.connect_clicked(move |_| {
            send!(sender, AppMsg::Decrement);
        });

        Self {
            window,
            vbox,
            inc_button,
            dec_button,
            label,
        }
    }

But what exactly happens here?

First, we initialize each of our widgets, mostly by using builder patterns.

Then we connect the widgets so that GTK4 knows how they are related to each other. The buttons and the label are added to the box, and the box is added to the window.

Now the magic happens: we connect the "clicked" event for both buttons and send a message from the closures back to the update loop. To do this, we only need to move a clone of our sender into the closures and send the message.

Alright, now every time we click our buttons a message will be sent to update our counter!

Yet our UI will not updated when the counter is changed. To do this, we need to implement the view function:

    /// Update the view to represent the updated model.
    fn view(&mut self, model: &AppModel, _sender: Sender<AppMsg>) {
        self.label.set_label(&format!("Counter: {}", model.counter));
    }

We just need to update the label to represent the new counter value.

We're almost done. To complete the Widgets trait we just need to implement the root_widget method.

    /// Return the root widget.
    fn root_widget(&self) -> Self::Root {
        self.window.clone()
    }

Running the App

The last step is to run the app we just wrote. To do so, we just need to initialize our model and pass it into RelmApp::new().

fn main() {
    let model = AppModel { counter: 0 };
    let app = RelmApp::new(model);
    app.run();
}

🎉 Congratulations! You just wrote your first app with Relm4! 🎉

Conclusion

There are a few concepts in Relm4 that might look complex at first but are actually quite easy to understand and help you keep your code structured. I hope this chapter made everything clear for you :)

If you found a mistake or there was something unclear, please open an issue here.

As you have seen, initializing the UI was by far the largest part of our app, with roughly one half of the total code. In the next chapter, we will have a look at the relm4-macros crate that offers a macro that helps us to reduce the amount of code we need to implement the Widgets trait.

As you might have noticed, storing inc_button, dec_button and vbox in our widgets struct is not necessary because GTK will keep them alive automatically. Therefore, we can remove them from AppWidgets to avoid compiler warnings.

The complete code

Let's review our code in one piece one more time to see how all these parts work together:

use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt};
use relm4::{send, AppUpdate, Model, RelmApp, Sender, WidgetPlus, Widgets};

struct AppModel {
    counter: u8,
}

enum AppMsg {
    Increment,
    Decrement,
}

impl Model for AppModel {
    type Msg = AppMsg;
    type Widgets = AppWidgets;
    type Components = ();
}

impl AppUpdate for AppModel {
    fn update(&mut self, msg: AppMsg, _components: &(), _sender: Sender<AppMsg>) -> bool {
        match msg {
            AppMsg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            AppMsg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
        true
    }
}

struct AppWidgets {
    window: gtk::ApplicationWindow,
    vbox: gtk::Box,
    inc_button: gtk::Button,
    dec_button: gtk::Button,
    label: gtk::Label,
}

impl Widgets<AppModel, ()> for AppWidgets {
    type Root = gtk::ApplicationWindow;

    /// Initialize the UI.
    fn init_view(model: &AppModel, _parent_widgets: &(), sender: Sender<AppMsg>) -> Self {
        let window = gtk::ApplicationWindow::builder()
            .title("Simple app")
            .default_width(300)
            .default_height(100)
            .build();
        let vbox = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(5)
            .build();
        vbox.set_margin_all(5);

        let inc_button = gtk::Button::with_label("Increment");
        let dec_button = gtk::Button::with_label("Decrement");

        let label = gtk::Label::new(Some(&format!("Counter: {}", model.counter)));
        label.set_margin_all(5);

        // Connect the widgets
        window.set_child(Some(&vbox));
        vbox.append(&inc_button);
        vbox.append(&dec_button);
        vbox.append(&label);

        // Connect events
        let btn_sender = sender.clone();
        inc_button.connect_clicked(move |_| {
            send!(btn_sender, AppMsg::Increment);
        });

        dec_button.connect_clicked(move |_| {
            send!(sender, AppMsg::Decrement);
        });

        Self {
            window,
            vbox,
            inc_button,
            dec_button,
            label,
        }
    }

    /// Return the root widget.
    fn root_widget(&self) -> Self::Root {
        self.window.clone()
    }

    /// Update the view to represent the updated model.
    fn view(&mut self, model: &AppModel, _sender: Sender<AppMsg>) {
        self.label.set_label(&format!("Counter: {}", model.counter));
    }
}

fn main() {
    let model = AppModel { counter: 0 };
    let app = RelmApp::new(model);
    app.run();
}