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!

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.