Factory
Factories define how to generate widgets from data collections. GTK also has factories, yet Relm4 uses its own factory implementation which is much easier to use in regular Rust code.
This app will have a dynamic number of counters. Also, the counters can be moved up and down by the user.
Factories in Relm4
Factories allow you to visualize data in a natural way.
If you wanted to store a set of counter values in regular Rust code, you'd probably use Vec<u8>
.
However, you can't simply generate widgets from a Vec
.
This is where factories are really useful.
Custom collection types like FactoryVecDeque
allow you to work with collections of data almost as comfortable as if they were stored in a Vec
.
At the same time, factories allow you to automatically visualize the data with widgets.
Additionally, factories are very efficient by reducing the amount of UI updates to a minimum.
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
First, we define the struct Counter
that just stores the value of a single counter.
Later, we will use a FactoryVecDeque
to store our counters.
#[derive(Debug)]
struct Counter {
value: u8,
}
The input message type
Each counter should be able to increment and decrement.
#[derive(Debug)]
enum CounterMsg {
Increment,
Decrement,
}
The output message type
A neat feature of factories is that each element can easily forward their output messages to the input of their parent component.
For example, this is necessary for modifications that require access to the whole FactoryVecDeque
, like moving an element to a new position.
Therefore, these actions are covered by the output type.
The actions we want to perform "from outside" are
- Move a counter up
- Move a counter down
- Move a counter to the first position
Accordingly, our message type looks like this:
#[derive(Debug)]
enum CounterOutput {
SendFront(DynamicIndex),
MoveUp(DynamicIndex),
MoveDown(DynamicIndex),
}
You might wonder why DynamicIndex
is used here.
First, the parent component needs to know which element should be moved, which is defined by the index.
Further, elements can move in the FactoryVecDeque
.
If we used usize
as index instead, it could happen that the index points to another element by the time it is processed.
The factory implementation
Factories use the FactoryComponent
trait which is very similar to regular components with some minor adjustments.
For example, FactoryComponent
needs the #[relm4::factory]
attribute macro and a few more associated types in the trait implementation.
#[relm4::factory]
impl FactoryComponent for Counter {
type Init = u8;
type Input = CounterMsg;
type Output = CounterOutput;
type CommandOutput = ();
type ParentWidget = gtk::Box;
Let's look at the associated types one by one:
- Init: The data required to initialize
Counter
, in this case the initial counter value. - Input: The input message type.
- Output: The output message type.
- CommandOutput: The command output message type, we don't need it here.
- ParentWidget: The container widget used to store the widgets of the factory, for example
gtk::Box
.
Creating the widget
The widget creation works as usual with our trusty view
macro.
The only difference is that we use self
to refer to the model due to differences in the FactoryComponent
trait.
view! {
#[root]
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
#[name(label)]
gtk::Label {
#[watch]
set_label: &self.value.to_string(),
set_width_chars: 3,
},
#[name(add_button)]
gtk::Button {
set_label: "+",
connect_clicked => CounterMsg::Increment,
},
#[name(remove_button)]
gtk::Button {
set_label: "-",
connect_clicked => CounterMsg::Decrement,
},
#[name(move_up_button)]
gtk::Button {
set_label: "Up",
connect_clicked[sender, index] => move |_| {
sender.output(CounterOutput::MoveUp(index.clone())).unwrap();
}
},
#[name(move_down_button)]
gtk::Button {
set_label: "Down",
connect_clicked[sender, index] => move |_| {
sender.output(CounterOutput::MoveDown(index.clone())).unwrap();
}
},
#[name(to_front_button)]
gtk::Button {
set_label: "To Start",
connect_clicked[sender, index] => move |_| {
sender.output(CounterOutput::SendFront(index.clone())).unwrap();
}
}
}
}
Initializing the model
FactoryComponent
has separate functions for initializing the model and the widgets.
This means, that we are a bit less flexible, but don't need view_output!()
here.
Also, we just need to implement the init_model
function because init_widgets
is already implemented by the macro.
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
Self { value }
}
The main component
Now, we have implemented the FactoryComponent
type for the elements in our factory.
The only thing left to do is to write our main component to complete our app.
The component types
For the main component we implement the familiar SimpleComponent
trait.
First we define the model and the input message type and then start the trait implementation.
struct App {
created_widgets: u8,
counters: FactoryVecDeque<Counter>,
}
#[derive(Debug)]
enum AppMsg {
AddCounter,
RemoveCounter,
SendFront(DynamicIndex),
MoveUp(DynamicIndex),
MoveDown(DynamicIndex),
}
#[relm4::component]
impl SimpleComponent for App {
type Init = u8;
type Input = AppMsg;
type Output = ();
Initializing the factory
We skip the view
macro for a moment and look at the init
method.
You see that we are initializing the FactoryVecDeque
using a builder pattern.
First, we call FactoryVecDeque::builder()
to create the builder and use launch()
to set the root widget of the factory.
This widget will store all the widgets created by the factory.
Then, we use the forward()
method to pass all output messages of our factory (with type CounterOutput
) to the input of our component (with type AppMsg
).
The last trick we have up our sleeves is to define a local variable counter_box
that is a reference to the container widget of our factory.
We'll use it in the view
macro in the next section.
// Initialize the UI.
fn init(
counter: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let counters = FactoryVecDeque::builder()
.launch(gtk::Box::default())
.forward(sender.input_sender(), |output| match output {
CounterOutput::SendFront(index) => AppMsg::SendFront(index),
CounterOutput::MoveUp(index) => AppMsg::MoveUp(index),
CounterOutput::MoveDown(index) => AppMsg::MoveDown(index),
});
let model = App {
created_widgets: counter,
counters,
};
let counter_box = model.counters.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
Initializing the widgets
The familiar view
macro comes into play again.
Most things should look familiar, but this time we use a #[local_ref]
attribute for the last widget to use the local variable we defined in the previous section.
This trick allows us to initialize the model with its FactoryVecDeque
before the widgets, which is more convenient in most cases.
view! {
gtk::Window {
set_title: Some("Factory example"),
set_default_size: (300, 100),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
gtk::Button {
set_label: "Add counter",
connect_clicked => AppMsg::AddCounter,
},
gtk::Button {
set_label: "Remove counter",
connect_clicked => AppMsg::RemoveCounter,
},
#[local_ref]
counter_box -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
}
}
}
}
The main update function
This time the main update function has actually quite a bit to do.
The code should be quite readable if you worked with Vec
or VecDeque
before.
One thing stands out though: We see a lot of calls to guard()
.
In fact, all mutating methods of FactoryVecDeque
need an RAII-guard.
This is similar to a MutexGuard
you get from locking a mutex.
The reason for this is simple. As long as the guard is alive, we can perform multiple operations. Once we're done, we just drop the guard (or rather leave the current scope) and this will cause the factory to update its widgets automatically. The neat thing: You can never forget to render changes, and the update algorithm can optimize widget updates for efficiency.
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
AppMsg::AddCounter => {
self.counters.guard().push_back(self.created_widgets);
self.created_widgets = self.created_widgets.wrapping_add(1);
}
AppMsg::RemoveCounter => {
self.counters.guard().pop_back();
}
AppMsg::SendFront(index) => {
self.counters.guard().move_front(index.current_index());
}
AppMsg::MoveDown(index) => {
let index = index.current_index();
let new_index = index + 1;
// Already at the end?
if new_index < self.counters.len() {
self.counters.guard().move_to(index, new_index);
}
}
AppMsg::MoveUp(index) => {
let index = index.current_index();
// Already at the start?
if index != 0 {
self.counters.guard().move_to(index, index - 1);
}
}
}
}
The main function
Awesome, we almost made it!
We only need to define the main function to run our application.
fn main() {
let app = RelmApp::new("relm4.example.factory");
app.run::<App>(0);
}
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::factory::{DynamicIndex, FactoryComponent, FactorySender, FactoryVecDeque};
use relm4::{gtk, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SimpleComponent};
#[derive(Debug)]
struct Counter {
value: u8,
}
#[derive(Debug)]
enum CounterMsg {
Increment,
Decrement,
}
#[derive(Debug)]
enum CounterOutput {
SendFront(DynamicIndex),
MoveUp(DynamicIndex),
MoveDown(DynamicIndex),
}
#[relm4::factory]
impl FactoryComponent for Counter {
type Init = u8;
type Input = CounterMsg;
type Output = CounterOutput;
type CommandOutput = ();
type ParentWidget = gtk::Box;
view! {
#[root]
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
#[name(label)]
gtk::Label {
#[watch]
set_label: &self.value.to_string(),
set_width_chars: 3,
},
#[name(add_button)]
gtk::Button {
set_label: "+",
connect_clicked => CounterMsg::Increment,
},
#[name(remove_button)]
gtk::Button {
set_label: "-",
connect_clicked => CounterMsg::Decrement,
},
#[name(move_up_button)]
gtk::Button {
set_label: "Up",
connect_clicked[sender, index] => move |_| {
sender.output(CounterOutput::MoveUp(index.clone())).unwrap();
}
},
#[name(move_down_button)]
gtk::Button {
set_label: "Down",
connect_clicked[sender, index] => move |_| {
sender.output(CounterOutput::MoveDown(index.clone())).unwrap();
}
},
#[name(to_front_button)]
gtk::Button {
set_label: "To Start",
connect_clicked[sender, index] => move |_| {
sender.output(CounterOutput::SendFront(index.clone())).unwrap();
}
}
}
}
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
Self { value }
}
fn update(&mut self, msg: Self::Input, _sender: FactorySender<Self>) {
match msg {
CounterMsg::Increment => {
self.value = self.value.wrapping_add(1);
}
CounterMsg::Decrement => {
self.value = self.value.wrapping_sub(1);
}
}
}
}
struct App {
created_widgets: u8,
counters: FactoryVecDeque<Counter>,
}
#[derive(Debug)]
enum AppMsg {
AddCounter,
RemoveCounter,
SendFront(DynamicIndex),
MoveUp(DynamicIndex),
MoveDown(DynamicIndex),
}
#[relm4::component]
impl SimpleComponent for App {
type Init = u8;
type Input = AppMsg;
type Output = ();
view! {
gtk::Window {
set_title: Some("Factory example"),
set_default_size: (300, 100),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
gtk::Button {
set_label: "Add counter",
connect_clicked => AppMsg::AddCounter,
},
gtk::Button {
set_label: "Remove counter",
connect_clicked => AppMsg::RemoveCounter,
},
#[local_ref]
counter_box -> gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
}
}
}
}
// Initialize the UI.
fn init(
counter: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let counters = FactoryVecDeque::builder()
.launch(gtk::Box::default())
.forward(sender.input_sender(), |output| match output {
CounterOutput::SendFront(index) => AppMsg::SendFront(index),
CounterOutput::MoveUp(index) => AppMsg::MoveUp(index),
CounterOutput::MoveDown(index) => AppMsg::MoveDown(index),
});
let model = App {
created_widgets: counter,
counters,
};
let counter_box = model.counters.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
AppMsg::AddCounter => {
self.counters.guard().push_back(self.created_widgets);
self.created_widgets = self.created_widgets.wrapping_add(1);
}
AppMsg::RemoveCounter => {
self.counters.guard().pop_back();
}
AppMsg::SendFront(index) => {
self.counters.guard().move_front(index.current_index());
}
AppMsg::MoveDown(index) => {
let index = index.current_index();
let new_index = index + 1;
// Already at the end?
if new_index < self.counters.len() {
self.counters.guard().move_to(index, new_index);
}
}
AppMsg::MoveUp(index) => {
let index = index.current_index();
// Already at the start?
if index != 0 {
self.counters.guard().move_to(index, index - 1);
}
}
}
}
}
fn main() {
let app = RelmApp::new("relm4.example.factory");
app.run::<App>(0);
}