The widget macro

To simplify the implementation of the Widgets trait, let's use the relm4-macros crate!

App screenshot dark

The app will look and behave identical to our first app from the previous chapter. Only the implementation is different.

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

What's different

The widgets macro will take care of creating the widgets struct and will also implement the Widgets trait for us. All other parts of the code remain untouched, so we can reuse most of the code from the previous chapter.

Let's have a look at the macro and go through the code step by step:

#[relm4_macros::widget]
impl Widgets<AppModel, ()> for AppWidgets {
    view! {
        gtk::ApplicationWindow {
            set_title: Some("Simple app"),
            set_default_width: 300,
            set_default_height: 100,
            set_child = Some(&gtk::Box) {
                set_orientation: gtk::Orientation::Vertical,
                set_margin_all: 5,
                set_spacing: 5,

                append = &gtk::Button {
                    set_label: "Increment",
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Increment);
                    },
                },
                append = &gtk::Button::with_label("Decrement") {
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Decrement);
                    },
                },
                append = &gtk::Label {
                    set_margin_all: 5,
                    set_label: watch! { &format!("Counter: {}", model.counter) },
                }
            },
        }
    }
}

The first line doesn't change. We still have to define what's the model and what's the parent model. The only difference is that the struct AppWidgets is never explicitly defined in the code, but generated by the macro.

And then... wait, where do we define the Root type? Actually, the macro knows that your outermost widget is going to be the root widget.

Next up - the heart of the widget macro - the nested view! macro. Here, we can easily define widgets and assign properties to them.

Properties

As you see, we start with the gtk::ApplicationWindow which is our root. Then we open up brackets and assign properties to the window. There's not much magic here but actually set_title is a method provided by gtk4-rs. So technically, the macro creates code like this:

window.set_title(Some("Simple app"));

Widgets

Eventually, we assign a new widget to the window.

            set_child = Some(&gtk::Box) {

The only difference to assigning properties is that we use = instead of :. We could also name widgets using the method: name = Widget syntax:

            set_child: vbox = Some(&gtk::Box) {

Sometimes we want to use a constructor function to initialize our widgets. For the second button we used the gtk::Button::with_label function. This function returns a new button with the "Decrement" label already set, so we don't have to call set_label afterwards.

                append = &gtk::Button::with_label("Decrement") {

Events

To connect events, we use this syntax.

method_name(cloned_var1, cloned_var2, ...) => move |args, ...| { code... }

Again, there's no magic. The macro will simply assign a closure to a method. Because closures often need to capture local variables that don't implement the Copy trait, we need to clone these variables. Therefore, we can list the variables we want to clone in the parentheses after the method name.

                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Increment);
                    },

UI updates

The last special syntax of the widgets macro we'll cover here is the watch! macro. It's just like the normal initialization except that it also updates the property in the view function. Without it, the counter label would never be updated.

                    set_label: watch! { &format!("Counter: {}", model.counter) },

The full reference for the syntax of the widget macro can be found here.

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, OrientableExt};
use relm4::{send, AppUpdate, Model, RelmApp, Sender, WidgetPlus, Widgets};

#[derive(Default)]
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
    }
}

#[relm4_macros::widget]
impl Widgets<AppModel, ()> for AppWidgets {
    view! {
        gtk::ApplicationWindow {
            set_title: Some("Simple app"),
            set_default_width: 300,
            set_default_height: 100,
            set_child = Some(&gtk::Box) {
                set_orientation: gtk::Orientation::Vertical,
                set_margin_all: 5,
                set_spacing: 5,

                append = &gtk::Button {
                    set_label: "Increment",
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Increment);
                    },
                },
                append = &gtk::Button::with_label("Decrement") {
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Decrement);
                    },
                },
                append = &gtk::Label {
                    set_margin_all: 5,
                    set_label: watch! { &format!("Counter: {}", model.counter) },
                }
            },
        }
    }
}

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