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
.
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.
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).
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 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.
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.
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.
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.
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::*;
.
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: >k4::Snapshot) {
/* ... */
}
}
}
In general the function signatures are mostly identical to the native
C API, except that self
is the private type.
Here are some quick recipes of how to do things.
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.
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
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: >k4::Snapshot) {
/* ... */
}
}
}
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 original version: Sebastian
Dröge for his thorough comments, and
#gtk-rs
IRC user piegames2
.
10 January 2023:
PropertySpec
example to highlight that read and write
properties are the default.