Subclassing GTK widgets in Rust - Updated for glib-rs 0.15 and GTK 4

This tech note is obsoleted by TN:004 that covers more recent version of the Rust bindings.
This tech note obsoletes TN:002 bringing a more recent version of the Rust bindings. It also covers GTK 4 briefly.

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 both GTK 3 and GTK 4 as it focuses on the Rust side and not the specifics of the two toolkit version.

At the time of writing this, gtk-rs 0.15.5 is being used. It is a set of Rust bindings for GTK 3. Your mileage may vary on later versions of GTK Rust bindings. As an example, the original version of this document used 0.8 and only one section needed to be edited, and largely simplified. For GTK 4 we’ll use gtk4-rs is 0.4.7. Both depends on glib-rs 0.15.x which set most of the subclassing API.

Throughout this document whenever we reference the gtk:: namespace, gtk4:: can be used 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. For example gtk::Widget is such a wrapper and is the type we use for any Rust function that expect a widget instance.

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

You also need to use the following:

  • glib::subclass::prelude::*
  • glib::translate::*

For GTK 3:

  • gtk::prelude::*
  • gtk::subclass::prelude::*

For GTK 4:

  • gtk4::prelude::*
  • gtk4::subclass::prelude::*

For GTK 3:

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

For GTK 4 it’s a bit different, this is because of the inheritance hiearchy that no longer has gtk::Container

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

This tells us that we have MyAwesomeWidget addMyAwesomeWidgetPriv and MyAwesomeWidgetClass. 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.

It also indicates that the type MyAwesomeWidgetPriv will be the one implementing the GObject boilerplate, it is the struct that will store your private data as well.

This pattern is not the only way to do this, there are others that can be used. This is left as an exercise to the reader.

Implementing the subclass

There is the object instance implementation. You have to declare the private implementation structure, MyAwesomeWidgetPriv here. It should have the same visibility as the instance.

pub struct MyAwesomeWidgetPriv {}

impl ObjectImpl for MyAwesomeWidgetPriv {
    fn constructed(&self, obj: &Self::Type) {
        self.parent_constructed(obj);
	    /* ... */
    }

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

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

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

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

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

Properties

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

fn properties() -> &'static [glib::ParamSpec] {
    use once_cell::sync::Lazy;
    static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
        vec![glib::ParamSpecBoolean::new(
            "auto-update",
            "Auto-update",
            "Whether to auto-update or not",
            true, // Default value
            glib::ParamFlags::READWRITE,
        )]
    });
    PROPERTIES.as_ref()
}

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 MyAwesomeWidgetPriv::signals() 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",
            &[<i32>::static_type().into()],
            <()>::static_type().into(),
        )
        .run_last()
        .build()]
    });
    SIGNALS.as_ref()
}

It use mostly the same pattern as for properties. Signals are build using the Signal::Builder.

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.

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

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

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

Here we set ParentType to be gtk::Box. NAME is a unique name, we recommend using the widget type name. If the class 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`

The only suggestion here 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(&[])
          .expect("Failed to create MyAwesome Widget")
    }
}

Blanket implementation traits

Then you need to have an explicit implementation for the widget struct (the Priv) 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 a virtual function.

impl BoxImpl for MyAwesomeWidgetPriv {}
impl ContainerImpl for MyAwesomeWidgetPriv {}
impl WidgetImpl for MyAwesomeWidgetPriv {}

Note that in the GTK 4 version of this code there is no more ContainerImpl.

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

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, in GTK 3, the draw method is, as expected, in gtk::WidgetImpl:

impl WidgetImpl for MyAwesomeWidgetPriv {
    fn draw(&self, _widget: &Self::Type, cr: &cairo::Context) -> Inhibit {
        /* ... */
        Inhibit(false)
    }
}

The GTK 4 equivalent is snapshot:

impl WidgetImpl for MyAwesomeWidgetPriv {
    fn snapshot(&self, widget: &Self::Type, snapshot: &gtk4::Snapshot) {
        /* ... */
    }
}

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

Recipes

Here are some quick recipes of how to do things.

Widget to Private struct and back

Getting the private data from the actual widget struct:

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

priv_ will be of type MyAwesomeWidgetPriv.

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

let priv_: MyAwesomeWidgetPriv;
/* ... */
let w = priv_.instance();

w is of type MyAwesomeWidget.

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, and you can’t implement the traits in the same module as the type or the trait, make MyPropertyType a tuple struct that contain said type.

Example:

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

The only requirement here is that OtherType also implements Clone as well, or that you be able to implement Clone for MyPropertyType safely. You can also wrap the orignal type inside an 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

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 previous version: Sebastian Dröge for his thorough comments, and #gtk-rs IRC user piegames2.