Widget templates

Widget templates are a simple way to define reusable UI elements. When building complex UIs, they allow you to focus on the application logic instead of complex trees of widgets. Yet most importantly, widget templates help you to reduce redundant code. For example, if you use a widget with the same properties multiple times in your code, templates will make your code a lot shorter.

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

Defining templates

To define a widget template, you need to implement the WidgetTemplate trait for a new type. You could do this manually, but the easiest solution is to use the #[relm4::widget_template] attribute macro. The macro will create the type and implement the trait for you.

For example, the following code block will create a template for a gtk::Box with a certain margin and custom CSS.

#[relm4::widget_template]
impl WidgetTemplate for MyBox {
    view! {
        gtk::Box {
            set_margin_all: 10,
            // Make the boxes visible
            inline_css: "border: 2px solid blue",
        }
    }
}

Similarly, we can create a template for a gtk::Spinner that already spins when it's created.

#[relm4::widget_template]
impl WidgetTemplate for MySpinner {
    view! {
        gtk::Spinner {
            set_spinning: true,
        }
    }
}

To create public templates, you can use #[relm4::widget_template(pub)], similar to the #[relm4::component(pub)] macro.

Template children

Templates are more than just pre-initialized widgets. They can also have children, which can be referred to later as template children. This is very useful if you use nested widget in you UI, because the template allows you to flatten the structure. In other words, no matter how deeply nested a template child is, it will always be accessible directly from the template. We'll see how this works in the next section, but first we'll create a deeply nested template. We use the templates we defined earlier by using the #[template] attribute. Also, we assign the name child_label to our last widget, which is all we need to make it a template child. In general, naming a widget in a template is all that's needed to make it a template child.

#[relm4::widget_template]
impl WidgetTemplate for CustomBox {
    view! {
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,
            set_margin_all: 5,
            set_spacing: 5,

            #[template]
            MyBox {
                #[template]
                MySpinner,

                #[template]
                MyBox {
                    #[template]
                    MySpinner,

                    #[template]
                    MyBox {
                        #[template]
                        MySpinner,

                        // Deeply nested!
                        #[name = "child_label"]
                        gtk::Label {
                            set_label: "This is a test",
                        }
                    }
                }
            }
        }
    }
}

Using templates

To use templates in a component, we use the #[template] and #[template_child] attributes. In this case, we use the CustomBox type we just defined with the #[template] attribute we already used. To access its child_label template child, we only need to use the #[template_child] attribute and the name of the child. As you can see, we now have access to the child_label widget, which actually is wrapped into 4 gtk::Box widgets. We can even use assign or overwrite properties of the template and its children, similar to regular widgets. Here, we use the #[watch] attribute to update the label with the latest counter value.

#[relm4::component]
impl SimpleComponent for AppModel {
    type Init = u8;
    type Input = AppMsg;
    type Output = ();

    view! {
        gtk::Window {
            set_title: Some("Widget template"),
            set_default_width: 300,
            set_default_height: 100,

            #[template]
            CustomBox {
                gtk::Button {
                    set_label: "Increment",
                    connect_clicked[sender] => move |_| {
                        sender.input(AppMsg::Increment);
                    },
                },
                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked[sender] => move |_| {
                        sender.input(AppMsg::Decrement);
                    },
                },
                #[template_child]
                child_label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                }
            },
        }
    }

Some notes on orders

If you run this code, you will notice that the label appears above the two buttons, which is contrary to our widget definition. This happens because widget templates are initialized before other modifications happen. The CustomBox template will initialize its child_label and append it to its internal gtk::Box widget and only then the two buttons are added. However, you can work around this by using methods like prepend, append or insert_child_after (if you use a gtk::Box as container) or by splitting your templates into smaller ones.

To make template children appear in the same order as they are used, widget templates would require dynamic initialization of its children. This would increase the complexity of the internal implementation by a lot (or might not be possible at all) and is therefore not planned at the moment.

The complete code

use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
use relm4::{
    gtk, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SimpleComponent, WidgetTemplate,
};

#[relm4::widget_template]
impl WidgetTemplate for MyBox {
    view! {
        gtk::Box {
            set_margin_all: 10,
            // Make the boxes visible
            inline_css: "border: 2px solid blue",
        }
    }
}

#[relm4::widget_template]
impl WidgetTemplate for MySpinner {
    view! {
        gtk::Spinner {
            set_spinning: true,
        }
    }
}

#[relm4::widget_template]
impl WidgetTemplate for CustomBox {
    view! {
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,
            set_margin_all: 5,
            set_spacing: 5,

            #[template]
            MyBox {
                #[template]
                MySpinner,

                #[template]
                MyBox {
                    #[template]
                    MySpinner,

                    #[template]
                    MyBox {
                        #[template]
                        MySpinner,

                        // Deeply nested!
                        #[name = "child_label"]
                        gtk::Label {
                            set_label: "This is a test",
                        }
                    }
                }
            }
        }
    }
}

#[derive(Default)]
struct AppModel {
    counter: u8,
}

#[derive(Debug)]
enum AppMsg {
    Increment,
    Decrement,
}

#[relm4::component]
impl SimpleComponent for AppModel {
    type Init = u8;
    type Input = AppMsg;
    type Output = ();

    view! {
        gtk::Window {
            set_title: Some("Widget template"),
            set_default_width: 300,
            set_default_height: 100,

            #[template]
            CustomBox {
                gtk::Button {
                    set_label: "Increment",
                    connect_clicked[sender] => move |_| {
                        sender.input(AppMsg::Increment);
                    },
                },
                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked[sender] => move |_| {
                        sender.input(AppMsg::Decrement);
                    },
                },
                #[template_child]
                child_label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                }
            },
        }
    }

    fn init(
        counter: Self::Init,
        root: Self::Root,
        sender: ComponentSender<Self>,
    ) -> ComponentParts<Self> {
        let model = Self { counter };

        let widgets = view_output!();

        ComponentParts { model, widgets }
    }

    fn update(&mut self, msg: AppMsg, _sender: ComponentSender<Self>) {
        match msg {
            AppMsg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            AppMsg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }
}

fn main() {
    let app = RelmApp::new("relm4.example.widget_template");
    app.run::<AppModel>(0);
}