View - Task list

Our view will have a two parts. If you know relm4 you will see lots of similarities here.

  1. Task widget and tasks list
  2. Main window

I've decided to split the view this way so each part of implementation is easier to understand.

List of tasks

All snippets in this section should go to view/task_list.rs

List of imports

There will be a lots of them here. I'm providing them here so they won't obstruct the examples later. We will cover all important parts later in this chapter.

use reexport::gtk;
use reexport::relm4;
use reexport::relm4_macros;
use store_view::View;

use gtk::Box;
use gtk::CheckButton;
use gtk::Label;
use gtk::Orientation;
use gtk::prelude::BoxExt;
use gtk::prelude::CheckButtonExt;
use gtk::prelude::EntryExt;
use gtk::prelude::EntryBufferExtManual;
use gtk::prelude::OrientableExt;
use gtk::prelude::WidgetExt;

use relm4::Model as ViewModel;
use relm4::send;
use relm4::Sender;
use relm4::Widgets;
use relm4::WidgetPlus;
use relm4_macros::widget;

use record::Id;
use record::Record;
use store::StoreViewPrototype;
use store::FactoryContainerWidgets;
use store::DataStore;
use store::Position;
use store::window::PositionTrackingWindow;

use crate::model::Task;
use crate::store::Tasks;

Task widget and task list

Firstly we need to define structures which will keep our widgets around.

type StoreMsg = store::StoreMsg<Task>;

pub enum TaskMsg {
    Toggle{
        complete: bool,
        id: Id<Task>,
    },
    New,
}

#[derive(Debug)]
#[allow(dead_code)]
pub struct TaskWidgets {
    checkbox: CheckButton,
    label: Label,
    root: Box,
}

pub trait TasksListConfiguration {
    type ParentViewModel: ViewModel;
    fn get_tasks(parent_view_model: &Self::ParentViewModel) -> Tasks;
}

pub struct TasksListViewModel<Config: TasksListConfiguration + 'static> {
    tasks: Tasks,
    store_view: View<Self>,
    new_task_description: gtk::EntryBuffer,
}

Let's discuss it one by one.

The first thing is definition of the StoreMsg type. In your code you will interact with store. Main goal of this type alias is to reduce amount of typing. All stores and store views are using store::StoreMsg to communicate between each other and using it is only way to affect state of the data store. store::StoreMsg is parametrized by the type of Record so you won't be able to send a message of the wrong type to the store.

The second one is TaskWidgets. Exactly same structure you would be defining if you would use relm4 factories. Checkbox to mark task as complete, label to keep task description and a box (root) to keep it together.

The third one is TasksListConfiguration. It's part of the component pattern to allow more then one instance of the component to be shown at the same time. It contains a method get_tasks which will return instance of the store::Store.

Finally TasksListViewModel. First really interesting things happens here. First attribute is tasks it's the data store which keeps all the data. We will need it to notify the store about status changes of the tasks. Second attribute is store_view. It will provide view into your store.

relm4::Model implementation for TasksListViewModel

This part is obvious

impl<Config: TasksListConfiguration> ViewModel for TasksListViewModel<Config> {
    type Msg = TaskMsg;
    type Widgets = TasksListViewWidgets;
    type Components = ();
}

StoreViewPrototype

In here we will implement store::StoreViewPrototype to provide a view for items in the store. You would use StoreViewPrototype trait in all the places where using pure relm4 you would use relm4::factory::FactoryPrototype.

Differences between the relm4::factory::FactoryPrototype and store::StoreViewPrototype

WhatFactoryPrototypeStoreViewPrototype
Target of implementationYou implement it for the ViewModel.You implement it for whatever you like. This makes this interface behave more like configuration.
Data containertype Factory which points to data container in the ViewModeltype Store which points to data container type. There is no requirement of it being inside of ViewModel.
Data visibilityAll data in the factory are visible.Only part of data in the Store is visible. type Window defines how the view window behaves (more in chapter 2 and 3).
Method signatureSince you implemented the factory for the ViewModel, it takes self as an argument. You create a widgets to display self. Second is key under which factory is going to find it. Key is unstable and managed by the factory.It's not bound to self. First is record for which widgets should be created. Second is position in the store. Position in the dataset at the time of widget generation. There is no guarantee to get the same widget in the future when asking store for record at the given position. Record is required to hold stable id by implementing model::Identifialble.

Let's create a file view/task.rs

impl<Config: TasksListConfiguration> StoreViewPrototype 
    for TasksListViewModel<Config> 
{
    type Store = Tasks;
    type StoreView = View<Self>;
    type RecordWidgets = TaskWidgets;
    type Root = gtk::Box;
    type View = gtk::Box;
    type Window = PositionTrackingWindow;
    type ViewModel = Self;
    type ParentViewModel = Config::ParentViewModel;

    fn init_store_view(
        store: Self::Store, 
        size: store::StoreSize, 
        redraw_sender: Sender<store::redraw_messages::RedrawMessages>
    ) -> Self::StoreView {
        View::new(store, size, redraw_sender)
    }

    fn init_view(
        record: &Task,
        _position: Position,
        sender: Sender<TaskMsg>,
    ) -> Self::RecordWidgets {
        let root = Box::builder()
            .orientation(Orientation::Horizontal)
            .build();

        let checkbox = CheckButton::builder()
            .margin_top(12)
            .margin_start(12)
            .margin_end(12)
            .margin_bottom(12)
            .active(record.completed)
            .build();

        {
            let sender = sender.clone();
            let id = record.get_id();

            checkbox.connect_toggled(move |btn| {
                send!(sender, TaskMsg::Toggle{
                    id,
                    complete: btn.is_active()
                });
            });
        }

        let label = Label::builder()
            .margin_top(12)
            .margin_start(12)
            .margin_end(12)
            .margin_bottom(12)
            .label(&record.description)
            .build();

        root.append(&checkbox);
        root.append(&label);

        TaskWidgets {
            checkbox,
            label,
            root,
        }
    }

    /// Function called when record is modified.
    fn view(
        record: Task,
        _position: Position,
        widgets: &Self::RecordWidgets,
    ) {
        widgets.checkbox.set_active(record.completed);

        let attrs = widgets.label.attributes().unwrap_or_default();
        attrs.change(gtk::pango::AttrInt::new_strikethrough(record.completed));
        widgets.label.set_attributes(Some(&attrs));
    }

    fn position(
        _model: Task, 
        _position: Position,
    ) {}

    /// Get the outermost widget from the widgets.
    fn root_widget(widgets: &Self::RecordWidgets) -> &Self::Root {
        &widgets.root
    }

    fn update(
        view_model: &mut Self::ViewModel, 
        msg: <Self as ViewModel>::Msg, 
        _sender: Sender<<Self as ViewModel>::Msg>
    ) {
        match msg {
            TaskMsg::New => {
                let description = view_model.new_task_description.text();
                let task = Task::new(description, false);
                view_model.new_task_description.set_text("");
                view_model.tasks.send(StoreMsg::Commit(task));
            },
            TaskMsg::Toggle{ complete, id } => {
                let tasks = &view_model.tasks;
                if let Some(record) = tasks.get(&id) {
                    let mut updated = record.clone();
                    updated.completed = complete;
                    tasks.send(StoreMsg::Commit(updated));
                }
            },
        }
    }

    fn init_view_model(
        parent_view_model: &Self::ParentViewModel, 
        store_view: &Self::StoreView
    ) -> Self {
        TasksListViewModel{
            tasks: Config::get_tasks(parent_view_model),
            new_task_description: gtk::EntryBuffer::new(None),
            store_view: store_view.clone(),
        }
    }
}

Let's look at the first part of StoreViewPrototype implementation

    type Store = Tasks;
    type StoreView = View<Self>;
    type RecordWidgets = TaskWidgets;
    type Root = gtk::Box;
    type View = gtk::Box;
    type Window = PositionTrackingWindow;
    type ViewModel = Self;
    type ParentViewModel = Config::ParentViewModel;
type namevaluemeaning
StoreTasksThis type provides information about which store type will be used. This itself also provides information abut the model which (DataStoreBase::Model) which is used by the related store and as the consequence this view. In relm4's FactoryPrototype you would provide factory type where your data would be stored.
StoreViewView<TasksListViewModel>This type provides information about which store view type will be used. In relm4 this would be part of FactoryPrototype. It's responsible for providing view into the store
RecordWidgetsTaskWidgetsThe same as in relm4's FactoryPrototype::Widgets. Type of structure holding all widgets.
Rootgtk::BoxType of widget which is a root for all widgets kept in the RecordWidgets. Same as in FactoryPrototype::Root.
Viewgtk::BoxType of widgets which will keep the list of widgets. (The widget to which factory should add widgets to). Same as in FactoryPrototype::View. There must exist implementations of relm4::factory::FactoryView and relm4::factory::FactoryListView for View.
WindowPositionTrackingWindowDescribes how the view window will behave in case of new data. For now use PositionTrackingWindow with annotation that if you don't know what to use, this one is probably the one.
ViewModelTasksListViewModelProvides information about type of view model used by implementation of the StoreViewPrototype
ParentViewModelConfig::ParentViewModelProvides information about the parent view model. Used during initialization of the view model

init_store_view

This method is responsible for creating instance of the store view. In here you connect your view with store and make sure your view has all required properties.

    fn init_store_view(
        store: Self::Store, 
        size: store::StoreSize, 
        redraw_sender: Sender<store::redraw_messages::RedrawMessages>
    ) -> Self::StoreView {
        View::new(store, size, redraw_sender)
    }

relm4::factory::FactoryPrototype

Next we implemented init_view, view, position and root_widget methods. All four methods are equivalents of the methods with the same name in FactoryPrototype.

relm4::ComponentUpdate

This method is equivalent of update for ComponentUpdate. init_view_model is init_model from ComponentUpdate.

TaskListViewWidgets

Now we can create our widgets for showing whole list

#[widget(visibility=pub, relm4=reexport::relm4)]
impl<Config: TasksListConfiguration> 
    Widgets<TasksListViewModel<Config>, Config::ParentViewModel> 
    for TasksListViewWidgets 
{
    view!{
        root = gtk::Box {
            set_margin_all: 12,
            set_orientation: gtk::Orientation::Vertical,
            append = &gtk::Entry::with_buffer(&model.new_task_description) {
                connect_activate(sender) => move |_| { 
                    send!(sender, TaskMsg::New); 
                } 
            },
            append = &gtk::ScrolledWindow {
                set_hexpand: true,
                set_vexpand: true,
                set_child: container = Some(&gtk::Box) {
                    set_orientation: gtk::Orientation::Vertical,
                    factory!(model.store_view)
                }
            }
        }
    }
}

There are only two interesting things here. First StoreView is kind of relm4 factory.

                    factory!(model.store_view)

Second is

                set_child: container = Some(&gtk::Box) {
                    set_orientation: gtk::Orientation::Vertical,
                    factory!(model.store_view)
                }

In here we've named container handling our list of tasks. It's important so the component knows which element to provide to relm4's Factory::init_view method.

Rest is classic relm4.

FactoryContainerWidgets

Now we need to implement extra trait store::FactoryContainerWidgets

impl<Config: TasksListConfiguration> 
    FactoryContainerWidgets<TasksListViewModel<Config>> 
    for TasksListViewWidgets 
{
    fn container_widget(&self) 
        -> &<TasksListViewModel<Config> as StoreViewPrototype>::View 
    {
        &self.container
    }
}

In here we return reference to the widget used to keep whole list of our tasks