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(>k::Box) {
set_orientation: gtk::Orientation::Vertical,
set_margin_all?: Some(5),
set_spacing: 5,
append: components.button1.root_widget(),
append: inc_button = >k::Button {
set_label: "Increment",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Increment);
},
add_css_class: iterate!(&model.classes),
},
append = >k::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 = >k::Grid {
set_vexpand: true,
set_hexpand: true,
set_row_spacing: 10,
set_column_spacing: 10,
set_column_homogeneous: true,
attach(1, 1, 1, 1) = >k::Label {
set_label: "grid test 1",
},
attach(1, 2, 1, 1) = >k::Label {
set_label: "grid test 2",
},
attach(2, 1, 1, 1) = >k::Label {
set_label: "grid test 3",
},
attach(2, 2, 1, 1): 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 post_view() {
self.test_field += 1;
println!("Post 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,
components: &AppComponents,
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.
main_window.set_child(Some(&_gtk_box_7));
_gtk_box_7.append(components.button1.root_widget());
_gtk_box_7.append(&inc_button);
_gtk_box_7.append(&_gtk_button_new_1);
_gtk_box_7.append(&_new_label_2);
_gtk_box_7.append(&_gtk_grid_6);
_gtk_grid_6.attach(&_gtk_label_3, 1, 1, 1, 1);
_gtk_grid_6.attach(&_gtk_label_4, 1, 2, 1, 1);
_gtk_grid_6.attach(&_gtk_label_5, 2, 1, 1, 1);
_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(>k::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): 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 post_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!("Post view! test_field: {}", self.test_field);
Just like with pre_init()
and post_init()
the code is exactly the same, too.
fn post_view() {
self.test_field += 1;
println!("Post 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: &(),
_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 }
}
/// 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, sender: Sender<AppMsg>) -> Self {
AppComponents {
button1: RelmComponent::new(model, sender.clone()),
button2: RelmComponent::new(model, sender),
}
}
fn connect_parent(&mut self, _parent_widgets: &AppWidgets) {}
}
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,
components: &AppComponents,
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);
});
}
main_window.set_child(Some(&_gtk_box_7));
_gtk_box_7.append(components.button1.root_widget());
_gtk_box_7.append(&inc_button);
_gtk_box_7.append(&_gtk_button_new_1);
_gtk_box_7.append(&_new_label_2);
_gtk_box_7.append(&_gtk_grid_6);
_gtk_grid_6.attach(&_gtk_label_3, 1, 1, 1, 1);
_gtk_grid_6.attach(&_gtk_label_4, 1, 2, 1, 1);
_gtk_grid_6.attach(&_gtk_label_5, 2, 1, 1, 1);
_gtk_grid_6.attach(components.button2.root_widget(), 2, 2, 1, 1);
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,
}
}
/// 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!("Post 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();
}