Cleaner Code With GObject

I see a lot of users approaching GNOME app development with prior language-specific experience, be it Python, Rust, or something else. But there’s another way to approach it: GObject-oriented and UI first.

This introduces more declarative code, which is generally considered cleaner and easier to parse. Since this approach is inherent to GTK, it can also be applied in every language binding. The examples in this post stick to Python and Blueprint.

Properties

While normal class properties for data work fine, using GObject properties allows developers to do more in UI through expressions.

Handling Properties Conventionally

Let’s look at a simple example: there’s a progress bar that needs to be updated. The conventional way of doing this would look something like the following:

using Gtk 4.0;
using Adw 1;

template $ExampleProgressBar: Adw.Bin {
  ProgressBar progress_bar {}
}

This defines a template called ExampleProgressBar which extends Adw.Bin and contains a Gtk.ProgressBar called progress_bar.

The reason why it extends Adw.Bin instead of Gtk.ProgressBar directly is because Gtk.ProgressBar is a final class, and final classes can’t be extended.

from gi.repository import Adw, GLib, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    progress_bar: Gtk.ProgressBar = Gtk.Template.Child()

    progress = 0.0

    def __init__() -> None:
        super().__init__()

        self.load()

    def load(self) -> None:
        self.progress += 0.1
        self.progress_bar.set_fraction(self.progress)

        if int(self.progress) == 1:
            return

        GLib.timeout_add(200, self.load)

This code references the earlier defined progress_bar and defines a float called progress. When initialized, it runs the load method which fakes a loading operation by recursively incrementing progress and setting the fraction of progress_bar. It returns once progress is 1.

This code is messy, as it splits up the operation into managing data and updating the UI to reflect it. It also requires a reference to progress_bar to set the fraction property using its setter method.

Handling Properties With GObject

Now, let’s look at an example of this utilizing a GObject property:

using Gtk 4.0;
using Adw 1;

template $ExampleProgressBar: Adw.Bin {
  ProgressBar {
    fraction: bind template.progress;
  }
}

Here, the progress_bar name was removed since it isn’t needed anymore. fraction is bound to the template’s (ExampleProgressBar‘s) progress property, meaning its value is synced.

from gi.repository import Adw, GLib, GObject, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    progress = GObject.Property(type=float)

    def __init__() -> None:
        super().__init__()

        self.load()

    def load(self) -> None:
        self.progress += 0.1

        if int(self.progress) == 1:
            return

        GLib.timeout_add(200, self.load)

The reference to progress_bar was removed in the code too, and progress was turned into a GObject property instead. fraction doesn’t have to be manually updated anymore either.

So now, managing the data and updating the UI merged into a single property through a binding, and part of the logic was put into a declarative UI file.

In a small example like this, it doesn’t matter too much which approach is used. But in a larger app, using GObject properties scales a lot better than having widget setters all over the place.

Communication

Properties are extremely useful on a class level, but once an app grows, there’s going to be state and data communication across classes. This is where GObject signals come in handy.

Handling Communication Conventionally

Let’s expand the previous example a bit. When the loading operation is finished, a new page has to appear. This can be done with a callback, a method that is designed to be called by another method, like so:

using Gtk 4.0;
using Adw 1;

template $ExampleNavigationView: Adw.Bin {
  Adw.NavigationView navigation_view {
    Adw.NavigationPage {
      child: $ExampleProgressBar progress_bar {};
    }

    Adw.NavigationPage {
      tag: "finished";

      child: Box {};
    }
  }
}

There’s now a template for ExampleNavigationView, which extends an Adw.Bin for the same reason as earlier, which holds an Adw.NavigationView with two Adw.NavigationPages.

The first page has ExampleProgressBar as its child, the other one holds a placeholder and has the tag “finished”. This tag allows for pushing the page without referencing the Adw.NavigationPage in the code.

from gi.repository import Adw, Gtk

from example.progress_bar import ExampleProgressBar

@Gtk.Template(resource_path="/org/example/App/navigation-view.ui")
class ExampleNavigationView(Adw.Bin):

    __gtype_name__ = "ExampleNavigationView"

    navigation_view: Adw.NavigationView = Gtk.Template.Child()
    progress_bar: ExampleProgressBar = Gtk.Template.Child()

    def __init__(self) -> None:
        super().__init__()

        def on_load_finished() -> None:
            self.navigation_view.push_by_tag("finished")

        self.progress_bar.load(on_load_finished)

The code references both navigation_view and progress_bar. When initialized, it runs the load method of progress_bar with a callback as an argument.

This callback pushes the Adw.NavigationPage with the tag “finished” onto the screen.

from typing import Callable

from gi.repository import Adw, GLib, GObject, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    progress = GObject.Property(type=float)

    def load(self, callback: Callable) -> None:
        self.progress += 0.1

        if int(self.creation_progress) == 1:
            callback()
            return

        GLib.timeout_add(200, self.load, callback)

ExampleProgressBar doesn’t run load itself anymore when initialized. The method also got an extra argument, which is the callback we passed in earlier. This callback gets run when the loading has finished.

This is pretty ugly, because the parent class has to run the operation now.

Another way to approach this is using a Gio.Action. However, this makes illustrating the point a bit more difficult, which is why a callback is used instead.

Handling Communication With GObject

With a GObject signal the logic can be reversed, so that the child class can communicate when it’s finished to the parent class:

using Gtk 4.0;
using Adw 1;

template $ExampleNavigationView: Adw.Bin {
  Adw.NavigationView navigation_view {
    Adw.NavigationPage {
      child: $ExampleProgressBar {
        load-finished => $_on_load_finished();
      };
    }

    Adw.NavigationPage {
      tag: "finished";

      child: Box {};
    }
  }
}

Here, we removed the name of progress_bar once again since we won’t need to access it anymore. It also has a signal called load-finished, which runs a callback called _on_load_finished.

from gi.repository import Adw, Gtk

from example.progress_bar import ExampleProgressBar

@Gtk.Template(resource_path="/org/example/App/navigation-view.ui")
class ExampleNavigationView(Adw.Bin):

    __gtype_name__ = "ExampleNavigationView"

    navigation_view: Adw.NavigationView = Gtk.Template.Child()

    @Gtk.Template.Callback()
    def _on_load_finished(self, _obj: ExampleProgressBar) -> None:
        self.navigation_view.push_by_tag("finished")

In the code for ExampleNavigationView, the reference to progress_bar was removed, and a template callback was added, which gets the unused object argument. It runs the same navigation action as before.

from gi.repository import Adw, GLib, GObject, Gtk

@Gtk.Template(resource_path="/org/example/App/progress-bar.ui")
class ExampleProgressBar(Adw.Bin):

    __gtype_name__ = "ExampleProgressBar"

    progress = GObject.Property(type=float)
    load_finished = GObject.Signal()

    def __init__(self) -> None:
        super().__init__()

        self.load()

    def load(self) -> None:
        self.progress += 0.1

        if int(self.creation_progress) == 1:
            self.emit("load-finished")
            return

        GLib.timeout_add(200, self.load)

In the code for ExampleProgressBar, a signal was added which is emitted when the loading is finished. The responsibility of starting the load operation can be moved back to this class too. The underscore and dash are interchangeable in the signal name in PyGObject.

So now, the child class communicates to the parent class that the operation is complete, and part of the logic is moved to a declarative UI file. This means that different parent classes can run different operations, while not having to worry about the child class at all.

Next Steps

Refine is a great example of an app experimenting with this development approach, so give that a look!

I would also recommend looking into closures, since it catches some cases where an operation needs to be performed on a property before using it in a binding.

Learning about passing data from one class to the other through a shared object with a signal would also be extremely useful, it comes in handy in a lot of scenarios.

And finally, experiment a lot, that’s the best way to learn after all.

Thanks to TheEvilSkeleton for refining the article, and Zoey for proofreading it.

Happy hacking!

Introducing Adwaita Fonts

Cantarell has been used as the default interface font since November 2010, but unfortunately, font technology is moving forward, while Cantarell isnʼt.

Similarly, Source Code Pro was used as the default monospace font, but its maintenance hasnʼt been well. Aesthetically, it has fallen out of taste too.

GNOME was ready to move on, which is why the Design Team has been putting effort into making the switch to different fonts in recent cycles.

The Sans

Inter was quite a straightforward choice, due to its modern design, active maintenance, and font feature support. It might be the most popular open source sans font, being used in Figma, GitLab, and many other places.

An issue was created to discuss the font. From this, a single design tweak was decided on: the lowercase L should be disambiguated.

A formal initiative was made for the broader community to try out the font, catch issues that had to be resolved, and look at the platform to see where we need to change anything in terms of visuals. Notably, the Shell lock screen got bolder text.

At this point, some issues started popping up, including some nasty Cantarell-specific hacks in Shell, and broken small caps in Software. These were quickly fixed thereafter, and due to GTKʼs robust font adaptivity, apps were mostly left untouched.

However, due to Interʼs aggressive use of calt, some unintended behavior arose in arbitrary strings as a result of ligatures. There were two fixes for this, but they would both add maintenance costs which is what weʼre trying to move away from:

  1. Subset the font to remove calt entirely
  2. Fork the font to remove the specific ligature that caused issues

This blocked the font from being the default in GNOME 47, as Rasmus, the Inter maintainer, was busy at the time, and the lack of contact brought some uncertainty into the Design Team. Luckily, when Rasmus returned during the 48 development cycle, he removed the problematic ligature and Inter was back in the race.

No further changes were required after this, and Inter, now as Adwaita Sans, was ready for GNOME 48.

The Mono

After the sans font was decided on as Inter, we wanted a matching monospace font. Our initial font selection consisted of popular monospace fonts and recommendations from Rasmus.

We also made a list of priorities, the new font would need:

  1. A style similar to Adwaita Sans
  2. Active maintenance
  3. Good legibility
  4. Large language coverage

Some fonts on our initial font selection fell off due to shortcomings in this list, and we were left with IBM Plex Mono, Commit Mono and Iosevka.

Just like for the sans font, we made a call for testing for these three fonts. The difference in monospace fonts can be quite hard to notice, so the non-visual benefits of the fonts were important.

The favorite among users was Commit Mono, due to its fairly close neutral design to Adwaita Sans. However, the font that we ended up with was Iosevka. This made some people upset, but this decision was made for a couple of reasons:

  1. Iosevka has more active maintenance
  2. Iosevkaʼs configuration might have the best free tooling out there
  3. When configured, Iosevka can look extremely similar to Adwaita Sans
  4. The language coverage of Iosevka is considerably larger

So, in the end, kramo and me went through all its glyphs, configured them to look as close to Adwaita Sans as possible, and made that Adwaita Mono.

Naming

We wanted unique names for the fonts, because it will allow us to more easily switch them out in the future if necessary. Only the underlying repository will have to change, nothing else.

The configured Inter was originally named GNOME UI Font, but due to the introduction of the monospace font and our design system being called Adwaita, we moved the fonts under its umbrella as Adwaita Fonts.

Technical Details

We use OpenType Feature Freezer to get the disambiguated lowercase L in Inter, as recommended by upstream.

Iosevka has their own configuration system which allows you to graphically customize the font, and export a configuration file that can be used later down the line.

The repository which hosts the fonts originally started out with the goal to allow distributions to build the fonts themselves, which is why it used Makefiles with the help of Rose.

Due to Iosevka requiring NPM packages to be configured, the scope was changed to shipping the TTF files themselves. Florian Müllner therefore ported the repository to shell scripts which allows us to update the files only, heavily simplifying the maintenance process.

The repository and fonts are licensed under the SIL Open Font License.

Conclusion

We want to thank everyone that contributed to this font switch by testing, discussing, and coding!

Adwaita Fonts will be the default in GNOME 48, and we hope youʼre as happy with this change as we are.