Reinventing tabs

GNOME Web 40

In GNOME 40, Epiphany will feature a new tab bar. This isn’t just a restyling of the old one, but a ground-up rewrite. But why was this needed?

Static vs dynamic

GNOME apps use tabs in a lot of different ways, but they can be roughly divided into two categories. I will refer to them as static and dynamic tabs.

Static tabs

They are completely immutable, each tab contains a title label and they usually don’t scroll. They are usually used in dialogs, but, for example, can also be used in a ribbon toolbar. Modern GNOME apps often use GtkStackSwitcher or HdyViewSwitcher instead, but tabs are still quite common.

Static tabs in various dialogs
Static tabs in various dialogs

Dynamic tabs

These tabs are used in multi-window apps such as web browsers, text editors, file managers or terminal emulators. They are opened and closed by the user, they can be reordered, dragged between windows or into new windows by dropping them on desktop. They need scrolling. They always have a title and a close button, sometimes an icon and/or a spinner. They hide when only one tab is present. They can be closed by middle clicking, they have context menus. Sometimes they can be pinned or have an extra icon, for example an audio playback indicator. Sometimes they need confirmation on closing.

Top to bottom: gedit, GNOME Terminal, Nautilus, GNOME Text Editor, Devhelp, Sysprof, Epiphany

While static tabs are pretty uniform across GNOME, dynamic tabs in every application look and feel very different. For example:

  • GNOME Text Editor has close buttons on selected and/or hovered tabs only, other apps have them on all tabs.
  • Some apps allow tabs to shrink to a very small size before scrolling, others start scrolling earlier.
  • Devhelp doesn’t scroll at all and just overflows tabs off the window.
  • Epiphany and GNOME Terminal have popovers showing all tabs; GNOME Terminal is using GtkMenu while Epiphany is using a GtkPopover.
  • While not visible on the screenshot, only Epiphany and gedit support dragging tabs between windows.
  • It’s possible to drag files onto tabs in Epiphany and Nautilus. In Nautilus, hovering over a tab while dragging a file will automatically switch to that tab, but not in Epiphany.
  • In gedit and GNOME Terminal it’s not possible to focus the active tab by clicking it again.
  • In GNOME Text Editor and Epiphany, moving to another tab when focusing the active tab will select the page instead.

But why such a difference?

GtkNotebook

All of those apps are using a widget called GtkNotebook. It implements a generic tabbed view and shows tabs on one of the sides, and each tab has a label that can be replaced with an arbitrary widget.

By default, GtkNotebook doesn’t scroll, has a simple title label on each tab, doesn’t expand tabs, doesn’t allow any kind of drag-n-drop.

In other words, it’s geared squarely for static tabs. It doesn’t mean it’s impossible to use it for dynamic tabs, but you will have to manually implement:

  • A tab layout with a close button, a title, an icon, and maybe a mute indicator, and set a reasonable minimum width for scrolling.
  • Shortcuts such as Ctrl+Tab or Alt+[1 – 9] for switching between tabs.
  • Context menu handling, including the keyboard shortcut and a long press gesture for touchscreens.
  • Middle click closing.
  • Tab opening/closing logic — for example, which tab to select when the active tab is closed.
  • If you need to drop data on tabs, a handler for that. You also need to be careful to not accidentally break the hover timeout in process, like Epiphany does in 3.38.x.
  • Hiding when only one tab is present.

Of course, every app ends up implementing these things slightly differently. And then on top of that there are issues that cannot be fully fixed from the app side, such as:

  • Tab scrolling is discrete. This looks confusing and introduces other issues:
    • Dynamic tabs can have a lot of tabs. Pressing arrows to scroll one tab at a time doesn’t really scale for this.
    • Related, it’s not possible to take a tab and reorder it over long distances, you’ll have to reorder it a few tabs forward, press the scroll buttons a few times, reorder it again etc.
    • When opening a new tab, it can end up offscreen with no indication whatsoever.
    • Minimum tab width changes depending on the window size.
  • There are no pinned tabs.
  • The UI in general isn’t very polished — for example, there’s no animation when opening or closing tabs.

Some of that can be papered over though:

  • Epiphany fakes pinned tabs via force reordering them at the beginning, using a compact layout, not expanding them and not allowing to close them, but they can still be scrolled away and the fact the tab width changes based on the window size affects these tabs especially badly:

    Pinned tabs in Epiphany 3.38

    Pinned tabs in Epiphany 3.38

  • It manually highlights arrows when a tab has been opened offscreen.
  • It provides a popover to quickly switch between tabs when they don’t fit on the screen. This doesn’t help with reordering though.
  • elementary’s DynamicNotebook implements a resize delay after closing a tab, but it’s not very reliable, for example it jumps when the scroll arrows disappear. It also can only work when tabs aren’t expanded like in GNOME apps.

Additionally, GtkNotebook contains both view and tabs, and they are inseparable, so it’s impossible to have tabs in fullscreen, autohiding with the header bar, or to put them in the header bar, away from the content.

Finally, it’s not adaptive and Epiphany currently has an in-tree implementation of a mobile tab switcher that every app that wants an adaptive tabbed UI would have to copy.

A rewrite

HdyTabBar demo in libhandy 1.2

So, with all of this, I believe the cleanest way forward is a completely new widget, implementing specifically dynamic tabs. While we’re there, we can also separate the tab view and tab bar into separate widgets just like GtkStack and GtkStackSwitcher are separate.

I started implementing it shortly before GUADEC 2020, and had mostly completed it by the end of September, when I joined Purism. In January me and Adrien finally took the time to finish, review and land it.

The result of this are two widgets called HdyTabView and HdyTabBar, available in the recently released libhandy 1.2.

HdyTabView contains a GtkStack and provides an API more suitable for tabs: for example, pages are strictly ordered, there’s API for tab reordering, but not transition or child names, etc. Additionally, because each page has a large amount of metadata, it uses GTK 4-like page objects (HdyTabPage) even in GTK 3, instead of child properties, which would be pretty annoying to use without property bindings etc.

Meanwhile, HdyTabBar is a tab bar that connects to a HdyTabView and shows its pages, implementing this design.

What it provides

  • Tabs have an icon, a title, a tooltip, a close button, a context menu, and they can show a loading spinner instead of the icon.
  • Pinned tabs. They are compact, placed at the beginning of the tab bar and don’t scroll along with the rest of the tabs; they also affect the behavior of shortcuts such as Ctrl+Home/End.
  • Unread indicators, including when the unread tab is scrolled offscreen.

  • Indicator icons: every page can have one indicator icon on its tab, optionally clickable. Epiphany currently uses it as an audio playback indicator/mute button. Indicators are located on the left side of the tab in order to not conflict with close buttons.

    Indicator icons in libhandy 1.2 demo

    When the tab is pinned, the indicator is displayed instead of the icon and not next to it like previously in Epiphany. To make sure the tab can be easily selected, it’s only clickable if the tab is already selected, so the first click selects the tab, the second click activates the indicator.

  • Autohide when only one tab is present.
  • When closing tabs with a pointer, they don’t fill the empty space immediately, instead they resize so that the next tab’s close button moves exactly where the last one was, allowing to close multiple tabs in a row without moving the pointer.
  • Automatic positioning for new tabs, though there is also API to insert tabs in arbitrary positions if needed.
  • Fully working drag-n-drop: tabs can be reordered, moved between windows, dropped on desktop to create a new window, tabs can accept arbitrary data being dropped. If a tab bar only has one tab, it will be automatically shown when a drag starts and hidden when it ends, so it’s still possible to drop another tab into it.
    Tabs can also be dropped on the tab view; in that case they are appended at the end of the tab bar.
  • Touchscreen support: drag the tab bar to scroll it, long press and drag to reorder a tab, longer press to open context menu.
  • Close confirmation API.
  • Tab opening, closing, reordering, drag-n-drop, showing and hiding of the tab bar are all animated.
  • Shortcuts:
    • Ctrl+Tab and Shift+Ctrl+Tab — switch between tabs, with wrapping.
    • Ctrl+PageUp/PageDown/Home/End — same, but without wrapping.
    • Ctrl+Shift+PageUp/PageDown/Home/End — tab reordering.
    • Alt+[1 – 9] — switching to one of the first 9 tabs.
  • The fact they are separate allows Epiphany to keep its tab bar in fullscreen in 40.

    Epiphany 40 in fullscreen mode

What it doesn’t provide

  • It doesn’t allow to set an arbitrary widget as a tab layout. The layout is fixed and managed exclusively through HdyTabPage properties.
  • It doesn’t allow to toggle reordering and detaching for different tabs separately. Tabs are always reorderable and detachable, there’s no way to disable that.
  • Similarly, there’s no way to make certain tabs expand or not expand. All tabs (except pinned) are expanded by default.
  • All tabs (except when pinned) have close buttons. They cannot be removed, although it’s possible to delay and/or reject a close request, for example if the app wants to show a confirmation dialog upon closing a tab.
  • Close buttons are visible on selected and/or hovered tabs. There’s no option to show them on all tabs.
  • Vertical tabs. HdyTabBar is strictly horizontal and its layout wouldn’t make sense vertical. However, it does have API to observe its pages through a GListModel (GtkSelectionModel in GTK 4), so it’s very easy to make a vertical list instead.

However, since Epiphany is also used on elementary OS, HdyTabBar does provide API to disable autohide and tab expansion, and to swap the close button and indicator. All those options in Epiphany still work as before.

Epiphany in “elementary mode”

Adaptiveness and future

However, HdyTabBar isn’t adaptive either. Epiphany still ships the same mobile UI in 40. However, it makes it a lot easier to implement alternative switchers. For example, a HdyFlap-based bottom sheet:

A work-in-progress mobile tab switcher

However, this is very basic. Mobile tab switchers are often very elaborate, for example using 3D stacks of cards, grids, carousels or lists with previews. Unfortunately, all of that is off limits in GTK 3. However, with GTK 4 it’s not, and we can do things such as this:

A work-in-progress carousel-based tab overview A work-in-progress grid-based tab overview

Having a proper overview would also allow to finally remove the tab popover from Epiphany, which is still there in 40, but only shows when the tab bar starts scrolling.

A GTK 4 port of HdyTabView and HdyTabBar is complete and fully working and just needs code review, though the overview is not a reusable widget yet.


Thanks to GNOME Design Team for designing it and answering my endless questions about how tiny and unimportant things should work, and Thibault Martin and Jordan Petridis for testing it.

Games 3.38

Games 3.38.0

I wanted to start this blog post with “It’s that time of year again”, but looks like Michael beat me to it. So, let’s take a look at some of the changes in GNOME Games 3.38:

retro-gtk 1.0

The library Games uses to implement Libretro frontend, retro-gtk, has been overhauled this cycle. I’ve already covered the major changes in previous blog post, but to recap:

  • Cores now run in a separate process. This provides a better isolation: a crashing core will display an error screen instead of crashing the whole app. This can also improve performance in case the window takes a long time to redraw, for example with fractional scaling.
  • Libretro cores that require OpenGL should now work correctly.
  • The core timing should now be more accurate.
  • Fast-forwarding should actually work, although we don’t make use of it in Games at the moment.

Finally, retro-gtk now has proper docs, published here to go along with the stable API.

Nintendo 64 support

The Legend of Zelda: Ocarina of Time running in Games 3.38, with controller pak switcher open

This was on the radar for a while, but wasn’t possible because all available Nintendo 64 cores use hardware acceleration. Now that retro-gtk supports OpenGL, we can ship ParaLLEl N64 core, and it just works. There’s even a menu to switch between Controller Pak and Rumble Pak.

Unfortunately, retro-gtk still doesn’t support Vulkan rendering, so we can’t enable the fast and accurate ParaLLEl RDP renderer yet.

Collections

Collections in Games 3.38

Neville Antony has been working on implementing collections as part of his GSoC project. So far we have favorites, recently played games and user-created collections.

You can read more about the project in Neville’s blog

Faster loading

In my previous blog post I already mentioned some improvements to the app startup time, but a noticeable progress was made since then. Newly added aggressive caching allows the app to show the collection almost instantly after the first run, and then it can scan and update the collection in background.

Search provider

Search Provider in Games 3.38

Having a complete cache of the collection also allowed to easily implement a fast search provider, so the collection can now be searched directly from GNOME Shell search.

Nintendo DS screen gap

Nintendo DS has two screens. Most games use one of the screens to display the game itself, and the other one for status or map. However, some games use both screens and rely on the fact they are arranged vertically with a gap between them.

If the two screens are shown immediately adjacent to each other, these games look confusing, so in 3.38 a screen gap will be automatically added when using vertical mode.

The size of the screen gap is optimized for a few known games, such as Contra 4, Sonic Colors or Yoshi’s Island DS, other games use a generic value of 80 pixels.

If you know another game that needs to use a specific gap size, please mention it in the comments or open an issue on GitLab.

Gamepad hot plugging in Flatpak

Previously, a major limitation of Flatpak version was that gamepads had to be plugged in before starting the app. Thanks to a change in libmanette 0.2.4, this has been fixed and gamepads can be connected or disconnected at any time now.

Technically, this isn’t a 3.38 addition, and in fact the 3.36.1 build on Flathub already supports it. Nevertheless, it was done since my last blog post and so here we are. :)

Miscellaneous changes

Games covers in Games 3.38

Game covers now look prettier thanks to Neville: they are now rounded and have a blurred version of the cover as background.

Preferences window in Games 3.38

The preferences window has been overhauled to be more similar to the libhandy preferences; unfortunately we can’t use HdyPreferencesWindow yet because of some missing functionality.

Controller testing in Games 3.38

When testing or re-mapping a controller, analog stick indicators now show the stick’s precise position instead of just highlighting one of the edges.

Search empty state in Games 3.38

Veerasamy Sevagen implemented an empty state screen for the collection search.

Swipe to go back in Games 3.38

Swipe gestures to go back are now supported throughout the app. The only place that lacks the gesture now is exiting from a running game, because the game may require pointer input, and having a swipe there may cause conflicts.

Getting Games

Download on Flathub

As always, the latest version of the app is available on Flathub.

On windows and titlebars

Web, Games and Boxes

Recently, I’ve been working on a few widgets for libhandy to provide applications more flexibility with how to handle their titlebars.

But doesn’t GTK already allow this? Let’s take a look.

First, GTK has a widget called GtkHeaderBar. It looks like a titlebar, has a close button, a title, a subtitle and allows to add widgets at the left or right sides, or to replace title and subtitle with a custom widget.

Second, there’s gtk_window_set_titlebar() function that allows to set a widget as a custom titlebar. GTK will place that widget above the main window area, and then it can be dragged to move the window, and will handle right click menu, double click and middle click. Additionally, GTK will draw client-side window border and/or shadows and provide an area to resize the window with. Naturally, GtkHeaderBar is a perfect fit for it, although nothing is preventing other widgets from being titlebars.

If nothing is set, GtkWindow will use a GtkHeaderBar with a special style class (.default-decoration) as a titlebar on Wayland, or legacy decorations on X11.

This approach works well if an application just wants to have a titlebar with some widgets in it. However, in many cases it’s more complex. Let’s take a look at some cases that are difficult or impossible to implement right now.

Split headerbars

Settings 3.36, using a split headerbar
Settings 3.36, using a split headerbar

A very common case is for dual-pane applications to have a separate titlebar for each pane, also known as split headerbars. To do this, you have to create a horizontal box with the panes divided by a separator, and put it into the window. Then you create another box containing two headerbars and another separator, and set that box as a titlebar. Then you need to ensure the width of the headerbars matches the content panes, either by hardcoding it or using a horizontal GtkSizeGroup. And then there’s the whole problem of ensuring the window controls show up on the correct headerbars.

Fast forward to 2020, now we have libhandy and HdyLeaflet. Now instead of two boxes you use two leaflets, and now it’s necessary to use a GtkSizeGroup for each pane, to ensure the folding animation doesn’t go out of sync. For window controls libhandy provides HdyHeaderGroup, so at least that’s simple. Also, you wrap the titlebar leaflet into a HdyTitleBar.

And then leaflet gained support for back/forward swipe gestures. Since we have two leaflets, their swipe progress has to be synchronized via a HdySwipeGroup, so that brings the number of the helper “group” objects to four.

Stack navigation

Software 3.36 showing Software 3.36
Software 3.36 showing Software 3.36

Somewhat related, many applications have multiple views, with a separate GtkHeaderBar for each view. This includes the split headerbar case on mobile, where the two leaflets shows only one view at a time.

It’s implemented via GtkStack. One stack in the window has the views, and another stack in titlebar has headerbars, then you always change their visible child at the same time. libhandy 1.0 will have HdyDeck allowing back/forward swipes the same way HdyLeaflet does, and so the two decks must be synchronized via a HdySwipeGroup too.

Stack navigation and split headerbars can be combined, with some views having split headerbars, and others having just one headerbar. Either way, you have to duplicate the whole hierarchy between the window and titlebar and keep it in sync, however complex it might be.

In both cases you might want to animate the transition. With swipes it’s pretty much necessary. And doing that breaks the window corners during the transition.

libhandy has a workaround for that called HdyTitleBar. It’s a simple GtkBin subclass that looks like a headerbar. Combined with the fact Adwaita CSS specifies transparent background for any nested headerbars, it means you can put a stack/box/leaflet/deck/anything with headerbars into a HdyTitleBar and animate them without moving the background, working around the corners issue. It’s not perfect, as tall content (such as a separator) would still overlap the corners, but it works in most cases.

Autohiding headerbar

Some applications want to hide headerbar to focus on the content. The easiest way to do it is to fade out the headerbar and just have a blank space there.

UberWriter 2.1.5
UberWriter 2.1.5

Since the titlebar is a separate area from the window, there’s no content behind it, so fading it out leaves an unsightly blank area.

There are ways to mitigate that. For example, one could hide the titlebar widget completely. That’s what UberWriter, now Apostrophe, does in the latest development version:

Apostrophe, development version
Apostrophe, development version

It works, but the code is not pretty. It uses two headerbars: one in the titlebar area, another one in the window area in a GtkOverlay. To hide the titlebar, it immediately hides the headerbar in the titlebar area, then shows the headerbar inside the window and fades it out. Then it adds some CSS to add round corners on the window instead of headerbar, and makes sure nothing overlaps them. Oh, and it also shifts the scroll position so that the text doesn’t jump.

Another way is to use a revealer with a slide transition inside the headerbar area. Then it’s possible to shift scroll position on each frame of the transition, although it leads to a visual glitch with window corners.

A demo I made half a year ago
A demo I made half a year ago

So there is no way right now to do it cleanly and without glitches, although you can get pretty close.

Fullscreen

Web 3.36, in fullscreen
Web 3.36, in fullscreen

I said above that Apostrophe uses two headerbars. Well, I lied! It uses three, the third one for fullscreen mode.

Like the titlebars drawn by X11 window managers, GtkWindow‘s titlebar area is hidden in fullscreen. At the same time, our HIG recommends to still have a headerbar in fullscreen, but to autohide it. The easiest way for apps to implement this is to have another headerbar inside a GtkRevealer in GtkOverlay in the window content area, that’s normally hidden and only shows up in fullscreen mode. Then it can be shown and hidden by application whenever wanted.

However, this means you have to have two (or more!) headerbars, or to reparent it from titlebar to the revealer when entering fullscreen and then back to titlebar when exiting it, like DzlApplicationWIndow from libdazzle does.

Showing content behind the headerbar

A camera app mockup, by Tobias Bernard
A camera app mockup, by Tobias Bernard

What, you were expecting a screenshot? Since the titlebar area is completely separate from the window, it’s impossible to show content behind it.

Similarly, things such as showing scrolling content behind titlebar, like macOS and Windows do, are impossible.

Window/titlebar split

All of these problems are caused by the fact titlebar is separate from the window. If that wasn’t the case, it would all be a lot simpler:

  • For split headerbars and stack navigation you would be able to use a single box/stack/leaflet/deck spanning the whole window, no need to duplicate the hierarchy in the titlebar.
  • Autohiding would be a matter of using GtkRevealer and/or GtkOverlay, just like in fullscreen.
  • The headerbar wouldn’t be hidden in fullscreen, so it would be possible to reuse the widget without reparenting or duplicating it.
  • Similarly, showing content behind the headerbar would be perfectly possible with just GtkOverlay.

So, how do we eliminate the split? Obviously, it’s not an option for GTK3. There’s GTK4, but it would be nice to have something working in the meantime. This means libhandy.

HdyWindowHandle

HdyWindowHandle example
HdyWindowHandle example

The first thing we need to have headerbar inside the window hierarchy is to make it act like a titlebar. GtkHeaderBar isn’t enough: while it has the overall layout and window controls, it doesn’t handle dragging, or right-click menu, or double click, or middle click. All of this is provided automatically for the titlebar widget, whatever that widget is, and that’s it.

To solve that, libhandy now has a HdyWindowHandle widget. It’s a GtkBin subclass that acts as a “titlebar area”. Dragging it will move the window, right clicking will produce a menu etc.

So, when a headerbar is used inside a window, it can be wrapped into a HdyWindowHandle and it will just work. It can also be used to easily make the whole window draggable. Or a random part of it for whatever reason.

HdyHeaderBar

However, the HdyWindowHandle has a downside of being easy to forget when creating a headerbar. If it’s not used, the window looks exactly the same, but the headerbar can’t be dragged. It’s especially easy to miss if you’re testing your app on a phone, where the window can’t be moved, or usually move windows while holding the Super key.

And it just so happened that (for unrelated reasons), libhandy has had a fork of GtkHeaderBar called HdyHeaderBar. It now features the same draggability and right/double/middle click handling as HdyWindowHandle, so can be used as is.

HdyWindow

One thing about the headerbars being in a separate area from the window content is that they can easily have round corners. While the window background can have round corners via CSS (and in fact that’s what elementary OS has been doing with the .rounded style class), nothing prevents the window content from overlapping them. This is true for the titlebar as well (see HdyTitleBar), but there’s a lot less chance of that happening.

However, if we want to display content behind the headerbar, or to autohide the headerbar, it’s pretty much guaranteed to happen.

One way to prevent that is to mask the corners. This guarantees nothing can ever overlap them. And that’s exactly what HdyWindow and HdyApplicationWindow are doing.

Coincidentally, GTK has no public API to inspect border-radius CSS property for each corner separately, so these windows also have round bottom corners.

They also draw a sheen on top of the window, which is normally on the headerbar. It looks the same way if a headerbar is used on top of the window, but still looks good if it’s hidden.

Games with hiding titlebar

Limitations

However, the corners come at a cost: they cause an overhead when OpenGL is used. Normally GTK has a fast path for when nothing is overlapping OpenGL drawing (such as inside a GtkGLArea). However, masking corners requires a redirection, so the gains from that are negated.

On my machine the only place I was able to notice it is in GNOME Games running particularly “heavy” games on a workspace with lots of windows.

At the same time I wasn’t able to notice any differences with Web or Boxes when using HdyApplicationWindow.

Additionally, in maximized, tiled or fullscreen mode, or simply with border-radius: 0; there’s no need to mask anything, so there should be no difference compared to regular GtkWindow.

Future

These widgets have already landed in libhandy master, and can be seen in action in nightly builds of GNOME Games. However, there are some pieces of the puzzle missing right now.

While the same headerbar can be used in windowed and fullscreen modes, there’s no widget to show it next to the content in windowed mode like in a GtkBox, and on top of the content in fullscreen mode like in a GtkOverlay. Right now I have such a widget implemented in Games, but it will need to be in libhandy so that each application doesn’t have to reimplement it.

Currently HdyHeaderGroup only supports GtkHeaderBar, but not HdyHeaderBar, so for split headerbars you still have to use GtkHeaderBar and HdyWindowHandle.

Additionally, the headerbar in the window won’t automatically have .titlebar style class, so won’t pick up styles such as the .devel cog and gradient, so either everybody must manually add it, or HdyHeaderBar could have it by default.

And most importantly, it needs to be implemented for GTK4.

But meanwhile, enjoy freeform round windows!

Games and retro-gtk happenings

Yesterday, I released GNOME Games 3.35.90, so we’re in feature freeze for 3.36.0. Let’s take a look at the changes during the 3.35.x cycle:

Faster collection loading

For a long time, Games loaded collection asynchronously using Vala async functions. While it didn’t block the UI completely, it was still slow and caused frequent UI stalls until it loaded completely. In 3.36, collection loading uses a separate thread instead and is noticeably faster as a result, while the UI is perfectly smooth the whole time.

Cover loading has been moved to a thread as well, so both initial loading and scrolling while covers are loading should now be fast and smooth.

There’s still lots of room for improvement, but for the moment this improves things somewhat. ?️

Steam integration improvements

New-style vertical covers are now supported for Steam games.

Steam covers in Games 3.35.90

Additionally, Steam tools such as Proton and Steamworks Common Redistributables don’t show up as games anymore.

Restarting games

Veerasamy Sevagen added a way to restart games without exiting them.

Secondary menu in GNOME Games 3.35.90

Other changes

Savestates were renamed to snapshots, and backup/restore was renamed to export/import.

Nightly and development builds have a new icon made with App Icon Preview.

GNOME Games icon in App Icon Preview

Lists now look more consistent with each other on mobile:

On desktop, the first two lists turn into sidebars and look mostly the same way as before, while other lists will still have rounded corners and separators.

And finally, there has been a lot of refactoring and code cleanups.


And that’s it? If the changelog for 3.36 seems a bit short, it’s because it is. Due to unfortunate timing, a lot of work planned for 3.36 has been postponed to 3.38 so that we get more than a few weeks before feature freeze to finish and test it. ?️ So, let’s take a look at what retro-gtk 1.0 will bring.

retro-gtk 1.0

API break

First of all, retro-gtk 1.0 will not be API-compatible with the previous versions. In some cases the API is just simplified or made nicer to use (for example, RetroMainLoop is gone, having been merged into RetroCore), but a lot of changes have a very specific reason, more on that later. Since there will be more changes down the road, there’s no point in describing the API changes just yet, so instead let’s take a look at the bigger features:

More precise timing

Currently, retro-gtk uses g_timeout_add() for doing the main loop in the game. It’s… Not very precise. Instead, we have a custom GSource now implementing a more precise timer that allows an error of just a few microseconds instead of a few hundred.

Of course, it’s still not synced to the GTK refresh rate and so occasionally results in dropped frames. I’m not particularly happy with that, but that’s for later.

Audio playback improvements

With such an imprecise timing, it would be expected that the games would run at a wrong speed. And sometimes they do, but more often than not they still work fine. This happens because audio playback is blocking (each call waits until the previous audio has finished playing) and so the game can never run faster than its audio can play.

And as long as games output audio once per frame, it’s not an issue. However, libretro defines 2 callbacks for audio: one sends 2 samples, one for each channel, and the other one sends a batch of arbitrary length. And of course a lot of cores use the former.

At first retro-gtk just played samples immediately as they arrived. Of course, the cores using the former way were slowed down to a crawl because each call waited for the previous samples to play.

A year ago Adrien added a workaround after a report that a core was slowing down by batching up to 512 samples and playing them all at once. This solved the immediate problem, because it meant cores could do many consecutive calls sending samples and not get slowed down. However, outputting more than 512 samples at once still caused slowdowns (a lot of cores output 1000 or more samples per frame), and it also means that if a core sends a number of samples that isn’t power of 2 per frame, or if it sends a different amount of samples on every frame, there will be inconsistent slowdowns or jittery audio.

Initially I opted for making audio playback threaded and queueing any additional samples to resolve this. It worked, but led to some subtle desync issues if the game ran a little faster. It could be as subtle as 6 extra audio frames (12 samples) every 22 seconds (0.45% assuming the framerate is 60 fps), but it was still noticeable, so instead we now queue any samples sent during the frame and then play them once at the end of every frame. While this is not perfect (if the core sends a different number of samples, it’s still possible to get slowdowns after a “long” frame), it works and it solves slowdown in cores like PX68K.

Fast-forwarding that actually works

While it might be good that audio playback naturally prevents games from running too fast, it also means that the speed rate property (unused in Games currently) did nothing when trying to speed the game up rather than slow it down. To solve this, retro-gtk now resamples audio to match speed rate using libsamplerate. Now setting speed rate to values larger than 1 works, although with a “chipmunk effect”, as resampling also changes audio pitch. ?️

Running cores in a separate process

ps output showing retro-runner process

This is a big change, and it’s something I’ve started working on not long after 3.34.0 release, though at one point put it into a hiatus and then resumed. It means moving the game logic into a separate process and only having a proxy that sends input and receives output in the UI process. It’s similar to what web browsers have been doing long before, with separate web processes for each tab.

Why?

Many reasons. For one, crash resilience. If a core crashes, currently the whole app is taken down. When the core runs in its own process, instead Games can show an error and offer to restart the game.

GNOME Games showing a crash screen

It ensures there are no UI stalls due to a slow core. Running multiple cores at the same time will start a separate runner process for each core, which means the runner process side code can assume there’s only ever one core and can be simplified a lot. It improves performance when drawing the game into the widget is slow, like with fractional scaling, as drawing now happens asynchronously and so even if the UI process can’t keep up drawing everything in time, the game still runs at full speed. After a core is stopped, it’s cleanly uninitialized because its process exits. And it also allows us to implement some new features, but more on that later.

How?

The implementation is very much inspired by Christian Hergert’s GUADEC 2019 talk and by Builder’s git plugin and Sysprof. The runner process helper is a separate lightweight binary called retro-runner that isn’t linked to retro-gtk or even to GTK.

For communication it uses both messaging and shared memory. Calls such as switching disks, saving/loading states, pausing and resuming use messaging, specifically D-Bus over a private socket connection. This makes it possible to use gdbus-codegen for generating boilerplate, though there’s still some boilerplate wrapping those calls to expose in a public API. It also allows to pass file descriptors, which are useful for…

…shared memory, specifically a memfd that is passed to the other process and mmap-ed on both sides, which is used for input and video.

This required some changes in how input works: libretro input is polling-based and retro-gtk follows that. However, in every single RetroController implementation retro_controller_poll() is no-op and instead the controller gets the input state on its own via signals, stores it and returns it on demand. With that in mind, poll() was removed and instead controller implementations can now notify about their state changes. When that happens, their complete state is serialized and written into a shared memory block. Since the complete state for one controller, including mouse, pointer, lightgun, gamepad and keyboard state is under 1kb, it’s fast enough to just write the whole thing and not only differing parts. Then, when the core polls input state, a copy of the contents of shared memory is made and then any queries return values from that copy. This means the input is still fast and has no noticeable latency increase over running in the same process, and at the same time we follow the spec closer because input is now actually polling-based on the runner process side, and asking for input state before polling it does actually return the previous state.

Additionally, the call to set controller rumble state previously returned a boolean value to indicate whether it was succcessful or not. That was previously exposed in RetroController as is, but calling it in sync just to get that value isn’t really an option. So instead there’s now a separate retro_controller_get_supports_rumble() call and retro_controller_set_rumble_state() returns nothing, allowing it to be called via D-Bus without degrading performance.

Video is passed similarly to input: the shared memory contains a framebuffer that runner process writes to and UI process reads from. Unfortunately, it’s not very efficient, because libretro gives us a preallocated framebuffer, resulting in a copy on the runner process side. It’s even worse on the UI process side, as there’s a copy (which is unnecessary, but I haven’t got to removing it yet) + uploading the texture to GPU to render it. And while libretro provides a callback to get an address of our framebuffer, retro-gtk doesn’t implement it yet and almost no core uses it. ?️ Thankfully, the resolution is usually pretty low (160×144, 240×160, 320×240, or sometimes 640×480), so this pipeline works, and it’s even noticeably faster than it was in a single process for me.

The problematic part is that actually telling UI process to redraw is currently done via a message. ?️ While it works, it’s extra latency that could be avoided. But there’s still time to change it.


OpenGL core support

One feature that we’ve wanted for a long time, but could never implement is supporting libretro cores that use hardware rendering. It should be simple, right? After all, we already use OpenGL in the widget that draws the game.

In the most basic form, the core sends us specs of a context and a framebuffer (API, version, framebuffer parameters such as whether it has depth/stencil buffer). We get a context from somewhere as asked and provide the framebuffer as needed. Additionally we’re expected to provide pointers to the GL functions by name.

The only quirk is that most cores want a compatibility profile context. And I couldn’t achieve it no matter what I did. Either it corrupted GTK state or eglCreateContext() created a core profile context even though I asked for compatibility profile.

Subprocess comes to the rescue! With that I can easily have whatever context the core wants on the runner process side and it just works. Hence:

With OpenGL cores working, we can run games for a lot of platforms we don’t currently support, such as Nintendo 64 and Sega Dreamcast. While I can’t say yet which platforms will be supported in 3.38, I’m pretty sure Nintendo 64 will make it. Less sure about Dreamcast, because while Flycast core runs pretty well, it has some quirks. For example, if a state was saved without a controller plugged in, it won’t ever see the controller after restoring that state again, and for some reason Sonic Adventure defaulted to Japanese language. More importantly, Dreamcast game detection in Games isn’t very good right now and needs improvements before it’s useful.

Additionally, for Nintendo 64 I implemented a simple controller expansion switcher, because with Nintendo 64 you have to choose between reliably saving games and having rumble. ?️

Nintendo 64 controller expansion switcher in GNOME Games

At least we can automatically disable rumble for controllers that don’t support it, such as the keyboard assigned for player 2 in the screenshot. The UI needs more work too, for example, I’m not particularly happy about Player 1/Player 2 labels, but I needed something for testing. ?️

So, is it done?

Not yet. While OpenGL support works pretty well, there are other things to do, such as Vulkan support, as both of these cores can use it. That’s going to be interesting, because I have absolutely no experience working with Vulkan, unlike with OpenGL. Looking forward to learning it, and looking forward to an awesome 3.38 release, even though we still haven’t released 3.36 yet. ?️

GNOME and gestures, Part 3: HdyLeaflet again

This is part 3 of a mini-series. Part 1, Part 2.


Last time I wrote about adding a back/forward swipe gesture to HdyLeaflet. That work has been finished and is available in libhandy 0.0.12.

Porting apps

To enable the gesture in an application using leaflets, the following needs to be done:

1. Syncing leaflet animation

Currently apps that use leaflets in both titlebar and content area just change their visible-child or visible-child-name property values synchronously. Libhandy 0.0.12 introduces HdySwipeGroup for this. It takes care of automatically switching children, and also of animating swipes. It’s used similarly to HdyHeaderGroup and GtkSizeGroup:

<object class="HdySwipeGroup">
    <swipeables>
        <swipeable name="title_leaflet"/>
        <swipeable name="content_leaflet"/>
    </swipeables>
</object>
HdySwipeGroup *group;

...

group = hdy_swipe_group_new ();
hdy_swipe_group_add_swipeable (group, HDY_SWIPEABLE (title_leaflet));
hdy_swipe_group_add_swipeable (group, HDY_SWIPEABLE (content_leaflet));

2. Marking separators

Leaflets often include separators between pages. By default the gesture will switch to any widget, and the separators should be excluded from that. It can be done using the new allow-visible child property. It’s set to TRUE by default and can be changed like this:

<packing>
    <property name="allow-visible">False</property>
</packing>

3. Enabling the gesture

HdyLeaflet in 0.0.12 has can-swipe-back and can-swipe-forward properties. Setting one or both of them to TRUE enables the gesture:

<property name="can-swipe-back">True</property>

Most of the time, apps will want only back gesture, but it’s possible to have back/forward or forward only if wanted. This should only be done for the content leaflet, and not for the title one. Enabling dragging in headerbar will conflict with window dragging on touchscreens!

4. Transitions

Libhandy 0.0.12 leaflet transition types

0.0.12 brings some changes to HdyLeaflet mode and child transitions. Separate mode and child transition types have been deprecated in favor of a unified transition-type property. It can take 4 values: none, slide, over, under. Crossfade doesn’t make much sense spatially and was deprecated as well, though it’s still works if used via child-transition-type property. Additionally, over and under transitions have a subtle shadow now, similar to the WebKit gesture.

It’s recommended that the apps using the gesture use over transition.

<property name="transition-type">over</property>

And that’s it! The libhandy commit that adapts the demo app can serve as an example. It also shows that nested swipeable widgets aren’t handled well, and require manual special casing. Most of the time that won’t be an issue though.

Thanks Adrien Plazas for all the reviews :)

GNOME and gestures, Part 2: HdyLeaflet

This is part 2 of a mini-series. Part 1, Part 3.


Shortly after the WebKit gesture was merged, I started experimenting with making this gesture more widely available. The first step was porting it to Vala and decoupling from WebKit. Since I wrote this part of the gesture tracker from scratch anyway, it was simple and straightforward. The resulting playground project was also used as a convenient place to quickly iterate on the WebKit gesture itself. Later I also reimplemented rendering to match the WebKit one. Here’s how it looked at various points of time:

Other than that, I used the swipe tracker to make a few more demos for Tobias Bernard:

Check out his GUADEC talk showcasing the second demo! :)

At the same time, I started integrating it into libhandy by supporting back/forward swipe in HdyLeaflet. And there I hit four problems:

1. Transitions and visible-child

A folded HdyLeaflet, just like GtkStack, shows one of its children at any given moment, even during child transitions. The second visible child during transitions is just a screenshot. But which child is “real” and which is a screenshot? Turns out the real child is the destination one, meaning the widget switches its visible child when the animation starts. It isn’t a problem if the animation is quick and time-based, but becomes very noticeable with a gesture. Additionally, it means that starting and cancelling a gesture switches the visible child two time.

One solution would be only switching the visible child at the end of the animation (or not at all if it was canceled). The problem is that it’s a major behavior change: applications that listen to visible-child to know when to update the widgets, or sync the property between two leaflets will break.

Another solution would be to draw both children during transitions, but it still means that visible-child changes two times if the gesture was canceled. The problem here is similar: applications wouldn’t expect the other child to still be drawn, but at least it’s just a visual breakage. And it still means that starting and canceling the gesture would mean two visible-child changes.

The second solution may sound better, and yet the current WIP code uses the first one.

2. Visuals

Leaflet had many issues in this area, such as over transition not making sense spatially and bottom widget being visible through the top widget. Additionally, Adrien liked the drop shadow and dimming in WebKit and the demo and wanted to have it in leaflet as well. :)

The first issue was solved by splitting the transition into over and under and clipping the bottom child. Similarly, I implemented shadow and dimming, though it’s pending on those transition types for mode transitions being merged first, so that the shadow can also be added to those, so that it’s consistent.

I’m also not happy with how the dimming and shadow are implemented, neither here nor in WebKit: it’s custom drawing with hardcoded values. Ideally, this needs to be controlled from CSS somehow. GTK itself uses gadgets for things like this (for example, the overshoot effect in GtkScrolledWindow), but that API is private. Having dimming and drop shadow widgets is an overkill, at least until GTK 4 arrives and makes GtkWidget instantiable. Maybe foreign drawing could work…

3. Syncing animation

Often, GTK applications have two leaflets: one in the window’s content area and one in titlebar. Their visible child is always changed at the same time, so it looks like they are one tall leaflet spanning both titlebar and content. This still needs to work with the gesture. And while it’s easy to make nice-looking throwaway demos that do this, syncing actual HdyLeaflets has to be a proper API.

Initially I suggested what I thought was a nice solution with having swipe tracker as a public object and connecting multiple widgets to it. Benjamin Otte and other people immediately pointed out many problems with it, so I researched how other platforms do it. The answer is simple: most platforms don’t. :)

Android has a rather silly way to sync multiple widgets together, but it’s rarely needed, as app bars are just widgets, so they can be packed into a ViewPager without a need to sync two pagers together.

Another constraint is that the solution must not expose animation progress as a write-able property, so it must not be possible to set this value to something arbitrary and get the transition stuck.

4. Interaction with GtkScrolledWindow

GTK event propagation works in two phases: capture and bubble. Widgets can connect to event signal and receive events on bubble phase. Then they return a value to either stop the event or propagate it further. More recently, GTK added various event controllers that allow choosing the phase where they run. With GTK_PHASE_CAPTURE it’s possible to handle events on the capture phase… But their signals don’t support fine-grained stopping/propagation, i.e. don’t have return values (they do in GTK4 though).
All in all, it means that there’s no way to get an event on capture phase and stop it atbitrarily…

Except there is, it’s private and it’s used by GtkScrolledWindow. This widget captures scroll events and stops some of them. For example, if the scrolled window has a vertical scrollbar, but not horizontal, it stops vertical scrolling events and propagates horizontal scrolling. This is harmless, but it also always stops events with is_stop set to TRUE, meaning a leaflet containing a GtkScrolledWindow will get stuck at the end of the gesture. So every single way of receiving events fails in a different and exciting way.

This last issue made me hate life and put the project on a long hiatus.

More demos

A while later while doing another demo (more on that in the next post) I discovered the horrible workaround: the private function for capturing events in GTK has a very simple implementation, so it’s easy to set this handler manually. And of course, with this workaround it just works. This solves the issue #4.

For the issue #3 I made a crude solution similar to already existing HdyHeaderGroup: HdySwipeable and HdySwipeGroup. It’s an RFC at this point, so criticism is welcome.

That allowed me to make a fully working (though still buggy) prototype of swipeable leaflet:

(Yes, that’s a bug there on 0:21)

Another visible problem in the video is that HdyHeaderGroup showing and hiding buttons doesn’t really work with the gesture. One possible solution here would be to show all buttons on all headerbars when folded, but that would once again involve an API break.

The (still very messy) code is here. Even though it’s not ready yet, Shortwave app already makes use of it:

Swipeable leaflet in Shortwave

This also uncovered a crash when a leaflet is created in unfolded state. Oops.

Thanks Felix Haecker for testing, and once again Tobias Bernard for feedback and suggestions while iterating on it.

GNOME and gestures, Part 1: WebKitGTK

This is part 1 of a mini-series. Part 2, Part 3.

Re-publishing, the original post is too old to show up on Planet GNOME at this point. It was published on August, 8th initially.


Swipe gesture in Epiphany

I’m a big fan of responsive touchpad gestures. For the last half a year (mostly January, February and during the summer) I’ve been working on improving gestures in many areas throughout GNOME. In this series I will do a (belated) overview.

Back/Forward Swipe

Late in the 3.32.x cycle, I saw a commit by Jan-Michael Brummer adding a back/forward swipe to Epiphany. It was really nice to finally have gestures, but it didn’t have any visual feedback. Less importantly, the direction was reversed, as if when scrolling with Natural Scrolling being off. I wanted to give a shot at improving it.

A proper gesture would have to “stick to finger”, showing screenshot of the previous or next page during the gesture, more or less what Safari does on macOS. Specifically, Epiphany would have to take screenshot of every page that is added into back/forward history, show it while the gesture is performed, then continue showing it until the next page loads enough to replace it. Unfortunately, this isn’t really possible to achieve in Epiphany itself: while WebKit does provide API to take snapshots, there’s no way to know when the previous/next page has loaded “enough”.

So I started looking into WebKit instead, where I found out that Safari’s gesture is actually implemented right there! Most parts are present, but the code was not cross-platform. So I started slowly adapting it for GTK. For the most part, the reusable code was a large part of the back end: page snapshot store and snapshot removal logic. That code is now shared between the platforms. The other parts, like actual event processing and drawing, had to be written from scratch.

One interesting detail about the gesture is that it doesn’t actually use gesture events! Libinput defines swipe gestures as synchronous movement of three or more fingers in the same direction. Mac gesture API is even more strict: it’s three fingers only, with four-finger swipes being reserved for the OS. But the gesture uses two fingers, how is this possible? Turns out it actually uses scroll events instead. (That’s also why it works with Magic Mouse in macOS, even though the code does not special-case anything for it)

When using scroll events, one has to be very careful. Touchpads generate scroll events continuously, in GTK it means that these gestures have GDK_SCROLL_SMOOTH scroll direction. At the very end of the scrolling, there will be a special event with is_stop field set to TRUE, which is used as a signal to start kinetic scrolling or, in our case, to end a swipe.

But there are other input devices, for example, mice. Most mice have “clicky” wheels that generate scroll events with direction instead of deltas. These events are impossible to use for swipes, so there’s no point in even trying to handle them. But there are also mice with freely scrolling wheels which generate the same events as touchpad, except there’s no event with is_stop == TRUE at the end. This means that they can be used to start a swipe, but it will get stuck as soon as the wheel stops. So, these mice have to be skipped too. Then there are touch mice where scrolling probably works same as on touchpad. I suspect swiping can work very well with them, same as it does with Magic Mouse on macOS, but there’s no way to distinguish these kinds of mice, at least as far as I know, so I had to disable it for any mice.

Another problem is that in order to not interfere with actual scrolling, the gesture controller must check whether it is possible to scroll the page first. Maybe there’s still space to scroll, maybe the page intercepts the scroll events. Then there has to be a threshold so that it’s hard to accidentally trigger the gesture. Thankfully, this part is shared with the Mac gesture. :)

A side effect of using scroll events is that this gesture still works with X11 and/or older semi-mt touchpads for which Libinput normally does not support any gestures.

So, now the gesture sticks to fingers. But there’s still an important bit missing: a snap-back animation. In order for a gesture to feel natural, it should snap back smoothly as soon as you lift your fingers, respecting the momentum. This was a lot easier to do than I expected, and after a few iterations applying Tobias Bernard’s suggestions I had an animation that I’m very satisfied with. How it works:

  • The animation uses easeOutCubic interpolation
  • Duration is naturally calculated as remaining distance divided by velocity, or if the velocity is 0, by a constant value instead
  • After that, duration is multiplied by 3, matching easeOutCubic derivative at t=0. This ensures that initial velocity is same as it was before lifting the fingers
  • Finally, the duration is clamped into [100ms, 400ms] range. This ensures that it’s never too slow or too fast, while still allowing it to respect momentum when possible
  • If the page was swiped less than halfway through the window, there’s a small velocity threshold. If fingers are lifted when not moving, the gesture will be canceled and the page will smoothly slide back. On the other hand, if the page was swiped more than half way through, just lifting the fingers would finish the gesture, and one has to specifically flick back to cancel it
  • If one starts swiping again while the animation is going, it will actually be stopped. This allows to continuously grab and release the page

Interestingly, Mac has a helper function that takes care of all this, and WebKit makes use of it.

Finally, the gesture should look nice. On Mac it uses CoreAnimation for drawing; WebKitGTK has to use Cairo. Since I didn’t have any mockups to work with, I reused Apple’s visuals, consisting of a dimming layer and a long subtle gradient for drop shadow, intending to replace them with something else later. But everybody whom I showed it liked it, so I left it as is.

The end result is that since version 2.24.0, WebKitGTK optionally supports 2-finger swipe gestures on touchpad.

Unfortunately, I was a little too late to enable it in Epiphany 3.32.0, but it was merged into 3.32.1 nevertheless. In 3.34.x, Yelp and Devhelp will also support it. Additionally, it’s enabled in Eolie and Odysseus browsers.

A bit later I also added touchscreen support. That was easy, because WebKit literally generates scroll events for touch scrolling, so it was simply a matter of feeding those events into the gesture controller. Additionally, I had to implement canceling, as all touchscreen gestures have to support it. Since scrolling on touchscreen is 1-finger swipe, the gesture is performed the same way.

This will be available in upcoming WebKitGTK 2.26.x, corresponding to GNOME 3.34.

This can be very disruptive feature in many cases, such as authentication widgets, so applications wanting to use it have to opt in by changing the value of this property.

Pinch Zoom

A smaller change was getting pinch zoom gesture to work on touchpads. Since this gesture was already available on touchscreens, it involved simply feeding touchpad gesture events into the gesture tracker, but the performance is severely lacking on heavy pages. Speeding it up is unfortunately still above my skill level. :)

Pinch zoom is enabled unconditionally, so it already works everywhere where WebKitGTK is used, including but not limited to Geary and documentation view in GNOME Builder.


I want to say thanks to the following people:

  • Michael Catanzaro and Carlos Garcia Campos for code review and helping me with understanding WebKit codebase
  • Tobias Bernard for testing and numerous design suggestions
  • Jonas Dreßler for testing and feedback, especially on a touchscreen

Part 2 here.

GNOME Games 3.34

Games 3.34

A year ago, Adrien Plazas stepped down as a maintainer, so Games 3.32.0 was released without an accompanying blog post, since I didn’t have a blog at the time. Now it’s time to make up for it with a blog post about 3.34.0.

GSoC and savestates

As part of his GSoC project, Andrei Lişiţă implemented a savestate manager.

Savestates in Games 3.34

Savestates are a common feature in game emulators, that work similarly to snapshots in virtualization: emulator takes a full snapshot of RAM and storage, which can be loaded later to restore the game to the same exact state it was in when saved.

The app has supported savestates for a long time: when you exit a game, a savestate is created. Then when you run it again, Games offers to restore that savestate or reset the game. However, there was no way to manage savestates during the game, or to have more than one savestate at a time.

Now we have a shiny new sidebar for managing savestates and can save and load them on demand. This can be used for saving memorable moments in games, or for cheating your way through difficult games via saving and reloading every time you make a mistake.

There is still room for improvement, for example, there is no way to use savestates with gamepad right now.

Since directory layout is different now, existing data is automatically migrated on the first run. This is a one-way process, downgrading to 3.32.1 after running 3.34.0 is not possible!

Nintendo DS screen layouts

GNOME Games supports running Nintendo DS games since 3.30. However, the system’s two screens make it awkward to play without having a screen in portrait orientation. While ideally we want retro-gtk to support support rearranging screens, for now I implemented this using options of DeSmuME core:

Nintendo DS layout switcher in Games 3.34

So far we support a vertical mode, two horizontal modes and a single screen mode, corresponding to DeSmuME’s top/bottom, left/right, right/left and quick switch modes. While the first 3 modes work exactly the same as in RetroArch, single screen mode has some improvements: we remember and restore the currently viewed screen, provide a visible button for switching between the screens and have separate keyboard shortcuts for that (it’s using top only and bottom only internally, not quick switch).

Naturally, a downside is that it only works with DeSmuME and DeSmuME 2015 cores.

At first it was a global setting. This led to some problems, such as squished screenshots when loading the game if the current layout doesn’t match the one the game was played with the last time, which was made even more apparent after Andrei’s savestate work landed, showing the squished screenshots even more prominently.

Because of that, I reworked the feature to store the screen layout inside savestate metadata instead of a global setting. While this required a fix in retro-gtk, screen layout now can be different not just for every game, but even for every savestate, meaning previews always perfectly match the loaded game.

Platforms instead of Extensions

For a long time, Games had an “Extensions” page in preferences, showing all the installed plugins. This was one of the more confusing parts of the app:

Extensions page in Games 3.32.1

There are many problems with this page: many people assumed that the listed platforms were the only platforms we support, even though there are 24 more platforms that are supported directly, not via a plugin. Additionally, “platform support”, via plugin or not, means that Games can list games from this platforms in collection view. It does not automatically mean that the app can run those games, that would also require the corresponding libretro core to be installed. And even then, some games need firmware to run. None of this is really obvious from the page.

All in all, this means that Games can run NES and Game Boy games, even though they aren’t listed on the page, and at the same time it cannot run DOS or Sega Saturn games, even though they are listed, which makes no sense unless you’re involved in development of the app.

Even more confusingly, the modules that the page lists are actually called “plugins” internally, not “extensions”.

To solve these problems, this page has been removed and replaced with a Platforms page that lists supported platforms, lists the currently used core for libretro platforms and grays out the platforms without cores, so that it’s visible which games can and cannot be run.

Platforms page in Games 3.34

While our official build on Flathub ships only one core per platform, it’s possible to have multiple cores for a single platform. In that case, the page will allow to choose the core to run the games with. I hope the new page will be a lot more useful.

Backup and Restore

Another feature that landed during this cycle is backing up and restoring for savestates, courtesy of Adwait Rawat.

Backup & Restore page in Games 3.34

These changes include a page in preferences that allows to export all the savestates as one archive, or to restore an existing backup. This can be useful, for example, to move data to a different device.

Goodbye, developers view

Developers view was implemented along with Platforms view by Saurabh Sabharwal as part of his GSoC 2018 project. While Platforms view was very successful, Developers view used thegamesdb metadata, which wasn’t very reliable. Often, it mixed developers and publishers, or duplicated the same developer multiple times with slight variations. And then, thegamesdb changed its API, so for the last few months, this view has been completely empty. Because of that, I went ahead and removed this view:

View switcher in Games 3.34.0

Adaptive UI

I’ve been slowly making the UI adaptive for the last year. 3.34.0 finally marks the point where the app can run on a phone:

However, there is still a very important bit missing: touch controls to actually play games without a gamepad or keyboard.

Better Fullscreen

In 3.32.1, headerbar in fullscreen is hidden, but shows up on any cursor movement. This can be annoying when playing a mouse-heavy game (for example, on Nintendo DS), so in 3.34.0 the headerbar can only be revealed by pushing the top of the screen, similar to the behavior in Epiphany and other apps.

Miscellaneous Changes

Andrei added an error message that shows up after opening a non-game file:

Error infobar in Games 3.34

Media switcher has a dropdown arrow now:

Media switcher in Games 3.34

Cursor now autohides in windowed mode after 3 seconds of inactivity, just as in fullscreen.

Game covers aren’t blurry on HiDPI screens and aren’t darkened anymore.


Thanks to all the contributors who made this release possible!

Getting Games

Download on Flathub

As always, the latest version of the app is available on Flathub.

Games and GSoC 2019

GNOME Games has been participating in Google Summer of Code for many years, and this one is no exception. This time Andrei Lişiţă a.k.a. Yetizone was implementing a savestate manager.

Andrei’s work involved redoing $XDG_DATA_HOME/gnome-games/ directory layout, writing a migrator for existing data, reworking the app to support having multiple savestates at once, implementing on-demand loading and saving, and implementing the UI.

Check out his blog posts for more information about the project:

See also his lightning talk at GUADEC 2019 about the project! I couldn’t attend it, but I watched the livestream. :)


It was my first year as a mentor. It did feel a little weird, since I never participated in GSoC as a student, and I’m also a student myself. Nevertheless, the project was finished successfully, so we have a shiny new feature. Most of the work is already merged and will be available in GNOME Games 3.34.0. The two remaining changes will be landed early in 3.36.x cycle.

Andrei is awesome to work with. He started coding early in community bonding period, and needed next to no handholding, so for the the most part of the coding period my involvement was simply answering questions and reviewing code, later testing and reporting bugs.

While the project was finished, I think there are things I think I could do a lot better as a mentor:

  • While we were focused on the technical side of the project, I didn’t pay enough attention to the social side. Namely, I didn’t track GUADEC announcements closely (since I couldn’t attend it this year anyway), so Andrei almost ended up missing it. Thanks to Gaurav Agrawal who helped Andrei with registration and requesting travel sponsorship.
  • Until the last few weeks, the project consisted of a huge merge request which quickly got unwieldy. For planning we used Matrix. I saw other people using more sophisticated workflows for GSoC, such as networks of small merge requests and issues in interns’ forks on GitLab, which can be reviewed and merged one by one, I think we should have used a workflow like that as well.
  • I ended up postponing 3.33.90 release for a week, merging many followup changes during the freeze, and even then merging changes up until 3.33.91 (Since Games is not a core app, we are allowed to do that, but usually we try to follow the freezes nevertheless). This probably wasn’t a good thing to do and I apologize to the translation teams. :(

All in all, I want to thank Andrei for the amazing contribution and for being an awesome person in general. :)