gerb font editor

graphical font editor Atom Feed

View project on GitHub

Part 1: Structuring a non-trivial GTK + Rust app

Context: I’m attempting to make a toy font editor GUI in Rust.

Investing in a UI framework is the greatest commitment for a GUI programming project. Looking at areweguiyet.com at the time of writing, most of the community solutions are wrappers of non-Rust frameworks, and there is also a substantial proportion of immediate GUI crates. Having delved into gtk before with the gtk-rs (Rust bindings for `gtk) examples, I chose building this app directly on it to have access to whatever capability it has directly.

This approach comes with setting up a lot of “boilerplate” code before you start actually implementing what your app does. That’s both a positive and a negative: on one hand you have total control over what happens in the UI but it will take time.

GTK lets you declare GUIs even with object signals, object properties (we will see what these are later) attached, in XML files. I personally find this unattractive, but it’s a valid alternative to my approach: build the widget tree on runtime by using the gtk::ExampleWidget::builder().option(value).build() pattern that gtk-rs provides.

Coming from a GTK/glib-less world you might be surprised to find every widget and more generally every “object” is memory managed by the framework. Each object can be cloned (and internally its reference count goes up) and you can perform mutations on it even if all you got is a read-only reference of &gtk::Widget. The borrow checker will therefore be absent in most of your boilerplating. Keep in mind that gtk is not thread-safe, all this is happening in one thread so at least we won’t have multiple threads mutating gtk objects.

An incredible surprise for people who need it, gtk-rs provides access to its async runtime for Rust futures. That saves you the trouble of pulling a runtime library if you need async work done.

Ways of keeping UI state

For a small, perhaps single file app I would use a reference counted Mutex of my state struct and pass it around in signal callback closures etc. You can then create standard widgets and build your application. This is a valid and good approach for small things.

This pattern will fall out of favor as the .clone()s you do for that state mutex increase. In fact, as we keep increasing the complexity of the UI widget tree it starts making sense to break down the state for some parts of the application. A list widget doesn’t need to know the state of a drawing widget, for example.

To quell the complexity you will need to start subclassing GTK widgets and make your own. This sounds alien in Rust, but thankfully the gtk-rs project provides traits and macros to help you do that. See the basic subclassing example.

After you subclass a widget, you can allocate your struct with a call to glib::Object::new(). Our custom struct/widget is actually a smart pointer.

As we said, widgets are to be treated as having interior mutability because we’ll have access to read-only references sometimes. Thus to use fields in your custom widget/struct you must use an interior mutable type like std::cell::Cell. To simulate immutability you can use OnceCell to set up values on widget construction time.

Ways of inner app communications

The separation of concerns and states is incomplete without a way for parts of the UI to communicate events to the rest of the application.

Here are some ways I implemented this:

  • Keep a reference of widget A inside widget B. Then B can call A’s methods any time it wants.
  • Use object properties. Properties are declared as part of the object trait you use for “subclassing” the glib object. You can then implement the methods .property::<Type>("name") and .set_property(name, value). But to do this, you must have a reference of the target object, so this is the same as the previous bullet point. GTK though has a concept of binding properties of different objects together so that when one changes, the change is communicated and perhaps even transformed with a custom callback to the other object. This way you can setup communication paths between widgets upon the app’s initialization.
  • Use GTK actions. You can register named actions to widgets, and then activate them from other widgets. You can even pass parameters! This sounds like a good way to implement user actions you would want to add in an undo/redo system.

Custom message queues are out of the question because widgets don’t “wake up” unless a connected signal arrives or the widget gets drawn (you can override the draw function). To send a signal to an object, you must have a reference to it, so it’s not really suited for message passing, it’s more like implementing traits with callbacks.

Configuration

For this part, leave GTK out of it. Use your favorite configuration solution. My suggestion is TOML + serde as the format and the XDG user configuration directory as the location of the configuration file.

Theming

GTK is mostly themed by CSS (and I say mostly because some stuff like widget dimensions or spacings can’t be defined with CSS). You can keep a .css file in your repository, include it as a static byte array in rust with include_bytes! and load it in your gtk::Application at startup. You can style widgets like you target HTML elements (for example label corresponds to any label widget just like div in HTML CSS corresponds to any div element). GTK allows you to add extra CSS classes to your widgets.

Just make sure your styling works with other GTK themes. Better be safe than sorry and override everything with your custom theme.

Debugging

GTK includes an inspector interface, that opens up in a separate window. You can see the widget hierarchy, the CSS nodes, the properties of each widget instance, the signals they have connected, trace signal calls, and more. It’s really helpful so don’t neglect it.

gtk::Window::set_interactive_debugging(true);

Handling embedded icons or other images

TODO: Pixbuf from raw

What if I need specific functionality that stock GTK widgets lack?

TODO: DrawingArea