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.

App screenshot dark

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);
}