Factory
Factories define how to generate widgets from collections of data. They are used inside GTK as well, but Relm4 uses them a bit differently.
This app will have a dynamic number of counters which can be changed by pressing the add or remove buttons. Clicking on a counter will decrement it.
Factories in Relm4
Let's have a look at factories in Relm4. We want to write a simple application that can create and remove many counters. Each counter needs to store its value and display widgets to allow modifying the counter. In this example we will only decrement the counter.
The app we will write in this chapter is also available here. Run
cargo run --example factory
from the example directory if you want to see the code in action.
The model
The most common solution for storing collections of data is a Vec
. Yet a Vec
can't help us with efficient UI updates because it does not track changes to itself. If we used a Vec
we'd have to assume everything could have changed and create all widgets over and over again. So instead we use a FactoryVec
to store our data. A FactoryVec
is a simple data structure provided by Relm4 that allows us to push, pop and modify elements. Additionally, it automatically keeps track of all the changes made to itself.
An overview over all available factory data structures can be found in the documentation here.
struct Counter {
value: u8,
}
struct AppModel {
counters: FactoryVec<Counter>,
created_counters: u8,
}
As you can see, we first define the struct Counter
that just stores the value of a single counter. Then we use a FactoryVec
to store our counters in the model. For now, all of this is just data. Similar to the model type, we need to define the data structures we need for our UI first. Then we will define how to create widgets from this data. Yet unlike the model type, we can have many counters in a FactoryVec
and each of them will be represented by its own widgets.
To give our counters an unique value at initialization, we also add a separate counter to the model to count the amount of counters we did already create.
The message type
The actions we want to perform are
- Add new counters
- Remove counters
- Decrement a counter
Accordingly, our message type looks like this:
#[derive(Debug)]
enum AppMsg {
Add,
Remove,
Clicked(usize),
}
You'll notice that an index is passed with AppMsg::Clicked
. This allows us to select the counter that emitted the clicked signal.
The update function
The update function takes care of adding, removing and decrementing counters. Each new counter will be initialized with the amount of counters created before it.
impl AppUpdate for AppModel {
fn update(&mut self, msg: AppMsg, _components: &(), _sender: Sender<AppMsg>) -> bool {
match msg {
AppMsg::Add => {
self.counters.push(Counter {
value: self.created_counters,
});
self.created_counters += 1;
}
AppMsg::Remove => {
self.counters.pop();
}
AppMsg::Clicked(index) => {
if let Some(counter) = self.counters.get_mut(index) {
counter.value = counter.value.wrapping_sub(1);
}
}
}
true
}
}
The
get
andget_mut
methods insideFactoryVec
returnSome
if the element exists andNone
if the index is invalid. It's recommended to not unwrap thisOption
because messages (and also the indices sent with them) are queued up if your update and view functions are slow and can be stale by the time they are handled.
The factory implementation
So far the code looked pretty normal. Now to the interesting part of the code.
The first thing we need to implement for a factory is a widgets type. That sounds familiar, right? The widgets used for the factory are actually very similar to the widgets used for your application. They define which widgets represent an element inside a factory data structure like FactoryVec
.
In our case, we just need a simple button that will decrement the counter when clicked and will also display the counter value.
#[derive(Debug)]
struct FactoryWidgets {
button: gtk::Button,
}
The FactoryPrototype
trait we need next is very similar to the Widgets
trait, too: it defines how widgets are created and updated. Let's have a look at the implementation:
impl FactoryPrototype for Counter {
type Factory = FactoryVec<Self>;
type Widgets = FactoryWidgets;
type Root = gtk::Button;
type View = gtk::Box;
type Msg = AppMsg;
Alright, there are quite a few types! Let's look at them one by one:
- Factory: the data structure we use to store our elements. In our case, a
FactoryVec
. - Widgets: the struct that stores out widgets. That's the
FactoryWidgets
type we just created. - Root: similar to the root in the
Widgets
trait, it represents the outermost widget. This is usually a container likegtk::Box
but in our case we just have agtk::Button
. - View: the container we want our widgets to be placed inside. The simplest solution for this is a
gtk::Box
. - Msg: the messages we want to send to the model containing this factory.
The init_view
function
The init_view
function is similar to init_view
in the Widgets
trait: it generates the widgets from data. You'll notice that there's an index as well that we can use to send messages that index the data these widgets represent. The index type might vary between different factory data structures. For the factory type FactoryVec
an index of the type usize
is being used.
fn init_view(&self, index: &usize, sender: Sender<AppMsg>) -> FactoryWidgets {
let button = gtk::Button::with_label(&self.value.to_string());
let index = *index;
button.connect_clicked(move |_| {
sender.send(AppMsg::Clicked(index)).unwrap();
});
FactoryWidgets { button }
}
As you can see, we send a message with the index back to the update function to decrement this specific counter when the button is pressed.
The position function
In our case, the function is pretty short:
fn position(&self, _index: &usize) {}
The gtk::Box
we use here is one-dimensional. This means that a FactoryVec
can perfectly resemble the layout with its own internal structure because it's one-dimenational as well. In other words, the first element of the FactoryVec
is also the first in the gtk::Box
. Yet, some container widgets such as gtk::Grid
place widgets at fixed two-dimensional positions and rely in the position function to know where a new widget should be added.
Because we don't use it here, the position function is explained in the next chapter.
The view function
The view
function is similar to view
in the Widgets
trait: it updates the widgets according to the updated data.
fn view(&self, _index: &usize, widgets: &FactoryWidgets) {
widgets.button.set_label(&self.value.to_string());
}
We just update the label of the button to represent the updated counter value.
The root_widget function
The last function we need is the root_widget
function. It's similar to the root_widget
in the Widgets
trait: it returns the root widget, the outermost of our widgets.
fn root_widget(widgets: &FactoryWidgets) -> >k::Button {
&widgets.button
}
The widgets
The last piece to make our code complete it the definition of the widgets for the application. There's mostly one notable thing: the factory!
macro.
#[relm4::widget]
impl Widgets<AppModel, ()> for AppWidgets {
view! {
gtk::ApplicationWindow {
set_default_width: 300,
set_default_height: 200,
set_child = Some(>k::Box) {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = >k::Button {
set_label: "Add",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Add);
}
},
append = >k::Button {
set_label: "Remove",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Remove);
}
},
append = >k::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
factory!(model.counters),
}
}
}
}
}
The factory!
macro that's almost at the end of the widgets definition now updates our widgets according to the changes we make to the data in our model. It sits inside of the gtk::Box
we want to use as a container for our counter.
The
factory!
macro simply expands tomodel.data.generate(&self.gen_box, sender)
wheregen_box
is thegtk::Box
we used as a container. Thegenerate
function is provided by theFactory
trait that's implemented forFactoryVec
and similar data structures.
Now to test this, we could add a print statement to the update function. It will show that decrementing one counter will only update the widgets of one counter. Great, that's exactly what we wanted!
The complete code
Let's review our code in one piece one more time to see how all these parts work together:
use gtk::glib::Sender;
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
use relm4::factory::{FactoryPrototype, FactoryVec};
use relm4::{send, AppUpdate, Model, RelmApp, WidgetPlus, Widgets};
#[derive(Debug)]
enum AppMsg {
Add,
Remove,
Clicked(usize),
}
struct Counter {
value: u8,
}
struct AppModel {
counters: FactoryVec<Counter>,
created_counters: u8,
}
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::Add => {
self.counters.push(Counter {
value: self.created_counters,
});
self.created_counters += 1;
}
AppMsg::Remove => {
self.counters.pop();
}
AppMsg::Clicked(index) => {
if let Some(counter) = self.counters.get_mut(index) {
counter.value = counter.value.wrapping_sub(1);
}
}
}
true
}
}
#[derive(Debug)]
struct FactoryWidgets {
button: gtk::Button,
}
impl FactoryPrototype for Counter {
type Factory = FactoryVec<Self>;
type Widgets = FactoryWidgets;
type Root = gtk::Button;
type View = gtk::Box;
type Msg = AppMsg;
fn init_view(&self, index: &usize, sender: Sender<AppMsg>) -> FactoryWidgets {
let button = gtk::Button::with_label(&self.value.to_string());
let index = *index;
button.connect_clicked(move |_| {
sender.send(AppMsg::Clicked(index)).unwrap();
});
FactoryWidgets { button }
}
fn position(&self, _index: &usize) {}
fn view(&self, _index: &usize, widgets: &FactoryWidgets) {
widgets.button.set_label(&self.value.to_string());
}
fn root_widget(widgets: &FactoryWidgets) -> >k::Button {
&widgets.button
}
}
#[relm4::widget]
impl Widgets<AppModel, ()> for AppWidgets {
view! {
gtk::ApplicationWindow {
set_default_width: 300,
set_default_height: 200,
set_child = Some(>k::Box) {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = >k::Button {
set_label: "Add",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Add);
}
},
append = >k::Button {
set_label: "Remove",
connect_clicked(sender) => move |_| {
send!(sender, AppMsg::Remove);
}
},
append = >k::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
factory!(model.counters),
}
}
}
}
}
fn main() {
let model = AppModel {
counters: FactoryVec::new(),
created_counters: 0,
};
let relm = RelmApp::new(model);
relm.run();
}