Subclassing GTK widgets in Rust - Updated for glib-rs 0.16

This tech note obsoletes TN:003 bringing glib-rs version 0.16.0 of the Rust bindings and focusing solely on Gtk4.

If you use GTK in Rust, you probably will need to write custom widgets. This document will show you how it is possible, and what tasks you need to go through. It will cover GTK 4 as it focuses on the Rust side, most of it should apply to GTK 3, but you can check for some of the difference in the previous technote.

At the time of writing this, gtk4-rs 0.5.1 is being used. It is a set of Rust bindings for GTK 4. It depends on glib-rs 0.16.x which set most of the subclassing API.

glib-rs 0.16 introduce a certain number of breaking changes that makes things easier. See the project what’s new. This article will go through some of the changes.

Throughout this document whenever we reference the gtk4:: namespace for GTK 4.

We want to create MyAwesomeWidget to be a container, a subclass of GtkBox.

Declarations

In gtk-rs each the GObject types are wrapped into a Rust type, that will be called instance object later on. For example gtk4::Widget is such a wrapper and is the type we use for any Rust function that expect a GtkWidget instance.

A GObject subclass implemented in Rust constist of two things: an instance object, that expose all the visible API, and an implementation object, that include the implementation details and that, with the help of macros, will implement the necessary boilerplate used to implement the GObject related code.

Declaring the wrapper for your subclassed gobject, the instace type, is done using the glib::wrapper!() macro. Just reference it using the module namespace as per Rust 2018.

glib::wrapper! {
	pub struct MyAwesomeWidget(
		ObjectSubclass<imp::MyAwesomeWidget>)
		@extends gtk4::Box, gtk4::Widget;
}

This tells us that we have MyAwesomeWidget, the instance type, and imp::MyAwesomeWidget the implememtation type. It also indicates the hierarchy: gtk4::Box, gtk4::Widget. The order is important and goes from down to top (the direct parent first). The macro will take care of most of the boilerplate based on this. The glib::wrapper! macro implies you are deriving a glib::Object so you don’t need to specify you extend it. If your class also implements interfaces, you can specify @interfaces to specify them.

The type imp::MyAwesomeWidget will be implementing the GObject boilerplate, it is also the struct that will store your private data.

Compared to the previous technote, we have changed the pattern to encapsulate the implementation into a submodule imp, instead of a type with the -Priv suffix. This doesn’t impact much, beside how the code is organized. The examples in gtk-rs use a separate file for the imp module, while here we put it in the same file. When mentioning the type, MyAwesomeWidget is the instance, and imp::MyAwesomeWidget is the implementation type.

All of this is more a matter of preference. Doing it differently is left as an exercise to the reader, and there is no right or wrong.

Implementing the subclass

Here is the object implementation. You have to declare the implementation struct, that we’ll name imp::MyAwesomeWidget. It should have the same visibility as the instance ; the compiler will let you know if not.

mod imp {
    pub struct MyAwesomeWidget {}

    impl ObjectImpl for MyAwesomeWidget {
        fn constructed(&self) {
            self.parent_constructed();
	        /* ... */
        }

        fn signals() -> &'static [Signal] {
            /* see below for the implementation */
        }

        fn properties() -> &'static [glib::ParamSpec] {
            /* see below for the implementation */
        }

        fn set_property(
            &self,
            id: usize,
            value: &glib::Value,
            pspec: &glib::ParamSpec,
        ) {
	        /* ... */
        }

        fn property(
            &self,
            id: usize,
            pspec: &glib::ParamSpec,
        ) -> glib::Value
        {
            /* ... */
        }
    }
}

Use constructed as an opportunity to do anything after the glib::Object instance has been constructed.

A change in glib-rs 0.16 make that these implementation methods no longer receive the GObject instance of type Self::Type as part of the function argument. To obtain it, you can call self.obj() (or self.instance() as previously).

Tip

If constructed only calls self.parent_constructed(), it can be omitted. If any of these associated functions have an empty body, then you can just write:

mod imp {
    impl ObjectImpl for MyAwesomeWidget {}
}

This will implement the trait ObjectImpl using the defaults.

Properties

Properties are declared in the properties() associated function of the imp::MyAwesomeWidget struct that will return a static array of glib::ParamSpec. This example declares one single property auto-update that is a boolean read-only:

fn properties() -> &'static [glib::ParamSpec] {
    use once_cell::sync::Lazy;
    static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
        vec![glib::ParamSpecBoolean::builder("auto-update")
                .nick("Auto-update")
                .blurb("Whether to auto-update or not")
                .default_value(true)
                .read_only()
                .build()
        ]
    });
    PROPERTIES.as_ref()
}

glib-rs 0.16 introduced a builder trait that we use above and that provides a more expressive way to build property specs. In the example above we chose read-only as read and write is the default and doesn’t need to be specified. If you want this property to be read and write, simple remove .read_only().

We use once_cell::sync::Lazy to lazy initialise the array of ParamSpec. Each type is represented by a different ParamSpec type. Here ParamSpecBoolean is used for a boolean property.

Signals

Like properties, signals are declared in the imp::MyAwesomeWidget::signals() associated function that will return a static array of glib::subclass::Signal. This examples declares one single signal rating-change that has an i32 argument:

fn signals() -> &'static [Signal] {
    use once_cell::sync::Lazy;
    static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
        vec![Signal::builder("rating-changed")
                .param_types([<i32>::static_type()])
                .run_last()
                .build()
        ]
    });
    SIGNALS.as_ref()
}

It use mostly the same pattern as for properties. Signals are build using the Signal::Builder. A change in glib-rs 0.16 is that the signal parameter are now declared by calling the builder associated functions param_types() and return_type() instead of being directly passed to the builder() assciated function. They can be omitted if there is no parameter or if the return type is the unit () type.

Subclassing

Then there is the object subclassing trait to implement the class methods. Use the glib::object_subclass procedural macro to have the boilerplate generated.

mod imp {
    #[glib::object_subclass]
    impl ObjectSubclass for MyAwesomeWidget {
        const NAME: &'static str = "MyAwesomeWidget";
        type Type =  super::MyAwesomeWidget;
        type ParentType = gtk4::Box;

        fn class_init(klass: &mut Self::Class) {
            // You can skip this if empty
        }

        fn new() -> Self {
            Self {}
        }
    }
}

Here we set ParentType to be gtk4::Box, as per the wrapper. NAME is a unique name, we recommend using the widget type name. This will be used in various places, including glib::Object::type_(). If the parent type isn’t subclassable because it is marked as final, you’ll get an error message like:

66  |     type ParentType = gtk4::IconView;
    |                       ^^^^^^^^^^^^^^ the trait `IsSubclassable<gtk4::MyAwesomeWidgetPriv>` is not implemented for `gtk4::IconView`
    |
note: required by a bound in `glib::subclass::types::ObjectSubclass::ParentType`
   --> /var/home/hub/.cargo/registry/src/github.com-1ecc6299db9ec823/glib-0.15.11/src/subclass/types.rs:542:22
    |
542 |     type ParentType: IsSubclassable<Self>
    |                      ^^^^^^^^^^^^^^^^^^^^ required by this bound in `glib::subclass::types::ObjectSubclass::ParentType`

There is no workaround thie, the only choice is to rethink why you want to subclass it and maybe use composition instead. In that example, gtk4::IconView can’t be subclassed.

Use class_init to do anything you might want. This will be called automatically to initialise the class. Properties and signals will be automatically registered.

Constructor

The public constructor is part of MyAwesomeWidget. This is what you use to actually construct an instance.

impl MyAwesomeWidget {
    pub fn new() -> MyAwesomeWidget {
        glib::Object::new(&[])
    }
}

Another change in glib-rs 0.16 is that glib::Object::new() now returns the object and will panic if it fails, like if one of the properties passed to the initializer is incorrect. This doesn’t change much from using expect as previously.

Blanket implementation traits

Then you need to have an explicit implementation for the widget struct (imp::MyAwesomWidget) of each parent class. In that case, since it is a GtkBox subclass, BoxImpl, ContainerImpl and WidgetImpl. Fortunately with the default trait implemention, these impl are empty, unless, as we’ll show, you need to implement any of the virtual functions.

mod imp {
    impl BoxImpl for MyAwesomeWidget {}
    impl WidgetImpl for MyAwesomeWidget {}
}

Just in case, you need to import these traits from the prelude use gtk::subclass::prelude::*;.

Virtual functions

Now we are hitting the parts that actually do the work specific to your widget.

If you need to override the virtual functions (also known as vfuncs in GObject documentation), it is done in their respective Impl traits, that would otherwise use the default implementation.

Notably, the snapshot method is, as expected, in gtk::WidgetImpl:

mod imp {
    impl WidgetImpl for MyAwesomeWidget {
        fn snapshot(&self, snapshot: &gtk4::Snapshot) {
            /* ... */
        }
    }
}

In general the function signatures are mostly identical to the native C API, except that self is the private type.

Recipes

Here are some quick recipes of how to do things.

Widget instance to implementation struct and back

The virtual methods now only receive the implementation widget as self. In other places, you only have access to the instance object. There are ways to go back and forth:

Getting the implementation from the widget instance struct:

let w: MyAwesomeWidget;
/* ... */
let priv_ = w.imp();
// or alternatively
let priv_ = imp::MyAwesomeWidget::from_instance(&w);

priv_ will be of type imp::MyAwesomeWidget.

Now the reverse, getting the widget instance struct (the GObject instance) from the implementation:

let priv_: imp::MyAwesomeWidget;
/* ... */
let w = priv_.obj();
// or alternatively
let w = priv_.instance();

w is of type MyAwesomeWidget.

The latter is useful in the vfuncs implementations.

Storing a Rust type in a property

To store a Rust type into a property, you need it to be clonable and glib::Boxed. From glib-rs 0.15.x all you need is to derive glib::Boxed and you can do that automatically. Just make sure the crate glib is imported for macro use.

Example with the type MyPropertyType

#[derive(Clone, glib::Boxed)]
#[boxed_type(name = "MyPropertyType")]
pub struct MyPropertyType {
}

When you declare the property as boxed the GLib type is obtained with MyPropertyType::get_type().

In the set_property() handler, you do:

let property = value
            .get::<&MyPropertyType>()
            .expect("type checked by set_property");

In that case property is of the type &MyPropertyType. We have to use glib::Value::get_some() since MyPropertyType isn’t nullable.

If you need to use a type that you don’t have control of, for which you can’t implement the traits in the same module as either the type or the trait, wrap the type into a tuple struct (this is called the newtype idiom).

Example:

#[derive(Clone, glib::Boxed)]
#[boxed_type(name = "MyPropertyType"]
pub struct MyPropertyType(OtherType);

The only requirement here is that OtherType also implements Clone, or that you be able to implement Clone for MyPropertyType safely. You can also wrap the orignal type inside a std::sync::Arc.

Note that this is not friendly to other languages. Unless you are prepared to write more interface code, don’t try to use a Rust type outside of Rust code. Keep this in mind when designing your widget API.

You can see an example of wrapping a type to use as a list store value

Source code

Here is the complete source code for the example above.

// SPDX-License: CC0-1.0

glib::wrapper! {
    pub struct MyAwesomeWidget(
        ObjectSubclass<imp::MyAwesomeWidget>)
        @extends gtk4::Box, gtk4::Widget;
}

impl MyAwesomeWidget {
    pub fn new() -> MyAwesomeWidget {
        glib::Object::new(&[])
    }
}

mod imp {
    use glib::prelude::*;
    use glib::subclass::Signal;
    use gtk4::subclass::prelude::*;

    pub struct MyAwesomeWidget {}

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

        fn class_init(klass: &mut Self::Class) {
            // You can skip this if empty
        }

        fn new() -> Self {
            Self {}
        }
    }

    impl ObjectImpl for MyAwesomeWidget {
        fn constructed(&self) {
            self.parent_constructed();
            /* ... */
        }

        fn signals() -> &'static [Signal] {
            use once_cell::sync::Lazy;
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![Signal::builder("rating-changed")
                    .param_types([<i32>::static_type()])
                    .run_last()
                    .build()]
            });
            SIGNALS.as_ref()
        }

        fn properties() -> &'static [glib::ParamSpec] {
            use once_cell::sync::Lazy;
            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
                vec![glib::ParamSpecBoolean::builder("auto-update")
                    .nick("Auto-update")
                    .blurb("Whether to auto-update or not")
                    .default_value(true)
                    .read_only()
                    .build()]
            });
            PROPERTIES.as_ref()
        }

        fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
            /* ... */
        }

        fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
            /* ... */

            let none: Option<&str> = None;
            none.to_value()
        }
    }

    impl BoxImpl for MyAwesomeWidget {}

    impl WidgetImpl for MyAwesomeWidget {
        fn snapshot(&self, snapshot: &gtk4::Snapshot) {
            /* ... */
        }
    }
}

Examples

Gtk-rs itself has plenty of Gtk Rust examples. Notably:

  • ListBox model show how to write a custom model via subclassing for a gtk::ListBox.
  • Basic subclass which is the first example I looked at, showing how to subclass a gtk::ApplicationWindow and a gtk::Application.

And then, some real examples of widgets in Rust that I wrote.

Niepce

Niepce is prototype for a photo management application. Started in C++ it is being rewritten progressively in Rust, including the UI.

  • ImageGridView a subclass of GtkIconView. Since in GTK 4 GtkIconView is final, the GTK 4 Port, it uses a trick of composing the widget and implementing the Deref trait so that the Rust code treats it as a widget. One of the functionality needed is provided by an event controller.
  • ThumbStripView another subclass of GtkIconView. Since in GTK 4 GtkIconView is final, the GTK 4 Port is no longer a GtkWidget. Instead is uses a trick of composing the widget and implementing the Deref trait so that the Rust code treats it as a widget.
  • LibraryCellRenderer a subclass of GtkCellRendererPixbuf to have a custom rendering in an icon view. This is not a widget, but this still applies as it is a GObject. The GTK 4 port is a subclass of GtkCellRenderer, it uses GdkPaintable instead of GdkPixbuf.
  • ThumbNav a subclass of GtkBox to compose a few widgets together with a scrolling area. The GTK 4 port
  • RatingLabel a subclass for a GtkDrawingArea to display a “star rating”. The GTK 4 version is just a widget that override snapshopt() to leverage snapshots and not use Cairo.
  • Wrapping a type for use in glib::Value in a gtk::ListStore: the LibFile type from another crate is wrapped to be used in the list store.

Companio

Compiano (né Minuit) is small digital piano application written in Rust.

  • PianoWidget a subclass of GtkDrawingArea that implements a Piano like widget including managing events, in GTK 4.

GStreamer

Writing GStreamer element in Rust is possible and the GStreamer team has a tutorial. The repository itself contains over 50 examples of elements subclasses.

Thanks

Thanks to the reviewers for the original version: Sebastian Dröge for his thorough comments, and #gtk-rs IRC user piegames2.

Update log

10 January 2023:

  • Changed the PropertySpec example to highlight that read and write properties are the default.
  • Added newtype idiom reference
  • Also reworked the outline hierarchy (headers).