GUI development with Rust and GTK 4

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 do not have to worry about hitting bottlenecks mid-project anymore. Additionally, with Rust you will have nice things like:

  • Thread safety
  • Memory safety
  • Sensible dependency management
  • Excellent third party libraries, which benefit from the same points as mentioned above

The gtk-rs project provides bindings to many GTK-related libraries which we will be using throughout this book.

Who this book is for

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.

How to use this book

In general, this book assumes that you’re 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 have 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.

License

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.

Prerequisites

There are a few recommended ways to set up your workstation in order to develop gtk-rs applications. Let us go through them one by one.

Cargo

Cargo is Rust's build system and package manager. If following the book is all you care about, using only Cargo will work fine for you.

Let us 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 by executing:

cargo new my-gtk-app

Add the following lines to your Cargo.toml.

[dependencies.gtk]
version = "0.1"
package = "gtk4"

Now you can run your application by executing:

cargo run

Cargo + Meson

Cargo is nearly enough, but it is not well suited for handling resources such as icons or UI definition files. That is why we recommend to use Meson on top of it. It is cross-platform and its syntax is very readable. Meson takes care of

Here as well, you first follow the instructions on the GTK website in order to install GTK 4. Then install Rust with rustup. Finally, install Meson by following the instructions on the Meson website.

You can download a ready-to-use gtk-rust-template here. Follow the instructions in the README to initialize your own application. Then configure your project.

meson --prefix=/usr build

In order to compile and install it run the following commands. You have to execute it every time you modify your application.

ninja -C build && ninja -C build install

Now the application should be in a folder included in your system path. You can either start it with the application launcher of your choice or in the terminal.

Cargo + Meson + Flatpak

If you develop on Linux, using Flatpak is the most convenient option. With Flatpak your whole workflow is containerized and your users get the very same application you develop on (including all dependencies). First, assure that Flatpak is installed on your system, check this website to see if any steps are necessary on your distribution. Then download the gtk-rust-template and follow the instructions in its README.

Then either install:

That is it. The build dependencies can be downloaded by the IDE. With GNOME Builder, you only have to press the run button for that.

Hello World!

Now that we have got a working installation, let us get right into it!

At the very least, we need to create an Application instance, with an application id and the default application flags. This guide helps you find a suitable application id for your app.

Filename: listings/hello_world/1/main.rs

use gtk::prelude::*;
use gtk::Application;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());

    // Run the application
    app.run();
}

It builds fine, but nothing appears on our screen. GTK warns us, that it would have expected that something would be called in its activate step. So let us create a window there.

Filename: listings/hello_world/2/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    window.present();
}

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::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::with_label("Press me!");

    // Set the button margins
    button.set_margin_top(12);
    button.set_margin_bottom(12);
    button.set_margin_start(12);
    button.set_margin_end(12);

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

There is now a button and if we click on it, its label becomes "Hello World!".

Was not that hard to create our first gtk-rs app, right? Let us now get a better understanding of what we did here.

Widgets

Widgets are the components that make up a GTK application. GTK offers many-preexisting ones and if those do not fit, you can even create custom ones. There are 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 us say we want to add a button to our app. We have quite a bit of choice here, but let us 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
╰── Widget
    ╰── Button

The GTK documentation also tells us that Button implements the interfaces GtkAccessible, GtkActionable, GtkBuildable, GtkConstraintTarget.

Now let us compare that with the corresponding Button struct in gtk-rs. The gtk-rs documentation tells us which methods and 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/widgets/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::with_label("Press me!");

    // Set the button margins
    button.set_margin_top(18);
    button.set_margin_bottom(18);
    button.set_margin_start(18);
    button.set_margin_end(18);

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });
    // Add button
    window.set_child(Some(&button));
    window.present();
}

Please note that Rust requires bringing traits into scope, before using one of its methods. In our example we did that by adding the following line:

Filename: listings/widgets/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::with_label("Press me!");

    // Set the button margins
    button.set_margin_top(18);
    button.set_margin_bottom(18);
    button.set_margin_start(18);
    button.set_margin_end(18);

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });
    // Add button
    window.set_child(Some(&button));
    window.present();
}

With it, we import all necessary traits for dealing with widgets. You probably want to bring the prelude into scope in most of your source files.

This is also a good moment to mention that all gtk-rs widgets support the builder pattern. This is especially nice for creating widgets where multiple widget characteristics are already known during its creation. We can then make the construction of our button neater, by creating a ButtonBuilder with the Button::builder method.

Filename: listings/widgets/2/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

GObject Concepts

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 already learned, that gtk-rs maps GObject concepts like inheritance and interfaces to Rust traits. In this chapter we will additionally find out:

  • How to manage the memory of GObjects.
  • How to create our own GObjects via subclassing.
  • How to deal with generic values.
  • How to use properties.
  • How to emit and receive signals.

Memory management of GObjects

A GObject (or glib::Object in Rust terms) is a reference-counted, mutable object. Let us see in a set of real life examples which consequences this has.

use gtk::prelude::*;
use gtk::{self, Application, ApplicationWindow, Button, Orientation};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);
    
    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Create two buttons
    let button_increase = Button::with_label("Increase");
    let button_decrease = Button::with_label("Decrease");

    // A mutable integer
    let mut number = 0;

    // Connect callbacks
    // When a button is clicked, `number` should be changed
    button_increase.connect_clicked(|_| number += 1);
    button_decrease.connect_clicked(|_| number -= 1);

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

Here we would like to create a simple app with two buttons. If we click on one button, an integer number should be increased. If we press the other one, it should be decreased. The Rust compiler refuses to compile it though.

For once the borrow checker kicked in:

error[E0499]: cannot borrow `number` as mutable more than once at a time
  --> main.rs:27:37
   |
26 |     button_increase.connect_clicked(|_| number += 1);
   |     ------------------------------------------------
   |     |                               |   |
   |     |                               |   first borrow occurs due to use of `number` in closure
   |     |                               first mutable borrow occurs here
   |     argument requires that `number` is borrowed for `'static`
27 |     button_decrease.connect_clicked(|_| number -= 1);
   |                                     ^^^ ------ second borrow occurs due to use of `number` in closure
   |                                     |
   |                                     second mutable borrow occurs here

Also, the compiler tells us that our closures may outlive number:


error[E0373]: closure may outlive the current function, but it borrows `number`, which is owned by the current function
  --> main.rs:26:37
   |
26 |     button_increase.connect_clicked(|_| number += 1);
   |                                     ^^^ ------ `number` is borrowed here
   |                                     |
   |                                     may outlive borrowed value `number`
   |
note: function requires argument type to outlive `'static`
  --> main.rs:26:5
   |
26 |     button_increase.connect_clicked(|_| number += 1);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `number` (and any other referenced variables), use the `move` keyword
   |
26 |     button_increase.connect_clicked(move |_| number += 1);
   |                                     ^^^^^^^^

Thinking about the second error message, it makes sense that the closure requires the lifetimes of references to be 'static. The compiler cannot know when the user presses a button, so references must live forever. And our number gets immediately deallocated after it reaches the end of its scope. The error message is also suggesting that we could take ownership of number. But is there actually a way that both closures could take ownership of the same value?

Yes! That is exactly what the Rc type is there for. The Rc counts the number of strong references created via Clone::clone and released via Drop::drop, and only deallocates it when this number drops to zero. We call every object containing a strong reference a shared owner of the value. If we want to modify the content of our Rc, we can use the Cell type.1

Filename: listings/gobject_memory_management/1/main.rs

use gtk::prelude::*;
use gtk::Application;
use gtk::{self, ApplicationWindow, Button, Orientation};
use std::{cell::Cell, rc::Rc};
fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}
fn build_ui(application: &Application) {
    // … create a new window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // 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_1 = number.clone();
    button_increase.connect_clicked(move |_| number_copy_1.set(number_copy_1.get() + 1));
    button_decrease.connect_clicked(move |_| number.set(number.get() - 1));

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

It is not very nice though to fill the scope with temporary variables like number_copy_1. We can improve that by using the glib::clone! macro.

Filename: listings/gobject_memory_management/2/main.rs

use glib::clone;
use gtk::prelude::*;
use gtk::{self, ApplicationWindow, Button, Orientation};
use gtk::{glib, Application};
use std::{cell::Cell, rc::Rc};
fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}
fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Reference-counted object with inner mutability
    let number = Rc::new(Cell::new(0));
    // Connect callbacks
    // 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);
    });

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

Just like Rc<Cell<T>>, 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/gobject_memory_management/3/main.rs

use std::{cell::Cell, rc::Rc};

use glib::clone;
use gtk::prelude::*;
use gtk::{self, ApplicationWindow, Button, Orientation};
use gtk::{glib, Application};

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    let number = Rc::new(Cell::new(0));

    // Connect callbacks
    // When a button is clicked, `number` and label of the other button will be changed
    button_increase.connect_clicked(clone!(@strong 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());
    }));

    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

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 do not 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 instead2.

Filename: listings/gobject_memory_management/4/main.rs

use std::{cell::Cell, rc::Rc};

use glib::clone;
use gtk::prelude::*;
use gtk::{self, ApplicationWindow, Button, Orientation};
use gtk::{glib, Application};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Reference-counted object with inner mutability
    let number = Rc::new(Cell::new(0));

    // Connect callbacks
    // When a button is clicked, `number` and label of the other button will be changed
    button_increase.connect_clicked(clone!(@strong 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());
    }));

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

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, upgrade will return None. Per default, it immediately returns from the closure with () as return value. In case the closure expects a different return value or a panic is preferred @default-return or @default-panic. For more information about glib::clone, please have a look at the docs.

Notice that we kept the strong reference to number. If we had a weak reference, no one 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/gobject_memory_management/4/main.rs

use std::{cell::Cell, rc::Rc};

use glib::clone;
use gtk::prelude::*;
use gtk::{self, ApplicationWindow, Button, Orientation};
use gtk::{glib, Application};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Reference-counted object with inner mutability
    let number = Rc::new(Cell::new(0));

    // Connect callbacks
    // When a button is clicked, `number` and label of the other button will be changed
    button_increase.connect_clicked(clone!(@strong 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());
    }));

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

When we append the buttons to the gtk_box, gtk_box keeps a strong reference to them.

Filename: listings/gobject_memory_management/4/main.rs

use std::{cell::Cell, rc::Rc};

use glib::clone;
use gtk::prelude::*;
use gtk::{self, ApplicationWindow, Button, Orientation};
use gtk::{glib, Application};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Reference-counted object with inner mutability
    let number = Rc::new(Cell::new(0));

    // Connect callbacks
    // When a button is clicked, `number` and label of the other button will be changed
    button_increase.connect_clicked(clone!(@strong 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());
    }));

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

When we set gtk_box as child of window, window keeps a strong reference to it.

Filename: listings/gobject_memory_management/4/main.rs

use std::{cell::Cell, rc::Rc};

use glib::clone;
use gtk::prelude::*;
use gtk::{self, ApplicationWindow, Button, Orientation};
use gtk::{glib, Application};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create two buttons
    let button_increase = Button::builder()
        .label("Increase")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();
    let button_decrease = Button::builder()
        .label("Decrease")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Reference-counted object with inner mutability
    let number = Rc::new(Cell::new(0));

    // Connect callbacks
    // When a button is clicked, `number` and label of the other button will be changed
    button_increase.connect_clicked(clone!(@strong 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());
    }));

    // Add buttons
    let gtk_box = gtk::Box::new(Orientation::Vertical, 0);
    window.set_child(Some(&gtk_box));
    gtk_box.append(&button_increase);
    gtk_box.append(&button_decrease);
    window.present();
}

During the creation of our window, we pass application to it. Because of that, application holds a strong reference to window. application lives for the whole lifetime of our program, which explains why weak references within our closures are sufficient.

As long as you use weak references whenever possible you will find it perfectly doable to avoid memory cycles within your application. Then, you can fully rely on GTK to properly manage the memory of GObjects you pass to it.

1

Please be aware that Cell is only a suitable type for Copy types. For other types, RefCell is the way to go. You can learn more about the two cell types in this section of an older edition of the Rust book.

2

In this simple example, GTK actually resolves the reference cycle on its own once you close the window. However, the general point to avoid strong references whenever possible remains valid.

Subclassing

GObjects rely heavily on inheritance. Therefore, it makes sense that if we want to create a custom GObject, this is done via subclassing. Let us 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. It is advised to keep it in a private module, since its state and methods are only meant to be used by the GObject itself. It therefore corresponds to the private section of objects in languages like Java and C++.

Filename: listings/gobject_subclassing/1/custom_button/imp.rs


#![allow(unused)]
fn main() {
use gtk::glib;
use gtk::subclass::prelude::*;

// Object holding the state
#[derive(Default)]
pub struct CustomButton;

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

// Trait shared by all buttons
impl ButtonImpl for CustomButton {}
}

The description of the subclassing is in ObjectSubclass.

  • NAME should consist of crate-name, module-path and object-name in order to avoid name collisions. Use PascalCase 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 our custom GObject.

Filename: listings/gobject_subclassing/1/custom_button/mod.rs

mod imp;

use glib::Object;
use gtk::glib;

glib::wrapper! {
    pub struct CustomButton(ObjectSubclass<imp::CustomButton>)
        @extends gtk::Button, gtk::Widget;
}

impl CustomButton {
    pub fn new() -> Self {
        Object::new(&[]).expect("Failed to create Button")
    }

    pub fn with_label(label: &str) -> Self {
        Object::new(&[("label", &label)]).expect("Failed to create Button")
    }
}

impl Default for CustomButton {
    fn default() -> Self {
        Self::new()
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

glib::wrapper! does the most of the work of subclassing for us. We only have to point to the implementation struct and which ancestor GObjects we extend. Please note that the list of ancestor GObjects does not mention glib::Object. This is because glib::Object is always the base class in the object hierarchy and therefore already implied.

After these steps, nothing is stopping us anymore from replacing gtk::Button with our CustomButton.

Filename: listings/gobject_subclassing/1/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // 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 callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

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 us make it a bit more interesting! gtk::Button does not hold much state, but we can let CustomButton hold a number.

Filename: listings/gobject_subclassing/2/custom_button/imp.rs

use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use std::cell::Cell;

// Object holding the state
#[derive(Default)]
pub struct CustomButton {
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {
    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);
        obj.set_label(&self.number.get().to_string());
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self, button: &Self::Type) {
        self.number.set(self.number.get() + 1);
        button.set_label(&self.number.get().to_string())
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

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/gobject_subclassing/2/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}
fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

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

    // Add button
    window.set_child(Some(&button));
    window.present();
}

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.

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.

Generic Values

Some GObject-related functions rely on generic values for their arguments or return parameters. Enums in C are not as powerful as the one in Rust, which is why glib::Value is used for this. Conceptually though, there are similar to a Rust enum defined like this:


#![allow(unused)]
fn main() {
enum Value <T> {
    bool(bool),
    i8(i8),
    i32(i32),
    u32(u32),
    i64(i64),
    u64(u64),
    f32(f32),
    f64(f64),
    // boxed types
    String(Option<String>),
    Object(Option<T: impl IsA<Object>>),
}

That means that a glib::Value can represent a set of types, but only one of them per instance. For example, this is how you would use a Value representing an i32.

Filename: listings/gobject_values/1/main.rs

use gtk::prelude::*;

fn main() {
    // Store `i32` as `Value`
    let integer_value = 10.to_value();

    // Retrieve `i32` from `Value`
    let integer = integer_value
        .get::<i32>()
        .expect("The value needs to be of type `i32`.");

    // Check if the retrieved value is correct
    assert_eq!(integer, 10);

    // Store string as `Value`
    let string_value = "Hello!".to_value();

    // Retrieve `String` from `Value`
    let string = string_value
        .get::<String>()
        .expect("The value needs to be of type `String`.");

    // Check if the retrieved value is correct
    assert_eq!(string, "Hello!".to_string());

    // Store `Option<String>` as `Value`
    let string_some_value = "Hello!".to_value();
    let string_none_value = None::<String>.to_value();

    // Retrieve `String` from `Value`
    let string_some = string_some_value
        .get::<Option<String>>()
        .expect("The value needs to be of type `String`.");
    let string_none = string_none_value
        .get::<Option<String>>()
        .expect("The value needs to be of type `String`.");

    // Check if the retrieved value is correct
    assert_eq!(string_some, Some("Hello!".to_string()));
    assert_eq!(string_none, None);
}

Please note that boxed types such as String or Object are wrapped in an Option. This comes from C, where boxed types can always 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/gobject_values/1/main.rs

use gtk::prelude::*;

fn main() {
    // Store `i32` as `Value`
    let integer_value = 10.to_value();

    // Retrieve `i32` from `Value`
    let integer = integer_value
        .get::<i32>()
        .expect("The value needs to be of type `i32`.");

    // Check if the retrieved value is correct
    assert_eq!(integer, 10);

    // Store string as `Value`
    let string_value = "Hello!".to_value();

    // Retrieve `String` from `Value`
    let string = string_value
        .get::<String>()
        .expect("The value needs to be of type `String`.");

    // Check if the retrieved value is correct
    assert_eq!(string, "Hello!".to_string());

    // Store `Option<String>` as `Value`
    let string_some_value = "Hello!".to_value();
    let string_none_value = None::<String>.to_value();

    // Retrieve `String` from `Value`
    let string_some = string_some_value
        .get::<Option<String>>()
        .expect("The value needs to be of type `String`.");
    let string_none = string_none_value
        .get::<Option<String>>()
        .expect("The value needs to be of type `String`.");

    // Check if the retrieved value is correct
    assert_eq!(string_some, Some("Hello!".to_string()));
    assert_eq!(string_none, None);
}

If you want to differentiate between specifying the wrong type and a Value representing None, just call get::<Option<T>> instead.

use gtk::prelude::*;

fn main() {
    // Store `i32` as `Value`
    let integer_value = 10.to_value();

    // Retrieve `i32` from `Value`
    let integer = integer_value
        .get::<i32>()
        .expect("The value needs to be of type `i32`.");

    // Check if the retrieved value is correct
    assert_eq!(integer, 10);

    // Store string as `Value`
    let string_value = "Hello!".to_value();

    // Retrieve `String` from `Value`
    let string = string_value
        .get::<String>()
        .expect("The value needs to be of type `String`.");

    // Check if the retrieved value is correct
    assert_eq!(string, "Hello!".to_string());

    // Store `Option<String>` as `Value`
    let string_some_value = "Hello!".to_value();
    let string_none_value = None::<String>.to_value();

    // Retrieve `String` from `Value`
    let string_some = string_some_value
        .get::<Option<String>>()
        .expect("The value needs to be of type `String`.");
    let string_none = string_none_value
        .get::<Option<String>>()
        .expect("The value needs to be of type `String`.");

    // Check if the retrieved value is correct
    assert_eq!(string_some, Some("Hello!".to_string()));
    assert_eq!(string_none, None);
}

For now, all you need to know is how to convert a Rust type into a glib::Value and vice versa. This knowledge will be useful in the next chapters where we discuss properties, signals and models.

Properties

Properties provide a public API for accessing state of GObjects.

Let us see how this is done by experimenting with the Switch widget. One of its properties is the state. According to the GTK docs, it can be read and be written to. That is why gtk-rs provides corresponding state and set_state methods.

Filename: listings/gobject_properties/1/main.rs

use gtk::prelude::*;
use gtk::{Align, Box, Orientation, Switch};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the switch
    let switch = Switch::new();

    // Set and then immediately obtain state
    switch.set_state(true);
    let current_state = switch.state();

    // This prints: "The current state is true"
    println!("The current state is {}", current_state);

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch);
    window.set_child(Some(&gtk_box));
    window.present();
}

Alternatively, we can use the general property and set_property methods. Because they can be used for properties of different types, they operate with glib::Value.

Filename: listings/gobject_properties/2/main.rs

use gtk::prelude::*;
use gtk::{Align, Box, Orientation, Switch};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the switch
    let switch = Switch::new();

    // Set and then immediately obtain state
    switch.set_property("state", &true).unwrap();
    let current_state = switch
        .property("state")
        .expect("The property needs to exist and be readable.")
        .get::<bool>()
        .expect("The property needs to be of type `bool`.");

    // This prints: "The current state is true"
    println!("The current state is {}", current_state);

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch);
    window.set_child(Some(&gtk_box));
    window.present();
}

Dealing with a glib::Value is quite verbose. This is why you only want to use the generic methods for accessing properties you have added to your custom GObjects.

Properties can not only be accessed via getters & setters, they can also be bound to each other. Let us see how that would look like for two Switch instances.

Filename: listings/gobject_properties/3/main.rs

use glib::BindingFlags;
use gtk::{glib, Align, Orientation, Switch};
use gtk::{prelude::*, Box};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the switches
    let switch_1 = Switch::new();
    let switch_2 = Switch::new();

    switch_1
        .bind_property("state", &switch_2, "state")
        .flags(BindingFlags::BIDIRECTIONAL)
        .build();

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch_1);
    gtk_box.append(&switch_2);
    window.set_child(Some(&gtk_box));
    window.present();
}

In our case, we want to bind the "state" property of switch_1 to the "state" property of switch_2. We also want the binding to be bidirectional, so we specify this with the BindingFlags.

Filename: listings/gobject_properties/3/main.rs

use glib::BindingFlags;
use gtk::{glib, Align, Orientation, Switch};
use gtk::{prelude::*, Box};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the switches
    let switch_1 = Switch::new();
    let switch_2 = Switch::new();

    switch_1
        .bind_property("state", &switch_2, "state")
        .flags(BindingFlags::BIDIRECTIONAL)
        .build();

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch_1);
    gtk_box.append(&switch_2);
    window.set_child(Some(&gtk_box));
    window.present();
}

Now when we click on one of the two switches, the other one is toggled as well.

We can also add properties to custom GObjects. We can demonstrate that by binding the number of our CustomButton to a property. For that we need to be able to lazily evaluate expressions. The crate once_cell provides the Lazy type which allows us to do that. once_cell is already part of Rust nightly. Until it hits stable, we will add it as external dependency.

Filename: listings/Cargo.toml

[dependencies]
once_cell = "1"

Now we define the "number" property within the ObjectImpl implementation. The properties method describes our set of properties. When naming our property, we make sure to do that in kebab-case. Then we describe its type, range and default value. We also declare that the property can be read and be written to. set_property describes how the underlying values can be changed. property takes care of returning the underlying value. The formerly private number is now accessible via the property and set_property methods.

Filename: listings/gobject_properties/4/custom_button/imp.rs

use glib::{BindingFlags, ParamFlags, ParamSpec, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use once_cell::sync::Lazy;

use std::cell::Cell;

// Object holding the state
#[derive(Default)]
pub struct CustomButton {
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {
    fn properties() -> &'static [ParamSpec] {
        static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
            vec![ParamSpec::new_int(
                // Name
                "number",
                // Nickname
                "number",
                // Short description
                "number",
                // Minimum value
                i32::MIN,
                // Maximum value
                i32::MAX,
                // Default value
                0,
                // The property can be read and written to
                ParamFlags::READWRITE,
            )]
        });
        PROPERTIES.as_ref()
    }

    fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
        match pspec.name() {
            "number" => {
                let input_number = value.get().unwrap();
                self.number.replace(input_number);
            }
            _ => unimplemented!(),
        }
    }

    fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
        match pspec.name() {
            "number" => self.number.get().to_value(),
            _ => unimplemented!(),
        }
    }

    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        obj.bind_property("number", obj, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self, button: &Self::Type) {
        let incremented_number = self.number.get() + 1;
        button.set_property("number", &incremented_number).unwrap();
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

We can immediately take advantage of this new property by binding the "label" property to it. It even converts the integer value of "number" to the string of "label". Now we do not have to adapt the label in the "clicked" callback anymore.

Let us see what we can do with this by creating two custom buttons.

Filename: listings/gobject_properties/4/main.rs

mod custom_button;

use custom_button::CustomButton;
use glib::BindingFlags;
use gtk::{glib, Align, Orientation};
use gtk::{prelude::*, Box};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the buttons
    let button_1 = CustomButton::new();
    let button_2 = CustomButton::new();

    // 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(|_, value| {
            let number = value
                .get::<i32>()
                .expect("The property needs to be of type `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(|_, value| {
            let number = value
                .get::<i32>()
                .expect("The property needs to be of type `i32`.");
            let decremented_number = number - 1;
            Some(decremented_number.to_value())
        })
        .flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
        .build();

    // The closure will be called whenever the property "number" of `button_1` gets changed
    button_1.connect_notify_local(Some("number"), move |button, _| {
        let number = button
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");
        println!("The current number of `button_1` is {}.", number);
    });

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button_1);
    gtk_box.append(&button_2);
    window.set_child(Some(&gtk_box));
    window.present();
}

We have already seen that bound properties do not 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/gobject_properties/4/main.rs

mod custom_button;

use custom_button::CustomButton;
use glib::BindingFlags;
use gtk::{glib, Align, Orientation};
use gtk::{prelude::*, Box};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the buttons
    let button_1 = CustomButton::new();
    let button_2 = CustomButton::new();

    // 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(|_, value| {
            let number = value
                .get::<i32>()
                .expect("The property needs to be of type `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(|_, value| {
            let number = value
                .get::<i32>()
                .expect("The property needs to be of type `i32`.");
            let decremented_number = number - 1;
            Some(decremented_number.to_value())
        })
        .flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
        .build();

    // The closure will be called whenever the property "number" of `button_1` gets changed
    button_1.connect_notify_local(Some("number"), move |button, _| {
        let number = button
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");
        println!("The current number of `button_1` is {}.", number);
    });

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button_1);
    gtk_box.append(&button_2);
    window.set_child(Some(&gtk_box));
    window.present();
}

Now if we click on one button, the "number" and "label" properties of the other button change as well.

The final nice feature of properties is, that you can connect a callback to the event when a property gets changed. We can do this like this:

Filename: listings/gobject_properties/4/main.rs

mod custom_button;

use custom_button::CustomButton;
use glib::BindingFlags;
use gtk::{glib, Align, Orientation};
use gtk::{prelude::*, Box};
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create the buttons
    let button_1 = CustomButton::new();
    let button_2 = CustomButton::new();

    // 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(|_, value| {
            let number = value
                .get::<i32>()
                .expect("The property needs to be of type `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(|_, value| {
            let number = value
                .get::<i32>()
                .expect("The property needs to be of type `i32`.");
            let decremented_number = number - 1;
            Some(decremented_number.to_value())
        })
        .flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
        .build();

    // The closure will be called whenever the property "number" of `button_1` gets changed
    button_1.connect_notify_local(Some("number"), move |button, _| {
        let number = button
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");
        println!("The current number of `button_1` is {}.", number);
    });

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button_1);
    gtk_box.append(&button_2);
    window.set_child(Some(&gtk_box));
    window.present();
}

Now, whenever the "number" property gets changed, the closure gets executed and prints the current value of "number".

Introducing properties to your custom GObjects is useful if you want to

  • allow consumers to be able to access internal state
  • bind state of (different) GObjects
  • notify consumers whenever a property value changes

Signals

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/gobject_signals/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

If we wanted to, we could have connected to it with the generic connect_local method.

Filename: listings/gobject_signals/2/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::new(application);

    // Set the window title
    window.set_title(Some("My GTK App"));

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button
        .connect_local("clicked", false, move |args| {
            // Get the button from the arguments
            let button = args[0]
                .get::<Button>()
                .expect("The value needs to be of type `Button`.");
            // Set the label to "Hello World!" after the button has been clicked on
            button.set_label("Hello World!");
            None
        })
        .unwrap();

    // Add button
    window.set_child(Some(&button));
    window.present();
}

Similar to the generic way of accessing properties, the advantage of connect_local is that it also works with custom signals1.

Let us see how we can create our own signals. Again we do that by extending our CustomButton. First we override the necessary methods in ObjectImpl.

Filename: listings/gobject_signals/3/custom_button/imp.rs

use glib::subclass::Signal;
use glib::{BindingFlags, ParamFlags, ParamSpec, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

use once_cell::sync::Lazy;
use std::cell::Cell;

// Object holding the state
#[derive(Default)]
pub struct CustomButton {
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {
    fn signals() -> &'static [Signal] {
        static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
            vec![Signal::builder(
                // Signal name
                "max-number-reached",
                // Types of the values which will be sent to the signal handler
                &[i32::static_type().into()],
                // Type of the value the signal handler sends back
                <()>::static_type().into(),
            )
            .build()]
        });
        SIGNALS.as_ref()
    }

    fn properties() -> &'static [ParamSpec] {
        static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
            vec![ParamSpec::new_int(
                // Name
                "number",
                // Nickname
                "number",
                // Short description
                "number",
                // Minimum value
                i32::MIN,
                // Maximum value
                i32::MAX,
                // Default value
                0,
                // The property can be read and written to
                ParamFlags::READWRITE,
            )]
        });
        PROPERTIES.as_ref()
    }

    fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
        match pspec.name() {
            "number" => {
                let input_number = value.get().unwrap();
                self.number.replace(input_number);
            }
            _ => unimplemented!(),
        }
    }

    fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
        match pspec.name() {
            "number" => self.number.get().to_value(),
            _ => unimplemented!(),
        }
    }

    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        obj.bind_property("number", obj, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

static MAX_NUMBER: i32 = 8;
// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self, button: &Self::Type) {
        let incremented_number = self.number.get() + 1;
        // If `number` reached `MAX_NUMBER`,
        // emit "max-number-reached" signal and set `number` back to 0
        if incremented_number == MAX_NUMBER {
            button
                .emit_by_name("max-number-reached", &[&incremented_number])
                .unwrap();
            button.set_property("number", &0).unwrap();
        } else {
            button.set_property("number", &incremented_number).unwrap();
        }
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

The signal 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 and expects nothing in return.

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/gobject_signals/3/custom_button/imp.rs

use glib::subclass::Signal;
use glib::{BindingFlags, ParamFlags, ParamSpec, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

use once_cell::sync::Lazy;
use std::cell::Cell;

// Object holding the state
#[derive(Default)]
pub struct CustomButton {
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
impl ObjectImpl for CustomButton {
    fn signals() -> &'static [Signal] {
        static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
            vec![Signal::builder(
                // Signal name
                "max-number-reached",
                // Types of the values which will be sent to the signal handler
                &[i32::static_type().into()],
                // Type of the value the signal handler sends back
                <()>::static_type().into(),
            )
            .build()]
        });
        SIGNALS.as_ref()
    }

    fn properties() -> &'static [ParamSpec] {
        static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
            vec![ParamSpec::new_int(
                // Name
                "number",
                // Nickname
                "number",
                // Short description
                "number",
                // Minimum value
                i32::MIN,
                // Maximum value
                i32::MAX,
                // Default value
                0,
                // The property can be read and written to
                ParamFlags::READWRITE,
            )]
        });
        PROPERTIES.as_ref()
    }

    fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
        match pspec.name() {
            "number" => {
                let input_number = value.get().unwrap();
                self.number.replace(input_number);
            }
            _ => unimplemented!(),
        }
    }

    fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
        match pspec.name() {
            "number" => self.number.get().to_value(),
            _ => unimplemented!(),
        }
    }

    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        obj.bind_property("number", obj, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

static MAX_NUMBER: i32 = 8;
// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self, button: &Self::Type) {
        let incremented_number = self.number.get() + 1;
        // If `number` reached `MAX_NUMBER`,
        // emit "max-number-reached" signal and set `number` back to 0
        if incremented_number == MAX_NUMBER {
            button
                .emit_by_name("max-number-reached", &[&incremented_number])
                .unwrap();
            button.set_property("number", &0).unwrap();
        } else {
            button.set_property("number", &incremented_number).unwrap();
        }
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

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/gobject_signals/3/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}
fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

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

    button
        .connect_local("max-number-reached", false, move |args| {
            // Get the number from the arguments
            // args[0] would return the `CustomButton` instance
            let number = args[1]
                .get::<i32>()
                .expect("The value needs to be of type `i32`.");
            println!("The maximum number {} has been reached", number);
            None
        })
        .unwrap();

    window.set_child(Some(&button));
    window.present();
}

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.

1

If you want to connect from a different thread than the main thread, make sure to use connect instead of connect_local. However, that also means that your connected closure has to implement Send + Sync.

The main event loop

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. Let us look at one example.

Filename: listings/main_event_loop/1/main.rs

use std::time::Duration;

use gtk::prelude::*;
use gtk::{self, Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |_| {
        // GUI is blocked for 5 seconds after the button is pressed
        let five_seconds = Duration::from_secs(5);
        std::thread::sleep(five_seconds);
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

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 it is not unusual wanting to run a slightly longer operation in one go. For that we just need to spawn a new thread and let the operation run there.

Filename: listings/main_event_loop/2/main.rs

use std::{thread, time::Duration};

use gtk::prelude::*;
use gtk::{self, Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |_| {
        // The long running operation runs now in a separate thread
        thread::spawn(move || {
            let five_seconds = Duration::from_secs(5);
            thread::sleep(five_seconds);
        });
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

If you come from another language than Rust, you might be uncomfortable with the thought of spawning new 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.

Normally we want to keep track of the work in the thread. In our case, we don't want the user to spawn additional threads while an existing one is still running. In order to achieve that we can create a channel. The main loop allows us to send a message from multiple places to a single receiver at the main thread. We want to send a bool to inform, whether we want the button to react to clicks or not.

Filename: listings/main_event_loop/3/main.rs

use std::{thread, time::Duration};

use glib::{clone, Continue, MainContext, PRIORITY_DEFAULT};
use gtk::glib;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
    // Connect callback
    button.connect_clicked(move |_| {
        let sender = sender.clone();
        // The long running operation runs now in a separate thread
        thread::spawn(move || {
            // Deactivate the button until the operation is done
            sender.send(false).unwrap();
            let ten_seconds = Duration::from_secs(10);
            thread::sleep(ten_seconds);
            // Activate the button again
            sender.send(true).unwrap();
        });
    });

    // The main loop executes the closure as soon as it receives the message
    receiver.attach(
        None,
        clone!(@weak button => @default-return Continue(false),
                    move |enable_button| {
                        button.set_sensitive(enable_button);
                        Continue(true)
                    }
        ),
    );

    // Add button
    window.set_child(Some(&button));
    window.present();
}

Spawning threads is not the only way to run operations asynchronously. You can also let the main loop take care of running async functions. If you do that from the main thread use spawn_local, from other threads spawn has to be used. The converted code looks and behaves very similar to the multi-threaded code.

Filename: listings/main_event_loop/4/main.rs

use glib::{clone, timeout_future_seconds, Continue, MainContext, PRIORITY_DEFAULT};
use gtk::glib;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
    // Connect callback
    button.connect_clicked(move |_| {
        let main_context = MainContext::default();
        // The main loop executes the asynchronous block
        main_context.spawn_local(clone!(@strong sender => async move {
            // Deactivate the button until the operation is done
            sender.send(false).unwrap();
            timeout_future_seconds(5).await;
            // Activate the button again
            sender.send(true).unwrap();
        }));
    });

    // The main loop executes the closure as soon as it receives the message
    receiver.attach(
        None,
        clone!(@weak button => @default-return Continue(false),
                    move |enable_button| {
                        button.set_sensitive(enable_button);
                        Continue(true)
                    }
        ),
    );

    // Add button
    window.set_child(Some(&button));
    window.present();
}

Since we are single-threaded again, we could even get rid of the channels while achieving the same result.

Filename: listings/main_event_loop/5/main.rs

use glib::{clone, timeout_future_seconds, MainContext};
use gtk::glib;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Connect callback
    button.connect_clicked(move |button| {
        let main_context = MainContext::default();
        // The main loop executes the asynchronous block
        main_context.spawn_local(clone!(@weak button => async move {
            // Deactivate the button until the operation is done
            button.set_sensitive(false);
            timeout_future_seconds(5).await;
            // Activate the button again
            button.set_sensitive(true);
        }));
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

But why did we not do the same thing with our multi-threaded example?

use std::{thread, time::Duration};

use glib::{clone, MainContext, PRIORITY_DEFAULT};
use gtk::glib;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Get command-line arguments
    let args: Vec<String> = args().collect();
    // Run the application
    app.run(&args);
}

// When the application is launched…
fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Create a button
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // DOES NOT COMPILE
    
    // Connect callback
    button.connect_clicked(move |button| {
        button.clone();
        // The long running operation runs now in a separate thread
        thread::spawn(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);
        });
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

Simply because we would get this error message:

error[E0277]: `NonNull<GObject>` cannot be shared between threads safely

help: within `gtk4::Button`, the trait `Sync` is not implemented for `NonNull<GObject>`

After reference cycles we found the second disadvantage of GTK GObjects: They are not thread safe.

So when should you spawn an async block and when should you spawn a thread?

  • If you have async functions for your IO-bound operations at your disposal, feel free to spawn them on the main loop.
  • If your operation is computation-bound or there is no async function available, you have to spawn threads.

Settings

We have now learned multiple ways to handle states. However, every time we close the application all of it is gone. Let us learn how to use GSettings 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.example.gschema.xml

<?xml version="1.0" encoding="utf-8"?>
<schemalist>
  <schema id="org.gtk.example" path="/org/gtk/example/">
    <key name="is-switch-enabled" type="b">
      <default>false</default>
      <summary>Default switch state</summary>
    </key>
  </schema>
</schemalist>

Let us 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;
use gtk::gio;
use gtk::{glib::signal::Inhibit, prelude::*};
use gtk::{Align, Application, ApplicationWindow, Switch};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Initialize settings
    let settings = Settings::new("org.gtk.example");

    // 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();

    switch.connect_state_set(move |_, is_enabled| {
        // Save changed switch state in the settings
        settings
            .set_boolean("is-switch-enabled", is_enabled)
            .unwrap();
        // Do not inhibit the the default handler
        Inhibit(false)
    });

    // Add button
    window.set_child(Some(&switch));
    window.present();
}

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). Finally, we define its default value and add a summary.

Now we have to install the GSchema by executing the following commands:

$ sudo install -D org.gtk.example.gschema.xml /usr/share/glib-2.0/schemas/
$ sudo glib-compile-schemas /usr/share/glib-2.0/schemas/

This has to be repeated every time we change the GSchema. That is why we probably want to use a build system like Meson to do it for us.

We initialize the Settings object by specifying the application id.

Filename: listings/settings/1/main.rs

use gio::Settings;
use gtk::gio;
use gtk::{glib::signal::Inhibit, prelude::*};
use gtk::{Align, Application, ApplicationWindow, Switch};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Initialize settings
    let settings = Settings::new("org.gtk.example");

    // 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();

    switch.connect_state_set(move |_, is_enabled| {
        // Save changed switch state in the settings
        settings
            .set_boolean("is-switch-enabled", is_enabled)
            .unwrap();
        // Do not inhibit the the default handler
        Inhibit(false)
    });

    // Add button
    window.set_child(Some(&switch));
    window.present();
}

Then we get the settings key and use it when we create our Switch.

Filename: listings/settings/1/main.rs

use gio::Settings;
use gtk::gio;
use gtk::{glib::signal::Inhibit, prelude::*};
use gtk::{Align, Application, ApplicationWindow, Switch};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Initialize settings
    let settings = Settings::new("org.gtk.example");

    // 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();

    switch.connect_state_set(move |_, is_enabled| {
        // Save changed switch state in the settings
        settings
            .set_boolean("is-switch-enabled", is_enabled)
            .unwrap();
        // Do not inhibit the the default handler
        Inhibit(false)
    });

    // Add button
    window.set_child(Some(&switch));
    window.present();
}

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;
use gtk::gio;
use gtk::{glib::signal::Inhibit, prelude::*};
use gtk::{Align, Application, ApplicationWindow, Switch};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Initialize settings
    let settings = Settings::new("org.gtk.example");

    // 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();

    switch.connect_state_set(move |_, is_enabled| {
        // Save changed switch state in the settings
        settings
            .set_boolean("is-switch-enabled", is_enabled)
            .unwrap();
        // Do not inhibit the the default handler
        Inhibit(false)
    });

    // Add button
    window.set_child(Some(&switch));
    window.present();
}

The Switch now retains its state even after closing the application. But we can make this even better. The Switch has a property "state" and Settings allows us to bind properties to a specific setting. So let us 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. Additionally, we specify SettingsBindFlags to control the direction in which the binding works.

Filename: listings/settings/2/main.rs

use gio::{Settings, SettingsBindFlags};
use gtk::gio;
use gtk::prelude::*;
use gtk::{Align, Application, ApplicationWindow, Switch};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .build();

    // Initialize settings
    let settings = Settings::new("org.gtk.example");

    // 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)
        .build();

    settings
        .bind("is-switch-enabled", &switch, "state")
        .flags(SettingsBindFlags::DEFAULT)
        .build();

    // Add button
    window.set_child(Some(&switch));
    window.present();
}

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.

Saving Window State

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 Settings.

Filename: listings/saving_window_state/1/org.gtk.example.gschema.xml

<?xml version="1.0" encoding="utf-8"?>
<schemalist>
  <schema id="org.gtk.example" path="/org/gtk/example/">
    <key name="window-width" type="i">
      <default>-1</default>
      <summary>Default window width</summary>
    </key>
    <key name="window-height" type="i">
      <default>-1</default>
      <summary>Default window height</summary>
    </key>
    <key name="is-maximized" type="b">
      <default>false</default>
      <summary>Default window maximized behaviour</summary>
    </key>
  </schema>
</schemalist>

Since we do not 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 methods for getting and setting the window state.

Filename: listings/saving_window_state/1/custom_window/mod.rs

mod imp;

use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::Application;
use gtk::{gio, glib};

glib::wrapper! {
    pub struct CustomWindow(ObjectSubclass<imp::CustomWindow>)
        @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow,
        @implements gio::ActionMap, gio::ActionGroup;
}

impl CustomWindow {
    pub fn new(app: &Application) -> Self {
        let window: Self = Object::new(&[]).expect("Failed to create CustomWindow");
        window.set_application(Some(app));
        window
    }

    pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
        // Get `settings` from `imp::CustomWindow`
        let settings = &imp::CustomWindow::from_instance(self).settings;

        // Get the size of the window
        let size = self.default_size();

        // Get the window state from `settings`
        settings.set_int("window-width", size.0)?;
        settings.set_int("window-height", size.1)?;
        settings.set_boolean("is-maximized", self.is_maximized())?;

        Ok(())
    }

    fn load_window_size(&self) {
        // Get `settings` from `imp::CustomWindow`
        let settings = &imp::CustomWindow::from_instance(self).settings;

        // Set the window state in `settings`
        let width = settings.int("window-width");
        let height = settings.int("window-height");
        let is_maximized = 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();
        }
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

The implementation struct holds the settings. We also overload 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;
use glib::signal::Inhibit;
use gtk::{gio, glib};
use gtk::{subclass::prelude::*, ApplicationWindow};

pub struct CustomWindow {
    pub settings: Settings,
}

#[glib::object_subclass]
impl ObjectSubclass for CustomWindow {
    const NAME: &'static str = "CustomWindow";
    type Type = super::CustomWindow;
    type ParentType = ApplicationWindow;

    fn new() -> Self {
        Self {
            settings: Settings::new("org.gtk.example"),
        }
    }
}
impl ObjectImpl for CustomWindow {
    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);
        // Load latest window state
        obj.load_window_size();
    }
}
impl WidgetImpl for CustomWindow {}
impl WindowImpl for CustomWindow {
    // Save window state right before the window will be closed
    fn close_request(&self, obj: &Self::Type) -> Inhibit {
        if let Err(err) = obj.save_window_size() {
            log::error!("Failed to save window state, {}", &err);
        }
        // Do not inhibit the the default handler
        Inhibit(false)
    }
}
impl ApplicationWindowImpl for CustomWindow {}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

That is it! Now our window retains its state between app sessions.

Please note how we handle a failure in saving into the settings. We do not want to panic for recoverable errors. We might also not want to present all problems at the GUI. In our case we could not even do this, because the window will be immediately closed after the error occurs. Logging is the standard way of handling a situation like this. For that, we need to add the log crate and one of its front-ends, such as pretty_env_logger, to our dependencies.

Filename: listings/Cargo.toml

[dependencies]
log = "0.4"
pretty_env_logger = "0.4"

We then have to initialize pretty_env_logger by calling init in main.

Filename: listings/saving_window_state/1/main.rs

mod custom_window;

use custom_window::CustomWindow;
use gtk::prelude::*;
use gtk::{Application, Button};

fn main() {
    // Initialize logger
    pretty_env_logger::init();

    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = CustomWindow::new(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 callback
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

We can now modify the log level by setting the RUST_LOG environment variable as can be seen here

Lists

Sometimes you want to display a list of elements in a certain arrangement. ListBox and FlowBox are two container widgets which allow you to do this. ListBox describes a vertical list and FlowBox describes a grid.

Let us 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/lists/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Label, ListBox, PolicyType, ScrolledWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

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

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_box)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

We cannot display so many widgets at once. Therefore, we add ListBox to a ScrolledWindow. Now we can scroll through our elements.

Filename: listings/lists/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Label, ListBox, PolicyType, ScrolledWindow};

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

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

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_box)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

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 us 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. What we would need is a GObject which holds an integer and exposes it as property. To get that we just have to adapt the CustomButton we created in the subclassing chapter. We only need to let it inherit from GObject instead of Button and let the new method accept an integer as parameter.

Filename: listings/lists/2/integer_object/mod.rs

mod imp;

use glib::Object;
use gtk::glib;

glib::wrapper! {
    pub struct IntegerObject(ObjectSubclass<imp::IntegerObject>);
}

impl IntegerObject {
    pub fn new(number: i32) -> Self {
        Object::new(&[("number", &number)]).unwrap()
    }
}

// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

We now fill the model with integers from 0 to 100 000. Please note that the models only takes care of the data. Neither Label nor any other widget is mentioned here.

Filename: listings/lists/2/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `i32` from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Set "label" to "number"
        label.set_label(&number.to_string());
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

The ListItemFactory takes care of the widgets as well as their relationship to the model. Here, we use the SignalListItemFactory which emits a signal for every relevant step in the life of a ListItem. The "setup" signal will be emitted when new widgets have to be created. We connect to it to create a Label for every requested widget.

Filename: listings/lists/2/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `i32` from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Set "label" to "number"
        label.set_label(&number.to_string());
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

In the "bind" step we bind the data in our model to the individual list items.

Filename: listings/lists/2/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `i32` from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Set "label" to "number"
        label.set_label(&number.to_string());
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

We only want single items to be selectable, so we choose SingleSelection. The other options would have been MultiSelection or NoSelection. Then we pass the model and the factory to the ListView.

Filename: listings/lists/2/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `i32` from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Set "label" to "number"
        label.set_label(&number.to_string());
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

Every ListView has to be a direct child of a ScrolledWindow, so we are adding it to one.

Filename: listings/lists/2/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `i32` from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Set "label" to "number"
        label.set_label(&number.to_string());
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

We can now easily scroll through our long list of integers.

Let us see what else we can do. We might want to increase the number every time we activate its row. For that we first add the method increase_number to our IntegerObject.

Filename: listings/lists/3/integer_object/mod.rs


#![allow(unused)]
fn main() {
mod imp;

use glib::Object;
use gtk::glib;
use gtk::prelude::*;

glib::wrapper! {
    pub struct IntegerObject(ObjectSubclass<imp::IntegerObject>);
}

impl IntegerObject {
    pub fn new(number: i32) -> Self {
        Object::new(&[("number", &number)]).unwrap()
    }

    pub fn increase_number(self) {
        let old_number = self
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        self.set_property("number", old_number + 1).unwrap();
    }
}
}

In order to interact with our ListView, we connect to its "activate" signal.

Filename: listings/lists/3/main.rs

mod integer_object;

use glib::BindingFlags;
use gtk::prelude::*;
use gtk::{gio, glib};
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Bind "label" to "number"
        integer_object
            .bind_property("number", &label, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    list_view.connect_activate(move |list_view, position| {
        // Get `IntegerObject` from model
        let model = list_view.model().expect("The model has to exist.");
        let integer_object = model
            .item(position)
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Increase "number" of `IntegerObject`
        integer_object.increase_number();
    });

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

Now every time we activate an element, for example by double-clicking on it, the corresponding "number" property of the IntegerObject in the model will be increased by 1. However, just because the IntegerObject has been modified the corresponding Label does not immediately change. One naive approach would be to bind the properties in the "bind" step of the SignalListItemFactory.

Filename: listings/lists/3/main.rs

mod integer_object;

use glib::BindingFlags;
use gtk::prelude::*;
use gtk::{gio, glib};
use gtk::{
    Application, ApplicationWindow, Label, ListView, PolicyType, ScrolledWindow,
    SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        let label = Label::new(None);
        list_item.set_child(Some(&label));
    });

    factory.connect_bind(move |_, list_item| {
        // Get `IntegerObject` from `ListItem`
        let integer_object = list_item
            .item()
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Get `Label` from `ListItem`
        let label = list_item
            .child()
            .expect("The child has to exist.")
            .downcast::<Label>()
            .expect("The child has to be a `Label`.");

        // Bind "label" to "number"
        integer_object
            .bind_property("number", &label, "label")
            .flags(BindingFlags::SYNC_CREATE)
            .build();
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    list_view.connect_activate(move |list_view, position| {
        // Get `IntegerObject` from model
        let model = list_view.model().expect("The model has to exist.");
        let integer_object = model
            .item(position)
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Increase "number" of `IntegerObject`
        integer_object.increase_number();
    });

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

At first glance, that seems to work. However, as you scroll around and activate a few list elements, you will notice that sometimes multiple numbers change even though you only activated a single one. This relates to how the view works internally. Not every model item belongs to a single widget, but the widgets get recycled instead as you scroll through the view. That also means that in our case, multiple numbers will be bound to the same widget.

Situations like these are so common that GTK offers an alternative to property binding: expressions. As a first step it allows us to remove the "bind" step. Let us see how the "setup" step now works.

Filename: listings/lists/4/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, ConstantExpression, Label, ListView, PolicyType,
    PropertyExpression, ScrolledWindow, SignalListItemFactory, SingleSelection,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        // Create label
        let label = Label::new(None);
        list_item.set_child(Some(&label));

        // Create expression describing `list_item->item->number`
        let list_item_expression = ConstantExpression::new(list_item);
        let integer_object_expression = PropertyExpression::new(
            gtk::ListItem::static_type(),
            Some(&list_item_expression),
            "item",
        );
        let number_expression = PropertyExpression::new(
            IntegerObject::static_type(),
            Some(&integer_object_expression),
            "number",
        );

        // Bind "number" to "label"
        number_expression.bind(&label, "label", Some(&label));
    });

    let selection_model = SingleSelection::new(Some(&model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    list_view.connect_activate(move |list_view, position| {
        // Get `IntegerObject` from model
        let model = list_view.model().expect("The model has to exist.");
        let integer_object = model
            .item(position)
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Increase "number" of `IntegerObject`
        integer_object.increase_number();
    });

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

An expression describes a reference to a value. So when we create a ConstantExpression of list_item, we create a reference to a ListItem. We then create a PropertyExpression to get a reference to the "item" property of list_item. With another PropertyExpression we get a reference to the "number" property of the "item" property of list_item. That already makes the first power of expressions obvious: It allows nested relationships. Finally, we bind "number" to "label". In pseudo code that would be label->label = list_item->item->number.

It is worth noting that at the "setup" stage there is no way of knowing which list item belongs to which label, simply because this changes as we scroll through the list. This is the power of expressions! We do not have to define a fixed relationship, the object and properties might not even exist yet. We just had to tell it to change the label whenever the number that belongs to it changes. That way, we also do not face the problem that multiple labels are bound to the same number. When we now activate a label, only the corresponding number visibly changes.

That is still not everything we can do. We can, for example, filter our model to only allow even numbers.

Filename: listings/lists/5/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, ConstantExpression, CustomSorter, FilterChange, Label,
    ListView, PolicyType, PropertyExpression, ScrolledWindow, SignalListItemFactory,
    SingleSelection, SortListModel, SorterChange,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        // Create label
        let label = Label::new(None);
        list_item.set_child(Some(&label));

        // Create expression describing `list_item->item->number`
        let list_item_expression = ConstantExpression::new(list_item);
        let integer_object_expression = PropertyExpression::new(
            gtk::ListItem::static_type(),
            Some(&list_item_expression),
            "item",
        );
        let number_expression = PropertyExpression::new(
            IntegerObject::static_type(),
            Some(&integer_object_expression),
            "number",
        );

        // Bind "number" to "label"
        number_expression.bind(&label, "label", Some(&label));
    });

    let filter = gtk::CustomFilter::new(move |obj| {
        // Get `IntegerObject` from `glib::Object`
        let integer_object = obj
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");

        // Get property "number" from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Only allow even numbers
        number % 2 == 0
    });
    let filter_model = gtk::FilterListModel::new(Some(&model), Some(&filter));

    let sorter = CustomSorter::new(move |obj1, obj2| {
        // Get `IntegerObject` from `glib::Object`
        let integer_object_1 = obj1
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");
        let integer_object_2 = obj2
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");

        // Get property "number" from `IntegerObject`
        let number_1 = integer_object_1
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");
        let number_2 = integer_object_2
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Reverse sorting order -> large numbers come first
        number_2.cmp(&number_1).into()
    });
    let sort_model = SortListModel::new(Some(&filter_model), Some(&sorter));

    let selection_model = SingleSelection::new(Some(&sort_model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    list_view.connect_activate(move |list_view, position| {
        // Get `IntegerObject` from model
        let model = list_view.model().expect("The model has to exist.");
        let integer_object = model
            .item(position)
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Increase "number" of `IntegerObject`
        integer_object.increase_number();

        // Notify that the filter and sorter has been changed
        filter.changed(FilterChange::Different);
        sorter.changed(SorterChange::Different);
    });

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

Additionally, we can reverse the order of our model.

Filename: listings/lists/5/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, ConstantExpression, CustomSorter, FilterChange, Label,
    ListView, PolicyType, PropertyExpression, ScrolledWindow, SignalListItemFactory,
    SingleSelection, SortListModel, SorterChange,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        // Create label
        let label = Label::new(None);
        list_item.set_child(Some(&label));

        // Create expression describing `list_item->item->number`
        let list_item_expression = ConstantExpression::new(list_item);
        let integer_object_expression = PropertyExpression::new(
            gtk::ListItem::static_type(),
            Some(&list_item_expression),
            "item",
        );
        let number_expression = PropertyExpression::new(
            IntegerObject::static_type(),
            Some(&integer_object_expression),
            "number",
        );

        // Bind "number" to "label"
        number_expression.bind(&label, "label", Some(&label));
    });

    let filter = gtk::CustomFilter::new(move |obj| {
        // Get `IntegerObject` from `glib::Object`
        let integer_object = obj
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");

        // Get property "number" from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Only allow even numbers
        number % 2 == 0
    });
    let filter_model = gtk::FilterListModel::new(Some(&model), Some(&filter));

    let sorter = CustomSorter::new(move |obj1, obj2| {
        // Get `IntegerObject` from `glib::Object`
        let integer_object_1 = obj1
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");
        let integer_object_2 = obj2
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");

        // Get property "number" from `IntegerObject`
        let number_1 = integer_object_1
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");
        let number_2 = integer_object_2
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Reverse sorting order -> large numbers come first
        number_2.cmp(&number_1).into()
    });
    let sort_model = SortListModel::new(Some(&filter_model), Some(&sorter));

    let selection_model = SingleSelection::new(Some(&sort_model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    list_view.connect_activate(move |list_view, position| {
        // Get `IntegerObject` from model
        let model = list_view.model().expect("The model has to exist.");
        let integer_object = model
            .item(position)
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Increase "number" of `IntegerObject`
        integer_object.increase_number();

        // Notify that the filter and sorter has been changed
        filter.changed(FilterChange::Different);
        sorter.changed(SorterChange::Different);
    });

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

To ensure that our filter and sorter get updated when we modify the numbers, we call the changed method on them.

Filename: listings/lists/5/main.rs

mod integer_object;

use gtk::gio;
use gtk::prelude::*;
use gtk::{
    Application, ApplicationWindow, ConstantExpression, CustomSorter, FilterChange, Label,
    ListView, PolicyType, PropertyExpression, ScrolledWindow, SignalListItemFactory,
    SingleSelection, SortListModel, SorterChange,
};
use integer_object::IntegerObject;

fn main() {
    // Create a new application
    let app = Application::new(Some("org.gtk.example"), Default::default());
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(application: &Application) {
    // Create a window
    let window = ApplicationWindow::builder()
        .application(application)
        .title("My GTK App")
        .default_width(600)
        .default_height(300)
        .build();

    let model = gio::ListStore::new(IntegerObject::static_type());
    for number in 0..=100_000 {
        let integer_object = IntegerObject::new(number);
        model.append(&integer_object);
    }

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        // Create label
        let label = Label::new(None);
        list_item.set_child(Some(&label));

        // Create expression describing `list_item->item->number`
        let list_item_expression = ConstantExpression::new(list_item);
        let integer_object_expression = PropertyExpression::new(
            gtk::ListItem::static_type(),
            Some(&list_item_expression),
            "item",
        );
        let number_expression = PropertyExpression::new(
            IntegerObject::static_type(),
            Some(&integer_object_expression),
            "number",
        );

        // Bind "number" to "label"
        number_expression.bind(&label, "label", Some(&label));
    });

    let filter = gtk::CustomFilter::new(move |obj| {
        // Get `IntegerObject` from `glib::Object`
        let integer_object = obj
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");

        // Get property "number" from `IntegerObject`
        let number = integer_object
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Only allow even numbers
        number % 2 == 0
    });
    let filter_model = gtk::FilterListModel::new(Some(&model), Some(&filter));

    let sorter = CustomSorter::new(move |obj1, obj2| {
        // Get `IntegerObject` from `glib::Object`
        let integer_object_1 = obj1
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");
        let integer_object_2 = obj2
            .downcast_ref::<IntegerObject>()
            .expect("The object needs to be of type `IntegerObject`.");

        // Get property "number" from `IntegerObject`
        let number_1 = integer_object_1
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");
        let number_2 = integer_object_2
            .property("number")
            .expect("The property needs to exist and be readable.")
            .get::<i32>()
            .expect("The property needs to be of type `i32`.");

        // Reverse sorting order -> large numbers come first
        number_2.cmp(&number_1).into()
    });
    let sort_model = SortListModel::new(Some(&filter_model), Some(&sorter));

    let selection_model = SingleSelection::new(Some(&sort_model));
    let list_view = ListView::new(Some(&selection_model), Some(&factory));

    list_view.connect_activate(move |list_view, position| {
        // Get `IntegerObject` from model
        let model = list_view.model().expect("The model has to exist.");
        let integer_object = model
            .item(position)
            .expect("The item has to exist.")
            .downcast::<IntegerObject>()
            .expect("The item has to be an `IntegerObject`.");

        // Increase "number" of `IntegerObject`
        integer_object.increase_number();

        // Notify that the filter and sorter has been changed
        filter.changed(FilterChange::Different);
        sorter.changed(SorterChange::Different);
    });

    let scrolled_window = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
        .min_content_width(360)
        .child(&list_view)
        .build();
    window.set_child(Some(&scrolled_window));
    window.show();
}

After our changes, the application looks like this:

We now know how to display a list of data. Small amount of elements can be handled by ListBox or FlowBox. These widgets are easy to use and allow, if necessary, to be bound to a model such as gio::ListStore. Their data can then be modified, sorted and filtered more easily. However, if we need the widgets to be scalable we still need to use ListView, ColumnView or GridView instead.