The widget macro
To simplify the implementation of the Widgets
trait, let's use the relm4-macros crate!
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::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(>k::Box) {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = >k::Button {
set_label: "Increment",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Increment);
},
},
append = >k::Button::with_label("Decrement") {
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Decrement);
},
},
append = >k::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(>k::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(>k::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 = >k::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::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(>k::Box) {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = >k::Button {
set_label: "Increment",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Increment);
},
},
append = >k::Button::with_label("Decrement") {
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Decrement);
},
},
append = >k::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();
}