Coding Custom Widgets with GTK 3 and GJS

24 November 2020

GTK provides a lot of useful widgets, but it inevitably can’t cover every possible use case. What if, for example, you need an image widget that resizes itself based on the available width, and clips its corners to fit in a popover? Fortunately, GTK makes it easy to create your own widgets that do whatever you want.

The Example Code

Here’s the complete source code, straight from GNOME Maps. In this post I’ll be using a condensed version:

const Cairo = imports.cairo;
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

var PlaceBubbleImage = GObject.registerClass(
class PlaceBubbleImage extends Gtk.DrawingArea {
    _init(params) {
        this._pixbuf = params.pixbuf;
        delete params.pixbuf;

        super._init(params);
    }

    vfunc_draw(cr) {
        let [{x, y, width, height}, baseline] = this.get_allocated_size();

        /* I've skipped a bunch of code here that caches the scaled image to
         * increase performance. See the full source code if you want to learn
         * how that works.*/

        cr.scale(width / this._pixbuf.width,
                 height / this._pixbuf.height);

        /* Also skipped the popover clipping code. */

        Gdk.cairo_set_source_pixbuf(cr, this._pixbuf, 0, 0);

        cr.paint();

        return false;
    }

    vfunc_get_request_mode() {
        return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
    }

    vfunc_get_preferred_height_for_width(width) {
        let height = (this._pixbuf.height / this._pixbuf.width) * width;
        return [height, height];
    }
});

Subclassing and Virtual Functions

GTK’s custom widgets work by subclassing. In GJS, we do that with these two lines:

var PlaceBubbleImage = GObject.registerClass(
class PlaceBubbleImage extends Gtk.DrawingArea {

This registers a class called PlaceBubbleImage with the GLib type system. It extends Gtk.DrawingArea, a generic class designed for this purpose.

Now that we have our own widget class, we customize it using virtual methods, which override the default functionality of Gtk.DrawingArea. In GJS, all you have to do is create a method titled vfunc_ followed by the name of the function you’re overriding. So this code:

vfunc_get_request_mode() {
    return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}

overrides the get_request_mode vfunc.

Implementing the Widget

Every widget needs to do at least two things:

Size Calculation

To calculate size, there are a number of vfuncs you can override. In this case, we only need vfunc_get_request_mode() and vfunc_get_preferred_height_for_width().

vfunc_get_request_mode(), as you saw above, just returns Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH. This indicates that if GTK tells us how much horizontal space is available, we can tell it how much vertical space we need. For a more detailed explanation of height-for-width geometry management, see the GtkContainer docs.

Meanwhile, vfunc_get_preferred_height_for_width() does the actual size calculation. It first finds the aspect ratio of the pixbuf (this._pixbuf.height / this._pixbuf.width), then multiplies by the width we’re given to calculate the appropriate height. We return it twice in an array–the first is the minimum height, and the second is the “natural” or preferred height. In this case, they’re the same.

Drawing

The biggest function here is vfunc_draw(). It takes a Cairo context and draws the widget’s contents to it.

First, it gets the final size of the widget using Gtk.Widget.get_allocated_size(). Then it scales the image by width / this._pixbuf.width and height / this._pixbuf.height. So if the image is 100x100 but the widget is only 50x50, we get 50/100=0.5, and the image is drawn at half size.

The next line, Gdk.cairo_set_source_pixbuf(cr, this._pixbuf, 0, 0), tells the Cairo context to draw from the pixbuf, and then cr.paint() does the actual drawing. Finally, the method returns false to let the draw call continue to propagate. If you return true, things like the blue highlight in the GTK inspector won’t work.

It doesn’t show up in this simplified bit of code, but you should be aware that GJS’s Cairo bindings convert all function names to camelCase. Thus, where you might read set_source_rgb and set_line_width in the C documentation, it’s actually setSourceRgb and setLineWidth in GJS. This only applies to Cairo.

Conclusion

If you’re making a GTK app and you can’t make what you want with the available widgets, don’t be afraid to write your own! It’s not very difficult and you’ll have full control to achieve your vision for your app, exactly the way you want it.


Next

GDScript's  _get() and  _set(): How and When to Use Them

I recently found myself writing some very messy code in Godot Engine, and I wondered if I could improve it with a bit of GDScript magic.

Previous

New in GNOME 40: Map Details!

I’ve contributed to quite a few GNOME apps over the years, but Maps is the one I keep going back to. It’s a pretty easy codebase, and I’m a huge map nerd.