Let To-Do App use Libadwaita

Within this chapter we will adapt our To-Do app so that it follows GNOME's HIG. Let's start by installing Libadwaita and adding the libadwaita crate to our dependencies as explained in the previous chapter.

The simplest way to take advantage of Libadwaita is by replacing gtk::Application with adw::Application.

Filename: listings/todo/5/main.rs

mod task_object; mod task_row; mod utils; mod window; use gtk::prelude::*; use gtk::{gio, glib}; use window::Window; const APP_ID: &str = "org.gtk_rs.Todo5"; fn main() -> glib::ExitCode { gio::resources_register_include!("todo_5.gresource") .expect("Failed to register resources."); // Create a new application // πŸ‘‡ changed let app = adw::Application::builder().application_id(APP_ID).build(); // Connect to signals app.connect_startup(setup_shortcuts); app.connect_activate(build_ui); // Run the application app.run() } // πŸ‘‡ changed fn setup_shortcuts(app: &adw::Application) { app.set_accels_for_action("win.filter('All')", &["<Ctrl>a"]); app.set_accels_for_action("win.filter('Open')", &["<Ctrl>o"]); app.set_accels_for_action("win.filter('Done')", &["<Ctrl>d"]); } // πŸ‘‡ changed fn build_ui(app: &adw::Application) { // Create a new custom window and present it let window = Window::new(app); window.present(); }

Filename: listings/todo/5/window/mod.rs

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &adw::Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); } fn remove_done_tasks(&self) { let tasks = self.tasks(); let mut position = 0; while let Some(item) = tasks.item(position) { // Get `TaskObject` from `glib::Object` let task_object = item .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

adw::Application calls adw::init internally and makes sure that translations, types, stylesheets, and icons are set up properly for Libadwaita. It also loads stylesheets automatically from resources as long as they are named correctly.

Looking at our To-Do app we can see that the looks of its widgets changed. This is because the Default stylesheet provided by GTK has been replaced with the Adwaita stylesheet provided by Libadwaita.

Transformation of To-Do app

Also, our app now switches to the dark style together with the rest of the system.

Boxed lists

Of course Libadwaita is more than just a couple of stylesheets and a StyleManager. But before we get to the interesting stuff, we will make our lives easier for the future by replacing all occurrences of gtk::prelude and gtk::subclass::prelude with adw::prelude and adw::subclass::prelude. This works because the adw preludes, in addition to the Libadwaita-specific traits, re-export the corresponding gtk preludes.

Now we are going let our tasks follow the boxed lists pattern. The HIG does not require us to use this style and there's a good reason for that: it is incompatible with recycling lists. This means they cannot be used with list views and are therefore only appropriate for relatively small lists.

Try to add tasks programmatically and see how many of them you have to add until the UI noticeably slows down. Determine for yourself if you think that is a reasonable number or if we should have rather stuck with list views.

We can use boxed lists by using gtk::ListBox instead of gtk::ListView. We will also add the boxed-list style class provided by Libadwaita.

Let's implement all these changes in the window.ui file. All of the changes are confined within the second child of the ApplicationWindow. To see the complete file, just click on the link after "Filename".

Filename: listings/todo/6/resources/window.ui

<child> <object class="GtkScrolledWindow"> <property name="hscrollbar-policy">never</property> <property name="min-content-height">420</property> <property name="vexpand">True</property> <property name="child"> <object class="AdwClamp"> <property name="child"> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">18</property> <property name="margin-top">24</property> <property name="margin-bottom">24</property> <property name="margin-start">12</property> <property name="margin-end">12</property> <child> <object class="GtkEntry" id="entry"> <property name="placeholder-text" translatable="yes">Enter a Task…</property> <property name="secondary-icon-name">list-add-symbolic</property> </object> </child> <child> <object class="GtkListBox" id="tasks_list"> <property name="visible">False</property> <property name="selection-mode">none</property> <style> <class name="boxed-list" /> </style> </object> </child> </object> </property> </object> </property> </object> </child>

In order to follow the boxed list pattern, we switched to gtk::ListBox, set its property "selection-mode" to "none" and added the boxed-list style class.

Let's continue with window/imp.rs. The member variable tasks_list now describes a ListBox rather than a ListView.

Filename: listings/todo/6/window/imp.rs

use std::cell::RefCell; use std::fs::File; use adw::subclass::prelude::*; use gio::Settings; use glib::subclass::InitializingObject; use adw::prelude::*; use gtk::{gio, glib, CompositeTemplate, Entry, ListBox}; use std::cell::OnceCell; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; // Object holding the state #[derive(CompositeTemplate, Default)] #[template(resource = "/org/gtk_rs/Todo6/window.ui")] pub struct Window { #[template_child] pub entry: TemplateChild<Entry>, #[template_child] pub tasks_list: TemplateChild<ListBox>, pub tasks: RefCell<Option<gio::ListStore>>, pub settings: OnceCell<Settings>, } // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { // `NAME` needs to match `class` attribute of template const NAME: &'static str = "TodoWindow"; type Type = super::Window; type ParentType = gtk::ApplicationWindow; fn class_init(klass: &mut Self::Class) { klass.bind_template(); // Create action to remove done tasks and add to action group "win" klass.install_action("win.remove-done-tasks", None, |window, _, _| { window.remove_done_tasks(); }); } fn instance_init(obj: &InitializingObject<Self>) { obj.init_template(); } } // Trait shared by all GObjects impl ObjectImpl for Window { fn constructed(&self) { // Call "constructed" on parent self.parent_constructed(); // Setup let obj = self.obj(); obj.setup_settings(); obj.setup_tasks(); obj.restore_data(); obj.setup_callbacks(); obj.setup_actions(); } } // Trait shared by all widgets impl WidgetImpl for Window {} // Trait shared by all windows impl WindowImpl for Window { fn close_request(&self) -> glib::Propagation { // Store task data in vector let backup_data: Vec<TaskData> = self .obj() .tasks() .iter::<TaskObject>() .filter_map(Result::ok) .map(|task_object| task_object.task_data()) .collect(); // Save state to file let file = File::create(data_path()).expect("Could not create json file."); serde_json::to_writer(file, &backup_data) .expect("Could not write data to json file"); // Pass close request on to the parent self.parent_close_request() } } // Trait shared by all application windows impl ApplicationWindowImpl for Window {}

We now move on to window/mod.rs. ListBox supports models just fine, but without any widget recycling we don't need factories anymore. setup_factory can therefore be safely deleted. To setup the ListBox, we call bind_model in setup_tasks. There we specify the model, as well as a closure describing how to transform the given GObject into a widget the list box can display.

Filename: listings/todo/6/window/mod.rs

mod imp; use std::fs::File; use adw::subclass::prelude::*; use adw::{prelude::*, ActionRow}; use gio::Settings; use glib::{clone, Object}; use gtk::{gio, glib, Align, CheckButton, CustomFilter, FilterListModel, NoSelection}; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &adw::Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list box let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.bind_model( Some(&selection_model), clone!( #[weak(rename_to = window)] self, #[upgrade_or_panic] move |obj| { let task_object = obj .downcast_ref() .expect("The object should be of type `TaskObject`."); let row = window.create_task_row(task_object); row.upcast() } ), ); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); // Assure that the task list is only visible when it is supposed to self.set_task_list_visible(&self.tasks()); self.tasks().connect_items_changed(clone!( #[weak(rename_to = window)] self, move |tasks, _, _, _| { window.set_task_list_visible(tasks); } )); } /// Assure that `tasks_list` is only visible /// if the number of tasks is greater than 0 fn set_task_list_visible(&self, tasks: &gio::ListStore) { self.imp().tasks_list.set_visible(tasks.n_items() > 0); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn create_task_row(&self, task_object: &TaskObject) -> ActionRow { // Create check button let check_button = CheckButton::builder() .valign(Align::Center) .can_focus(false) .build(); // Create row let row = ActionRow::builder() .activatable_widget(&check_button) .build(); row.add_prefix(&check_button); // Bind properties task_object .bind_property("completed", &check_button, "active") .bidirectional() .sync_create() .build(); task_object .bind_property("content", &row, "title") .sync_create() .build(); // Return row row } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_actions(&self) { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); } fn remove_done_tasks(&self) { let tasks = self.tasks(); let mut position = 0; while let Some(item) = tasks.item(position) { // Get `TaskObject` from `glib::Object` let task_object = item .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

We still have to specify the create_task_row method. Here, we create an adw::ActionRow with a gtk::CheckButton as activatable widget. Without recycling, a GObject will always belong to the same widget. That means we can just bind their properties without having to worry about unbinding them later on.

Filename: listings/todo/6/window/mod.rs

mod imp; use std::fs::File; use adw::subclass::prelude::*; use adw::{prelude::*, ActionRow}; use gio::Settings; use glib::{clone, Object}; use gtk::{gio, glib, Align, CheckButton, CustomFilter, FilterListModel, NoSelection}; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &adw::Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list box let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.bind_model( Some(&selection_model), clone!( #[weak(rename_to = window)] self, #[upgrade_or_panic] move |obj| { let task_object = obj .downcast_ref() .expect("The object should be of type `TaskObject`."); let row = window.create_task_row(task_object); row.upcast() } ), ); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); // Assure that the task list is only visible when it is supposed to self.set_task_list_visible(&self.tasks()); self.tasks().connect_items_changed(clone!( #[weak(rename_to = window)] self, move |tasks, _, _, _| { window.set_task_list_visible(tasks); } )); } /// Assure that `tasks_list` is only visible /// if the number of tasks is greater than 0 fn set_task_list_visible(&self, tasks: &gio::ListStore) { self.imp().tasks_list.set_visible(tasks.n_items() > 0); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn create_task_row(&self, task_object: &TaskObject) -> ActionRow { // Create check button let check_button = CheckButton::builder() .valign(Align::Center) .can_focus(false) .build(); // Create row let row = ActionRow::builder() .activatable_widget(&check_button) .build(); row.add_prefix(&check_button); // Bind properties task_object .bind_property("completed", &check_button, "active") .bidirectional() .sync_create() .build(); task_object .bind_property("content", &row, "title") .sync_create() .build(); // Return row row } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_actions(&self) { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); } fn remove_done_tasks(&self) { let tasks = self.tasks(); let mut position = 0; while let Some(item) = tasks.item(position) { // Get `TaskObject` from `glib::Object` let task_object = item .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

When using boxed lists, you also have to take care to hide the ListBox when there is no task present.

Filename: listings/todo/6/window/mod.rs

mod imp; use std::fs::File; use adw::subclass::prelude::*; use adw::{prelude::*, ActionRow}; use gio::Settings; use glib::{clone, Object}; use gtk::{gio, glib, Align, CheckButton, CustomFilter, FilterListModel, NoSelection}; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &adw::Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list box let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.bind_model( Some(&selection_model), clone!( #[weak(rename_to = window)] self, #[upgrade_or_panic] move |obj| { let task_object = obj .downcast_ref() .expect("The object should be of type `TaskObject`."); let row = window.create_task_row(task_object); row.upcast() } ), ); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); // Assure that the task list is only visible when it is supposed to self.set_task_list_visible(&self.tasks()); self.tasks().connect_items_changed(clone!( #[weak(rename_to = window)] self, move |tasks, _, _, _| { window.set_task_list_visible(tasks); } )); } /// Assure that `tasks_list` is only visible /// if the number of tasks is greater than 0 fn set_task_list_visible(&self, tasks: &gio::ListStore) { self.imp().tasks_list.set_visible(tasks.n_items() > 0); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn create_task_row(&self, task_object: &TaskObject) -> ActionRow { // Create check button let check_button = CheckButton::builder() .valign(Align::Center) .can_focus(false) .build(); // Create row let row = ActionRow::builder() .activatable_widget(&check_button) .build(); row.add_prefix(&check_button); // Bind properties task_object .bind_property("completed", &check_button, "active") .bidirectional() .sync_create() .build(); task_object .bind_property("content", &row, "title") .sync_create() .build(); // Return row row } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_actions(&self) { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); } fn remove_done_tasks(&self) { let tasks = self.tasks(); let mut position = 0; while let Some(item) = tasks.item(position) { // Get `TaskObject` from `glib::Object` let task_object = item .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

Finally, we define the set_task_list_visible method.

Filename: listings/todo/6/window/mod.rs

mod imp; use std::fs::File; use adw::subclass::prelude::*; use adw::{prelude::*, ActionRow}; use gio::Settings; use glib::{clone, Object}; use gtk::{gio, glib, Align, CheckButton, CustomFilter, FilterListModel, NoSelection}; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &adw::Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list box let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.bind_model( Some(&selection_model), clone!( #[weak(rename_to = window)] self, #[upgrade_or_panic] move |obj| { let task_object = obj .downcast_ref() .expect("The object should be of type `TaskObject`."); let row = window.create_task_row(task_object); row.upcast() } ), ); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); // Assure that the task list is only visible when it is supposed to self.set_task_list_visible(&self.tasks()); self.tasks().connect_items_changed(clone!( #[weak(rename_to = window)] self, move |tasks, _, _, _| { window.set_task_list_visible(tasks); } )); } /// Assure that `tasks_list` is only visible /// if the number of tasks is greater than 0 fn set_task_list_visible(&self, tasks: &gio::ListStore) { self.imp().tasks_list.set_visible(tasks.n_items() > 0); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn create_task_row(&self, task_object: &TaskObject) -> ActionRow { // Create check button let check_button = CheckButton::builder() .valign(Align::Center) .can_focus(false) .build(); // Create row let row = ActionRow::builder() .activatable_widget(&check_button) .build(); row.add_prefix(&check_button); // Bind properties task_object .bind_property("completed", &check_button, "active") .bidirectional() .sync_create() .build(); task_object .bind_property("content", &row, "title") .sync_create() .build(); // Return row row } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_actions(&self) { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); } fn remove_done_tasks(&self) { let tasks = self.tasks(); let mut position = 0; while let Some(item) = tasks.item(position) { // Get `TaskObject` from `glib::Object` let task_object = item .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

This is how the boxed list style looks like in our app.

The To-Do app using libadwaita