Libadwaita 1.0

Libadwaita 1.0 Demo

Libadwaita 1.0 has been released, just at the end of the year.

Libadwaita is a GTK 4 library implementing the GNOME HIG, complementing GTK. For GTK 3 this role has increasingly been played by Libhandy, and so Libadwaita is a direct Libhandy successor.

You can read more in Adrien’s announcement.

What’s New

Since Libadwaita is a Libhandy successor, it includes most features from it in one form or another, so the changes are presented compared to it.

If you’ve been following This Week in GNOME, you may have already seen a large part of changes.

Updated Stylesheet

Probably the most noticeable change is the reworked stylesheet.

For the past 7 years, the Adwaita style has been a part of GTK. Now it’s a part of Libadwaita instead, while the GTK style has been renamed to Default.

Since we have this opportunity, the stylesheet has been completely redesigned with several goals in mind:

Modernizing the style

GNOME designers have long wanted to do this, and the GTK 4 Default style contains a few changes in that direction compared to the GTK 3 version of Adwaita, and GNOME Shell has been using a similar style as well. Libadwaita takes it much further. You can read more about it Allan Day’s blog post.

The changes are not fully compatible with GTK Default and may require changes on the application side when porting. I’ve also blogged in detail about the biggest breaking change: the updated header bar style.

Runtime recoloring

Ever since Adwaita started using SCSS, it couldn’t really be recolored at all without recompiling it. This created big problems for applications that wanted to do that.

For example, GNOME Web makes its header bar blue in incognito mode. This may sound simple, but involves copy-pasting large chunks of Adwaita into the app itself and making many small changes everywhere to adjust it, as well as using SCSS for it because the original style is SCSS. More recently, GNOME Console and Apostrophe started doing the same thing – copy-pasted from Web, as a matter of fact. This approach means the style is messy and extremely hard to keep up to date with Adwaita changes – I have updated this style for the 3.32 style refresh and never want to do this again.

Another approach applications like Contrast are using (were using with GTK 3, anyway), is copying the whole stylesheet from GTK, and using libsass to recompile it in runtime. This worked – it’s much more maintainable than the first approach, but fell apart when libsass got deprecated.

Meanwhile, the elementary OS stylesheet has been doing recoloring just fine with nothing but @define-color – and so Libadwaita does exactly that, it exposes all of the colors it uses (31 as of the moment of writing) as named colors. The new colors are also documented and will be treated as a proper API.

It also drops all of the formerly used PNG assets, so the colors can affect the elements that used them.

It also reworks the high contrast variant to use the same colors when possible to make sure that changing color for the regular style also works with high contrast.

Another thing the new stylesheet does is simplifying how it handles colors in general. The new simplified style comes in very handy here.

For example, many parts of the UI are now derived from the text color and change with it automatically, and widgets that don’t absolutely need to define their own text color don’t do that anymore, so it can propagate. Where possible, transparency is used instead of mixing or hardcoding colors.

All in all it means that simple custom styles like this one:

/* Solarized popovers */
popover > arrow,
popover > contents {
  background-color: #fdf6e3;
  color: #586e75;
}

actually work correctly and with no glitches, in both light and dark variants, as well as high contrast style. Try doing that with GTK 4 Default and compare the results:

Dark Variant Contrast

The dark variant of Adwaita has historically been intended to be used as a lights-out, low distraction appearance for media apps – video players, image viewers, games – and not as a general purpose dark style. As such, it has pretty low contrast and can be hard to see at times.

A primary example is the accent color – historically, Adwaita has never really had a proper accent color as a named color – and many applications have been using @theme_selected_bg_color – a background color used for selected text and list items – as an accent color for text and icons. Not only does it not have a good enough contrast to be used as text color, the dark variant dims it even further, to make this background color less distracting – so while it’s not too bad in the light variant, it falls apart with dark.

Libadwaita fixes that – it makes the accent brighter (made possible by not using it in contexts where it can be distracting), and introduces a second color to be used for cases like this. This second color does vary between the light and dark variants, and this allows it to be much brighter in dark variant, and darker in light variant, so it’s suitable for text.

It also changes many other things. The window background is now darker, while elements like buttons and boxed lists are lighter, GtkSwitch and GtkScale sliders are light, etc.

Style Classes

Button styles: regular (no style), flat, suggested action, destructive action, a few custom colored buttons, circular, pill, osd
Various button styles

The updated stylesheet includes many new style classes for app developers to use, in a lot of cases codifying existing patterns that applications have been using via custom styles, but also adding new things.

Some highlights:

  • .pill makes a button large and rounded – in other words, makes it a pill button
  • .flat can now be used with GtkHeaderBar
  • .accent colors a label into the accent color (using the correct color as per the above section)
  • .numeric makes a label use tabular figures
  • .card makes a widget have the same background and shadow as a boxed list.

And speaking of boxed lists, the old .content style class from Libhandy has finally been renamed to .boxed-list, matching the HIG name.

The available style classes (both existing and new) are now documented, and Libadwaita demo now includes a sample of each of them.

Refactoring and cleanups

Adwaita has historically been a big SCSS file containing most styles, another file containing complex mixins for drawing buttons, entries and other widgets, and a few more files for colors.

Libadwaita splits all of that into small manageable files. It removes the complicated mixins, because the new style is simple enough that they aren’t needed. It removes tons of unused and redundant styles, some of which were leftovers from early GTK 3 days, and so on. And, of course, the new style itself allows making styles significantly simpler.

The end result is a much more maintainable and less arcane stylesheet.

Dark Preference

Settings, light
A work in progress dark preference in Settings. Dark version

I’ve blogged about this in much more detail a few months ago, but in short, Libadwaita includes API to support the new cross-desktop dark style preference, as well as streamline the high contrast mode handling.

This has also been backported to Libhandy and will be available in the next release.

While the Libhandy version is strictly opt-in, Libadwaita flips the switch and follows the preference by default, unless the application opts out. This means that any new applications will support the preference by default – and that supporting it is an expected step when porting an application from GTK3 and Libhandy. The documentation now also includes a guide on how to handle application styles.

Many third party applications have already adopted it by now, and there has been good progress on supporting it in the core GNOME applications – though at the moment it’s unlikely that all of the core applications will support it in GNOME 42. If you maintain a core app, it’s a perfect time to start supporting it in order to avoid that ?️.

Libadwaita GtkInspector page

A new GtkInspector page is also available to help testing the style and high contrast preferences.

Documentation

Like GTK 4 itself, Libadwaita features new documentation using the awesome gi-docgen generator by Emmanuele Bassi.

The docs themselves have been reworked and expanded, and feature new generated screenshots, which all come in light and dark versions to match the documentation pages:

Toasts

Toast saying "'Lorem Ipsum' Deleted", with an Undo button

While in-app notifications aren’t a new pattern by any means, we’ve never really had a ready to use widget. Sure, GdNotification exists, but it leaves a lot of decisions to applications, e.g how to deal with multiple notifications at once, or even what notifications should contain – essentially it only provides the notification style, a close button and a timeout.

A big feature that made GdNotification attractive was the ability to animate its visibility before GTK had a widget for that purpose. Now GtkRevealer exists (which our new widget ironically doesn’t use), and most apps currently use that to re-implement in-app notifications from scratch. This has lead to major inconsistencies between apps, and situations like this:

GNOME Boxes, two undo notifications, awkwardly stacked
GNOME Boxes, two undo notifications

To help fixing this, Maximiliano has implemented a new widget to replace them. Its API is very streamlined, and is modeled after notifications. The widget part is not a notification, but rather a notification area that toasts (which are just generic objects and not widgets) are added into. If multiple toasts are added in a quick succession, they are queued based on their priority.

A big difference from GNotification though is that toasts are mutable – and can be changed after they have been shown. This is useful when using toasts as undo bars, for example.

Animations

Libadwaita animation demo

Manuel Genovés has implemented an animation API as part of his GSoC project. Unfortunately not everything that was planned has been implemented, but we have basic timed animations and spring animations.

Timed animations provide simple transitions from one value to another one in a given time and with a given curve. They can repeat, reverse their direction, and alternate with each iteration.

Spring animations don’t have a fixed duration, and instead use physical properties to describe their curve: damping ratio (or optionally just damping), mass, stiffness, an initial velocity and an epsilon to determine when to stop it. The fact they have a variable initial velocity makes them perfect to animate deceleration after performing a gesture:

AdwLeaflet, AdwFlap and AdwCarousel all use spring animations now, and AdwSwipeTracker provides the final velocity after a swipe is finished, instead of pre-calculated duration.

Unfortunately, due to time constraints, none of the above widgets support overshoot when animating. Since they use a critically damped spring by default (meaning it takes the shortest possible time to reach the end and doesn’t overshoot unless the velocity is very high), it’s not really visible unless you swipe really hard, and it can be fixed after the initial release without any API changes.

Unread Badges

An example of an unread badge

AdwViewSwitcher and related widgets now can display unread badges and not just needs-attention dots. This means they don’t use GtkStack anymore, but a new widget called AdwViewStack. For the most part, it’s a drop-in replacement, although it does trim down the API not necessary for this use case.

Thanks to Frederick Schenk for implementing this!

Application

Naiara has implemented AdwApplication – a GtkApplication subclass that automatically initializes Libadwaita when used. It also automatically loads styles from GResource relative to the application base path. For example, if your application has org.example.App application ID, it will automatically load /org/example/App/style.css. It also loads style-dark.css, style-hc.css, and style-hc-dark.css, allowing to add styles for dark or high contrast styles only.

Helper Widgets

Libadwaita provides a few widgets to simplify common tasks:

  • GtkHeaderBar in GTK4 does not provide a direct way to set a title and a subtitle, and just shows the window title by default. If you want to have a subtitle or to simply display a title that’s different from the window title – for example, for split header bars – the recommended way to do that is to construct two labels manually. That can be tedious and easy to get wrong. AdwWindowTitle aims to help with that. It can be used as follows:

    <object class="GtkHeaderBar">
      <property name="title-widget">
        <object class="AdwWindowTitle">
          <property name="title">Title</property>
          <property name="subtitle">Subtitle</property>
        </object>
      </property>
    </object>>
    
  • AdwBin is a widget that uses GtkBinLayout, has one child, provides API to manage it, implements GtkBuildable accordingly, implements GtkWidget.compute_expand(), and unparents the child in GObject.dispose(). Applications can subclass it instead of GtkWidget without worrying about those things. It can also be used directly without subclassing it.
  • AdwSplitButton provides an easy way to create a, well, split button that will use the correct appearance in a header bar or a toolbar.
  • AdwButtonContent can be used to create a button with an icon and a label without needing to manually set up the button mnemonic:

    <object class="GtkButton">
      <property name="child">
        <object class="AdwButtonContent">
          <property name="label">_Open</property>
          <property name="icon-name">document-open-symbolic</property>
          <property name="use-underline">True</property>
        </object>
      </property>
    </object>
    

API Cleanups

Large parts of the API have been streamlined. Check out the migration guide for more details.

Some highlights:

  • AdwHeaderBar provides separate properties for controlling window buttons at the 2 ends of the header bar, instead of one controlling both sides. This can be used to implement split header bar layouts and removes the need for HdyHeaderGroup.
  • Ever since HdyWindow we’ve had an easy way to support leaflet swipes spanning both the window and the titlebar without HdyHeaderGroup. All of the features HdyWindow and HdyWindowHandle provided have been added into GTK itself, and have been available since 4.0. In acknowledgement of that, Libadwaita doesn’t include a HdySwipeGroup equivalent.

    It still includes AdwWindow and AdwApplicationWindow as helpers, but it’s very easy to achieve the same result with a GtkWindow.

    <object class="GtkWindow">
      <property name="titlebar">
        <object class="GtkBox">
          <property name="visible">False</property>
        </object>
      </property>
      <property name="child">
        <!-- ... -->
      </property>
    </object>
    
  • AdwComboRow has been completely overhauled and is now basically a GtkDropDown clone. This means it uses the same list item factories, allows setting the models from UI files, etc. One thing it doesn’t provide is binding enums – to replace that, Libadwaita includes AdwEnumListModel.
  • AdwAvatar removes the old complicated image loading API and instead just allows setting a GdkPaintable as a custom image.
  • AdwLeaflet now supports the back/forward mouse buttons and keyboard keys, as well as Alt+arrow shortcuts, in addition to swipe gestures, and the properties controlling that have been renamed to reflect the addition.

What hasn’t made it

About Window

Work-in-progress about window

Even though it was featured in Allan Day’s blog post a few months ago, the new About window Adrien has been working on hasn’t made it into Libadwaita 1.0. There were still unresolved design and API questions, and we decided to wait until the next release to have time to polish it instead of rushing it.

Color API

While overriding colors via @define-color is far simpler than it was before (essentially, copying the entire style of the widgets you want to change, with different colors), it’s still not as easy as it could be.

For example, if an application wants to override its accent color, it needs to override 3 colors. One of them (@accent_color) exists pretty much only for contrast, and also differs between light and dark variants. In an ideal world, this color would be calculated automatically based on @accent_bg_color.

Chris has been working on a programmatic API to manage colors, and that should improve this situation a lot. As with the about window, though, it wasn’t ready in time for 1.0.

Touchpad Swipes

A known regression compared to Libhandy is that swipes in widgets such as AdwLeaflet only work on touchscreen, but not touchpad, if the pointer is above a scrolling view. This is something I really hoped to fix before 1.0, but it’s a surprisingly complex issue. It needs extensive GTK changes in order to be fixable, involving essentially replacing and deprecating the existing API for dealing with scrolling, and it’s not something that should be rushed.

Thanks To

  • Adrien Plazas, Christopher Davis, Frederick Schenk, Manuel Genovés, Maximiliano
  • GNOME design team
  • GTK developers
  • Frederik Feichtmeier, nana-4 and other members of the community

I also want to thank my employer, Purism, for letting me and others work on Libadwaita and GTK to make this happen.

Happy holidays and happy hacking!

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.