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
.
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.
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 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.
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
.
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.
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")
}
}
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.
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: >k4::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.
Here are some quick recipes of how to do things.
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
.
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
Gtk-rs itself has plenty of Gtk Rust examples. Notably:
gtk::ListBox
.gtk::ApplicationWindow
and a gtk::Application
.And then, some real examples of widgets in Rust that I wrote.
Niepce is prototype for a photo management application. Started in C++ it is being rewritten progressively in Rust, including the UI.
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.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.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
.GtkBox
to compose a few widgets together with a
scrolling area. The GTK 4 portGtkDrawingArea
to display a “star rating”. The GTK 4 version is just a widget that override snapshopt()
to leverage snapshots and not use Cairo.glib::Value
in a gtk::ListStore
: the LibFile
type from another crate is
wrapped to be used in the list store.Compiano (né Minuit) is small digital piano application written in Rust.
GtkDrawingArea
that implements a Piano like widget
including managing events, in GTK 4.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 to the reviewers for the previous version: Sebastian
Dröge for his thorough comments, and
#gtk-rs
IRC user piegames2
.