From 15d2569956d7dd4c820e4b8064c9e8d1b527c65f Mon Sep 17 00:00:00 2001 From: bilelmoussaoui Date: Sun, 21 Jan 2024 08:23:17 +0000 Subject: [PATCH] deploy: e28d2fb823f1dc0191ac45b1e4ee32e6881ea8c3 --- stable/latest/book/css.html | 6 +++--- stable/latest/book/print.html | 10 +++++----- stable/latest/book/project_setup.html | 4 ++-- stable/latest/book/searchindex.js | 2 +- stable/latest/book/searchindex.json | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/stable/latest/book/css.html b/stable/latest/book/css.html index 531ed56a22e9..7f27e7a1638a 100644 --- a/stable/latest/book/css.html +++ b/stable/latest/book/css.html @@ -217,7 +217,7 @@

CSS

fn load_css() { // Load the CSS file and add it to the provider let provider = CssProvider::new(); - provider.load_from_data(include_str!("style.css")); + provider.load_from_string(include_str!("style.css")); // Add the provider to the default screen gtk::style_context_add_provider_for_display( @@ -300,7 +300,7 @@

fn load_css() { // Load the CSS file and add it to the provider let provider = CssProvider::new(); - provider.load_from_data(include_str!("style.css")); + provider.load_from_string(include_str!("style.css")); // Add the provider to the default screen gtk::style_context_add_provider_for_display( @@ -372,7 +372,7 @@

fn load_css() { // Load the CSS file and add it to the provider let provider = CssProvider::new(); - provider.load_from_data(include_str!("style.css")); + provider.load_from_string(include_str!("style.css")); // Add the provider to the default screen gtk::style_context_add_provider_for_display( diff --git a/stable/latest/book/print.html b/stable/latest/book/print.html index 7c4ed9b3d76b..0fba23cbf749 100644 --- a/stable/latest/book/print.html +++ b/stable/latest/book/print.html @@ -398,8 +398,8 @@

gtk4 crate to your dependencies in Cargo.toml. -At the time of this writing the newest version is 4.8.

-
cargo add gtk4 --rename gtk --features v4_8
+At the time of this writing the newest version is 4.12.

+
cargo add gtk4 --rename gtk --features v4_12
 

By specifying this feature you opt-in to API that was added with minor releases of GTK 4.

Now, you can run your application by executing:

@@ -10792,7 +10792,7 @@

fn load_css() { // Load the CSS file and add it to the provider let provider = CssProvider::new(); - provider.load_from_data(include_str!("style.css")); + provider.load_from_string(include_str!("style.css")); // Add the provider to the default screen gtk::style_context_add_provider_for_display( @@ -10947,7 +10947,7 @@

fn load_css() { // Load the CSS file and add it to the provider let provider = CssProvider::new(); - provider.load_from_data(include_str!("style.css")); + provider.load_from_string(include_str!("style.css")); // Add the provider to the default screen gtk::style_context_add_provider_for_display( diff --git a/stable/latest/book/project_setup.html b/stable/latest/book/project_setup.html index 9c1e11e8922e..d00702e642f2 100644 --- a/stable/latest/book/project_setup.html +++ b/stable/latest/book/project_setup.html @@ -189,8 +189,8 @@

Project Setup
pkg-config --modversion gtk4
 

Use this information to add the gtk4 crate to your dependencies in Cargo.toml. -At the time of this writing the newest version is 4.8.

-
cargo add gtk4 --rename gtk --features v4_8
+At the time of this writing the newest version is 4.12.

+
cargo add gtk4 --rename gtk --features v4_12
 

By specifying this feature you opt-in to API that was added with minor releases of GTK 4.

Now, you can run your application by executing:

diff --git a/stable/latest/book/searchindex.js b/stable/latest/book/searchindex.js index b3dd0d2d6c12..d023d2de384f 100644 --- a/stable/latest/book/searchindex.js +++ b/stable/latest/book/searchindex.js @@ -1 +1 @@ -Object.assign(window.search, {"doc_urls":["introduction.html#gui-development-with-rust-and-gtk-4","introduction.html#who-this-book-is-for","introduction.html#how-to-use-this-book","introduction.html#license","installation.html#installation","installation_linux.html#linux","installation_macos.html#macos","installation_windows.html#windows","installation_windows.html#install-rustup","installation_windows.html#install-gtk-4","installation_windows.html#set-rust-toolchain-to-msvc","installation_windows.html#build-gtk-4","installation_windows.html#update-path-environment-variable","installation_windows.html#set-rust-toolchain-to-msvc-1","installation_windows.html#visual-studio","installation_windows.html#git","installation_windows.html#cmake","installation_windows.html#python","installation_windows.html#meson","installation_windows.html#gettext-021","installation_windows.html#pkg-config","installation_windows.html#update-environment-variables","installation_windows.html#compile-and-install-gtk-4","installation_windows.html#install-rustup-1","installation_windows.html#remove-residues-from-the-msvc-toolchain","installation_windows.html#msys2","installation_windows.html#install-gtk-4-1","installation_windows.html#update-path-environment-variable-1","installation_windows.html#setup-the-gnu-toolchain-for-rust","project_setup.html#project-setup","hello_world.html#hello-world","widgets.html#widgets","g_object_concepts.html#gobject-concepts","g_object_memory_management.html#memory-management","g_object_subclassing.html#subclassing","g_object_subclassing.html#adding-functionality","g_object_values.html#generic-values","g_object_values.html#value","g_object_values.html#variant","g_object_properties.html#properties","g_object_properties.html#adding-properties-to-custom-gobjects","g_object_signals.html#signals","g_object_signals.html#adding-signals-to-custom-gobjects","main_event_loop.html#the-main-event-loop","main_event_loop.html#how-to-avoid-blocking-the-main-loop","main_event_loop.html#channels","main_event_loop.html#embed-blocking-calls-in-an-async-context","main_event_loop.html#run-async-functions-from-external-crates","main_event_loop.html#tokio","main_event_loop.html#conclusion","settings.html#settings","saving_window_state.html#saving-window-state","list_widgets.html#list-widgets","list_widgets.html#views","list_widgets.html#expressions","list_widgets.html#string-list","list_widgets.html#conclusion","composite_templates.html#composite-templates","composite_templates.html#resources","composite_templates.html#custom-widgets","composite_templates.html#template-callbacks","composite_templates.html#registering-types","composite_templates.html#conclusion","todo_1.html#building-a-simple-to-do-app","todo_1.html#window","todo_1.html#task-object","todo_1.html#task-row","actions.html#actions","actions.html#parameter-and-state","actions.html#actionable","actions.html#menus","actions.html#settings","todo_2.html#manipulating-state-of-to-do-app","todo_2.html#filtering-tasks","todo_2.html#saving-and-restoring-tasks","css.html#css","css.html#style-classes-applied-by-gtk","css.html#adding-your-own-style-class","css.html#specifying-name-of-a-widget","css.html#css-rules-provided-by-gtk","css.html#interface-builder","css.html#pseudo-classes","css.html#nodes","css.html#set-css-name-and-use-exported-colors","css.html#adapt-todo-app","css.html#conclusion","libadwaita.html#libadwaita","libadwaita.html#linux","libadwaita.html#macos","libadwaita.html#windows","libadwaita.html#if-using-gvsbuild","libadwaita.html#if-building-manually-with-msvc","libadwaita.html#work-around-missing-icons","libadwaita.html#gvsbuild","libadwaita.html#manually-with-msvc","todo_3.html#let-to-do-app-use-libadwaita","todo_3.html#boxed-lists","todo_4.html#adding-collections","todo_4.html#sidebar","todo_4.html#placeholder-page","todo_4.html#collections","todo_4.html#window","todo_4.html#message-dialog"],"index":{"documentStore":{"docInfo":{"0":{"body":83,"breadcrumbs":6,"title":5},"1":{"body":43,"breadcrumbs":2,"title":1},"10":{"body":9,"breadcrumbs":6,"title":4},"100":{"body":281,"breadcrumbs":4,"title":1},"101":{"body":5381,"breadcrumbs":4,"title":1},"102":{"body":2384,"breadcrumbs":5,"title":2},"11":{"body":17,"breadcrumbs":5,"title":3},"12":{"body":63,"breadcrumbs":6,"title":4},"13":{"body":9,"breadcrumbs":6,"title":4},"14":{"body":15,"breadcrumbs":4,"title":2},"15":{"body":3,"breadcrumbs":3,"title":1},"16":{"body":3,"breadcrumbs":3,"title":1},"17":{"body":12,"breadcrumbs":3,"title":1},"18":{"body":7,"breadcrumbs":3,"title":1},"19":{"body":9,"breadcrumbs":4,"title":2},"2":{"body":78,"breadcrumbs":3,"title":2},"20":{"body":14,"breadcrumbs":4,"title":2},"21":{"body":39,"breadcrumbs":5,"title":3},"22":{"body":124,"breadcrumbs":6,"title":4},"23":{"body":5,"breadcrumbs":4,"title":2},"24":{"body":14,"breadcrumbs":6,"title":4},"25":{"body":3,"breadcrumbs":3,"title":1},"26":{"body":49,"breadcrumbs":5,"title":3},"27":{"body":21,"breadcrumbs":6,"title":4},"28":{"body":36,"breadcrumbs":6,"title":4},"29":{"body":78,"breadcrumbs":4,"title":2},"3":{"body":17,"breadcrumbs":2,"title":1},"30":{"body":269,"breadcrumbs":4,"title":2},"31":{"body":194,"breadcrumbs":2,"title":1},"32":{"body":46,"breadcrumbs":4,"title":2},"33":{"body":1343,"breadcrumbs":6,"title":2},"34":{"body":309,"breadcrumbs":4,"title":1},"35":{"body":215,"breadcrumbs":5,"title":2},"36":{"body":24,"breadcrumbs":6,"title":2},"37":{"body":333,"breadcrumbs":5,"title":1},"38":{"body":184,"breadcrumbs":5,"title":1},"39":{"body":310,"breadcrumbs":4,"title":1},"4":{"body":26,"breadcrumbs":2,"title":1},"40":{"body":833,"breadcrumbs":7,"title":4},"41":{"body":227,"breadcrumbs":4,"title":1},"42":{"body":457,"breadcrumbs":7,"title":4},"43":{"body":171,"breadcrumbs":6,"title":3},"44":{"body":140,"breadcrumbs":7,"title":4},"45":{"body":593,"breadcrumbs":4,"title":1},"46":{"body":170,"breadcrumbs":8,"title":5},"47":{"body":203,"breadcrumbs":8,"title":5},"48":{"body":533,"breadcrumbs":4,"title":1},"49":{"body":101,"breadcrumbs":4,"title":1},"5":{"body":44,"breadcrumbs":3,"title":1},"50":{"body":682,"breadcrumbs":2,"title":1},"51":{"body":356,"breadcrumbs":6,"title":3},"52":{"body":195,"breadcrumbs":4,"title":2},"53":{"body":1359,"breadcrumbs":3,"title":1},"54":{"body":911,"breadcrumbs":3,"title":1},"55":{"body":294,"breadcrumbs":4,"title":2},"56":{"body":35,"breadcrumbs":3,"title":1},"57":{"body":215,"breadcrumbs":4,"title":2},"58":{"body":598,"breadcrumbs":3,"title":1},"59":{"body":282,"breadcrumbs":4,"title":2},"6":{"body":20,"breadcrumbs":3,"title":1},"60":{"body":581,"breadcrumbs":4,"title":2},"61":{"body":268,"breadcrumbs":4,"title":2},"62":{"body":73,"breadcrumbs":3,"title":1},"63":{"body":24,"breadcrumbs":6,"title":3},"64":{"body":534,"breadcrumbs":4,"title":1},"65":{"body":221,"breadcrumbs":5,"title":2},"66":{"body":1675,"breadcrumbs":5,"title":2},"67":{"body":540,"breadcrumbs":2,"title":1},"68":{"body":205,"breadcrumbs":3,"title":2},"69":{"body":483,"breadcrumbs":2,"title":1},"7":{"body":24,"breadcrumbs":3,"title":1},"70":{"body":659,"breadcrumbs":2,"title":1},"71":{"body":997,"breadcrumbs":2,"title":1},"72":{"body":0,"breadcrumbs":6,"title":3},"73":{"body":2999,"breadcrumbs":5,"title":2},"74":{"body":1055,"breadcrumbs":6,"title":3},"75":{"body":208,"breadcrumbs":2,"title":1},"76":{"body":47,"breadcrumbs":5,"title":4},"77":{"body":153,"breadcrumbs":4,"title":3},"78":{"body":154,"breadcrumbs":4,"title":3},"79":{"body":103,"breadcrumbs":5,"title":4},"8":{"body":5,"breadcrumbs":4,"title":2},"80":{"body":80,"breadcrumbs":3,"title":2},"81":{"body":122,"breadcrumbs":3,"title":2},"82":{"body":107,"breadcrumbs":2,"title":1},"83":{"body":428,"breadcrumbs":7,"title":6},"84":{"body":36,"breadcrumbs":4,"title":3},"85":{"body":52,"breadcrumbs":2,"title":1},"86":{"body":100,"breadcrumbs":2,"title":1},"87":{"body":21,"breadcrumbs":2,"title":1},"88":{"body":3,"breadcrumbs":2,"title":1},"89":{"body":0,"breadcrumbs":2,"title":1},"9":{"body":6,"breadcrumbs":5,"title":3},"90":{"body":9,"breadcrumbs":3,"title":2},"91":{"body":43,"breadcrumbs":4,"title":3},"92":{"body":6,"breadcrumbs":5,"title":4},"93":{"body":16,"breadcrumbs":2,"title":1},"94":{"body":11,"breadcrumbs":3,"title":2},"95":{"body":527,"breadcrumbs":7,"title":3},"96":{"body":2004,"breadcrumbs":6,"title":2},"97":{"body":0,"breadcrumbs":5,"title":2},"98":{"body":1174,"breadcrumbs":4,"title":1},"99":{"body":238,"breadcrumbs":5,"title":2}},"docs":{"0":{"body":"by Julian Hofer, with contributions from the community GTK 4 is the newest version of a popular cross-platform widget toolkit written in C. Thanks to GObject-Introspection, GTK's API can be easily targeted by various programming languages. The API even describes the ownership of its parameters! Managing ownership without giving up speed is one of Rust's greatest strengths, which makes it an excellent choice to develop GTK apps with. With this combination you don't have to worry about hitting bottlenecks mid-project anymore. Additionally, with Rust you will have nice things such as thread safety, memory safety, sensible dependency management as well as excellent third party libraries. The gtk-rs project provides bindings to many GTK-related libraries which we will be using throughout this book.","breadcrumbs":"Introduction » GUI development with Rust and GTK 4","id":"0","title":"GUI development with Rust and GTK 4"},"1":{"body":"This book assumes that you know your way around Rust code. If this is not already the case, reading The Rust Programming Language is an enjoyable way to get you to that stage. If you have experience with another low-level language such as C or C++ you might find that reading A half hour to learn Rust gives you sufficient information as well. Luckily, this — together with the wish to develop graphical applications — is all that is necessary to benefit from this book.","breadcrumbs":"Introduction » Who this book is for","id":"1","title":"Who this book is for"},"10":{"body":"Set the Rust toolchain to MSVC by executing: rustup default stable-msvc","breadcrumbs":"Installation » Windows » Set Rust toolchain to MSVC","id":"10","title":"Set Rust toolchain to MSVC"},"100":{"body":"We still need a way to store our collections. Just like we have already created TaskObject, we will now introduce CollectionObject. It will have the members title and tasks, both of which will be exposed as properties. As usual, the full implementation can be seen by clicking at the eye symbol at the top right of the snippet. Filename: listings/todo/8/collection_object/imp.rs # use std::cell::RefCell;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use glib::Properties;\n# use gtk::{gio, glib};\n# use std::cell::OnceCell;\n# // Object holding the state\n#[derive(Properties, Default)]\n#[properties(wrapper_type = super::CollectionObject)]\npub struct CollectionObject { #[property(get, set)] pub title: RefCell, #[property(get, set)] pub tasks: OnceCell,\n} // The central trait for subclassing a GObject\n#[glib::object_subclass]\nimpl ObjectSubclass for CollectionObject { const NAME: &'static str = \"TodoCollectionObject\"; type Type = super::CollectionObject;\n}\n# # // Trait shared by all GObjects\n# #[glib::derived_properties]\n# impl ObjectImpl for CollectionObject {} We also add the struct CollectionData to aid in serialization and deserialization. Filename: listings/todo/8/collection_object/mod.rs # mod imp;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use glib::Object;\n# use gtk::{gio, glib};\n# use serde::{Deserialize, Serialize};\n# # use crate::task_object::{TaskData, TaskObject};\n# # glib::wrapper! {\n# pub struct CollectionObject(ObjectSubclass);\n# }\n# # impl CollectionObject {\n# pub fn new(title: &str, tasks: gio::ListStore) -> Self {\n# Object::builder()\n# .property(\"title\", title)\n# .property(\"tasks\", tasks)\n# .build()\n# }\n# # pub fn to_collection_data(&self) -> CollectionData {\n# let title = self.imp().title.borrow().clone();\n# let tasks_data = self\n# .tasks()\n# .iter::()\n# .filter_map(Result::ok)\n# .map(|task_object| task_object.task_data())\n# .collect();\n# CollectionData { title, tasks_data }\n# }\n# # pub fn from_collection_data(collection_data: CollectionData) -> Self {\n# let title = collection_data.title;\n# let tasks_to_extend: Vec = collection_data\n# .tasks_data\n# .into_iter()\n# .map(TaskObject::from_task_data)\n# .collect();\n# # let tasks = gio::ListStore::new::();\n# tasks.extend_from_slice(&tasks_to_extend);\n# # Self::new(&title, tasks)\n# }\n# }\n# #[derive(Default, Clone, Serialize, Deserialize)]\npub struct CollectionData { pub title: String, pub tasks_data: Vec,\n} Finally, we add methods to CollectionObject in order to construct it with new, easily access the tasks ListStore with tasks and convert to and from CollectionData with to_collection_data and from_collection_data. Filename: listings/todo/8/collection_object/mod.rs # mod imp;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use glib::Object;\n# use gtk::{gio, glib};\n# use serde::{Deserialize, Serialize};\n# # use crate::task_object::{TaskData, TaskObject};\n# # glib::wrapper! {\n# pub struct CollectionObject(ObjectSubclass);\n# }\n# impl CollectionObject { pub fn new(title: &str, tasks: gio::ListStore) -> Self { Object::builder() .property(\"title\", title) .property(\"tasks\", tasks) .build() } pub fn to_collection_data(&self) -> CollectionData { let title = self.imp().title.borrow().clone(); let tasks_data = self .tasks() .iter::() .filter_map(Result::ok) .map(|task_object| task_object.task_data()) .collect(); CollectionData { title, tasks_data } } pub fn from_collection_data(collection_data: CollectionData) -> Self { let title = collection_data.title; let tasks_to_extend: Vec = collection_data .tasks_data .into_iter() .map(TaskObject::from_task_data) .collect(); let tasks = gio::ListStore::new::(); tasks.extend_from_slice(&tasks_to_extend); Self::new(&title, tasks) }\n}\n# # #[derive(Default, Clone, Serialize, Deserialize)]\n# pub struct CollectionData {\n# pub title: String,\n# pub tasks_data: Vec,\n# }","breadcrumbs":"Libadwaita » Adding Collections » Collections","id":"100","title":"Collections"},"101":{"body":"In order to hook up the new logic, we have to add more state to imp::Window. There are additional widgets that we access via the template_child macro. Additionally, we reference the collections list store, the current_collection as well as the current_filter_model. We also store tasks_changed_handler_id. Its purpose will become clear in later snippets. Filename: listings/todo/8/window/imp.rs # use std::cell::RefCell;\n# use std::fs::File;\n# # use adw::subclass::prelude::*;\n# use adw::{prelude::*, NavigationSplitView};\n# use gio::Settings;\n# use glib::subclass::InitializingObject;\n# use gtk::glib::SignalHandlerId;\n# use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};\n# use std::cell::OnceCell;\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::utils::data_path;\n# // Object holding the state\n#[derive(CompositeTemplate, Default)]\n#[template(resource = \"/org/gtk_rs/Todo8/window.ui\")]\npub struct Window { pub settings: OnceCell, #[template_child] pub entry: TemplateChild, #[template_child] pub tasks_list: TemplateChild, // 👇 all members below are new #[template_child] pub collections_list: TemplateChild, #[template_child] pub split_view: TemplateChild, #[template_child] pub stack: TemplateChild, pub collections: OnceCell, pub current_collection: RefCell>, pub current_filter_model: RefCell>, pub tasks_changed_handler_id: RefCell>,\n}\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for Window {\n# // `NAME` needs to match `class` attribute of template\n# const NAME: &'static str = \"TodoWindow\";\n# type Type = super::Window;\n# type ParentType = adw::ApplicationWindow;\n# # fn class_init(klass: &mut Self::Class) {\n# klass.bind_template();\n# # // Create action to remove done tasks and add to action group \"win\"\n# klass.install_action(\"win.remove-done-tasks\", None, |window, _, _| {\n# window.remove_done_tasks();\n# });\n# # // Create async action to create new collection and add to action group \"win\"\n# klass.install_action_async(\n# \"win.new-collection\",\n# None,\n# |window, _, _| async move {\n# window.new_collection().await;\n# },\n# );\n# }\n# # fn instance_init(obj: &InitializingObject) {\n# obj.init_template();\n# }\n# }\n# # // Trait shared by all GObjects\n# impl ObjectImpl for Window {\n# fn constructed(&self) {\n# // Call \"constructed\" on parent\n# self.parent_constructed();\n# # // Setup\n# let obj = self.obj();\n# obj.setup_settings();\n# obj.setup_collections();\n# obj.restore_data();\n# obj.setup_callbacks();\n# obj.setup_actions();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for Window {}\n# # // Trait shared by all windows\n# impl WindowImpl for Window {\n# fn close_request(&self) -> glib::Propagation {\n# // Store task data in vector\n# let backup_data: Vec = self\n# .obj()\n# .collections()\n# .iter::()\n# .filter_map(|collection_object| collection_object.ok())\n# .map(|collection_object| collection_object.to_collection_data())\n# .collect();\n# # // Save state to file\n# let file = File::create(data_path()).expect(\"Could not create json file.\");\n# serde_json::to_writer(file, &backup_data)\n# .expect(\"Could not write data to json file\");\n# # // Pass close request on to the parent\n# self.parent_close_request()\n# }\n# }\n# # // Trait shared by all application windows\n# impl ApplicationWindowImpl for Window {}\n# # // Trait shared by all adwaita application windows\n# impl AdwApplicationWindowImpl for Window {} Further, we add a couple of helper methods which will come in handy later on. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# fn tasks(&self) -> gio::ListStore { self.current_collection().tasks() } fn current_collection(&self) -> CollectionObject { self.imp() .current_collection .borrow() .clone() .expect(\"`current_collection` should be set in `set_current_collections`.\") } fn collections(&self) -> gio::ListStore { self.imp() .collections .get() .expect(\"`collections` should be set in `setup_collections`.\") .clone() } fn set_filter(&self) { self.imp() .current_filter_model .borrow() .clone() .expect(\"`current_filter_model` should be set in `set_current_collection`.\") .set_filter(self.filter().as_ref()); }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } As always, we want our data to be saved when we close the window. Since most of the implementation is in the method CollectionObject::to_collection_data, the implementation of close_request doesn't change much. Filename: listings/todo/8/window/imp.rs # use std::cell::RefCell;\n# use std::fs::File;\n# # use adw::subclass::prelude::*;\n# use adw::{prelude::*, NavigationSplitView};\n# use gio::Settings;\n# use glib::subclass::InitializingObject;\n# use gtk::glib::SignalHandlerId;\n# use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};\n# use std::cell::OnceCell;\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::utils::data_path;\n# # // Object holding the state\n# #[derive(CompositeTemplate, Default)]\n# #[template(resource = \"/org/gtk_rs/Todo8/window.ui\")]\n# pub struct Window {\n# pub settings: OnceCell,\n# #[template_child]\n# pub entry: TemplateChild,\n# #[template_child]\n# pub tasks_list: TemplateChild,\n# // 👇 all members below are new\n# #[template_child]\n# pub collections_list: TemplateChild,\n# #[template_child]\n# pub split_view: TemplateChild,\n# #[template_child]\n# pub stack: TemplateChild,\n# pub collections: OnceCell,\n# pub current_collection: RefCell>,\n# pub current_filter_model: RefCell>,\n# pub tasks_changed_handler_id: RefCell>,\n# }\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for Window {\n# // `NAME` needs to match `class` attribute of template\n# const NAME: &'static str = \"TodoWindow\";\n# type Type = super::Window;\n# type ParentType = adw::ApplicationWindow;\n# # fn class_init(klass: &mut Self::Class) {\n# klass.bind_template();\n# # // Create action to remove done tasks and add to action group \"win\"\n# klass.install_action(\"win.remove-done-tasks\", None, |window, _, _| {\n# window.remove_done_tasks();\n# });\n# # // Create async action to create new collection and add to action group \"win\"\n# klass.install_action_async(\n# \"win.new-collection\",\n# None,\n# |window, _, _| async move {\n# window.new_collection().await;\n# },\n# );\n# }\n# # fn instance_init(obj: &InitializingObject) {\n# obj.init_template();\n# }\n# }\n# # // Trait shared by all GObjects\n# impl ObjectImpl for Window {\n# fn constructed(&self) {\n# // Call \"constructed\" on parent\n# self.parent_constructed();\n# # // Setup\n# let obj = self.obj();\n# obj.setup_settings();\n# obj.setup_collections();\n# obj.restore_data();\n# obj.setup_callbacks();\n# obj.setup_actions();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for Window {}\n# // Trait shared by all windows\nimpl WindowImpl for Window { fn close_request(&self) -> glib::Propagation { // Store task data in vector let backup_data: Vec = self .obj() .collections() .iter::() .filter_map(|collection_object| collection_object.ok()) .map(|collection_object| collection_object.to_collection_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() }\n}\n# # // Trait shared by all application windows\n# impl ApplicationWindowImpl for Window {}\n# # // Trait shared by all adwaita application windows\n# impl AdwApplicationWindowImpl for Window {} constructed stays mostly the same as well. Instead of setup_tasks we now call setup_collections. Filename: listings/todo/8/window/imp.rs # use std::cell::RefCell;\n# use std::fs::File;\n# # use adw::subclass::prelude::*;\n# use adw::{prelude::*, NavigationSplitView};\n# use gio::Settings;\n# use glib::subclass::InitializingObject;\n# use gtk::glib::SignalHandlerId;\n# use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};\n# use std::cell::OnceCell;\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::utils::data_path;\n# # // Object holding the state\n# #[derive(CompositeTemplate, Default)]\n# #[template(resource = \"/org/gtk_rs/Todo8/window.ui\")]\n# pub struct Window {\n# pub settings: OnceCell,\n# #[template_child]\n# pub entry: TemplateChild,\n# #[template_child]\n# pub tasks_list: TemplateChild,\n# // 👇 all members below are new\n# #[template_child]\n# pub collections_list: TemplateChild,\n# #[template_child]\n# pub split_view: TemplateChild,\n# #[template_child]\n# pub stack: TemplateChild,\n# pub collections: OnceCell,\n# pub current_collection: RefCell>,\n# pub current_filter_model: RefCell>,\n# pub tasks_changed_handler_id: RefCell>,\n# }\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for Window {\n# // `NAME` needs to match `class` attribute of template\n# const NAME: &'static str = \"TodoWindow\";\n# type Type = super::Window;\n# type ParentType = adw::ApplicationWindow;\n# # fn class_init(klass: &mut Self::Class) {\n# klass.bind_template();\n# # // Create action to remove done tasks and add to action group \"win\"\n# klass.install_action(\"win.remove-done-tasks\", None, |window, _, _| {\n# window.remove_done_tasks();\n# });\n# # // Create async action to create new collection and add to action group \"win\"\n# klass.install_action_async(\n# \"win.new-collection\",\n# None,\n# |window, _, _| async move {\n# window.new_collection().await;\n# },\n# );\n# }\n# # fn instance_init(obj: &InitializingObject) {\n# obj.init_template();\n# }\n# }\n# // Trait shared by all GObjects\nimpl ObjectImpl for Window { fn constructed(&self) { // Call \"constructed\" on parent self.parent_constructed(); // Setup let obj = self.obj(); obj.setup_settings(); obj.setup_collections(); obj.restore_data(); obj.setup_callbacks(); obj.setup_actions(); }\n}\n# # // Trait shared by all widgets\n# impl WidgetImpl for Window {}\n# # // Trait shared by all windows\n# impl WindowImpl for Window {\n# fn close_request(&self) -> glib::Propagation {\n# // Store task data in vector\n# let backup_data: Vec = self\n# .obj()\n# .collections()\n# .iter::()\n# .filter_map(|collection_object| collection_object.ok())\n# .map(|collection_object| collection_object.to_collection_data())\n# .collect();\n# # // Save state to file\n# let file = File::create(data_path()).expect(\"Could not create json file.\");\n# serde_json::to_writer(file, &backup_data)\n# .expect(\"Could not write data to json file\");\n# # // Pass close request on to the parent\n# self.parent_close_request()\n# }\n# }\n# # // Trait shared by all application windows\n# impl ApplicationWindowImpl for Window {}\n# # // Trait shared by all adwaita application windows\n# impl AdwApplicationWindowImpl for Window {} setup_collections sets up the collections list store as well as assuring that changes in the model will be reflected in the collections_list. To do that it uses the method create_collection_row. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# fn setup_collections(&self) { let collections = gio::ListStore::new::(); self.imp() .collections .set(collections.clone()) .expect(\"Could not set collections\"); self.imp().collections_list.bind_model( Some(&collections), clone!(@weak self as window => @default-panic, move |obj| { let collection_object = obj .downcast_ref() .expect(\"The object should be of type `CollectionObject`.\"); let row = window.create_collection_row(collection_object); row.upcast() }), ) }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } create_collection_row takes a CollectionObject and builds a gtk::ListBoxRow from its information. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# fn create_collection_row( &self, collection_object: &CollectionObject, ) -> ListBoxRow { let label = Label::builder() .ellipsize(pango::EllipsizeMode::End) .xalign(0.0) .build(); collection_object .bind_property(\"title\", &label, \"label\") .sync_create() .build(); ListBoxRow::builder().child(&label).build() }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } We also adapt restore_data. Again, the heavy lifting comes from CollectionObject::from_collection_data, so we don't have to change too much here. Since the rows of collections_list can be selected, we have to select one of them after restoring the data. We choose the first one and let the method set_current_collection do the rest. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec = serde_json::from_reader(file) .expect( \"It should be possible to read `backup_data` from the json file.\", ); // Convert `Vec` to `Vec` let collections: Vec = backup_data .into_iter() .map(CollectionObject::from_collection_data) .collect(); // Insert restored objects into model self.collections().extend_from_slice(&collections); // Set first collection as current if let Some(first_collection) = collections.first() { self.set_current_collection(first_collection.clone()); } } }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } set_current_collection assures that all elements accessing tasks refer to the task model of the current collection. We bind the tasks_list to the current collection and store the filter model. Whenever there are no tasks in our current collection we want to hide our tasks list. Otherwise, the list box will leave a bad-looking line behind. However, we don't want to accumulate signal handlers whenever we switch collections. This is why we store the tasks_changed_handler_id and disconnect the old handler as soon as we set a new collection. Finally, we select the collection row. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# fn set_current_collection(&self, collection: CollectionObject) { // Wrap model with filter and selection and pass it to the list box let tasks = collection.tasks(); let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.bind_model( Some(&selection_model), clone!(@weak self as window => @default-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() }), ); // Store filter model self.imp().current_filter_model.replace(Some(filter_model)); // If present, disconnect old `tasks_changed` handler if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() { self.tasks().disconnect(handler_id); } // Assure that the task list is only visible when it is supposed to self.set_task_list_visible(&tasks); let tasks_changed_handler_id = tasks.connect_items_changed( clone!(@weak self as window => move |tasks, _, _, _| { window.set_task_list_visible(tasks); }), ); self.imp() .tasks_changed_handler_id .replace(Some(tasks_changed_handler_id)); // Set current tasks self.imp().current_collection.replace(Some(collection)); self.select_collection_row(); }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } Previously, we used the method set_task_list_visible. It assures that tasks_list is only visible if the number of tasks is greater than 0. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# fn set_task_list_visible(&self, tasks: &gio::ListStore) { self.imp().tasks_list.set_visible(tasks.n_items() > 0); }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } select_collection_row assures that the row for the current collection is selected in collections_list. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# fn select_collection_row(&self) { if let Some(index) = self.collections().find(&self.current_collection()) { let row = self.imp().collections_list.row_at_index(index as i32); self.imp().collections_list.select_row(row.as_ref()); } }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# }","breadcrumbs":"Libadwaita » Adding Collections » Window","id":"101","title":"Window"},"102":{"body":"There isn't yet a way to add a collection. Let's implement that functionality. A video which shows the new dialog The screencast above demonstrates the desired behavior. When we activate the button with the + symbol, a dialog appears. While the entry is empty, the \"Create\" button remains insensitive. As soon as we start typing, the button becomes sensitive. When we remove all typed letters and the entry becomes empty again, the \"Create\" button becomes insensitive and the entry gets the \"error\" style. After clicking the \"Create\" button, a new collection is created, and we navigate to its task view. To implement that behavior we will first add a \"new-collection\" action to class_init method. This action will be activated by a click on the + button as well as on the button in the placeholder page. We are using install_action_async . It is a convenient way to add asynchronous actions to subclassed widgets. Filename: listings/todo/8/window/imp.rs # use std::cell::RefCell;\n# use std::fs::File;\n# # use adw::subclass::prelude::*;\n# use adw::{prelude::*, NavigationSplitView};\n# use gio::Settings;\n# use glib::subclass::InitializingObject;\n# use gtk::glib::SignalHandlerId;\n# use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};\n# use std::cell::OnceCell;\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::utils::data_path;\n# # // Object holding the state\n# #[derive(CompositeTemplate, Default)]\n# #[template(resource = \"/org/gtk_rs/Todo8/window.ui\")]\n# pub struct Window {\n# pub settings: OnceCell,\n# #[template_child]\n# pub entry: TemplateChild,\n# #[template_child]\n# pub tasks_list: TemplateChild,\n# // 👇 all members below are new\n# #[template_child]\n# pub collections_list: TemplateChild,\n# #[template_child]\n# pub split_view: TemplateChild,\n# #[template_child]\n# pub stack: TemplateChild,\n# pub collections: OnceCell,\n# pub current_collection: RefCell>,\n# pub current_filter_model: RefCell>,\n# pub tasks_changed_handler_id: RefCell>,\n# }\n# // The central trait for subclassing a GObject\n#[glib::object_subclass]\nimpl ObjectSubclass for Window { // `NAME` needs to match `class` attribute of template const NAME: &'static str = \"TodoWindow\"; type Type = super::Window; type ParentType = adw::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(); }); // Create async action to create new collection and add to action group \"win\" klass.install_action_async( \"win.new-collection\", None, |window, _, _| async move { window.new_collection().await; }, ); } fn instance_init(obj: &InitializingObject) { obj.init_template(); }\n}\n# # // Trait shared by all GObjects\n# impl ObjectImpl for Window {\n# fn constructed(&self) {\n# // Call \"constructed\" on parent\n# self.parent_constructed();\n# # // Setup\n# let obj = self.obj();\n# obj.setup_settings();\n# obj.setup_collections();\n# obj.restore_data();\n# obj.setup_callbacks();\n# obj.setup_actions();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for Window {}\n# # // Trait shared by all windows\n# impl WindowImpl for Window {\n# fn close_request(&self) -> glib::Propagation {\n# // Store task data in vector\n# let backup_data: Vec = self\n# .obj()\n# .collections()\n# .iter::()\n# .filter_map(|collection_object| collection_object.ok())\n# .map(|collection_object| collection_object.to_collection_data())\n# .collect();\n# # // Save state to file\n# let file = File::create(data_path()).expect(\"Could not create json file.\");\n# serde_json::to_writer(file, &backup_data)\n# .expect(\"Could not write data to json file\");\n# # // Pass close request on to the parent\n# self.parent_close_request()\n# }\n# }\n# # // Trait shared by all application windows\n# impl ApplicationWindowImpl for Window {}\n# # // Trait shared by all adwaita application windows\n# impl AdwApplicationWindowImpl for Window {} As soon as the \"new-collection\" action is activated, the async new_collection method is called. Here, we create the adw::MessageDialog , set up the buttons as well as add the entry to it. We add a callback to the entry to ensure that when the content changes, an empty content sets dialog_button as insensitive and adds an \"error\" CSS class to the entry. We then await on the user pressing a button on the dialog. If they click \"Cancel\", we simply return. However, if they click \"Create\", we want a new collection to be created and set as current collection. Afterwards we navigate forward on our leaflet, which means we navigate to the task view. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# async fn new_collection(&self) { // Create entry let entry = Entry::builder() .placeholder_text(\"Name\") .activates_default(true) .build(); let cancel_response = \"cancel\"; let create_response = \"create\"; // Create new dialog let dialog = MessageDialog::builder() .heading(\"New Collection\") .transient_for(self) .modal(true) .destroy_with_parent(true) .close_response(cancel_response) .default_response(create_response) .extra_child(&entry) .build(); dialog .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]); // Make the dialog button insensitive initially dialog.set_response_enabled(create_response, false); dialog.set_response_appearance(create_response, ResponseAppearance::Suggested); // Set entry's css class to \"error\", when there is no text in it entry.connect_changed(clone!(@weak dialog => move |entry| { let text = entry.text(); let empty = text.is_empty(); dialog.set_response_enabled(create_response, !empty); if empty { entry.add_css_class(\"error\"); } else { entry.remove_css_class(\"error\"); } })); let response = dialog.choose_future().await; // Return if the user chose `cancel_response` if response == cancel_response { return; } // Create a new list store let tasks = gio::ListStore::new::(); // Create a new collection object from the title the user provided let title = entry.text().to_string(); let collection = CollectionObject::new(&title, tasks); // Add new collection object and set current tasks self.collections().append(&collection); self.set_current_collection(collection); // Show the content self.imp().split_view.set_show_content(true); }\n# } We also add more callbacks to setup_callbacks. Importantly, we want to filter our current task model whenever the value of the \"filter\" setting changes. Whenever the items of our collections change we also want to set the stack. This makes sure that our placeholder page is shown if there are no collections. Finally, we assure that when we click on a row of collections_list, current_collection is set to the selected collection and the split view shows the task view. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# // Filter model whenever the value of the key \"filter\" changes self.settings().connect_changed( Some(\"filter\"), clone!(@weak self as window => move |_, _| { window.set_filter(); }), ); // Setup callback when items of collections change self.set_stack(); self.collections().connect_items_changed( clone!(@weak self as window => move |_, _, _, _| { window.set_stack(); }), ); // Setup callback for activating a row of collections list self.imp().collections_list.connect_row_activated( clone!(@weak self as window => move |_, row| { let index = row.index(); let selected_collection = window.collections() .item(index as u32) .expect(\"There needs to be an object at this position.\") .downcast::() .expect(\"The object needs to be a `CollectionObject`.\"); window.set_current_collection(selected_collection); window.imp().split_view.set_show_content(true); }), );\n# }\n# # fn set_stack(&self) {\n# if self.collections().n_items() > 0 {\n# self.imp().stack.set_visible_child_name(\"main\");\n# } else {\n# self.imp().stack.set_visible_child_name(\"placeholder\");\n# }\n# }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } Before, we called the method set_stack. This method ensure when there is at least one collection, the \"main\" page is shown, and the \"placeholder\" page otherwise. Filename: listings/todo/8/window/mod.rs # mod imp;\n# # use std::fs::File;\n# # use adw::prelude::*;\n# use adw::subclass::prelude::*;\n# use adw::{ActionRow, MessageDialog, ResponseAppearance};\n# use gio::Settings;\n# use glib::{clone, Object};\n# use gtk::{\n# gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,\n# ListBoxRow, NoSelection,\n# };\n# # use crate::collection_object::{CollectionData, CollectionObject};\n# use crate::task_object::TaskObject;\n# use crate::utils::data_path;\n# use crate::APP_ID;\n# # glib::wrapper! {\n# pub struct Window(ObjectSubclass)\n# @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,\n# @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,\n# gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n# }\n# # impl Window {\n# pub fn new(app: &adw::Application) -> Self {\n# // Create new window\n# Object::builder().property(\"application\", app).build()\n# }\n# # fn setup_settings(&self) {\n# let settings = Settings::new(APP_ID);\n# self.imp()\n# .settings\n# .set(settings)\n# .expect(\"`settings` should not be set before calling `setup_settings`.\");\n# }\n# # fn settings(&self) -> &Settings {\n# self.imp()\n# .settings\n# .get()\n# .expect(\"`settings` should be set in `setup_settings`.\")\n# }\n# # fn tasks(&self) -> gio::ListStore {\n# self.current_collection().tasks()\n# }\n# # fn current_collection(&self) -> CollectionObject {\n# self.imp()\n# .current_collection\n# .borrow()\n# .clone()\n# .expect(\"`current_collection` should be set in `set_current_collections`.\")\n# }\n# # fn collections(&self) -> gio::ListStore {\n# self.imp()\n# .collections\n# .get()\n# .expect(\"`collections` should be set in `setup_collections`.\")\n# .clone()\n# }\n# # fn set_filter(&self) {\n# self.imp()\n# .current_filter_model\n# .borrow()\n# .clone()\n# .expect(\"`current_filter_model` should be set in `set_current_collection`.\")\n# .set_filter(self.filter().as_ref());\n# }\n# # fn filter(&self) -> Option {\n# // Get filter state from settings\n# let filter_state: String = self.settings().get(\"filter\");\n# # // Create custom filters\n# let filter_open = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow completed tasks\n# !task_object.is_completed()\n# });\n# let filter_done = CustomFilter::new(|obj| {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = obj\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # // Only allow done tasks\n# task_object.is_completed()\n# });\n# # // Return the correct filter\n# match filter_state.as_str() {\n# \"All\" => None,\n# \"Open\" => Some(filter_open),\n# \"Done\" => Some(filter_done),\n# _ => unreachable!(),\n# }\n# }\n# # fn setup_collections(&self) {\n# let collections = gio::ListStore::new::();\n# self.imp()\n# .collections\n# .set(collections.clone())\n# .expect(\"Could not set collections\");\n# # self.imp().collections_list.bind_model(\n# Some(&collections),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let collection_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `CollectionObject`.\");\n# let row = window.create_collection_row(collection_object);\n# row.upcast()\n# }),\n# )\n# }\n# # fn restore_data(&self) {\n# if let Ok(file) = File::open(data_path()) {\n# // Deserialize data from file to vector\n# let backup_data: Vec = serde_json::from_reader(file)\n# .expect(\n# \"It should be possible to read `backup_data` from the json file.\",\n# );\n# # // Convert `Vec` to `Vec`\n# let collections: Vec = backup_data\n# .into_iter()\n# .map(CollectionObject::from_collection_data)\n# .collect();\n# # // Insert restored objects into model\n# self.collections().extend_from_slice(&collections);\n# # // Set first collection as current\n# if let Some(first_collection) = collections.first() {\n# self.set_current_collection(first_collection.clone());\n# }\n# }\n# }\n# # fn create_collection_row(\n# &self,\n# collection_object: &CollectionObject,\n# ) -> ListBoxRow {\n# let label = Label::builder()\n# .ellipsize(pango::EllipsizeMode::End)\n# .xalign(0.0)\n# .build();\n# # collection_object\n# .bind_property(\"title\", &label, \"label\")\n# .sync_create()\n# .build();\n# # ListBoxRow::builder().child(&label).build()\n# }\n# # fn set_current_collection(&self, collection: CollectionObject) {\n# // Wrap model with filter and selection and pass it to the list box\n# let tasks = collection.tasks();\n# let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());\n# let selection_model = NoSelection::new(Some(filter_model.clone()));\n# self.imp().tasks_list.bind_model(\n# Some(&selection_model),\n# clone!(@weak self as window => @default-panic, move |obj| {\n# let task_object = obj\n# .downcast_ref()\n# .expect(\"The object should be of type `TaskObject`.\");\n# let row = window.create_task_row(task_object);\n# row.upcast()\n# }),\n# );\n# # // Store filter model\n# self.imp().current_filter_model.replace(Some(filter_model));\n# # // If present, disconnect old `tasks_changed` handler\n# if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {\n# self.tasks().disconnect(handler_id);\n# }\n# # // Assure that the task list is only visible when it is supposed to\n# self.set_task_list_visible(&tasks);\n# let tasks_changed_handler_id = tasks.connect_items_changed(\n# clone!(@weak self as window => move |tasks, _, _, _| {\n# window.set_task_list_visible(tasks);\n# }),\n# );\n# self.imp()\n# .tasks_changed_handler_id\n# .replace(Some(tasks_changed_handler_id));\n# # // Set current tasks\n# self.imp().current_collection.replace(Some(collection));\n# # self.select_collection_row();\n# }\n# # fn set_task_list_visible(&self, tasks: &gio::ListStore) {\n# self.imp().tasks_list.set_visible(tasks.n_items() > 0);\n# }\n# # fn select_collection_row(&self) {\n# if let Some(index) = self.collections().find(&self.current_collection()) {\n# let row = self.imp().collections_list.row_at_index(index as i32);\n# self.imp().collections_list.select_row(row.as_ref());\n# }\n# }\n# # fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {\n# // Create check button\n# let check_button = CheckButton::builder()\n# .valign(Align::Center)\n# .can_focus(false)\n# .build();\n# # // Create row\n# let row = ActionRow::builder()\n# .activatable_widget(&check_button)\n# .build();\n# row.add_prefix(&check_button);\n# # // Bind properties\n# task_object\n# .bind_property(\"completed\", &check_button, \"active\")\n# .bidirectional()\n# .sync_create()\n# .build();\n# task_object\n# .bind_property(\"content\", &row, \"title\")\n# .sync_create()\n# .build();\n# # // Return row\n# row\n# }\n# # fn setup_callbacks(&self) {\n# // Setup callback for activation of the entry\n# self.imp()\n# .entry\n# .connect_activate(clone!(@weak self as window => move |_| {\n# window.new_task();\n# }));\n# # // Setup callback for clicking (and the releasing) the icon of the entry\n# self.imp().entry.connect_icon_release(\n# clone!(@weak self as window => move |_,_| {\n# window.new_task();\n# }),\n# );\n# # // Filter model whenever the value of the key \"filter\" changes\n# self.settings().connect_changed(\n# Some(\"filter\"),\n# clone!(@weak self as window => move |_, _| {\n# window.set_filter();\n# }),\n# );\n# # // Setup callback when items of collections change\n# self.set_stack();\n# self.collections().connect_items_changed(\n# clone!(@weak self as window => move |_, _, _, _| {\n# window.set_stack();\n# }),\n# );\n# # // Setup callback for activating a row of collections list\n# self.imp().collections_list.connect_row_activated(\n# clone!(@weak self as window => move |_, row| {\n# let index = row.index();\n# let selected_collection = window.collections()\n# .item(index as u32)\n# .expect(\"There needs to be an object at this position.\")\n# .downcast::()\n# .expect(\"The object needs to be a `CollectionObject`.\");\n# window.set_current_collection(selected_collection);\n# window.imp().split_view.set_show_content(true);\n# }),\n# );\n# }\n# fn set_stack(&self) { if self.collections().n_items() > 0 { self.imp().stack.set_visible_child_name(\"main\"); } else { self.imp().stack.set_visible_child_name(\"placeholder\"); } }\n# # fn new_task(&self) {\n# // Get content from entry and clear it\n# let buffer = self.imp().entry.buffer();\n# let content = buffer.text().to_string();\n# if content.is_empty() {\n# return;\n# }\n# buffer.set_text(\"\");\n# # // Add new task to model\n# let task = TaskObject::new(false, content);\n# self.tasks().append(&task);\n# }\n# # fn setup_actions(&self) {\n# // Create action from key \"filter\" and add to action group \"win\"\n# let action_filter = self.settings().create_action(\"filter\");\n# self.add_action(&action_filter);\n# }\n# # fn remove_done_tasks(&self) {\n# let tasks = self.tasks();\n# let mut position = 0;\n# while let Some(item) = tasks.item(position) {\n# // Get `TaskObject` from `glib::Object`\n# let task_object = item\n# .downcast_ref::()\n# .expect(\"The object needs to be of type `TaskObject`.\");\n# # if task_object.is_completed() {\n# tasks.remove(position);\n# } else {\n# position += 1;\n# }\n# }\n# }\n# # async fn new_collection(&self) {\n# // Create entry\n# let entry = Entry::builder()\n# .placeholder_text(\"Name\")\n# .activates_default(true)\n# .build();\n# # let cancel_response = \"cancel\";\n# let create_response = \"create\";\n# # // Create new dialog\n# let dialog = MessageDialog::builder()\n# .heading(\"New Collection\")\n# .transient_for(self)\n# .modal(true)\n# .destroy_with_parent(true)\n# .close_response(cancel_response)\n# .default_response(create_response)\n# .extra_child(&entry)\n# .build();\n# dialog\n# .add_responses(&[(cancel_response, \"Cancel\"), (create_response, \"Create\")]);\n# // Make the dialog button insensitive initially\n# dialog.set_response_enabled(create_response, false);\n# dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);\n# # // Set entry's css class to \"error\", when there is no text in it\n# entry.connect_changed(clone!(@weak dialog => move |entry| {\n# let text = entry.text();\n# let empty = text.is_empty();\n# # dialog.set_response_enabled(create_response, !empty);\n# # if empty {\n# entry.add_css_class(\"error\");\n# } else {\n# entry.remove_css_class(\"error\");\n# }\n# }));\n# # let response = dialog.choose_future().await;\n# # // Return if the user chose `cancel_response`\n# if response == cancel_response {\n# return;\n# }\n# # // Create a new list store\n# let tasks = gio::ListStore::new::();\n# # // Create a new collection object from the title the user provided\n# let title = entry.text().to_string();\n# let collection = CollectionObject::new(&title, tasks);\n# # // Add new collection object and set current tasks\n# self.collections().append(&collection);\n# self.set_current_collection(collection);\n# # // Show the content\n# self.imp().split_view.set_show_content(true);\n# }\n# } And that was it! Now we can enjoy the final result. A video which shows the final To-Do app You might have noticed that there is not yet a way to remove a collection. Try to implement this missing piece of functionality in your local version of the To-Do app. Which edge cases do you have to consider?","breadcrumbs":"Libadwaita » Adding Collections » Message Dialog","id":"102","title":"Message Dialog"},"11":{"body":"Follow the gvsbuild docs to build GTK 4 . When choosing the GTK version to build, select gtk4 instead of gtk3: gvsbuild build gtk4","breadcrumbs":"Installation » Windows » Build GTK 4","id":"11","title":"Build GTK 4"},"12":{"body":"Add New User Variable and update Path in environment variable to include PKG_CONFIG_PATH and the GTK 4 libraries: Go to settings -> Search and open Advanced system settings -> Click on Environment variables Select New -> Input Variable name : PKG_CONFIG_PATH & Variable value : C:\\gtk-build\\gtk\\x64\\release\\lib\\pkgconfig Select Path -> Click on Edit -> Add C:\\gtk-build\\gtk\\x64\\release\\bin You can now continue with the project setup . Build GTK 4 manually with MSVC If it's not possible to build with gvsbuild (or you want to customize your build), you can build GTK 4 and the minimum dependencies you need manually.","breadcrumbs":"Installation » Windows » Update Path environment variable","id":"12","title":"Update Path environment variable"},"13":{"body":"Set the Rust toolchain to MSVC by executing: rustup default stable-msvc","breadcrumbs":"Installation » Windows » Set Rust toolchain to MSVC","id":"13","title":"Set Rust toolchain to MSVC"},"14":{"body":"Install Visual Studio Community from visualstudio.microsoft.com . Make sure to check the box \"Desktop development with C++\" during the installation process.","breadcrumbs":"Installation » Windows » Visual Studio","id":"14","title":"Visual Studio"},"15":{"body":"Download git from gitforwindows.org .","breadcrumbs":"Installation » Windows » Git","id":"15","title":"Git"},"16":{"body":"Download CMake from https://cmake.org/download/","breadcrumbs":"Installation » Windows » CMake","id":"16","title":"CMake"},"17":{"body":"Download python from python.org . Make sure to opt-in to adding Python to your Path during the installation process.","breadcrumbs":"Installation » Windows » Python","id":"17","title":"Python"},"18":{"body":"Install meson by executing: pip install meson ninja","breadcrumbs":"Installation » Windows » Meson","id":"18","title":"Meson"},"19":{"body":"Download Gettext 0.21 from mlocati.github.io . Make sure to select the static version.","breadcrumbs":"Installation » Windows » Gettext 0.21","id":"19","title":"Gettext 0.21"},"2":{"body":"In general, this book assumes that you are reading it in sequence from front to back. However, if you are using it as a reference for a certain topic, you might find it useful to just jump into it. There are two kinds of chapters in this book: concept chapters and project chapters. In concept chapters, you will learn about an aspect of GTK development. In project chapters, we will build small programs together, applying what you've learned so far. The book strives to explain essential GTK concepts paired with practical examples. However, if a concept can be better conveyed with a less practical example, we took this path most of the time. If you are interested in contained and useful examples, we refer you to the corresponding section of gtk4-rs' repository . Every valid code snippet in the book is part of a listing. Like the examples, the listings be found in the repository of gtk4-rs.","breadcrumbs":"Introduction » How to use this book","id":"2","title":"How to use this book"},"20":{"body":"Download pkg-config-lite from sourceforge.net . Then extract and unpack it in C:/, so that the executable is in C:\\pkg-config-lite-0.28-1\\bin.","breadcrumbs":"Installation » Windows » Pkg-config","id":"20","title":"Pkg-config"},"21":{"body":"Go to settings -> Search and open Advanced system settings -> Click on Environment variables Select Path -> Click on Edit -> Add the following entries: C:\\pkg-config-lite-0.28-1\\bin\nC:\\gnome\\bin Go back to Environment variables Under User variables click on New and add: Variable name: PKG_CONFIG_PATH Variable value: C:\\gnome\\lib\\pkgconfig","breadcrumbs":"Installation » Windows » Update environment variables","id":"21","title":"Update environment variables"},"22":{"body":"From the Windows start menu, search for x64 Native Tools Command Prompt for VS 2019. That will open a terminal configured to use MSVC x64 tools. From there, run the following commands: cd /\ngit clone https://gitlab.gnome.org/GNOME/gtk.git --depth 1\ngit clone https://gitlab.gnome.org/GNOME/libxml2.git --depth 1\ngit clone https://gitlab.gnome.org/GNOME/librsvg.git --depth 1 :: Make sure that cmd finds pkg-config-lite when searching for pkg-config\nwhere pkg-config :: Make sure that setuptools is available. pip install setuptools cd gtk\nmeson setup builddir --prefix=C:/gnome -Dbuild-tests=false -Dmedia-gstreamer=disabled\nmeson install -C builddir\ncd / cd libxml2\ncmake -S . -B build -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=C:\\gnome -D LIBXML2_WITH_ICONV=OFF -D LIBXML2_WITH_LZMA=OFF -D LIBXML2_WITH_PYTHON=OFF -D LIBXML2_WITH_ZLIB=OFF\ncmake --build build --config Release\ncmake --install build\ncd / cd librsvg/win32\nnmake /f generate-msvc.mak generate-nmake-files\nnmake /f Makefile.vc CFG=release install PREFIX=C:\\gnome\ncd / You can now continue with the project setup . Install GTK 4 with MSYS2 and the GNU toolchain","breadcrumbs":"Installation » Windows » Compile and install GTK 4","id":"22","title":"Compile and install GTK 4"},"23":{"body":"Install the rust toolchain via rustup .","breadcrumbs":"Installation » Windows » Install Rustup","id":"23","title":"Install Rustup"},"24":{"body":"If you used the MSVC toolchain before, make sure to revert all changes you made to environment variables during the installation process.","breadcrumbs":"Installation » Windows » Remove residues from the MSVC toolchain","id":"24","title":"Remove residues from the MSVC toolchain"},"25":{"body":"Install MSYS2 from www.msys2.org","breadcrumbs":"Installation » Windows » MSYS2","id":"25","title":"MSYS2"},"26":{"body":"From the Windows start menu, search for MSYS2 MinGW 64-bit. That will open a terminal configured to use MinGW x64 tools. There, execute the following commands to install GTK 4, pkgconf and gcc. pacman -S mingw-w64-x86_64-gtk4 mingw-w64-x86_64-gettext mingw-w64-x86_64-libxml2 mingw-w64-x86_64-librsvg mingw-w64-x86_64-pkgconf mingw-w64-x86_64-gcc","breadcrumbs":"Installation » Windows » Install GTK 4","id":"26","title":"Install GTK 4"},"27":{"body":"Go to settings -> Search and open Advanced system settings -> Click on Environment variables Select Path -> Click on Edit -> Add the following three entries: C:\\msys64\\mingw64\\include\nC:\\msys64\\mingw64\\bin\nC:\\msys64\\mingw64\\lib","breadcrumbs":"Installation » Windows » Update Path environment variable","id":"27","title":"Update Path environment variable"},"28":{"body":"The default toolchain on windows is stable-msvc. To switch to stable-gnu, run the following commands from your terminal: rustup toolchain install stable-gnu rustup default stable-gnu Please note that this command might change in the future. If it does not work anymore, please open an issue on our repo. You can now continue with the project setup .","breadcrumbs":"Installation » Windows » Setup the GNU toolchain for Rust","id":"28","title":"Setup the GNU toolchain for Rust"},"29":{"body":"Let's begin by installing all necessary tools. First, follow the instructions on the GTK website in order to install GTK 4. Then install Rust with rustup . Now, create a new project and move into the newly created folder by executing: cargo new my-gtk-app\ncd my-gtk-app Find out the GTK 4 version on your machine by running pkg-config --modversion gtk4 Use this information to add the gtk4 crate to your dependencies in Cargo.toml. At the time of this writing the newest version is 4.8. cargo add gtk4 --rename gtk --features v4_8 By specifying this feature you opt-in to API that was added with minor releases of GTK 4. Now, you can run your application by executing: cargo run","breadcrumbs":"Project Setup » Project Setup","id":"29","title":"Project Setup"},"3":{"body":"The book itself is licensed under the Creative Commons Attribution 4.0 International license . The only exception are the code snippets which are licensed under the MIT license .","breadcrumbs":"Introduction » License","id":"3","title":"License"},"30":{"body":"Now that we've got a working installation, let's get right into it! At the very least, we need to create a gtk::Application instance with an application id . For that we use the builder pattern which many gtk-rs objects support. Note that we also import the prelude to bring the necessary traits into scope. Filename: listings/hello_world/1/main.rs use gtk::prelude::*;\nuse gtk::{glib, Application}; const APP_ID: &str = \"org.gtk_rs.HelloWorld1\"; fn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Run the application app.run()\n} It builds fine, but nothing but a warning in our terminal appears. GLib-GIO-WARNING: Your application does not implement g_application_activate()\nand has no handlers connected to the 'activate' signal. It should do one of these. GTK tells us that something should be called in its activate step. So let's create a gtk::ApplicationWindow there. Filename: listings/hello_world/2/main.rs use gtk::prelude::*;\nuse gtk::{glib, Application, ApplicationWindow}; const APP_ID: &str = \"org.gtk_rs.HelloWorld2\"; fn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Connect to \"activate\" signal of `app` app.connect_activate(build_ui); // Run the application app.run()\n} fn build_ui(app: &Application) { // Create a window and set the title let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .build(); // Present window window.present();\n} That is better! Normally we expect to be able to interact with the user interface. Also, the name of the chapter suggests that the phrase \"Hello World!\" will be involved. Filename: listings/hello_world/3/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# const APP_ID: &str = \"org.gtk_rs.HelloWorld3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# fn build_ui(app: &Application) { // Create a button with label and margins let button = Button::builder() .label(\"Press me!\") .margin_top(12) .margin_bottom(12) .margin_start(12) .margin_end(12) .build(); // Connect to \"clicked\" signal of `button` button.connect_clicked(|button| { // Set the label to \"Hello World!\" after the button has been clicked on button.set_label(\"Hello World!\"); }); // Create a window let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .child(&button) .build(); // Present window window.present();\n} If you look closely at the code snippet you will notice that it has a small eye symbol on its top right. After you press on it you can see the full code of the listing. We will use this throughout the book to hide details which are not important to bring the message across. Pay attention to this if you want to write apps by following the book step-by-step. Here, we've hidden that we brought gtk::Button into scope. There is now a button and if we click on it, its label becomes \"Hello World!\". A video which shows that pressing on the button changes it's label Wasn't that hard to create our first gtk-rs app, right? Let's now get a better understanding of what we did here.","breadcrumbs":"Hello World! » Hello World!","id":"30","title":"Hello World!"},"31":{"body":"Widgets are the components that make up a GTK application. GTK offers many widgets and if those don't fit, you can even create custom ones. There are, for example, display widgets, buttons, containers and windows. One kind of widget might be able to contain other widgets, it might present information and it might react to interaction. The Widget Gallery is useful to find out which widget fits your needs. Let's say we want to add a button to our app. We have quite a bit of choice here, but let's take the simplest one — a Button. GTK is an object-oriented framework, so all widgets are part of an inheritance tree with GObject at the top. The inheritance tree of a Button looks like this: GObject\n╰── Widget ╰── Button The GTK documentation also tells us that Button implements the interfaces GtkAccessible, GtkActionable, GtkBuildable, GtkConstraintTarget. Now let's compare that with the corresponding Button struct in gtk-rs. The gtk-rs documentation tells us which traits it implements. We find that these traits either have a corresponding base class or interface in the GTK docs. In the \"Hello World\" app we wanted to react to a button click. This behavior is specific to a button, so we expect to find a suitable method in the ButtonExt trait. And indeed, ButtonExt includes the method connect_clicked . Filename: listings/hello_world/3/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# const APP_ID: &str = \"org.gtk_rs.HelloWorld3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Create a button with label and margins let button = Button::builder() .label(\"Press me!\") .margin_top(12) .margin_bottom(12) .margin_start(12) .margin_end(12) .build(); // Connect to \"clicked\" signal of `button` button.connect_clicked(|button| { // Set the label to \"Hello World!\" after the button has been clicked on button.set_label(\"Hello World!\"); });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# }","breadcrumbs":"Widgets » Widgets","id":"31","title":"Widgets"},"32":{"body":"GTK is an object-oriented framework. It is written in C, which does not support object-orientation out of the box. That is why GTK relies on the GObject library to provide the object system. We have already learned that gtk-rs maps GObject concepts, like inheritance and interfaces, to Rust traits. In this chapter we will learn: How memory of GObjects is managed How to create our own GObjects via subclassing How to deal with generic values How to use properties How to emit and receive signals","breadcrumbs":"GObject Concepts » GObject Concepts","id":"32","title":"GObject Concepts"},"33":{"body":"Memory management when writing a gtk-rs app can be a bit tricky. Let's have a look why that is the case and how to deal with that. With our first example, we have window with a single button. Every button click should increment an integer number by one. #use gtk::prelude::*;\n#use gtk::{self, glib, Application, ApplicationWindow, Button};\n#\n#const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement0\";\n#\n// DOES NOT COMPILE!\nfn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Connect to \"activate\" signal of `app` app.connect_activate(build_ui); // Run the application app.run()\n} fn build_ui(application: &Application) { // Create two buttons let button_increase = Button::builder() .label(\"Increase\") .margin_top(12) .margin_bottom(12) .margin_start(12) .margin_end(12) .build(); // A mutable integer let mut number = 0; // Connect callbacks // When a button is clicked, `number` should be changed button_increase.connect_clicked(|_| number += 1); // Create a window let window = ApplicationWindow::builder() .application(application) .title(\"My GTK App\") .child(&button_increase) .build(); // Present the window window.present();\n} The Rust compiler refuses to compile this application while spitting out multiple error messages. Let's have a look at them one by one. error[E0373]: closure may outlive the current function, but it borrows `number`, which is owned by the current function |\n32 | button_increase.connect_clicked(|_| number += 1); | ^^^ ------ `number` is borrowed here | | | may outlive borrowed value `number` |\nnote: function requires argument type to outlive `'static` |\n32 | button_increase.connect_clicked(|_| number += 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nhelp: to force the closure to take ownership of `number` (and any other referenced variables), use the `move` keyword |\n32 | button_increase.connect_clicked(move |_| number += 1); | Our closure only borrows number. Signal handlers in GTK require static' lifetimes for their references, so we cannot borrow a variable that only lives for the scope of the function build_ui. The compiler also suggests how to fix this. By adding the move keyword in front of the closure, number will be moved into the closure. #use gtk::prelude::*;\n#use gtk::{self, glib, Application, ApplicationWindow, Button};\n#\n#const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement0\";\n#\n#fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n#\n# // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n#\n# // Run the application\n# app.run()\n#}\n#\n#fn build_ui(application: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // DOES NOT COMPILE! // A mutable integer let mut number = 0; // Connect callbacks // When a button is clicked, `number` should be changed button_increase.connect_clicked(move |_| number += 1);\n#\n# // Create a window\n# let window = ApplicationWindow::builder()\n# .application(application)\n# .title(\"My GTK App\")\n# .child(&button_increase)\n# .build();\n#\n# // Present the window\n# window.present();\n#} This still leaves the following error message: error[E0594]: cannot assign to `number`, as it is a captured variable in a `Fn` closure |\n32 | button_increase.connect_clicked(move |_| number += 1); | ^^^^^^^^^^^ cannot assign In order to understand that error message we have to understand the difference between the three closure traits FnOnce, FnMut and Fn. APIs that take closures implementing the FnOnce trait give the most freedom to the API consumer. The closure is called only once, so it can even consume its state. Signal handlers can be called multiple times, so they cannot accept FnOnce. The more restrictive FnMut trait doesn't allow closures to consume their state, but they can still mutate it. Signal handlers can't allow this either, because they can be called from inside themselves. This would lead to multiple mutable references which the borrow checker doesn't appreciate at all. This leaves Fn. State can be immutably borrowed, but then how can we modify number? We need a data type with interior mutability like std::cell::Cell . The Cell class is only suitable for objects that implement the Copy trait. For other objects, RefCell is the way to go. You can learn more about interior mutability in this section of the book Rust Atomics and Locks . Filename: listings/g_object_memory_management/1/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# use std::cell::Cell;\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# fn build_ui(application: &Application) { // Create two buttons let button_increase = Button::builder() .label(\"Increase\") .margin_top(12) .margin_bottom(12) .margin_start(12) .margin_end(12) .build(); // A mutable integer let number = Cell::new(0); // Connect callbacks // When a button is clicked, `number` should be changed button_increase.connect_clicked(move |_| number.set(number.get() + 1)); // Create a window let window = ApplicationWindow::builder() .application(application) .title(\"My GTK App\") .child(&button_increase) .build(); // Present the window window.present();\n} This now compiles as expected. Let's try a slightly more complicated example: two buttons which both modify the same number. For that, we need a way that both closures take ownership of the same value? That is exactly what the std::rc::Rc type is there for. Rc counts the number of strong references created via Clone::clone and released via Drop::drop , and only deallocates the value when this number drops to zero. If we want to modify the content of our Rc , we can again use the Cell type. Filename: listings/g_object_memory_management/2/main.rs # use std::cell::Cell;\n# use std::rc::Rc;\n# # use gtk::prelude::*;\n# use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement2\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# fn build_ui(app: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let button_decrease = Button::builder()\n# .label(\"Decrease\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Reference-counted object with inner-mutability let number = Rc::new(Cell::new(0)); // Connect callbacks, when a button is clicked `number` will be changed let number_copy = number.clone(); button_increase.connect_clicked(move |_| number_copy.set(number_copy.get() + 1)); button_decrease.connect_clicked(move |_| number.set(number.get() - 1));\n# # // Add buttons to `gtk_box`\n# let gtk_box = gtk::Box::builder()\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_increase);\n# gtk_box.append(&button_decrease);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } It is not very nice though to fill the scope with temporary variables like number_copy. We can improve that by using the glib::clone! macro. Filename: listings/g_object_memory_management/3/main.rs # use std::cell::Cell;\n# use std::rc::Rc;\n# # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# fn build_ui(app: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let button_decrease = Button::builder()\n# .label(\"Decrease\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# # // Reference-counted object with inner mutability\n# let number = Rc::new(Cell::new(0));\n# // Connect callbacks\n# // When a button is clicked, `number` will be changed button_increase.connect_clicked(clone!(@strong number => move |_| { number.set(number.get() + 1); })); button_decrease.connect_clicked(move |_| { number.set(number.get() - 1); });\n# # // Add buttons to `gtk_box`\n# let gtk_box = gtk::Box::builder()\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_increase);\n# gtk_box.append(&button_decrease);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } Just like Rc>, GObjects are reference-counted and mutable. Therefore, we can pass the buttons the same way to the closure as we did with number. Filename: listings/g_object_memory_management/4/main.rs # use std::cell::Cell;\n# use std::rc::Rc;\n# # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement4\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let button_decrease = Button::builder()\n# .label(\"Decrease\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# # let number = Rc::new(Cell::new(0));\n# // Connect callbacks // When a button is clicked, `number` and label of the other button will be changed button_increase.connect_clicked(clone!(@weak number, @strong button_decrease => move |_| { number.set(number.get() + 1); button_decrease.set_label(&number.get().to_string()); })); button_decrease.connect_clicked(clone!(@strong button_increase => move |_| { number.set(number.get() - 1); button_increase.set_label(&number.get().to_string()); }));\n# # // Add buttons to `gtk_box`\n# let gtk_box = gtk::Box::builder()\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_increase);\n# gtk_box.append(&button_decrease);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } If we now click on one button, the other button's label gets changed. But whoops! Did we forget about one annoyance of reference-counted systems? Yes we did: reference cycles . button_increase holds a strong reference to button_decrease and vice-versa. A strong reference keeps the referenced value from being deallocated. If this chain leads to a circle, none of the values in this cycle ever get deallocated. With weak references we can break this cycle, because they don't keep their value alive but instead provide a way to retrieve a strong reference if the value is still alive. Since we want our apps to free unneeded memory, we should use weak references for the buttons instead. Filename: listings/g_object_memory_management/5/main.rs # use std::cell::Cell;\n# use std::rc::Rc;\n# # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement5\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let button_decrease = Button::builder()\n# .label(\"Decrease\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# # // Reference-counted object with inner mutability\n# let number = Rc::new(Cell::new(0));\n# // Connect callbacks // When a button is clicked, `number` and label of the other button will be changed button_increase.connect_clicked(clone!(@weak number, @weak button_decrease => move |_| { number.set(number.get() + 1); button_decrease.set_label(&number.get().to_string()); })); button_decrease.connect_clicked(clone!(@weak button_increase => move |_| { number.set(number.get() - 1); button_increase.set_label(&number.get().to_string()); }));\n# # // Add buttons to `gtk_box`\n# let gtk_box = gtk::Box::builder()\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_increase);\n# gtk_box.append(&button_decrease);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } The reference cycle is broken. Every time the button is clicked, glib::clone tries to upgrade the weak reference. If we now for example click on one button and the other button is not there anymore, the callback will be skipped. Per default, it immediately returns from the closure with () as return value. In case the closure expects a different return value @default-return can be specified. Notice that we move number in the second closure. If we had moved weak references in both closures, nothing would have kept number alive and the closure would have never been called. Thinking about this, button_increase and button_decrease are also dropped at the end of the scope of build_ui. Who then keeps the buttons alive? Filename: listings/g_object_memory_management/5/main.rs # use std::cell::Cell;\n# use std::rc::Rc;\n# # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement5\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let button_decrease = Button::builder()\n# .label(\"Decrease\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# # // Reference-counted object with inner mutability\n# let number = Rc::new(Cell::new(0));\n# # // Connect callbacks\n# // When a button is clicked, `number` and label of the other button will be changed\n# button_increase.connect_clicked(clone!(@weak number, @weak button_decrease =>\n# move |_| {\n# number.set(number.get() + 1);\n# button_decrease.set_label(&number.get().to_string());\n# }));\n# button_decrease.connect_clicked(clone!(@weak button_increase =>\n# move |_| {\n# number.set(number.get() - 1);\n# button_increase.set_label(&number.get().to_string());\n# }));\n# // Add buttons to `gtk_box` let gtk_box = gtk::Box::builder() .orientation(Orientation::Vertical) .build(); gtk_box.append(&button_increase); gtk_box.append(&button_decrease);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } When we append the buttons to the gtk_box, gtk_box keeps a strong reference to them. Filename: listings/g_object_memory_management/5/main.rs # use std::cell::Cell;\n# use std::rc::Rc;\n# # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectMemoryManagement5\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create two buttons\n# let button_increase = Button::builder()\n# .label(\"Increase\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let button_decrease = Button::builder()\n# .label(\"Decrease\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# # // Reference-counted object with inner mutability\n# let number = Rc::new(Cell::new(0));\n# # // Connect callbacks\n# // When a button is clicked, `number` and label of the other button will be changed\n# button_increase.connect_clicked(clone!(@weak number, @weak button_decrease =>\n# move |_| {\n# number.set(number.get() + 1);\n# button_decrease.set_label(&number.get().to_string());\n# }));\n# button_decrease.connect_clicked(clone!(@weak button_increase =>\n# move |_| {\n# number.set(number.get() - 1);\n# button_increase.set_label(&number.get().to_string());\n# }));\n# # // Add buttons to `gtk_box`\n# let gtk_box = gtk::Box::builder()\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_increase);\n# gtk_box.append(&button_decrease);\n# // Create a window let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .child(>k_box) .build();\n# # // Present the window\n# window.present();\n# } When we set gtk_box as child of window, window keeps a strong reference to it. Until we close the window it keeps gtk_box and with it the buttons alive. Since our application has only one window, closing it also means exiting the application. As long as you use weak references whenever possible, you will find it perfectly doable to avoid memory cycles within your application. Without memory cycles, you can rely on GTK to properly manage the memory of GObjects you pass to it.","breadcrumbs":"GObject Concepts » Memory Management » Memory Management","id":"33","title":"Memory Management"},"34":{"body":"GObjects rely heavily on inheritance. Therefore, it makes sense that if we want to create a custom GObject, this is done via subclassing. Let's see how this works by replacing the button in our \"Hello World!\" app with a custom one. First, we need to create an implementation struct that holds the state and overrides the virtual methods. Filename: listings/g_object_subclassing/1/custom_button/imp.rs use gtk::glib;\nuse gtk::subclass::prelude::*; // Object holding the state\n#[derive(Default)]\npub struct CustomButton; // The central trait for subclassing a GObject\n#[glib::object_subclass]\nimpl ObjectSubclass for CustomButton { const NAME: &'static str = \"MyGtkAppCustomButton\"; type Type = super::CustomButton; type ParentType = gtk::Button;\n} // Trait shared by all GObjects\nimpl ObjectImpl for CustomButton {} // Trait shared by all widgets\nimpl WidgetImpl for CustomButton {} // Trait shared by all buttons\nimpl ButtonImpl for CustomButton {} The description of the subclassing is in ObjectSubclass. NAME should consist of crate-name and object-name in order to avoid name collisions. Use UpperCamelCase here. Type refers to the actual GObject that will be created afterwards. ParentType is the GObject we inherit of. After that, we would have the option to override the virtual methods of our ancestors. Since we only want to have a plain button for now, we override nothing. We still have to add the empty impl though. Next, we describe the public interface of our custom GObject. Filename: listings/g_object_subclassing/1/custom_button/mod.rs mod imp; use glib::Object;\nuse gtk::glib; glib::wrapper! { pub struct CustomButton(ObjectSubclass) @extends gtk::Button, gtk::Widget, @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;\n} impl CustomButton { pub fn new() -> Self { Object::builder().build() } pub fn with_label(label: &str) -> Self { Object::builder().property(\"label\", label).build() }\n}\n# # impl Default for CustomButton {\n# fn default() -> Self {\n# Self::new()\n# }\n# } glib::wrapper! implements the same traits that our ParentType implements. Theoretically that would mean that the ParentType is also the only thing we have to specify here. Unfortunately, nobody has yet found a good way to do that. Which is why, as of today, subclassing of GObjects in Rust requires to mention all ancestors and interfaces apart from GObject and GInitiallyUnowned. For gtk::Button, we can look up the ancestors and interfaces in the corresponding doc page of GTK4. After these steps, nothing is stopping us from replacing gtk::Button with our CustomButton. Filename: listings/g_object_subclassing/1/main.rs mod custom_button; use custom_button::CustomButton;\nuse gtk::prelude::*;\nuse gtk::{glib, Application, ApplicationWindow}; const APP_ID: &str = \"org.gtk_rs.GObjectSubclassing1\"; fn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Connect to \"activate\" signal of `app` app.connect_activate(build_ui); // Run the application app.run()\n} fn build_ui(app: &Application) { // Create a button let button = CustomButton::with_label(\"Press me!\"); button.set_margin_top(12); button.set_margin_bottom(12); button.set_margin_start(12); button.set_margin_end(12); // Connect to \"clicked\" signal of `button` button.connect_clicked(move |button| { // Set the label to \"Hello World!\" after the button has been clicked on button.set_label(\"Hello World!\"); }); // Create a window let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .child(&button) .build(); // Present window window.present();\n} Describing objects with two structs is a peculiarity coming from how GObjects are defined in C. imp::CustomButton handles the state of the GObject and the overridden virtual methods. CustomButton determines the exposed methods from the implemented traits and added methods.","breadcrumbs":"GObject Concepts » Subclassing » Subclassing","id":"34","title":"Subclassing"},"35":{"body":"We are able to use CustomButton as a drop-in replacement for gtk::Button. This is cool, but also not very tempting to do in a real application. For the gain of zero benefits, it did involve quite a bit of boilerplate after all. So let's make it a bit more interesting! gtk::Button does not hold much state, but we can let CustomButton hold a number. Filename: listings/g_object_subclassing/2/custom_button/imp.rs use std::cell::Cell; use gtk::glib;\nuse gtk::prelude::*;\nuse gtk::subclass::prelude::*; // Object holding the state\n#[derive(Default)]\npub struct CustomButton { number: Cell,\n} // The central trait for subclassing a GObject\n#[glib::object_subclass]\nimpl ObjectSubclass for CustomButton { const NAME: &'static str = \"MyGtkAppCustomButton\"; type Type = super::CustomButton; type ParentType = gtk::Button;\n} // Trait shared by all GObjects\nimpl ObjectImpl for CustomButton { fn constructed(&self) { self.parent_constructed(); self.obj().set_label(&self.number.get().to_string()); }\n} // Trait shared by all widgets\nimpl WidgetImpl for CustomButton {} // Trait shared by all buttons\nimpl ButtonImpl for CustomButton { fn clicked(&self) { self.number.set(self.number.get() + 1); self.obj().set_label(&self.number.get().to_string()) }\n} We override constructed in ObjectImpl so that the label of the button initializes with number. We also override clicked in ButtonImpl so that every click increases number and updates the label. Filename: listings/g_object_subclassing/2/main.rs # mod custom_button;\n# # use custom_button::CustomButton;\n# use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectSubclassing2\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# fn build_ui(app: &Application) { // Create a button let button = CustomButton::new(); button.set_margin_top(12); button.set_margin_bottom(12); button.set_margin_start(12); button.set_margin_end(12); // Create a window let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .child(&button) .build(); // Present window window.present();\n} In build_ui we stop calling connect_clicked, and that was it. After a rebuild, the app now features our CustomButton with the label \"0\". Every time we click on the button, the number displayed by the label increases by 1. A video showing that pressing on a button increases the number So, when do we want to inherit from GObject? We want to use a certain widget, but with added state and overridden virtual functions. We want to pass a Rust object to a function, but the function expects a GObject. We want to add properties or signals to an object.","breadcrumbs":"GObject Concepts » Subclassing » Adding Functionality","id":"35","title":"Adding Functionality"},"36":{"body":"Some GObject-related functions rely on generic values for their arguments or return parameters. Since GObject introspection works through a C interface, these functions cannot rely on any powerful Rust concepts. In these cases glib::Value or glib::Variant are used.","breadcrumbs":"GObject Concepts » Generic Values » Generic Values","id":"36","title":"Generic Values"},"37":{"body":"Let's start with Value. Conceptually, a Value is similar to a Rust enum defined like this: enum Value { bool(bool), i8(i8), i32(i32), u32(u32), i64(i64), u64(u64), f32(f32), f64(f64), // boxed types String(Option), Object(Option>),\n} For example, this is how you would use a Value representing an i32. Filename: listings/g_object_values/1/main.rs # use gtk::prelude::*;\n# # fn main() { // Store `i32` as `Value` let integer_value = 10.to_value(); // Retrieve `i32` from `Value` let integer = integer_value .get::() .expect(\"The value needs to be of type `i32`.\"); // Check if the retrieved value is correct assert_eq!(integer, 10);\n# # // Store string as `Value`\n# let string_value = \"Hello!\".to_value();\n# # // Retrieve `String` from `Value`\n# let string = string_value\n# .get::()\n# .expect(\"The value needs to be of type `String`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(string, \"Hello!\".to_string());\n# # // Store `Option` as `Value`\n# let string_some_value = \"Hello!\".to_value();\n# let string_none_value = None::.to_value();\n# # // Retrieve `String` from `Value`\n# let string_some = string_some_value\n# .get::>()\n# .expect(\"The value needs to be of type `Option`.\");\n# let string_none = string_none_value\n# .get::>()\n# .expect(\"The value needs to be of type `Option`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(string_some, Some(\"Hello!\".to_string()));\n# assert_eq!(string_none, None);\n# } Also note that in the enum above boxed types such as String or glib::Object are wrapped in an Option. This comes from C, where every boxed type can potentially be None (or NULL in C terms). You can still access it the same way as with the i32 above. get will then not only return Err if you specified the wrong type, but also if the Value represents None. Filename: listings/g_object_values/1/main.rs # use gtk::prelude::*;\n# # fn main() {\n# // Store `i32` as `Value`\n# let integer_value = 10.to_value();\n# # // Retrieve `i32` from `Value`\n# let integer = integer_value\n# .get::()\n# .expect(\"The value needs to be of type `i32`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(integer, 10);\n# // Store string as `Value` let string_value = \"Hello!\".to_value(); // Retrieve `String` from `Value` let string = string_value .get::() .expect(\"The value needs to be of type `String`.\"); // Check if the retrieved value is correct assert_eq!(string, \"Hello!\".to_string());\n# # // Store `Option` as `Value`\n# let string_some_value = \"Hello!\".to_value();\n# let string_none_value = None::.to_value();\n# # // Retrieve `String` from `Value`\n# let string_some = string_some_value\n# .get::>()\n# .expect(\"The value needs to be of type `Option`.\");\n# let string_none = string_none_value\n# .get::>()\n# .expect(\"The value needs to be of type `Option`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(string_some, Some(\"Hello!\".to_string()));\n# assert_eq!(string_none, None);\n# } If you want to differentiate between specifying the wrong type and a Value representing None, just call get::> instead. Filename: listings/g_object_values/1/main.rs # use gtk::prelude::*;\n# # fn main() {\n# // Store `i32` as `Value`\n# let integer_value = 10.to_value();\n# # // Retrieve `i32` from `Value`\n# let integer = integer_value\n# .get::()\n# .expect(\"The value needs to be of type `i32`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(integer, 10);\n# # // Store string as `Value`\n# let string_value = \"Hello!\".to_value();\n# # // Retrieve `String` from `Value`\n# let string = string_value\n# .get::()\n# .expect(\"The value needs to be of type `String`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(string, \"Hello!\".to_string());\n# // Store `Option` as `Value` let string_some_value = \"Hello!\".to_value(); let string_none_value = None::.to_value(); // Retrieve `String` from `Value` let string_some = string_some_value .get::>() .expect(\"The value needs to be of type `Option`.\"); let string_none = string_none_value .get::>() .expect(\"The value needs to be of type `Option`.\"); // Check if the retrieved value is correct assert_eq!(string_some, Some(\"Hello!\".to_string())); assert_eq!(string_none, None);\n# } We will use Value when we deal with properties and signals later on.","breadcrumbs":"GObject Concepts » Generic Values » Value","id":"37","title":"Value"},"38":{"body":"A Variant is used whenever data needs to be serialized, for example for sending it to another process or over the network, or for storing it on disk. Although GVariant supports arbitrarily complex types, the Rust bindings are currently limited to bool, u8, i16, u16, i32, u32, i64, u64, f64, &str/String, and VariantDict . Containers of the above types are possible as well, such as HashMap, Vec, Option, tuples up to 16 elements, and Variant. Variants can even be derived from Rust structs as long as its members can be represented by variants. In the most simple case, converting Rust types to Variant and vice-versa is very similar to the way it worked with Value. Filename: listings/g_object_values/2/main.rs # use gtk::prelude::*;\n# # fn main() { // Store `i32` as `Variant` let integer_variant = 10.to_variant(); // Retrieve `i32` from `Variant` let integer = integer_variant .get::() .expect(\"The variant needs to be of type `i32`.\"); // Check if the retrieved value is correct assert_eq!(integer, 10);\n# # let variant = vec![\"Hello\", \"there!\"].to_variant();\n# assert_eq!(variant.n_children(), 2);\n# let vec = &variant\n# .get::>()\n# .expect(\"The variant needs to be of type `String`.\");\n# assert_eq!(vec[0], \"Hello\");\n# } However, a Variant is also able to represent containers such as HashMap or Vec . The following snippet shows how to convert between Vec and Variant. More examples can be found in the docs . Filename: listings/g_object_values/2/main.rs # use gtk::prelude::*;\n# # fn main() {\n# // Store `i32` as `Variant`\n# let integer_variant = 10.to_variant();\n# # // Retrieve `i32` from `Variant`\n# let integer = integer_variant\n# .get::()\n# .expect(\"The variant needs to be of type `i32`.\");\n# # // Check if the retrieved value is correct\n# assert_eq!(integer, 10);\n# let variant = vec![\"Hello\", \"there!\"].to_variant(); assert_eq!(variant.n_children(), 2); let vec = &variant .get::>() .expect(\"The variant needs to be of type `String`.\"); assert_eq!(vec[0], \"Hello\");\n# } We will use Variant when saving settings using gio::Settings or activating actions via gio::Action .","breadcrumbs":"GObject Concepts » Generic Values » Variant","id":"38","title":"Variant"},"39":{"body":"Properties provide a public API for accessing state of GObjects. Let's see how this is done by experimenting with the Switch widget. One of its properties is called active . According to the GTK docs, it can be read and be written to. That is why gtk-rs provides corresponding is_active and set_active methods. Filename: listings/g_object_properties/1/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectProperties1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Create the switch let switch = Switch::new(); // Set and then immediately obtain active property switch.set_active(true); let switch_active = switch.is_active(); // This prints: \"The active property of switch is true\" println!(\"The active property of switch is {}\", switch_active);\n# # // Set up box\n# let gtk_box = Box::builder()\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .spacing(12)\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&switch);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } Properties can not only be accessed via getters & setters, they can also be bound to each other. Let's see how that would look like for two Switch instances. Filename: listings/g_object_properties/2/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectProperties3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Create the switches let switch_1 = Switch::new(); let switch_2 = Switch::new();\n# # switch_1\n# .bind_property(\"active\", &switch_2, \"active\")\n# .bidirectional()\n# .build();\n# # // Set up box\n# let gtk_box = Box::builder()\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .spacing(12)\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&switch_1);\n# gtk_box.append(&switch_2);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } In our case, we want to bind the \"active\" property of switch_1 to the \"active\" property of switch_2. We also want the binding to be bidirectional, so we specify by calling the bidirectional method. Filename: listings/g_object_properties/2/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectProperties3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create the switches\n# let switch_1 = Switch::new();\n# let switch_2 = Switch::new();\n# switch_1 .bind_property(\"active\", &switch_2, \"active\") .bidirectional() .build();\n# # // Set up box\n# let gtk_box = Box::builder()\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .spacing(12)\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&switch_1);\n# gtk_box.append(&switch_2);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } Now when we click on one of the two switches, the other one is toggled as well. A video which shows that toggling one button also toggles the other one","breadcrumbs":"GObject Concepts » Properties » Properties","id":"39","title":"Properties"},"4":{"body":"In order to develop a gtk-rs app, you basically need two things on your workstation: the Rust toolchain, and the GTK 4 library. As so often the devil hides in the details, which is why we will list the installation instructions for each operating system in the following chapters.","breadcrumbs":"Installation » Installation","id":"4","title":"Installation"},"40":{"body":"We can also add properties to custom GObjects. We can demonstrate that by binding the number of our CustomButton to a property. Most of the work is done by the glib::Properties derive macro. We tell it that the wrapper type is super::CustomButton. We also annotate number, so that macro knows that it should create a property \"number\" that is readable and writable. It also generates wrapper methods which we are going to use later in this chapter. Filename: listings/g_object_properties/3/custom_button/imp.rs # use std::cell::Cell;\n# # use glib::Properties;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# // Object holding the state\n#[derive(Properties, Default)]\n#[properties(wrapper_type = super::CustomButton)]\npub struct CustomButton { #[property(get, set)] number: Cell,\n}\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for CustomButton {\n# const NAME: &'static str = \"MyGtkAppCustomButton\";\n# type Type = super::CustomButton;\n# type ParentType = gtk::Button;\n# }\n# # // Trait shared by all GObjects\n# #[glib::derived_properties]\n# impl ObjectImpl for CustomButton {\n# fn constructed(&self) {\n# self.parent_constructed();\n# # // Bind label to number\n# // `SYNC_CREATE` ensures that the label will be immediately set\n# let obj = self.obj();\n# obj.bind_property(\"number\", obj.as_ref(), \"label\")\n# .sync_create()\n# .build();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for CustomButton {}\n# # // Trait shared by all buttons\n# impl ButtonImpl for CustomButton {\n# fn clicked(&self) {\n# let incremented_number = self.obj().number() + 1;\n# self.obj().set_number(incremented_number);\n# }\n# } The glib::derived_properties macro generates boilerplate that is the same for every GObject that generates its properties with the Property macro. In constructed we use our new property \"number\" by binding the \"label\" property to it. bind_property converts the integer value of \"number\" to the string of \"label\" on its own. Now we don't have to adapt the label in the \"clicked\" callback anymore. Filename: listings/g_object_properties/3/custom_button/imp.rs # use std::cell::Cell;\n# # use glib::Properties;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# # // Object holding the state\n# #[derive(Properties, Default)]\n# #[properties(wrapper_type = super::CustomButton)]\n# pub struct CustomButton {\n# #[property(get, set)]\n# number: Cell,\n# }\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for CustomButton {\n# const NAME: &'static str = \"MyGtkAppCustomButton\";\n# type Type = super::CustomButton;\n# type ParentType = gtk::Button;\n# }\n# // Trait shared by all GObjects\n#[glib::derived_properties]\nimpl ObjectImpl for CustomButton { fn constructed(&self) { self.parent_constructed(); // Bind label to number // `SYNC_CREATE` ensures that the label will be immediately set let obj = self.obj(); obj.bind_property(\"number\", obj.as_ref(), \"label\") .sync_create() .build(); }\n}\n# # // Trait shared by all widgets\n# impl WidgetImpl for CustomButton {}\n# # // Trait shared by all buttons\n# impl ButtonImpl for CustomButton {\n# fn clicked(&self) {\n# let incremented_number = self.obj().number() + 1;\n# self.obj().set_number(incremented_number);\n# }\n# } We also have to adapt the clicked method. Before we modified number directly, now we can use the generated wrapper methods number and set_number. This way the \"notify\" signal will be emitted, which is necessary for the bindings to work as expected. # use std::cell::Cell;\n# # use glib::Properties;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# # // Object holding the state\n# #[derive(Properties, Default)]\n# #[properties(wrapper_type = super::CustomButton)]\n# pub struct CustomButton {\n# #[property(get, set)]\n# number: Cell,\n# }\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for CustomButton {\n# const NAME: &'static str = \"MyGtkAppCustomButton\";\n# type Type = super::CustomButton;\n# type ParentType = gtk::Button;\n# }\n# # // Trait shared by all GObjects\n# #[glib::derived_properties]\n# impl ObjectImpl for CustomButton {\n# fn constructed(&self) {\n# self.parent_constructed();\n# # // Bind label to number\n# // `SYNC_CREATE` ensures that the label will be immediately set\n# let obj = self.obj();\n# obj.bind_property(\"number\", obj.as_ref(), \"label\")\n# .sync_create()\n# .build();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for CustomButton {}\n# // Trait shared by all buttons\nimpl ButtonImpl for CustomButton { fn clicked(&self) { let incremented_number = self.obj().number() + 1; self.obj().set_number(incremented_number); }\n} Let's see what we can do with this by creating two custom buttons. Filename: listings/g_object_properties/3/main.rs # mod custom_button;\n# # use custom_button::CustomButton;\n# use gtk::prelude::*;\n# use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectProperties4\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Create the buttons let button_1 = CustomButton::new(); let button_2 = CustomButton::new();\n# # // Assure that \"number\" of `button_2` is always 1 higher than \"number\" of `button_1`\n# button_1\n# .bind_property(\"number\", &button_2, \"number\")\n# // How to transform \"number\" from `button_1` to \"number\" of `button_2`\n# .transform_to(|_, number: i32| {\n# let incremented_number = number + 1;\n# Some(incremented_number.to_value())\n# })\n# // How to transform \"number\" from `button_2` to \"number\" of `button_1`\n# .transform_from(|_, number: i32| {\n# let decremented_number = number - 1;\n# Some(decremented_number.to_value())\n# })\n# .bidirectional()\n# .sync_create()\n# .build();\n# # // The closure will be called\n# // whenever the property \"number\" of `button_1` gets changed\n# button_1.connect_number_notify(|button| {\n# println!(\"The current number of `button_1` is {}.\", button.number());\n# });\n# # // Set up box\n# let gtk_box = Box::builder()\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .spacing(12)\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_1);\n# gtk_box.append(&button_2);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } We have already seen that bound properties don't necessarily have to be of the same type. By leveraging transform_to and transform_from , we can assure that button_2 always displays a number which is 1 higher than the number of button_1. Filename: listings/g_object_properties/3/main.rs # mod custom_button;\n# # use custom_button::CustomButton;\n# use gtk::prelude::*;\n# use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectProperties4\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create the buttons\n# let button_1 = CustomButton::new();\n# let button_2 = CustomButton::new();\n# // Assure that \"number\" of `button_2` is always 1 higher than \"number\" of `button_1` button_1 .bind_property(\"number\", &button_2, \"number\") // How to transform \"number\" from `button_1` to \"number\" of `button_2` .transform_to(|_, number: i32| { let incremented_number = number + 1; Some(incremented_number.to_value()) }) // How to transform \"number\" from `button_2` to \"number\" of `button_1` .transform_from(|_, number: i32| { let decremented_number = number - 1; Some(decremented_number.to_value()) }) .bidirectional() .sync_create() .build();\n# # // The closure will be called\n# // whenever the property \"number\" of `button_1` gets changed\n# button_1.connect_number_notify(|button| {\n# println!(\"The current number of `button_1` is {}.\", button.number());\n# });\n# # // Set up box\n# let gtk_box = Box::builder()\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .spacing(12)\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_1);\n# gtk_box.append(&button_2);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } Now if we click on one button, the \"number\" and \"label\" properties of the other button change as well. A video which shows that pressing on one button also changes the number on the other one Another nice feature of properties is, that you can connect a callback to the event, when a property gets changed. For example like this: Filename: listings/g_object_properties/3/main.rs # mod custom_button;\n# # use custom_button::CustomButton;\n# use gtk::prelude::*;\n# use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectProperties4\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create the buttons\n# let button_1 = CustomButton::new();\n# let button_2 = CustomButton::new();\n# # // Assure that \"number\" of `button_2` is always 1 higher than \"number\" of `button_1`\n# button_1\n# .bind_property(\"number\", &button_2, \"number\")\n# // How to transform \"number\" from `button_1` to \"number\" of `button_2`\n# .transform_to(|_, number: i32| {\n# let incremented_number = number + 1;\n# Some(incremented_number.to_value())\n# })\n# // How to transform \"number\" from `button_2` to \"number\" of `button_1`\n# .transform_from(|_, number: i32| {\n# let decremented_number = number - 1;\n# Some(decremented_number.to_value())\n# })\n# .bidirectional()\n# .sync_create()\n# .build();\n# // The closure will be called // whenever the property \"number\" of `button_1` gets changed button_1.connect_number_notify(|button| { println!(\"The current number of `button_1` is {}.\", button.number()); });\n# # // Set up box\n# let gtk_box = Box::builder()\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .spacing(12)\n# .orientation(Orientation::Vertical)\n# .build();\n# gtk_box.append(&button_1);\n# gtk_box.append(&button_2);\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(>k_box)\n# .build();\n# # // Present the window\n# window.present();\n# } Now, whenever the \"number\" property gets changed, the closure gets executed and prints the current value of \"number\" to standard output. Introducing properties to your custom GObjects is useful if you want to bind state of (different) GObjects notify consumers whenever a property value changes Note that it has a (computational) cost to send a signal each time the value changes. If you only want to expose internal state, adding getter and setter methods is the better option.","breadcrumbs":"GObject Concepts » Properties » Adding Properties to Custom GObjects","id":"40","title":"Adding Properties to Custom GObjects"},"41":{"body":"GObject signals are a system for registering callbacks for specific events. For example, if we press on a button, the \"clicked\" signal will be emitted. The signal then takes care that all the registered callbacks will be executed. gtk-rs provides convenience methods for registering callbacks. In our \"Hello World\" example we connected the \"clicked\" signal to a closure which sets the label of the button to \"Hello World\" as soon as it gets called. Filename: listings/hello_world/3/main.rs # use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# const APP_ID: &str = \"org.gtk_rs.HelloWorld3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button with label and margins\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Connect to \"clicked\" signal of `button` button.connect_clicked(|button| { // Set the label to \"Hello World!\" after the button has been clicked on button.set_label(\"Hello World!\"); });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } If we wanted to, we could have connected to it with the generic connect_closure method and the glib::closure_local! macro. Filename: listings/g_object_signals/1/main.rs # use glib::closure_local;\n# use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectSignals1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Connect to \"clicked\" signal of `button` button.connect_closure( \"clicked\", false, closure_local!(move |button: Button| { // Set the label to \"Hello World!\" after the button has been clicked on button.set_label(\"Hello World!\"); }), ); # # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } The advantage of connect_closure is that it also works with custom signals. If you need to clone reference counted objects into your closure you don't have to wrap it within another clone! macro. closure_local! accepts the same syntax for creating strong/weak references, plus a watch feature that automatically disconnects the closure once the watched object is dropped.","breadcrumbs":"GObject Concepts » Signals » Signals","id":"41","title":"Signals"},"42":{"body":"Let's see how we can create our own signals. Again we do that by extending our CustomButton. First we override the signals method in ObjectImpl. In order to do that, we need to lazily initialize a static item SIGNALS. Until std::sync::LazyLock is stabilized, we use once_cell::sync::Lazy instead. This also means, we have to add the once_cell crate as dependency by executing: cargo add once_cell Filename: listings/g_object_signals/2/custom_button/imp.rs # use std::cell::Cell;\n# # use glib::subclass::Signal;\n# use glib::Properties;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# use once_cell::sync::Lazy;\n# # // Object holding the state\n# #[derive(Properties, Default)]\n# #[properties(wrapper_type = super::CustomButton)]\n# pub struct CustomButton {\n# #[property(get, set)]\n# number: Cell,\n# }\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for CustomButton {\n# const NAME: &'static str = \"MyGtkAppCustomButton\";\n# type Type = super::CustomButton;\n# type ParentType = gtk::Button;\n# }\n# // Trait shared by all GObjects\n#[glib::derived_properties]\nimpl ObjectImpl for CustomButton { fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![Signal::builder(\"max-number-reached\") .param_types([i32::static_type()]) .build()] }); SIGNALS.as_ref() }\n# # fn constructed(&self) {\n# self.parent_constructed();\n# # // Bind label to number\n# // `SYNC_CREATE` ensures that the label will be immediately set\n# let obj = self.obj();\n# obj.bind_property(\"number\", obj.as_ref(), \"label\")\n# .sync_create()\n# .build();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for CustomButton {}\n# # static MAX_NUMBER: i32 = 8;\n# # // Trait shared by all buttons\n# impl ButtonImpl for CustomButton {\n# fn clicked(&self) {\n# let incremented_number = self.obj().number() + 1;\n# let obj = self.obj();\n# // If `number` reached `MAX_NUMBER`,\n# // emit \"max-number-reached\" signal and set `number` back to 0\n# if incremented_number == MAX_NUMBER {\n# obj.emit_by_name::<()>(\"max-number-reached\", &[&incremented_number]);\n# obj.set_number(0);\n# } else {\n# obj.set_number(incremented_number);\n# }\n# }\n# } The signals method is responsible for defining a set of signals. In our case, we only create a single signal named \"max-number-reached\". When naming our signal, we make sure to do that in kebab-case . When emitted, it sends a single i32 value. We want the signal to be emitted, whenever number reaches MAX_NUMBER. Together with the signal we send the value number currently holds. After we did that, we set number back to 0. Filename: listings/g_object_signals/2/custom_button/imp.rs # use std::cell::Cell;\n# # use glib::subclass::Signal;\n# use glib::Properties;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# use once_cell::sync::Lazy;\n# # // Object holding the state\n# #[derive(Properties, Default)]\n# #[properties(wrapper_type = super::CustomButton)]\n# pub struct CustomButton {\n# #[property(get, set)]\n# number: Cell,\n# }\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for CustomButton {\n# const NAME: &'static str = \"MyGtkAppCustomButton\";\n# type Type = super::CustomButton;\n# type ParentType = gtk::Button;\n# }\n# # // Trait shared by all GObjects\n# #[glib::derived_properties]\n# impl ObjectImpl for CustomButton {\n# fn signals() -> &'static [Signal] {\n# static SIGNALS: Lazy> = Lazy::new(|| {\n# vec![Signal::builder(\"max-number-reached\")\n# .param_types([i32::static_type()])\n# .build()]\n# });\n# SIGNALS.as_ref()\n# }\n# # fn constructed(&self) {\n# self.parent_constructed();\n# # // Bind label to number\n# // `SYNC_CREATE` ensures that the label will be immediately set\n# let obj = self.obj();\n# obj.bind_property(\"number\", obj.as_ref(), \"label\")\n# .sync_create()\n# .build();\n# }\n# }\n# # // Trait shared by all widgets\n# impl WidgetImpl for CustomButton {}\n# static MAX_NUMBER: i32 = 8; // Trait shared by all buttons\nimpl ButtonImpl for CustomButton { fn clicked(&self) { let incremented_number = self.obj().number() + 1; let obj = self.obj(); // If `number` reached `MAX_NUMBER`, // emit \"max-number-reached\" signal and set `number` back to 0 if incremented_number == MAX_NUMBER { obj.emit_by_name::<()>(\"max-number-reached\", &[&incremented_number]); obj.set_number(0); } else { obj.set_number(incremented_number); } }\n} If we now press on the button, the number of its label increases until it reaches MAX_NUMBER. Then it emits the \"max-number-reached\" signal which we can nicely connect to. Whenever we now receive the \"max-number-reached\" signal, the accompanying number is printed to standard output . Filename: listings/g_object_signals/2/main.rs # mod custom_button;\n# # use custom_button::CustomButton;\n# use glib::closure_local;\n# use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow};\n# # const APP_ID: &str = \"org.gtk_rs.GObjectSignals2\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# fn build_ui(app: &Application) {\n# // Create a button\n# let button = CustomButton::new();\n# button.set_margin_top(12);\n# button.set_margin_bottom(12);\n# button.set_margin_start(12);\n# button.set_margin_end(12);\n# button.connect_closure( \"max-number-reached\", false, closure_local!(move |_button: CustomButton, number: i32| { println!(\"The maximum number {} has been reached\", number); }), );\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } You now know how to connect to every kind of signal and how to create your own. Custom signals are especially useful, if you want to notify consumers of your GObject that a certain event occurred.","breadcrumbs":"GObject Concepts » Signals » Adding Signals to Custom GObjects","id":"42","title":"Adding Signals to Custom GObjects"},"43":{"body":"We now got comfortable using callbacks, but how do they actually work? All of this happens asynchronously, so there must be something managing the events and scheduling the responses. Unsurprisingly, this is called the main event loop. The main loop manages all kinds of events — from mouse clicks and keyboard presses to file events. It does all of that within the same thread. Quickly iterating between all tasks gives the illusion of parallelism. That is why you can move the window at the same time as a progress bar is growing. However, you surely saw GUIs that became unresponsive, at least for a few seconds. That happens when a single task takes too long. The following example uses std::thread::sleep to represent a long-running task. Filename: listings/main_event_loop/1/main.rs use std::thread;\nuse std::time::Duration; use gtk::prelude::*;\nuse gtk::{self, glib, Application, ApplicationWindow, Button}; const APP_ID: &str = \"org.gtk_rs.MainEventLoop1\"; fn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Connect to \"activate\" signal of `app` app.connect_activate(build_ui); // Run the application app.run()\n} fn build_ui(app: &Application) { // Create a button let button = Button::builder() .label(\"Press me!\") .margin_top(12) .margin_bottom(12) .margin_start(12) .margin_end(12) .build(); // Connect to \"clicked\" signal of `button` button.connect_clicked(move |_| { // GUI is blocked for 5 seconds after the button is pressed let five_seconds = Duration::from_secs(5); thread::sleep(five_seconds); }); // Create a window let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .child(&button) .build(); // Present window window.present();\n} After we press the button, the GUI is completely frozen for five seconds. We can't even move the window. The sleep call is an artificial example, but frequently, we want to run a slightly longer operation in one go. A video which shows that after pressing the button, the window can still be moved","breadcrumbs":"The Main Event Loop » The Main Event Loop","id":"43","title":"The Main Event Loop"},"44":{"body":"In order to avoid blocking the main loop, we can spawn a new task with gio::spawn_blocking and let the operation run on the thread pool. Filename: listings/main_event_loop/2/main.rs # use std::thread;\n# use std::time::Duration;\n# # use gtk::prelude::*;\n# use gtk::{self, gio, glib, Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop2\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Connect to \"clicked\" signal of `button` button.connect_clicked(move |_| { // The long running operation runs now in a separate thread gio::spawn_blocking(move || { let five_seconds = Duration::from_secs(5); thread::sleep(five_seconds); }); });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } Now the GUI doesn't freeze when we press the button. However, nothing stops us from spawning as many tasks as we want at the same time. This is not necessarily what we want. A video which shows that after pressing the button, the window can still be moved If you come from another language than Rust, you might be uncomfortable with the thought of running tasks in separate threads before even looking at other options. Luckily, Rust's safety guarantees allow you to stop worrying about the nasty bugs that concurrency tends to bring.","breadcrumbs":"The Main Event Loop » How to Avoid Blocking the Main Loop","id":"44","title":"How to Avoid Blocking the Main Loop"},"45":{"body":"Typically, we want to keep track of the work in the task. In our case, we don't want the user to spawn additional tasks while an existing one is still running. In order to exchange information with the task we can create a channel with the crate async-channel . Let's add it by executing the following in the terminal: cargo add async-channel We want to send a bool to inform, whether we want the button to react to clicks or not. Since we send in a separate thread, we can use send_blocking . But what about receiving? Every time we get a message, we want to set the sensitivity of the button according to the bool we've received. However, we don't want to block the main loop while waiting for a message to receive. That is the whole point of the exercise after all! We solve that problem by waiting for messages in an async block. This async block is spawned on the glib main loop with spawn_future_local See also spawn_future for spawning async blocks on the main loop from outside the main thread. Filename: listings/main_event_loop/3/main.rs # use std::thread;\n# use std::time::Duration;\n# # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{gio, glib, Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop3\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Create channel that can hold at most 1 message at a time let (sender, receiver) = async_channel::bounded(1); // Connect to \"clicked\" signal of `button` button.connect_clicked(move |_| { let sender = sender.clone(); // The long running operation runs now in a separate thread gio::spawn_blocking(move || { // Deactivate the button until the operation is done sender .send_blocking(false) .expect(\"The channel needs to be open.\"); let five_seconds = Duration::from_secs(5); thread::sleep(five_seconds); // Activate the button again sender .send_blocking(true) .expect(\"The channel needs to be open.\"); }); }); // The main loop executes the asynchronous block glib::spawn_future_local(clone!(@weak button => async move { while let Ok(enable_button) = receiver.recv().await { button.set_sensitive(enable_button); } }));\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } As you can see, spawning a task still doesn't freeze our user interface. However, now we can't spawn multiple tasks at the same time since the button becomes insensitive after the first task has been spawned. After the task is finished, the button becomes sensitive again. The button now stops being responsive for 10 seconds after being pressed What if the task is asynchronous by nature? Let's try glib::timeout_future_seconds as representation for our task instead of std::thread::sleep. It returns a std::future::Future , which means we can await on it within an async context. The converted code looks and behaves very similar to the multithreaded code. Filename: listings/main_event_loop/4/main.rs # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop4\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Create channel that can hold at most 1 message at a time let (sender, receiver) = async_channel::bounded(1); // Connect to \"clicked\" signal of `button` button.connect_clicked(move |_| { glib::spawn_future_local(clone!(@strong sender => async move { // Deactivate the button until the operation is done sender.send(false).await.expect(\"The channel needs to be open.\"); glib::timeout_future_seconds(5).await; // Activate the button again sender.send(true).await.expect(\"The channel needs to be open.\"); })); }); // The main loop executes the asynchronous block glib::spawn_future_local(clone!(@weak button => async move { while let Ok(enable_button) = receiver.recv().await { button.set_sensitive(enable_button); } }));\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } Since we are single-threaded again, we can even get rid of the channel while achieving the same result. Filename: listings/main_event_loop/5/main.rs # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop5\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Connect to \"clicked\" signal of `button` button.connect_clicked(move |button| { glib::spawn_future_local(clone!(@weak button => async move { // Deactivate the button until the operation is done button.set_sensitive(false); glib::timeout_future_seconds(5).await; // Activate the button again button.set_sensitive(true); })); });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } But why did we not do the same thing with our multithreaded example? # use std::{thread, time::Duration};\n# # use glib::{clone, MainContext, PRIORITY_DEFAULT};\n# use gtk::{glib, gio};\n# use gtk::prelude::*;\n# use gtk::{Application, ApplicationWindow, Button};\n# # fn main() {\n# // Create a new application\n# let app = Application::builder()\n# .application_id(\"org.gtk_rs.MainEventLoop6\")\n# .build();\n#\n# // Connect to \"activate\" signal\n# app.connect_activate(build_ui);\n# # // Get command-line arguments\n# let args: Vec = args().collect();\n# // Run the application\n# app.run(&args);\n# }\n# # // When the application is launched…\n# fn build_ui(application: &Application) {\n# // Create a window\n# let window = ApplicationWindow::builder()\n# .application(application)\n# .title(\"My GTK App\")\n# .build();\n# # // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // DOES NOT COMPILE! // Connect to \"clicked\" signal of `button` button.connect_clicked(move |button| { button.clone(); // The long running operation runs now in a separate thread gio::spawn_blocking(move || { // Deactivate the button until the operation is done button.set_sensitive(false); let five_seconds = Duration::from_secs(5); thread::sleep(five_seconds); // Activate the button again button.set_sensitive(true); }); });\n# # // Add button\n# window.set_child(Some(&button));\n# window.present();\n# } Simply because we would get this error message: error[E0277]: `NonNull` cannot be shared between threads safely help: within `gtk4::Button`, the trait `Sync` is not implemented for `NonNull` After reference cycles we found the second disadvantage of GTK GObjects: They are not thread safe.","breadcrumbs":"The Main Event Loop » Channels","id":"45","title":"Channels"},"46":{"body":"We've seen in the previous snippets that spawning an async block or async future on the glib main loop can lead to more concise code than running tasks on separate threads. Let's focus on a few more aspects that are interesting to know when running async functions with gtk-rs apps. For a start, blocking functions can be embedded within an async context. In the following listing, we want to execute a synchronous function that returns a boolean and takes ten seconds to run. In order to integrate it in our async block, we run the function in a separate thread via spawn_blocking. We can then get the return value of the function by calling await on the return value of spawn_blocking. Filename: listings/main_event_loop/6/main.rs # use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{gio, glib};\n# use gtk::{Application, ApplicationWindow, Button};\n# use std::thread;\n# use std::time::Duration;\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop6\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Connect to \"clicked\" signal of `button` button.connect_clicked(move |button| { // The main loop executes the asynchronous block glib::spawn_future_local(clone!(@weak button => async move { // Deactivate the button until the operation is done button.set_sensitive(false); let enable_button = gio::spawn_blocking(move || { let five_seconds = Duration::from_secs(5); thread::sleep(five_seconds); true }) .await .expect(\"Task needs to finish successfully.\"); // Set sensitivity of button to `enable_button` button.set_sensitive(enable_button); })); });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# }","breadcrumbs":"The Main Event Loop » Embed blocking calls in an async context","id":"46","title":"Embed blocking calls in an async context"},"47":{"body":"Asynchronous functions from the glib ecosystem can always be spawned on the glib main loop. Typically, crates depending on async-std or smol work as well. Let us take ashpd for example which allows sandboxed applications to interact with the desktop. Per default it depends on async-std. We can add it to our dependencies by running the following command. cargo add ashpd --features gtk4 You need to use a Linux desktop environment in order to run the following example locally. This example is using ashpd::desktop::account::UserInformation to access user information. We are getting a gtk::Native object from our button, create a ashpd::WindowIdentifier and pass it to the user information request. We need to pass the WindowIdentifier to make the dialog modal. This means that it will be on top of the window and freezes the rest of the application from user input. Filename: listings/main_event_loop/7/main.rs # use ashpd::desktop::account::UserInformation;\n# use ashpd::WindowIdentifier;\n# use glib::clone;\n# use gtk::prelude::*;\n# use gtk::{glib, Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop7\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# // Connect to \"clicked\" signal of `button` button.connect_clicked(move |button| { // The main loop executes the asynchronous block glib::spawn_future_local(clone!(@weak button => async move { // Get native of button for window identifier let native = button.native().expect(\"Need to be able to get native.\"); // Get window identifier so that the dialog will be modal to the main window let identifier = WindowIdentifier::from_native(&native).await; let request = UserInformation::request() .reason(\"App would like to access user information.\") .identifier(identifier) .send() .await; if let Ok(response) = request.and_then(|r| r.response()) { println!(\"User name: {}\", response.name()); } else { println!(\"Could not access user information.\") } })); });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } After pressing the button, a dialog should open that shows the information that will be shared. If you decide to share it, you user name will be printed on the console.","breadcrumbs":"The Main Event Loop » Run async functions from external crates","id":"47","title":"Run async functions from external crates"},"48":{"body":"tokio is Rust's most popular asynchronous platform. Therefore, many high-quality crates are part of its ecosystem. The web client reqwest belongs to this group. Let's add it by executing the following command cargo add reqwest@0.11 --features rustls-tls --no-default-features As soon as the button is pressed, we want to send a GET request to www.gtk-rs.org . The response should then be sent to the main thread via a channel. Filename: listings/main_event_loop/8/main.rs # use glib::clone;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::{Application, ApplicationWindow, Button};\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop8\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let (sender, receiver) = async_channel::bounded(1); // Connect to \"clicked\" signal of `button` button.connect_clicked(move |_| { // The main loop executes the asynchronous block glib::spawn_future_local(clone!(@strong sender => async move { let response = reqwest::get(\"https://www.gtk-rs.org\").await; sender.send(response).await.expect(\"The channel needs to be open.\"); })); }); // The main loop executes the asynchronous block glib::spawn_future_local(async move { while let Ok(response) = receiver.recv().await { if let Ok(response) = response { println!(\"Status: {}\", response.status()); } else { println!(\"Could not make a `GET` request.\"); } } });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } This compiles fine and even seems to run. However, nothing happens when we press the button. Inspecting the console gives the following error message: thread 'main' panicked at\n'there is no reactor running, must be called from the context of a Tokio 1.x runtime' At the time of writing, reqwest doesn't document this requirement. Unfortunately, that is also the case for other libraries depending on tokio. Let's bite the bullet and add tokio: cargo add tokio@1 --features rt-multi-thread Since we already run the glib main loop on our main thread, we don't want to run the tokio runtime there. For this reason, we avoid using the #[tokio::main] macro or using a top-level block_on call. Doing this will block one of the runtime's threads with the GLib main loop, which is a waste of resources and a potential source of strange bugs. Instead, the runtime is manually created and used where needed. Let's bind it to a static variable and initialize it lazily. Filename: listings/main_event_loop/9/main.rs # use glib::clone;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::{Application, ApplicationWindow, Button};\n# use once_cell::sync::Lazy;\n# use tokio::runtime::Runtime;\n# const APP_ID: &str = \"org.gtk_rs.MainEventLoop9\";\nstatic RUNTIME: Lazy = Lazy::new(|| Runtime::new().expect(\"Setting up tokio runtime needs to succeed.\")); fn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Connect to \"activate\" signal of `app` app.connect_activate(build_ui); // Run the application app.run()\n}\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# # let (sender, receiver) = async_channel::bounded(1);\n# // Connect to \"clicked\" signal of `button`\n# button.connect_clicked(move |_| {\n# RUNTIME.spawn(clone!(@strong sender => async move {\n# let response = reqwest::get(\"https://www.gtk-rs.org\").await;\n# sender.send(response).await.expect(\"The channel needs to be open.\");\n# }));\n# });\n# # // The main loop executes the asynchronous block\n# glib::spawn_future_local(async move {\n# while let Ok(response) = receiver.recv().await {\n# if let Ok(response) = response {\n# println!(\"Status: {}\", response.status());\n# } else {\n# println!(\"Could not make a `GET` request.\");\n# }\n# }\n# });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } The button callback stays mostly the same. However, we now spawn the async block with tokio rather than with glib. Filename: listings/main_event_loop/9/main.rs # use glib::clone;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::{Application, ApplicationWindow, Button};\n# use once_cell::sync::Lazy;\n# use tokio::runtime::Runtime;\n# # const APP_ID: &str = \"org.gtk_rs.MainEventLoop9\";\n# static RUNTIME: Lazy =\n# Lazy::new(|| Runtime::new().expect(\"Setting up tokio runtime needs to succeed.\"));\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a button\n# let button = Button::builder()\n# .label(\"Press me!\")\n# .margin_top(12)\n# .margin_bottom(12)\n# .margin_start(12)\n# .margin_end(12)\n# .build();\n# let (sender, receiver) = async_channel::bounded(1); // Connect to \"clicked\" signal of `button` button.connect_clicked(move |_| { RUNTIME.spawn(clone!(@strong sender => async move { let response = reqwest::get(\"https://www.gtk-rs.org\").await; sender.send(response).await.expect(\"The channel needs to be open.\"); })); }); // The main loop executes the asynchronous block glib::spawn_future_local(async move { while let Ok(response) = receiver.recv().await { if let Ok(response) = response { println!(\"Status: {}\", response.status()); } else { println!(\"Could not make a `GET` request.\"); } } });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&button)\n# .build();\n# # // Present window\n# window.present();\n# } If we now press the button, we should find the following message in our console: Status: 200 OK We will not need tokio, reqwest or ashpd in the following chapters, so let's remove them again by executing: cargo remove reqwest tokio ashpd How to find out whether you can spawn an async task on the glib main loop? glib should be able to spawn the task when the called functions come from libraries that either: come from the glib ecosystem, don't depend on a runtime but only on the futures family of crates (futures-io, futures-core etc), depend on the async-std or smol runtimes, or have cargo features that let them depend on async-std/smol instead of tokio.","breadcrumbs":"The Main Event Loop » Tokio","id":"48","title":"Tokio"},"49":{"body":"You don't want to block the main thread long enough that it is noticeable by the user. But when should you spawn an async task, instead of spawning a task in a separate thread? Let's go again through the different scenarios. If the task spends its time calculating rather than waiting for a web response, it is CPU-bound . That means you have to run the task in a separate thread and let it send results back via a channel. If your task is IO bound , the answer depends on the crates at your disposal and the type of work to be done. Light I/O work with functions from crates using glib, smol, async-std or the futures trait family can be spawned on the main loop. This way, you can often avoid synchronization via channels. Heavy I/O work might still benefit from running in a separate thread / an async executor to avoid saturating the main loop. If you are unsure, benchmarking is advised. If the best crate for the job relies on tokio, you will have to spawn it with the tokio runtime and communicate via channels.","breadcrumbs":"The Main Event Loop » Conclusion","id":"49","title":"Conclusion"},"5":{"body":"You first have to install rustup. You can find the up-to-date instructions on rustup.rs . Then install GTK 4 and the build essentials. To do this, execute the command belonging to the distribution you are using. Fedora and derivatives: sudo dnf install gtk4-devel gcc Debian and derivatives: sudo apt install libgtk-4-dev build-essential Arch and derivatives: sudo pacman -S gtk4 base-devel","breadcrumbs":"Installation » Linux » Linux","id":"5","title":"Linux"},"50":{"body":"We have now learned multiple ways to handle states. However, every time we close the application all of it is gone. Let's learn how to use gio::Settings by storing the state of a Switch in it. At the very beginning we have to create a GSchema xml file in order to describe the kind of data our application plans to store in the settings. Filename: listings/settings/1/org.gtk_rs.Settings1.gschema.xml \n false Default switch state \n Let's get through it step by step. The id is the same application id we used when we created our application. Filename: listings/settings/1/main.rs # use gio::Settings;\n# use gtk::prelude::*;\n# use gtk::{gio, glib, Align, Application, ApplicationWindow, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.Settings1\";\n# # fn main() -> glib::ExitCode { // Create a new application let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Initialize settings\n# let settings = Settings::new(APP_ID);\n# # // Get the last switch state from the settings\n# let is_switch_enabled = settings.boolean(\"is-switch-enabled\");\n# # // Create a switch\n# let switch = Switch::builder()\n# .margin_top(48)\n# .margin_bottom(48)\n# .margin_start(48)\n# .margin_end(48)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .state(is_switch_enabled)\n# .build();\n# # switch.connect_state_set(move |_, is_enabled| {\n# // Save changed switch state in the settings\n# settings\n# .set_boolean(\"is-switch-enabled\", is_enabled)\n# .expect(\"Could not set setting.\");\n# // Allow to invoke other event handlers\n# glib::Propagation::Proceed\n# });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&switch)\n# .build();\n# # // Present window\n# window.present();\n# } The path must start and end with a forward slash character ('/') and must not contain two sequential slash characters. When creating a path, we advise to take the id, replace the '.' with '/' and add '/' at the front and end of it. We only want to store a single key with the name \"is-switch-enabled\". This is a boolean value so its type is \"b\" (see GVariant Format Strings for the other options). We also set its default value to false (see GVariant Text Format for the full syntax). Finally, we add a summary. Now we need to copy and compile the schema. You can install the schema by executing the following commands on a Linux or macOS machine: mkdir -p $HOME/.local/share/glib-2.0/schemas\ncp org.gtk_rs.Settings1.gschema.xml $HOME/.local/share/glib-2.0/schemas/\nglib-compile-schemas $HOME/.local/share/glib-2.0/schemas/ On Windows run: mkdir C:/ProgramData/glib-2.0/schemas/\ncp org.gtk_rs.Settings1.gschema.xml C:/ProgramData/glib-2.0/schemas/\nglib-compile-schemas C:/ProgramData/glib-2.0/schemas/ We initialize the Settings object by specifying the application id. Filename: listings/settings/1/main.rs # use gio::Settings;\n# use gtk::prelude::*;\n# use gtk::{gio, glib, Align, Application, ApplicationWindow, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.Settings1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Initialize settings let settings = Settings::new(APP_ID);\n# # // Get the last switch state from the settings\n# let is_switch_enabled = settings.boolean(\"is-switch-enabled\");\n# # // Create a switch\n# let switch = Switch::builder()\n# .margin_top(48)\n# .margin_bottom(48)\n# .margin_start(48)\n# .margin_end(48)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .state(is_switch_enabled)\n# .build();\n# # switch.connect_state_set(move |_, is_enabled| {\n# // Save changed switch state in the settings\n# settings\n# .set_boolean(\"is-switch-enabled\", is_enabled)\n# .expect(\"Could not set setting.\");\n# // Allow to invoke other event handlers\n# glib::Propagation::Proceed\n# });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&switch)\n# .build();\n# # // Present window\n# window.present();\n# } Then we get the settings key and use it when we create our Switch. Filename: listings/settings/1/main.rs # use gio::Settings;\n# use gtk::prelude::*;\n# use gtk::{gio, glib, Align, Application, ApplicationWindow, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.Settings1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Initialize settings\n# let settings = Settings::new(APP_ID);\n# // Get the last switch state from the settings let is_switch_enabled = settings.boolean(\"is-switch-enabled\"); // Create a switch let switch = Switch::builder() .margin_top(48) .margin_bottom(48) .margin_start(48) .margin_end(48) .valign(Align::Center) .halign(Align::Center) .state(is_switch_enabled) .build();\n# # switch.connect_state_set(move |_, is_enabled| {\n# // Save changed switch state in the settings\n# settings\n# .set_boolean(\"is-switch-enabled\", is_enabled)\n# .expect(\"Could not set setting.\");\n# // Allow to invoke other event handlers\n# glib::Propagation::Proceed\n# });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&switch)\n# .build();\n# # // Present window\n# window.present();\n# } Finally, we assure that the switch state is stored in the settings whenever we click on it. Filename: listings/settings/1/main.rs # use gio::Settings;\n# use gtk::prelude::*;\n# use gtk::{gio, glib, Align, Application, ApplicationWindow, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.Settings1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Initialize settings\n# let settings = Settings::new(APP_ID);\n# # // Get the last switch state from the settings\n# let is_switch_enabled = settings.boolean(\"is-switch-enabled\");\n# # // Create a switch\n# let switch = Switch::builder()\n# .margin_top(48)\n# .margin_bottom(48)\n# .margin_start(48)\n# .margin_end(48)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .state(is_switch_enabled)\n# .build();\n# switch.connect_state_set(move |_, is_enabled| { // Save changed switch state in the settings settings .set_boolean(\"is-switch-enabled\", is_enabled) .expect(\"Could not set setting.\"); // Allow to invoke other event handlers glib::Propagation::Proceed });\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&switch)\n# .build();\n# # // Present window\n# window.present();\n# } A video which shows that the app can now store the app state The Switch now retains its state even after closing the application. But we can make this even better. The Switch has a property \"active\" and Settings allows us to bind properties to a specific setting. So let's do exactly that. We can remove the boolean call before initializing the Switch as well as the connect_state_set call. We then bind the setting to the property by specifying the key, object and name of the property. Filename: listings/settings/2/main.rs # use gio::Settings;\n# use gtk::prelude::*;\n# use gtk::{gio, glib, Align, Application, ApplicationWindow, Switch};\n# # const APP_ID: &str = \"org.gtk_rs.Settings2\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Initialize settings\n# let settings = Settings::new(APP_ID);\n# # // Create a switch\n# let switch = Switch::builder()\n# .margin_top(48)\n# .margin_bottom(48)\n# .margin_start(48)\n# .margin_end(48)\n# .valign(Align::Center)\n# .halign(Align::Center)\n# .build();\n# settings .bind(\"is-switch-enabled\", &switch, \"active\") .build();\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .child(&switch)\n# .build();\n# # // Present window\n# window.present();\n# } Whenever you have a property which nicely correspond to a setting, you probably want to bind it to it. In other cases, interacting with the settings via the getter and setter methods tends to be the right choice.","breadcrumbs":"Settings » Settings","id":"50","title":"Settings"},"51":{"body":"Quite often, we want the window state to persist between sessions. If the user resizes or maximizes the window, they might expect to find it in the same state the next time they open the app. GTK does not provide this functionality out of the box, but luckily it is not too hard to manually implement it. We basically want two integers (height & width) and a boolean (is_maximized) to persist. We already know how to do this by using gio::Settings . Filename: listings/saving_window_state/1/org.gtk_rs.SavingWindowState1.gschema.xml \n -1 Default window width -1 Default window height false Default window maximized behaviour \n Since we don't care about intermediate state, we only load the window state when the window is constructed and save it when we close the window. That can be done by creating a custom window. First, we create one and add convenience methods for accessing settings as well as the window state. Filename: listings/saving_window_state/1/custom_window/mod.rs # mod imp;\n# # use gio::Settings;\n# use glib::Object;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# use gtk::{gio, glib, Application};\n# # use crate::APP_ID;\n# glib::wrapper! { pub struct Window(ObjectSubclass) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;\n} impl Window { pub fn new(app: &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`.\") } pub fn save_window_size(&self) -> Result<(), glib::BoolError> { // Get the size of the window let size = self.default_size(); // Set the window state in `settings` self.settings().set_int(\"window-width\", size.0)?; self.settings().set_int(\"window-height\", size.1)?; self.settings() .set_boolean(\"is-maximized\", self.is_maximized())?; Ok(()) } fn load_window_size(&self) { // Get the window state from `settings` let width = self.settings().int(\"window-width\"); let height = self.settings().int(\"window-height\"); let is_maximized = self.settings().boolean(\"is-maximized\"); // Set the size of the window self.set_default_size(width, height); // If the window was maximized when it was closed, maximize it again if is_maximized { self.maximize(); } }\n} We set the property \"application\" by passing it to glib::Object::new . You can even set multiple properties that way. When creating new GObjects, this is nicer than calling the setter methods manually. The implementation struct holds the settings. You can see that we embed Settings in std::cell::OnceCell . This is a nice alternative to RefCell> when you know that you will initialize the value only once. We also override the constructed and close_request methods, where we load or save the window state. Filename: listings/saving_window_state/1/custom_window/imp.rs # use gio::Settings;\n# use gtk::subclass::prelude::*;\n# use gtk::{gio, glib, ApplicationWindow};\n# use std::cell::OnceCell;\n# #[derive(Default)]\npub struct Window { pub settings: OnceCell,\n} #[glib::object_subclass]\nimpl ObjectSubclass for Window { const NAME: &'static str = \"MyGtkAppWindow\"; type Type = super::Window; type ParentType = ApplicationWindow;\n}\nimpl ObjectImpl for Window { fn constructed(&self) { self.parent_constructed(); // Load latest window state let obj = self.obj(); obj.setup_settings(); obj.load_window_size(); }\n}\nimpl WidgetImpl for Window {}\nimpl WindowImpl for Window { // Save window state right before the window will be closed fn close_request(&self) -> glib::Propagation { // Save window size self.obj() .save_window_size() .expect(\"Failed to save window state\"); // Allow to invoke other event handlers glib::Propagation::Proceed }\n}\nimpl ApplicationWindowImpl for Window {} That is it! Now our window retains its state between app sessions.","breadcrumbs":"Saving Window State » Saving Window State","id":"51","title":"Saving Window State"},"52":{"body":"Sometimes you want to display a list of elements in a certain arrangement. gtk::ListBox and gtk::FlowBox are two container widgets which allow you to do this. ListBox describes a vertical list and FlowBox describes a grid. Let's explore this concept by adding labels to a ListBox. Each label will display an integer starting from 0 and ranging up to 100. Filename: listings/list_widgets/1/main.rs # use gtk::prelude::*;\n# use gtk::{\n# glib, Application, ApplicationWindow, Label, ListBox, PolicyType, ScrolledWindow,\n# };\n# # const APP_ID: &str = \"org.gtk_rs.ListWidgets1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Create a `ListBox` and add labels with integers from 0 to 100 let list_box = ListBox::new(); for number in 0..=100 { let label = Label::new(Some(&number.to_string())); list_box.append(&label); }\n# # let scrolled_window = ScrolledWindow::builder()\n# .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling\n# .min_content_width(360)\n# .child(&list_box)\n# .build();\n# # // Create a window\n# let window = ApplicationWindow::builder()\n# .application(app)\n# .title(\"My GTK App\")\n# .default_width(600)\n# .default_height(300)\n# .child(&scrolled_window)\n# .build();\n# # // Present window\n# window.present();\n# } We cannot display so many widgets at once. Therefore, we add ListBox to a gtk::ScrolledWindow . Now we can scroll through our elements. Filename: listings/list_widgets/1/main.rs # use gtk::prelude::*;\n# use gtk::{\n# glib, Application, ApplicationWindow, Label, ListBox, PolicyType, ScrolledWindow,\n# };\n# # const APP_ID: &str = \"org.gtk_rs.ListWidgets1\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) {\n# // Create a `ListBox` and add labels with integers from 0 to 100\n# let list_box = ListBox::new();\n# for number in 0..=100 {\n# let label = Label::new(Some(&number.to_string()));\n# list_box.append(&label);\n# }\n# let scrolled_window = ScrolledWindow::builder() .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling .min_content_width(360) .child(&list_box) .build(); // Create a window let window = ApplicationWindow::builder() .application(app) .title(\"My GTK App\") .default_width(600) .default_height(300) .child(&scrolled_window) .build(); // Present window window.present();\n# }","breadcrumbs":"List Widgets » List Widgets","id":"52","title":"List Widgets"},"53":{"body":"That was easy enough. However, we currently create one widget per element. Since each widget takes up a bit of resources, many of them can lead to slow and unresponsive user interfaces. Depending on the widget type even thousands of elements might not be a problem. But how could we possibly deal with the infinite amount of posts in a social media timeline? We use scalable lists instead! The model holds our data, filters it and describes its order. The list item factory defines how the data transforms into widgets. The view specifies how the widgets are then arranged. What makes this concept scalable is that GTK only has to create slightly more widgets than we can currently look at. As we scroll through our elements, the widgets which become invisible will be reused. The following figure demonstrates how this works in practice. 100 000 elements is something ListBox will struggle with, so let's use this to demonstrate scalable lists. We start by defining and filling up our model. The model is an instance of gio::ListStore . The main limitation here is that gio::ListStore only accepts GObjects. So let's create a custom GObject IntegerObject that which is initialized with a number. Filename: listings/list_widgets/2/integer_object/mod.rs # mod imp;\n# # use glib::Object;\n# use gtk::glib;\n# glib::wrapper! { pub struct IntegerObject(ObjectSubclass);\n} impl IntegerObject { pub fn new(number: i32) -> Self { Object::builder().property(\"number\", number).build() }\n}\n# This number represents the internal state of IntegerObject. Filename: listings/list_widgets/2/integer_object/imp.rs # use std::cell::Cell;\n# # use glib::Properties;\n# use gtk::glib;\n# use gtk::prelude::*;\n# use gtk::subclass::prelude::*;\n# // Object holding the state\n#[derive(Properties, Default)]\n#[properties(wrapper_type = super::IntegerObject)]\npub struct IntegerObject { #[property(get, set)] number: Cell,\n}\n# # // The central trait for subclassing a GObject\n# #[glib::object_subclass]\n# impl ObjectSubclass for IntegerObject {\n# const NAME: &'static str = \"MyGtkAppIntegerObject\";\n# type Type = super::IntegerObject;\n# }\n# # // Trait shared by all GObjects\n# #[glib::derived_properties]\n# impl ObjectImpl for IntegerObject {}\n# We now fill the model with integers from 0 to 100 000. Please note that models only takes care of the data. Neither Label nor any other widget is mentioned here. Filename: listings/list_widgets/2/main.rs # mod integer_object;\n# # use gtk::{\n# gio, glib, Application, ApplicationWindow, Label, ListView, PolicyType,\n# ScrolledWindow, SignalListItemFactory, SingleSelection,\n# };\n# use gtk::{prelude::*, ListItem};\n# use integer_object::IntegerObject;\n# # const APP_ID: &str = \"org.gtk_rs.ListWidgets2\";\n# # fn main() -> glib::ExitCode {\n# // Create a new application\n# let app = Application::builder().application_id(APP_ID).build();\n# # // Connect to \"activate\" signal of `app`\n# app.connect_activate(build_ui);\n# # // Run the application\n# app.run()\n# }\n# # fn build_ui(app: &Application) { // Create a `Vec` with numbers from 0 to 100_000 let vector: Vec = (0..=100_000).map(IntegerObject::new).collect(); // Create new model let model = gio::ListStore::new::(); // Add the vector to the model model.extend_from_slice(&vector);\n# # let factory = SignalListItemFactory::new();\n# factory.connect_setup(move |_, list_item| {\n# let label = Label::new(None);\n# list_item\n# .downcast_ref::()\n# .expect(\"Needs to be ListItem\")\n# .set_child(Some(&label));\n# });\n# # factory.connect_bind(move |_, list_item| {\n# // Get `IntegerObject` from `ListItem`\n# let integer_object = list_item\n# .downcast_ref::()\n# .expect(\"Needs to be ListItem\")\n# .item()\n# .and_downcast::()\n# .expect(\"The item has to be an `IntegerObject`.\");\n# # // Get `Label` from `ListItem`\n# let label = list_item\n# .downcast_ref::()\n# .expect(\"Needs to be ListItem\")\n# .child()\n# .and_downcast::