Advanced factories
In this chapter we will build an even more advanced UI for modifying the available counters:
Additionally, certain counters can now be removed or inserted above or below an existing counter.
The FactoryVec
we used in the previous chapter is sufficient for simple applications where elements only need to be added and removed from the back. Yet a common use case would be to add elements before another one or to remove a specific element. That introduces additional complexity that needs to be taken care of but fortunately this is mostly handled by Relm4.
To show this, we'll create a similar counter app to the one of the previous chapter, but this time on steroids: we'll add functionality to add counters before and after a specific counter and to remove a certain counter. To get the required flexibility, we'll use the FactoryVecDeque
type instead of a FactoryVec
.
The app we will write in this chapter is also available here. Run
cargo run --example factory_advanced
from the example directory if you want to see the code in action.
Indices
The indices of a FactoryVec
were just numbers of type usize
. That's great unless elements can move and change their index. This tragedy starts when we, for example, add an element to the front: the new element now has index 0
, the element that had index 0
before now has index 1
and so on. Adding one element will shift the indices of all following elements. If we naively create a signal handler similar to the previous chapter were we just copied the index at start and moved it into the closure, we will quickly end up with quite wrong or even out-of-bounds indices as elements are added and removed at arbitrary positions.
One solution would be to recreate all signal handlers with the updated indices once an element's index has been changed. However, that's complicated because you need to remove the old signal handlers first and therefore you have to store all signal handler IDs.
The solution Relm4 chose was dynamic indices. These indices are updated automatically to always point at the same element.
The message type
type MsgIndex = WeakDynamicIndex;
#[derive(Debug)]
enum AppMsg {
AddFirst,
RemoveLast,
CountAt(MsgIndex),
RemoveAt(MsgIndex),
InsertBefore(MsgIndex),
InsertAfter(MsgIndex),
}
As you can see, we use a lot of MsgIndex
aka WeakDynamicIndex
. This allows us to always hold a reference to the dynamic index value.
The reason we use the weak index here is that we don’t want to hold references to invalid indices. We don’t know if our messages are handled immediately or queued up instead, so the data the index was pointing at could have been replaced by a new data in the meantime. Usually this happens so rarely that this can be ignored, but with the weak index we guarantee that the indices are not kept alive in the message queue and we will never use a stale index.
The model
The model is very similar to the previous chapter. The only difference is that we use FactoryVecDeque
as a data structure now.
struct Counter {
value: u8,
}
struct AppModel {
counters: FactoryVecDeque<Counter>,
received_messages: u8,
}
The update function
The update function now handles quite a lot of events. We want to
- Add elements at the start
- Remove elements from the back
- Decrement (count) a counter at a specific index
- Insert a new counter before another counter
- Insert a new counter after another counter
impl AppUpdate for AppModel {
fn update(&mut self, msg: AppMsg, _components: &(), _sender: Sender<AppMsg>) -> bool {
match msg {
AppMsg::AddFirst => {
self.counters.push_front(Counter {
value: self.received_messages,
});
}
AppMsg::RemoveLast => {
self.counters.pop_back();
}
AppMsg::CountAt(weak_index) => {
if let Some(index) = weak_index.upgrade() {
if let Some(counter) = self.counters.get_mut(index.current_index()) {
counter.value = counter.value.wrapping_sub(1);
}
}
}
AppMsg::RemoveAt(weak_index) => {
if let Some(index) = weak_index.upgrade() {
self.counters.remove(index.current_index());
}
}
AppMsg::InsertBefore(weak_index) => {
if let Some(index) = weak_index.upgrade() {
self.counters.insert(
index.current_index(),
Counter {
value: self.received_messages,
},
);
}
}
AppMsg::InsertAfter(weak_index) => {
if let Some(index) = weak_index.upgrade() {
self.counters.insert(
index.current_index() + 1,
Counter {
value: self.received_messages,
},
);
}
}
}
self.received_messages += 1;
true
}
}
To get the current index value from the dynamic index, we simply call index.current_index()
.
The factory implementation
The factory implementation is mostly the same, so we'll just have a look at what has changed.
The widgets type
Because we have four actions per counter now, we also need an additional box to store these buttons.
To be able to provide the root widget via the root_widget
function we need to store the box in the widgets type.
#[derive(Debug)]
struct FactoryWidgets {
hbox: gtk::Box,
counter_button: gtk::Button,
}
The init_view function
For the init_view
function, we need to first generate the new buttons and the box.
fn init_view(&self, index: &DynamicIndex, sender: Sender<AppMsg>) -> FactoryWidgets {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(5)
.build();
let counter_button = gtk::Button::with_label(&self.value.to_string());
let index: DynamicIndex = index.clone();
let remove_button = gtk::Button::with_label("Remove");
let ins_above_button = gtk::Button::with_label("Add above");
let ins_below_button = gtk::Button::with_label("Add below");
Then we need to place the buttons inside of the box.
hbox.append(&counter_button);
hbox.append(&remove_button);
hbox.append(&ins_above_button);
hbox.append(&ins_below_button);
Now we can connect the messages. We always send a weak pointer of our dynamic index.
{
let sender = sender.clone();
let index = index.clone();
counter_button.connect_clicked(move |_| {
send!(sender, AppMsg::CountAt(index.downgrade()));
});
}
{
let sender = sender.clone();
let index = index.clone();
remove_button.connect_clicked(move |_| {
send!(sender, AppMsg::RemoveAt(index.downgrade()));
});
}
{
let sender = sender.clone();
let index = index.clone();
ins_above_button.connect_clicked(move |_| {
send!(sender, AppMsg::InsertBefore(index.downgrade()));
});
}
ins_below_button.connect_clicked(move |_| {
send!(sender, AppMsg::InsertAfter(index.downgrade()));
});
FactoryWidgets {
hbox,
counter_button,
}
And that's it! All the other complex operations that keep track of changes are implemented in Relm4 already, we just need to use dynamic indices to make out program work :)
The complete code
Let's review our code in one piece one more time to see how all these parts work together:
Unlike the example in the previous chapter, the following code does not use the widget macro from relm4-macros but implements the
Widgets
trait manually. Yet, the generated code from the macro and the manual code should be almost identical.
use gtk::glib::Sender;
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt};
use relm4::factory::{DynamicIndex, Factory, FactoryPrototype, FactoryVecDeque, WeakDynamicIndex};
use relm4::*;
type MsgIndex = WeakDynamicIndex;
#[derive(Debug)]
enum AppMsg {
AddFirst,
RemoveLast,
CountAt(MsgIndex),
RemoveAt(MsgIndex),
InsertBefore(MsgIndex),
InsertAfter(MsgIndex),
}
struct Counter {
value: u8,
}
struct AppModel {
counters: FactoryVecDeque<Counter>,
received_messages: 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::AddFirst => {
self.counters.push_front(Counter {
value: self.received_messages,
});
}
AppMsg::RemoveLast => {
self.counters.pop_back();
}
AppMsg::CountAt(weak_index) => {
if let Some(index) = weak_index.upgrade() {
if let Some(counter) = self.counters.get_mut(index.current_index()) {
counter.value = counter.value.wrapping_sub(1);
}
}
}
AppMsg::RemoveAt(weak_index) => {
if let Some(index) = weak_index.upgrade() {
self.counters.remove(index.current_index());
}
}
AppMsg::InsertBefore(weak_index) => {
if let Some(index) = weak_index.upgrade() {
self.counters.insert(
index.current_index(),
Counter {
value: self.received_messages,
},
);
}
}
AppMsg::InsertAfter(weak_index) => {
if let Some(index) = weak_index.upgrade() {
self.counters.insert(
index.current_index() + 1,
Counter {
value: self.received_messages,
},
);
}
}
}
self.received_messages += 1;
true
}
}
#[derive(Debug)]
struct FactoryWidgets {
hbox: gtk::Box,
counter_button: gtk::Button,
}
impl FactoryPrototype for Counter {
type Factory = FactoryVecDeque<Self>;
type Widgets = FactoryWidgets;
type Root = gtk::Box;
type View = gtk::Box;
type Msg = AppMsg;
fn init_view(&self, index: &DynamicIndex, sender: Sender<AppMsg>) -> FactoryWidgets {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(5)
.build();
let counter_button = gtk::Button::with_label(&self.value.to_string());
let index: DynamicIndex = index.clone();
let remove_button = gtk::Button::with_label("Remove");
let ins_above_button = gtk::Button::with_label("Add above");
let ins_below_button = gtk::Button::with_label("Add below");
hbox.append(&counter_button);
hbox.append(&remove_button);
hbox.append(&ins_above_button);
hbox.append(&ins_below_button);
{
let sender = sender.clone();
let index = index.clone();
counter_button.connect_clicked(move |_| {
send!(sender, AppMsg::CountAt(index.downgrade()));
});
}
{
let sender = sender.clone();
let index = index.clone();
remove_button.connect_clicked(move |_| {
send!(sender, AppMsg::RemoveAt(index.downgrade()));
});
}
{
let sender = sender.clone();
let index = index.clone();
ins_above_button.connect_clicked(move |_| {
send!(sender, AppMsg::InsertBefore(index.downgrade()));
});
}
ins_below_button.connect_clicked(move |_| {
send!(sender, AppMsg::InsertAfter(index.downgrade()));
});
FactoryWidgets {
hbox,
counter_button,
}
}
fn position(&self, _index: &DynamicIndex) {}
fn view(&self, _index: &DynamicIndex, widgets: &FactoryWidgets) {
widgets.counter_button.set_label(&self.value.to_string());
}
fn root_widget(widget: &FactoryWidgets) -> >k::Box {
&widget.hbox
}
}
struct AppWidgets {
main: gtk::ApplicationWindow,
gen_box: gtk::Box,
}
impl Widgets<AppModel, ()> for AppWidgets {
type Root = gtk::ApplicationWindow;
fn init_view(_model: &AppModel, _components: &(), sender: Sender<AppMsg>) -> Self {
let main = gtk::builders::ApplicationWindowBuilder::new()
.default_width(300)
.default_height(200)
.build();
let main_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.margin_end(5)
.margin_top(5)
.margin_start(5)
.margin_bottom(5)
.spacing(5)
.build();
let gen_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.margin_end(5)
.margin_top(5)
.margin_start(5)
.margin_bottom(5)
.spacing(5)
.build();
let add = gtk::Button::with_label("Add");
let remove = gtk::Button::with_label("Remove");
main_box.append(&add);
main_box.append(&remove);
main_box.append(&gen_box);
main.set_child(Some(&main_box));
let cloned_sender = sender.clone();
add.connect_clicked(move |_| {
cloned_sender.send(AppMsg::AddFirst).unwrap();
});
remove.connect_clicked(move |_| {
sender.send(AppMsg::RemoveLast).unwrap();
});
AppWidgets { main, gen_box }
}
fn view(&mut self, model: &AppModel, sender: Sender<AppMsg>) {
model.counters.generate(&self.gen_box, sender);
}
fn root_widget(&self) -> gtk::ApplicationWindow {
self.main.clone()
}
}
fn main() {
let model = AppModel {
counters: FactoryVecDeque::new(),
received_messages: 0,
};
let relm = RelmApp::new(model);
relm.run();
}