The Internals of Fractal-Next

As you probably know already we are in the process of rewriting Fractal. Since my previous blogpost about Fractal-Next a lot has happened. We are now at the point where the bare minimum is working, including login, the sidebar, and the room history. These things are not totally complete yet, but it’s already possible to run Fractal-Next, and read and send messages.
The previous post was kept for a general audience, but this one goes into more technical details of the foundation we put in place for Fractal-Next. In this post I will try to provide a more detailed look into the machinery, to give newcomers a general idea on how Fractal works. This is just an introduction and we are working on improving the docs for the entire project.

Fractal-Next uses GTK4 and therefore also glib’s GObjects. Every major part of Fractal is implemented as a subclass of a GObject or a GtkWidget. This being said, anything that doesn’t depend on GTK or glib will go, whenever possible, into the matrix-rust-sdk so that other projects can benefit from it. In return this will reduce the code maintained by the Fractal team and it keeps a clear separation between UI code and the backend code.
Fractal’s old code base doesn’t use subclassing nor composite templates, because when Fractal was started the Rust bindings for GTK didn’t have support for it yet. Today we can conveniently create subclassed objects with great support for composite templates to define the UI.

Subclassing and composite templates

We make heavy use of subclassing and composite templates. This allows us to create a GObject that contains the data and lets the object worry about maintaining consistency of the data with the Matrix homeserver. The object itself makes the appropriate calls to the SDK and then it exposes the information as GObject properties. This way, without going into too much detail, any GtkWidget can simply bind to the object’s property and it doesn’t have to worry about updating, requesting, and not even about the actual data it displays, because the binding keeps the widget and the object property in sync automatically.
We create GObject wrappers for many cases but not for everything. Matrix is built around sending events to logical places called Rooms. Generally, Matrix events can’t change their content and therefore we don’t need to make sure they are kept up-to-date, therefore we can use the rust struct directly instead of wrapping each single Matrix event into a GOjbect. It would mean that we need to writing a lot of additional code that for the definition and creation of the object. In short, it would result in a lot of overhead without any real benefit. Therefore we are using the Rust struct returned by the SDK directly to create the event widgets.

Async calls

You may wonder how we handle async functions from the matrix-rust-sdk. It’s simple, we created do_async() (thanks to slomo for the help making the function much nicer). The function takes two async closures, one that is executed on the tokio runtime and a second one that is spawned to the main context used by GTK. Obviously this function can only be called from the thread where the main context is running, but that’s not a problem for us since GTK can only run on a single thread and Fractal is mostly single threaded as well. We only use tokio with multiple threads for the SDK.

For example, the method to get the display name of a room looks like the following code snippet. self.set_display_name() sets the display-name property and emits a notify signal.


fn load_display_name(&self) {
  let priv_ = imp::Room::from_instance(&self);
  let matrix_room = priv_.matrix_room.get().unwrap().clone();
  do_async(
    async move { matrix_room.display_name().await },
    clone!(@weak self as obj => move |display_name| async move {
      match display_name {
        Ok(display_name) => obj.set_display_name(Some(display_name)),
        Err(error) => error!("Couldn't fetch display name: {}", error),
      };
   }),
  );
}

Important objects and widgets

Let’s have a look at some of the important GObjects and GtkWidgets we created. In some cases we create subclassed objects and in some cases we subclass widgets that keep track of the data produced by the SDK. This is because in many cases the data has only one consumer, the widget, so it doesn’t make sense to create a GObject and a GtkWidget that essentially contain the same data. Note that a widget is also a GObject. However, whenever we have information that is consumed by multiple widgets (e.g. a Matrix room) we create an object so that we don’t need to perform the same calls to the SDK multiple times and aggregate it for each single widget.

  • Login is a widget that handles the login flow and produces a Session.
  • The Session is the core of the client. It handles the login to the Matrix server and the sync, obviously via the matrix-rust-sdk. It also creates the UI and every widget needed to display a logged-in matrix account is a child of the Session or a submodule in the rust world.
  • The Room is an object that stores all information pertaining to a Matrix room, e.g. the display name and timeline, and makes sure that the correct values are exposed as GObject properties.
    • The Timeline is a GListModel, the data structur behind GtkListView, containing all events that are shown in the room history.
  • User is an object that tracks a Matrix user and exposes the display name and the avatar conveniently as GObject properties. To work properly it needs to receive room member state events. A User needs to be specific to each room because Matrix users can have different names and avatars for different rooms.
  • Sidebar is the widget that contains the sidebar, build around a GtkListView.
  • Content is the widgets to display a room’s content, including the room history. The room history uses a GtkListView that is connected to the Timeline.

All objects and widgets can be found in the Rust docs. Even though most entries don’t have any description yet, it should help you gain a deeper insight into the structure of the code.

Final words

Up to this point it was pretty much impossible to contribute to Fractal-Next because not much was set in stone and many parts still needed to be figured out. However, it’s now at the point where people can start contributing and help make Fractal-Next as awesome as the old Fractal, and of course way beyond that. We already have a few new contributors. To name two: Veli Tasalı, who worked on improving things around meson and added the generation of the rust docs to the CI, and Kévin Commaille who worked on improving the look and feel of the sidebar among other things.

Fractal-Next wouldn’t be possible without the awesome people working on the GTK Rust bindings, libadwaita, matrix-rust-sdk, and many more. Therefore a big thanks goes out to them. And of course, a huge thank you to NLnet and NEXT GENERATION INTERNET for sponsoring my work.

2 thoughts on “The Internals of Fractal-Next”

  1. I can’t express how happy I am about progress on Fractal-Next. The work on the SDK is of course slowing down progress at first, but will pay off many times through synergies with other Matrix-based software. I’m really happy to use Fractal on my Librem5 once I receive it (planned for October 2021, let’s see…) because having a native application appears more attractive to me than Element based on electron. Also looking forward to replace Element on my desktop, but of course I can only fully replace it once E2EE is available. Thanks for your hard and transparent work on the project!

Leave a Reply

Your email address will not be published. Required fields are marked *