View - Task list
Our view will have a two parts. If you know relm4
you will see lots of similarities here.
- Task widget and tasks list
- 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
What | FactoryPrototype | StoreViewPrototype |
---|---|---|
Target of implementation | You implement it for the ViewModel. | You implement it for whatever you like. This makes this interface behave more like configuration. |
Data container | type Factory which points to data container in the ViewModel | type Store which points to data container type. There is no requirement of it being inside of ViewModel . |
Data visibility | All 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 signature | Since 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 name | value | meaning |
---|---|---|
Store | Tasks | This 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. |
StoreView | View<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 |
RecordWidgets | TaskWidgets | The same as in relm4's FactoryPrototype::Widgets . Type of structure holding all widgets. |
Root | gtk::Box | Type of widget which is a root for all widgets kept in the RecordWidgets . Same as in FactoryPrototype::Root . |
View | gtk::Box | Type 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 . |
Window | PositionTrackingWindow | Describes 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. |
ViewModel | TasksListViewModel | Provides information about type of view model used by implementation of the StoreViewPrototype |
ParentViewModel | Config::ParentViewModel | Provides 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 = >k::Entry::with_buffer(&model.new_task_description) {
connect_activate(sender) => move |_| {
send!(sender, TaskMsg::New);
}
},
append = >k::ScrolledWindow {
set_hexpand: true,
set_vexpand: true,
set_child: container = Some(>k::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(>k::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