Loading...

Introducing Grex: Reactive GTK4 UIs

2022-04-17 ⋅ Comments

I’ve long been a fan of the GTK GUI library, using it on various personal projects. Despite this, I’ve started to find myself missing a more reactive UI development approach, as seen in modern web frameworks. In these libraries, you don’t mutate your UI; instead, you mutate the state that’s used to build the UI, and the framework updates the UI to match.

A few years ago, I started analyzing the way these frameworks work internally, trying to determine the best way of bringing it to GTK. After various failed ideas, I finally settled on one that I think has the potential to be fully usable: Grex.

Oh, did I mention it has hot reload?

What are Reactive UIs?

First, let’s take a step back: what the heck is a reactive UI? Well, this is a concept that tends to be given slightly different meaning across different frameworks, but here’s the core principle:

Your UI is a function of your state.

What does this mean? Imagine you have a counter widget; the state it holds might look something like this:

{ count: 0 }

Now, if we wanted to make a UI in a traditional framework like GTK, we might do something like this (note that this code is untested and might not, err, actually work):

count = 0

counter = Gtk.Label(label=f'Count: {count}')

def on_click(button):
  nonlocal count
  count += 1

  counter.set_text(f'Count: {count}')

button = Gtk.Button(label='Increment the counter!')
button.connect('clicked', on_click)

box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.append(counter)
box.append(button)

Notice that, when our state changes, we directly mutate the widgets ourselves. This may be any changes ranging from simply setting a label (like here) to adding or removing children of a widget.

Although this approach is perfectly suitable, it can get a bit…​messy, as anyone who’s worked on a large-scale UI can vouch for. Note that I’m not referring to issues from lack of separation of concerns between your UI and business logic, which is what architectures such as MVC/MVVM aim at solving. Rather, it’s the fact that it’s easy to forget how to change your state. You might use a property somewhere but forget to listen to changes, or you listen to changes but forget to perform some of the needed mutations. In other words, your state and the representation that should be accompanying it are now out of sync.

Imagine that, instead of all of this, we have our state in a simple object somewhere:

@dataclass
class CounterState:
  count: int

Then, we also have a function that takes in that state and builds some widgets:

def build_ui(state: CounterState) -> Gtk.Widget:
  def on_click():
    state.count += 1

  label = Gtk.Label(label=f'Count: {state.count}')
  button = Gtk.Button(label='Increment the counter!')

  box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
  box.append(label)
  box.append(button)

  return box

Now, imagine that, whenever the state changes, we just call build_ui(state) again and show this new UI to the user. Since we’re remaking the UI each time, it’s now impossible for it to ever be out of sync with our state.

(Virtually) Rebuilding the World

The usability of this approach is nice…​but the efficiency isn’t that great. We’re recreating the entire UI each time, which also means redoing the text and widget layout—​not exactly cheap operations. In addition, individual widgets can’t ever retain their own state, because any individual change in our state recreates every single widget.

In order to solve this, most JS frameworks adopt something called the virtual DOM. Instead of our function returning the actual widget tree, it returns a virtual widget tree, a very lightweight recreation that mimicks the structure of the actual tree. Then, that virtual tree is compared to the previous incarnation, and the differences are applied to the actual tree.

If we were to apply this concept to GTK, it might look a bit like this:

def build_ui(state: CounterState) -> Gtk.Widget:
  def on_click():
    state.count += 1

  return VGtk.Box(
    orientation=Gtk.Orientation.VERTICAL,
    children=[
      VGtk.Label(label=f'Count: {state.count}'),
      VGtk.Button(label='Increment the counter!'),
    ]
  )

The returned virtual tree can then be converted to actual GTK widgets. When the state is modified, we diff the newly returned virtual tree with previous one, and then any changes found are applied to the actual tree. This has some concrete benefits:

  • Manipulating the virtual tree is easy, simple, and cheap.

  • The actual widget tree is only created once, with minimal modifications applied as needed.

It does bring some trade-offs, however:

  • A lot of values are allocated every update. In fact, some languages that rely on mechanisms like this specifically optimize their GCs for fast object creation. This is harder with GObjects themselves, which are rather expensive to create, and using custom types to make it faster would be rather ugly.

  • The tree doesn’t need to specifically be defined from code (you could, say, build the UI via a JSX-like syntax), but it will inevitably have to be manipulated from code at some point. Making this elegant-ish across C and all the languages with GObject Introspection is tricky, and the memory management in C would be frightening.

My original ideas revolved around a vdom-like approach, but I ended up scrapping them because it was quite awkward to use.

Piece by Piece?

Later on, I was reading up on Angular’s new Ivy compiler, and something stood out to me: incremental DOM. The idea here is a bit different: instead of building up a whole tree on every update, build up the new "tree" by walking it and applying changes as needed. Our application of this to GTK might look a bit like this:

def build_ui(builder: Builder, state: CounterState) -> Gtk.Widget:
  def on_click():
    state.count += 1

  with builder.element(Gtk.Box, orientation=Gtk.Orientation.VERTICAL):
    builder.element(Gtk.Label, label=f'Count: {state.count}')
    builder.element(Gtk.Button, label='Increment the counter!')

As the builder.element methods get called, they would compare their values with the current widget tree, and then add/remove properties and children as needed to make it match with what was requested. In the case of Angular, the code to build this is all auto-generated, so you just write your nice HTML templates, and the potentially (well, likely, if you want to be efficient) more verbose builder code is auto-generated.

I figured this approach would have some distinct advantages for use with GTK:

  • No virtual DOM tree! No large amounts of allocations every render!

  • Manipulating the rendering process is done via imperative code, which would be much simpler to support for both C and languages with GObject bindings.

So we’re done now, right! Well…​

Off to XML Land We Go

There’s still a catch: an introspection-friendly API won’t be nearly as elegant as what’s outlined above. It might look a bit more like this:

def build_ui(builder: Builder, state: CounterState):
  builder.start_element(Gtk.Box)
  builder.add_property('orientation', Gtk.Orientation.VERTICAL)

  builder.start_element(Gtk.Label)
  builder.add_property('label', f'Count: {state.count}')
  builder.end_element()

  # ditto for Button...

  builder.end_element()

I omitted the button because I already got tired of writing this code! We need a nicer way to define these.

DSLs here are an attractive proposition, as you can define a syntax that works perfectly with your goals…​and then also write the IDE integration and ecosystem around it. To be frank, that’s not an adventure I wanted to embark on! (If you’re familiar with Blueprint, please read on towards the end.)

With this in mind, I figured I could use a language that, well, it’s not the prettiest, but it’s okay as long as it’s not abused, and GLib has built-in support for parsing a subset: XML. Then, the code could maybe look like this:

<GtkBox orientation="vertical">
  <GtkLabel label="Count: [count]" />
  <GtkButton label="Increment the counter!" />
</GtkButton>

My original idea was to compile the XML to rendering code, but that requires a separate build step and would need generators for all the GObject languages. Maybe there’s a simpler solution?

(Also, you may have noted that we omitted the callbacks…​and the actual reactivity. These were specific problems I needed to solve for Grex)

Meet Grex

Now with all of that background out of the way, let’s see what the heck Grex is.

Take a look at this XML:

<GtkLabel label="Fecto" />

These XML nodes are what we call fragments. Fragments are a bit like virtual DOM, but the fragments themselves are never modified after they’re parsed! Instead, we can walk the fragments and incrementally convert them to GTK widgets, like incremental DOM incrementally updates the actual DOM. The process of converting the fragments into widgets is called inflation, and it’s performed by the inflator, GrexInflator.

The key trick here is this: any time the code needs to interact with the inflation process, it never modifies the fragments themselves. Instead, it hooks into the actual inflation process and can change the incremental update steps that are performed. In order words, you can customize the way things render without having to perform large amounts of allocations every update.

In order to actually update, though, we need to know what’s changed. If a new fragment is rendered next that looks like:

<GtkLabel label="Chaos" />

How does the inflator know what actually changed? It could compare it to the widget itself, sure, but a widget might have dozens of properties, and we don’t know which ones the user had actually set before. We need some way to store the previous fragment’s information and compare it during the next inflation.

Grex’s solution to this is the fragment host. A GrexFragmentHost is attached to each widget, and it stores the properties set on it and their values. When a new inflation takes place, the inflator gives the fragment host the new properties, and the fragment host can then apply the changes to the widget.

The actual API looks a bit like:

widget = Gtk.Label()
host = Grex.FragmentHost.new(widget)

host.begin_inflation()
host.add_property('label', 'Fecto')
host.commit_inflation()

# Later...

host.begin_inflation()
host.add_property('label', 'Chaos')
host.commit_inflation()

Expressive Bindings

All of this is great, but…​how do we actually make this reactive? That is, how do we get this to read the label from a property, and track when we should do a new inflation? There’s a piece to the puzzle that was missing: bindings.

In order to get a fragment that actually behaves like our original counter, it would look more like:

<GtkLabel label='Count: [count]' />

But, what’s [count]? The answer is that the value assigned to label is a binding; a value that can be computed and is reactive. These bindings can contain expressions, the thing inside the square brackets. In this case, this will look for a property named count in the expression context, which is just a sort of "scope" that all expressions in an inflation are evaluated in.

The key part here is that, when an expression is evaluated, it tells the context to keep track of all the properties it’s used. In addition, the inflator tells the context to let it know when any properties that were used have been modified, so a new inflation can take place.

You can also have two-way bindings:

<GtkSwitch active='{active}' />

If the switch is toggled, this will then modify the active property that was found in the expression context.

Bindings can even be used to handle signals:

<GtkButton on.clicked='emit object.thing($0)' />

When the clicked signal is emitted on the button, this will emit the thing signal on object, passing the first argument to the clicked signal $0.

Birthing Children

Fragments, being XML elements, can contain other fragments:

<GtkBox orientation='vertical'>
  <GtkLabel label='Count: [count]' />
</GtkBox>

In this case, each child is given a key denoting its place in the parent. As new children are added or removed, they’re matched to the previously known ones via their keys.

Unfortunately, GTK4 has no singular API to add children to a container. As an ugly workaround, each container is assigned a container adapter, which determines how children should actually be added to a parent. There’s some interesting magic that goes into how these are assigned, but yet another concept comes into play first.

Prime Directives

What if we want to customize the way fragments are mapped to their host somehow? We might want to add or change a few properties, or change the way children are added. This is accomplished via directives, which look like normal properties that start with an uppercase letter:

<GtkLabel MyDirective='something' />

The inflator is given some directive factories, which are just objects that can identify and create directives. Each inflation, the directive is called with the fragment host, and it can then modify the host as it sees fit:

class MyDirective(Grex.PropertyDirective):
  def do_update(self, host):
    host.add_property('label', Grex.ValueHolder.new('A label here'))

class MyDirectiveFactory(Grex.PropertyDirectiveFactory):
    def do_get_name(self):
      return 'MyDirective'

    def do_get_property_format(self):
      return Grex.DirectivePropertyFormat.IMPLICIT_VALUE

    def do_create(self):
      return MyDirective()

The update virtual method is the one called with the host.

Note the get_property_format part. We can have the value passed to the directive in the fragment (something above) assigned to a property on the directive named value, or have the user explicitly specify properties to assign to (<GtkLabel MyDirective.x='[1]' />).

Directive factories can also automatically attach their directives to fragments. This is used so that GTK containers can have their container adapters automatically set.

In addition, structural directives can modify how a child is added to its parent, specified by prefixing the capital letter with an underscore:

<GtkBox>
  <GtkLabel _Grex.if='[condition]' />
</GtkBox>

This will only add the label if condition is true.

Too Many Words, Too Little Code

In the end, our counter example might look like:

<Counter>
  <GtkBox orientation='vertical'>
    <GtkLabel label='Counter: [count]' />
    <GtkButton on.clicked='emit increment()' label='Increment the counter!' />
  </GtkBox>
</Counter>

with this Python code paired with it:

class Counter(Gtk.Widget):
  TEMPLATE = Grex.Template.load_from_resource(
    '/org/hello/Hello/hello-window.xml', None, None)

  def __init__(self):
    super(Counter, self).__init__()

    self._count = 0

    self._inflator = self.TEMPLATE.create_inflator(self)
    self._inflator.get_base_inflator().add_directives([
      Grex.GtkBoxContainerDirectiveFactory(),
      Grex.GtkWidgetContainerDirectiveFactory(),
    ])

    self._inflator.inflate()

  @GObject.Property(type=int)
  def count(self):
    return self._count

  @count.setter
  def count(self, new_count):
    self._count = new_count

  @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST)
  def increment(self):
    self.count += 1

There’s a new concept introduced here: the template, which is just a term used for a tree of fragments used to render a particular widget. Here, we load a template from a [GLib GResource](https://docs.gtk.org/gio/struct.Resource.html).

This probably seems like a lot of code. But, remember: reactivity! All of this is fully reactive! We never mutate the GUI; instead, we mutate our state (the count property), and the UI is updated to match.

Yo, We Got It! Hot Reloads!

Remember, I promised hot reloads at the start? Well, Grex can do those! (In fact, this was the main thing I missed about GTK after using Flutter for a while.) Since our entire UI is just a function of the state, we can swap out the entire fragment whenever we want, and the UI will magically update to match it, while preserving as much of the state as possible.

The only requirement is that, instead of loading the GResource normally, it has to be loaded via Grex.ResourceLoader:

Grex.ResourceLoader.default().register('the-file.gresource')

Then, you can start the application with GREX_RELOAD=1, and any changes made to the GResource file will reload the UI.

Here’s a demo with the incredibly ugly examples I have in Grex right now:

What Next?

Although this is the first time I’ve publicly shared Grex, there’s still a lot of work to do, ranging from the mundane to the fancy:

  • Right now…​everything leaks. The inflator holds a strong reference to the widget, which holds a strong reference to the inflator…​classic circular reference.

  • But it doesn’t matter a lot anyway, since you can’t embed a widget using Grex into another widget using Grex; they’ll both try to attach a different fragment host to the same widget.

  • Construct-only properties can’t be set.

  • Grex only has container adapters for a few widgets in GTK, and none for widgets in libadwaita.

  • There are a lot of low-lying optimizations available, such as caching constant expressions (stuff like [1] doesn’t need to be evaluated more than once, and the same goes for [x] if x is a read-only property).

  • There is very little documentation. Very little.

  • etc, etc, etc.

That being said, I do hope there’s enough here to garner some interest!!

Appendix: Uncanny Timing

If you’re in touch with recent developments in the GNOME/GTK ecosystem, you’re probably familiar with Blueprint and might be wondering how they overlap. Currently: they don’t. Blueprint is a beautiful language that compiles to GTK’s builder templates, and Grex pretty much replaces builder templates. Thus, there’s no reason that Blueprint couldn’t, say, compile to Grex templates instead. Simple, right?

Well…​not so much. If you’re in touch with recent developments, you probably know about the plans to add reactivity to Blueprint. This does overlap with Grex, and it leaves the question of how these would play together.

I’m going to be entirely transparent: I’m an incredibly devout perfectionist, thus I have a tendency to keep projects private for a very long time, until I feel like it’s in a "reasonably elegant" state. Unfortunately, there are also a non-zero amount of times where things such as duplicate work or general disillusion result in me abandoning the projects in the end.

Recently though, I stumbled upon one of the very, very rare cases where an HN comment includes actually useful advise (and not just torment):

If you’re not a little bit embarrassed by what you’re showing, you waited too long

So, that’s what I’m trying to do now. The Grex I presented above is an incredibly early WIP, but I think it has a lot of potential. In the worst case, even if Blueprint has a superior reactivity system (the developer is brilliant, and I have no doubt they have their own plans here!), I’m hoping at least some interesting lessons can be learned from my attempts with Grex.

Theme

Background
Animations