Macro expansion

To better understand the widget macro, we will have a look at how the different parts of the widget macro are translated into real Rust code (aka the macro expansion). Therefore, we will write a small app that uses as many widget macro features as possible.

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

The boilerplate

First, let's have a look at the parts of the code that are later used by the macro.

The model

The model stores a counter, several class names and a decrement field that will indicate if the counter was last decremented or not. This will be used later in a tracker that only updates when the user decrements the counter.

struct AppModel {
    counter: u8,
    classes: Vec<&'static str>,
    decrement: bool,
}

The message type

The message type is the same as in our first app.

enum AppMsg {
    Increment,
    Decrement,
}

The update function

The update function is very simple, too. The only difference is that we set the decrement field to true if the Decrement message was sent.

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

The component

We will use a minimal button component that just has a button as widget to showcase the component! macro later.

enum ButtonMsg {}

struct ButtonModel {}

impl Model for ButtonModel {
    type Msg = ButtonMsg;
    type Widgets = ButtonWidgets;
    type Components = ();
}

impl ComponentUpdate<AppModel> for ButtonModel {
    fn init_model(_parent_model: &AppModel) -> Self {
        ButtonModel {}
    }

    fn update(
        &mut self,
        _msg: ButtonMsg,
        _components: &(),
        _sender: Sender<ButtonMsg>,
        _parent_sender: Sender<AppMsg>,
    ) {
    }
}

#[relm4_macros::widget]
impl Widgets<ButtonModel, AppModel> for ButtonWidgets {
    view! {
        gtk::Button {
            set_label: "ButtonComponent!",
        }
    }
}

pub struct AppComponents {
    button1: RelmComponent<ButtonModel, AppModel>,
    button2: RelmComponent<ButtonModel, AppModel>,
}

A custom widget function

Also, we add a small function that simply returns a gtk::Label.

fn new_label() -> gtk::Label {
    gtk::Label::new(Some("test"))
}

The macro

Let's have a look at the whole macro before we will break it down into smaller parts. If you're unfamiliar with the macro syntax, have a look at the previous chapter.

#[relm4_macros::widget]
impl Widgets<AppModel, ()> for AppWidgets {
    view! {
            main_window = gtk::ApplicationWindow {
                gtk::prelude::GtkWindowExt::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?: Some(5),
                    set_spacing: 5,

                    append: component!(components.button1.root_widget()),
                    append: inc_button = &gtk::Button {
                        set_label: "Increment",
                        connect_clicked(sender) => move |_| {
                            send!(sender, AppMsg::Increment);
                        },
                        add_css_class: iterate!(&model.classes),
                    },
                    append = &gtk::Button::new() {
                        set_label: track!(model.decrement, &format!("Last decrement at {}", model.counter)),
                        connect_clicked(sender) => move |_| {
                            send!(sender, AppMsg::Decrement);
                        },
                    },
                    append = &new_label() -> gtk::Label {
                        set_margin_all: 5,
                        set_label: watch! { &format!("Counter: {}", model.counter) },
                    },
                    append = &gtk::Grid {
                        set_vexpand: true,
                        set_hexpand: true,
                        set_row_spacing: 10,
                        set_column_spacing: 10,
                        set_column_homogeneous: true,
                        attach(1, 1, 1, 1) = &gtk::Label {
                            set_label: "grid test 1",
                        },
                        attach(1, 2, 1, 1) = &gtk::Label {
                            set_label: "grid test 2",
                        },
                        attach(2, 1, 1, 1) = &gtk::Label {
                            set_label: "grid test 3",
                        },
                        attach(2, 2, 1, 1): component!(components.button2.root_widget())
                    }
                },
            }
        }

    additional_fields! {
        test_field: u8,
    }

    fn pre_init() {
        let mut test_field = 0;
        println!("Pre init! test_field: {}", test_field);
    }

    fn post_init() {
        relm4::set_global_css(b".first { color: green; } .second { border: 1px solid orange; }");
        test_field = 42;
        println!("Post init! test_field: {}", test_field);
    }

    fn manual_view() {
        self.test_field += 1;
        println!("Manual view! test_field: {}", self.test_field);
    }
}

The expansion

The macro expansion is not supposed to be readable, so the code might look a bit ugly.

The widgets struct

The fields of the widgets struct cover all widgets we created, plus the additional fields we added manually. Names fields like main_window and inc_button keep their names. Unnamed fields will get automatically generated names with an unique ID. You should never refer to unnamed fields in your code because their names might change. At the end, we can find the additional field called test_field that we added manually.

#[allow(dead_code)]
struct AppWidgets {
    main_window: gtk::ApplicationWindow,
    _gtk_box_7: gtk::Box,
    inc_button: gtk::Button,
    _gtk_button_new_1: gtk::Button,
    _new_label_2: gtk::Label,
    _gtk_grid_6: gtk::Grid,
    _gtk_label_3: gtk::Label,
    _gtk_label_4: gtk::Label,
    _gtk_label_5: gtk::Label,
    test_field: u8,
}

The Widgets trait implementation

The next thing the macro does is generating the Widgets trait implementation block.

The start of the implementation block is very similar to the implementation block we use in the macro. Most notably, the Root type is automatically inserted. All attributes and comments you add to the widget macro before the impl block should be kept as well.

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

Pre-initialization

At the start of the view initialization, we find — to no surprise — the code of the pre_init() function.

    /// Initialize the UI.
    fn init_view(
        model: &AppModel,
        parent_widgets: &(),
        sender: ::gtk::glib::Sender<AppMsg>,
    ) -> Self {
        let mut test_field = 0;
        println!("Pre init! test_field: {}", test_field);

It's exactly the the code of the pre_init() function.

    fn pre_init() {
        let mut test_field = 0;
        println!("Pre init! test_field: {}", test_field);
    }

Widget initialization

The macro now initializes all widgets. Widgets that were defined by their type are initialized with the relm4::util::default_widgets::DefaultWidget trait that basically calls Widget::builder().build() to initialize a widget with default configuration. Obviously, that only works for widgets that support this builder pattern.

We also see gtk::Button::new() and new_label() used to initialize widgets. These widgets were explicitly initialized with a function.

        let main_window = gtk::ApplicationWindow::default();
        let _gtk_box_7 = gtk::Box::default();
        let inc_button = gtk::Button::default();
        let _gtk_button_new_1 = gtk::Button::new();
        let _new_label_2 = new_label();

Assigning properties

Assigning properties looks pretty normal as well.

        gtk::prelude::GtkWindowExt::set_title(&main_window, Some("Simple app"));
        main_window.set_default_width(300);
        main_window.set_default_height(100);
        _gtk_box_7.set_orientation(gtk::Orientation::Vertical);
        if let Some(__p_assign) = Some(5) {
            _gtk_box_7.set_margin_all(__p_assign);
        }
        _gtk_box_7.set_spacing(5);
        inc_button.set_label("Increment");
        for __elem in &model.classes {
            inc_button.add_css_class(__elem);
        }

At the start, we find the code for the assignment from the macro that uses a trait function.

                gtk::prelude::GtkWindowExt::set_title: Some("Simple app"),

In the middle we have the optional assign, that uses an if let statement to only assign properties that match Some(data). In the macro we marked this line with a ?.

                    set_margin_all?: Some(5),

At the end we have our iterator from the macro.

                        add_css_class: iterate!(&model.classes),

There are some properties missing here because I only showed the relevant section for the purpose of this book.

Events

Now the macro generates the code for connecting events.

        {
            #[allow(clippy::redundant_clone)]
            let sender = sender.clone();
            inc_button.connect_clicked(move |_| {
                send!(sender, AppMsg::Increment);
            });
        }
        {
            #[allow(clippy::redundant_clone)]
            let sender = sender.clone();
            _gtk_button_new_1.connect_clicked(move |_| {
                send!(sender, AppMsg::Decrement);
            });
        }

The code looks very similar to what we wrote in the macro.

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

Most notably, the sender we put in the parenthesis is cloned as we requested.

Post-initialization

At the end, we find the code of our post_init() function.

        relm4::set_global_css(b".first { color: green; } .second { border: 1px solid orange; }");
        test_field = 42;
        println!("Post init! test_field: {}", test_field);

Again, the code is exactly the same.

    fn post_init() {
        relm4::set_global_css(b".first { color: green; } .second { border: 1px solid orange; }");
        test_field = 42;
        println!("Post init! test_field: {}", test_field);
    }

Return

At the end, we return the widgets struct with all initialized widgets.

        Self {
            main_window,
            _gtk_box_7,
            inc_button,
            _gtk_button_new_1,
            _new_label_2,
            _gtk_grid_6,
            _gtk_label_3,
            _gtk_label_4,
            _gtk_label_5,
            test_field,
        }
    }

Assigning widgets and components

To keep every widget in order, all widgets are assigned in connect_components function. In the first stable version of Relm4 (0.1.0), regular widgets were already assigned in the init_view function. This caused problems with the ordering of elements because components were added after all other widgets were already in place. For Relm4 0.2 this behavior was changed so that all widgets are now added at the same place so that components keep their correct order.

    fn connect_components(&self, model: &AppModel, components: &<AppModel as ::relm4::Model>::Components) {
        self.main_window.set_child(Some(&self._gtk_box_7));
        self._gtk_box_7.append(components.button1.root_widget());
        self._gtk_box_7.append(&self.inc_button);
        self._gtk_box_7.append(&self._gtk_button_new_1);
        self._gtk_box_7.append(&self._new_label_2);
        self._gtk_box_7.append(&self._gtk_grid_6);
        self._gtk_grid_6.attach(&self._gtk_label_3, 1, 1, 1, 1);
        self._gtk_grid_6.attach(&self._gtk_label_4, 1, 2, 1, 1);
        self._gtk_grid_6.attach(&self._gtk_label_5, 2, 1, 1, 1);
        self._gtk_grid_6
            .attach(components.button2.root_widget(), 2, 2, 1, 1);
    }

At the beginning, we find the code for the set_child property we used in the macro.

                set_child = Some(&gtk::Box) {

In the macro we used the nested component! macro to add a component to our UI. This component can now be found in the last line of the connect_components function.

                        attach(2, 2, 1, 1): component!(components.button2.root_widget())

Root widget

The macro also implements the root_widget function that returns the outermost widget that is also the first we use in the view! macro.

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

Manual UI updates

The last step of the macro is to generate the update logic with the view function. At the start of this function, we can find the code from the manual_view() function of the macro.

    /// Update the view to represent the updated model.
    fn view(
        &mut self,
        model: &AppModel,
        sender: ::gtk::glib::Sender<<AppModel as ::relm4::Model>::Msg>,
    ) {
        self.test_field += 1;
        println!("Manual view! test_field: {}", self.test_field);

Just like with pre_init() and post_init() the code is exactly the same, too.

    fn manual_view() {
        self.test_field += 1;
        println!("Manual view! test_field: {}", self.test_field);
    }

Generated UI updates

After the manually defined update logic, the macro generates its own code.

        self._new_label_2
            .set_label(&format!("Counter: {}", model.counter));

        if model.decrement {
            self._gtk_button_new_1
                .set_label(&format!("Last decrement at {}", model.counter));
        }

The first update comes from the nested watch! macro and is unconditional.

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

The second update rule sits behind an if statement because it comes from the nested track! macro. In this case, the condition for the tracker is simply the model.decrement field.

                        set_label: track!(model.decrement, &format!("Last decrement at {}", model.counter)),

Conclusion

Congrats for making it this far 🎉! You're now a real expert of Relm4!

As you have seen, the macro is nothing magical. It simply works with the information you give to it.

The whole macro expansion

If you want to look at the whole macro expansion at once, here it is.

use gtk::prelude::{BoxExt, ButtonExt, GridExt, GtkWindowExt, OrientableExt, WidgetExt};
use relm4::{
    send, AppUpdate, ComponentUpdate, Components, Model, RelmApp, RelmComponent, Sender,
    WidgetPlus, Widgets,
};

struct AppModel {
    counter: u8,
    classes: Vec<&'static str>,
    decrement: bool,
}

enum AppMsg {
    Increment,
    Decrement,
}

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

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

enum ButtonMsg {}

struct ButtonModel {}

impl Model for ButtonModel {
    type Msg = ButtonMsg;
    type Widgets = ButtonWidgets;
    type Components = ();
}

impl ComponentUpdate<AppModel> for ButtonModel {
    fn init_model(_parent_model: &AppModel) -> Self {
        ButtonModel {}
    }
    fn update(
        &mut self,
        _msg: ButtonMsg,
        _components: &(),
        _sender: Sender<ButtonMsg>,
        _parent_sender: Sender<AppMsg>,
    ) {
    }
}

#[allow(dead_code)]
struct ButtonWidgets {
    _gtk_button_0: gtk::Button,
}

impl Widgets<ButtonModel, AppModel> for ButtonWidgets {
    type Root = gtk::Button;
    /// Initialize the UI.
    fn init_view(
        model: &ButtonModel,
        parent_widgets: &<AppModel as ::relm4::Model>::Widgets,
        sender: ::gtk::glib::Sender<<ButtonModel as ::relm4::Model>::Msg>,
    ) -> Self {
        let _gtk_button_0 = gtk::Button::default();
        _gtk_button_0.set_label("ButtonComponent!");
        Self { _gtk_button_0 }
    }
    fn connect_components(&self, model: &ButtonModel, components: &<ButtonModel as ::relm4::Model>::Components) {}
    /// Return the root widget.
    fn root_widget(&self) -> Self::Root {
        self._gtk_button_0.clone()
    }
    /// Update the view to represent the updated model.
    fn view(
        &mut self,
        model: &ButtonModel,
        sender: ::gtk::glib::Sender<<ButtonModel as ::relm4::Model>::Msg>,
    ) {
    }
}

pub struct AppComponents {
    button1: RelmComponent<ButtonModel, AppModel>,
    button2: RelmComponent<ButtonModel, AppModel>,
}

impl Components<AppModel> for AppComponents {
    fn init_components(
        model: &AppModel,
        parent_widgets: &AppWidgets,
        sender: Sender<AppMsg>,
    ) -> Self {
        AppComponents {
            button1: RelmComponent::new(model, parent_widgets, sender.clone()),
            button2: RelmComponent::new(model, parent_widgets, sender),
        }
    }
}

fn new_label() -> gtk::Label {
    gtk::Label::new(Some("test"))
}

#[allow(dead_code)]
struct AppWidgets {
    main_window: gtk::ApplicationWindow,
    _gtk_box_7: gtk::Box,
    inc_button: gtk::Button,
    _gtk_button_new_1: gtk::Button,
    _new_label_2: gtk::Label,
    _gtk_grid_6: gtk::Grid,
    _gtk_label_3: gtk::Label,
    _gtk_label_4: gtk::Label,
    _gtk_label_5: gtk::Label,
    test_field: u8,
}

impl Widgets<AppModel, ()> for AppWidgets {
    type Root = gtk::ApplicationWindow;
    /// Initialize the UI.
    fn init_view(
        model: &AppModel,
        parent_widgets: &(),
        sender: ::gtk::glib::Sender<AppMsg>,
    ) -> Self {
        let mut test_field = 0;
        println!("Pre init! test_field: {}", test_field);
        let main_window = gtk::ApplicationWindow::default();
        let _gtk_box_7 = gtk::Box::default();
        let inc_button = gtk::Button::default();
        let _gtk_button_new_1 = gtk::Button::new();
        let _new_label_2 = new_label();
        let _gtk_grid_6 = gtk::Grid::default();
        let _gtk_label_3 = gtk::Label::default();
        let _gtk_label_4 = gtk::Label::default();
        let _gtk_label_5 = gtk::Label::default();
        gtk::prelude::GtkWindowExt::set_title(&main_window, Some("Simple app"));
        main_window.set_default_width(300);
        main_window.set_default_height(100);
        _gtk_box_7.set_orientation(gtk::Orientation::Vertical);
        if let Some(__p_assign) = Some(5) {
            _gtk_box_7.set_margin_all(__p_assign);
        }
        _gtk_box_7.set_spacing(5);
        inc_button.set_label("Increment");
        for __elem in &model.classes {
            inc_button.add_css_class(__elem);
        }
        _gtk_button_new_1.set_label(&format!("Last decrement at {}", model.counter));
        _new_label_2.set_margin_all(5);
        _new_label_2.set_label(&format!("Counter: {}", model.counter));
        _gtk_grid_6.set_vexpand(true);
        _gtk_grid_6.set_hexpand(true);
        _gtk_grid_6.set_row_spacing(10);
        _gtk_grid_6.set_column_spacing(10);
        _gtk_grid_6.set_column_homogeneous(true);
        _gtk_label_3.set_label("grid test 1");
        _gtk_label_4.set_label("grid test 2");
        _gtk_label_5.set_label("grid test 3");
        {
            #[allow(clippy::redundant_clone)]
            let sender = sender.clone();
            inc_button.connect_clicked(move |_| {
                send!(sender, AppMsg::Increment);
            });
        }
        {
            #[allow(clippy::redundant_clone)]
            let sender = sender.clone();
            _gtk_button_new_1.connect_clicked(move |_| {
                send!(sender, AppMsg::Decrement);
            });
        }
        relm4::set_global_css(b".first { color: green; } .second { border: 1px solid orange; }");
        test_field = 42;
        println!("Post init! test_field: {}", test_field);
        Self {
            main_window,
            _gtk_box_7,
            inc_button,
            _gtk_button_new_1,
            _new_label_2,
            _gtk_grid_6,
            _gtk_label_3,
            _gtk_label_4,
            _gtk_label_5,
            test_field,
        }
    }

    fn connect_components(&self, model: &AppModel, components: &<AppModel as ::relm4::Model>::Components) {
        self.main_window.set_child(Some(&self._gtk_box_7));
        self._gtk_box_7.append(components.button1.root_widget());
        self._gtk_box_7.append(&self.inc_button);
        self._gtk_box_7.append(&self._gtk_button_new_1);
        self._gtk_box_7.append(&self._new_label_2);
        self._gtk_box_7.append(&self._gtk_grid_6);
        self._gtk_grid_6.attach(&self._gtk_label_3, 1, 1, 1, 1);
        self._gtk_grid_6.attach(&self._gtk_label_4, 1, 2, 1, 1);
        self._gtk_grid_6.attach(&self._gtk_label_5, 2, 1, 1, 1);
        self._gtk_grid_6
            .attach(components.button2.root_widget(), 2, 2, 1, 1);
    }

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

    /// Update the view to represent the updated model.
    fn view(
        &mut self,
        model: &AppModel,
        sender: ::gtk::glib::Sender<<AppModel as ::relm4::Model>::Msg>,
    ) {
        self.test_field += 1;
        println!("Manual view! test_field: {}", self.test_field);

        self._new_label_2
            .set_label(&format!("Counter: {}", model.counter));

        if model.decrement {
            self._gtk_button_new_1
                .set_label(&format!("Last decrement at {}", model.counter));
        }
    }
}

fn main() {
    let model = AppModel {
        counter: 0,
        classes: vec!["first", "second"],
        decrement: false,
    };
    let app = RelmApp::new(model);
    app.run();
}