Implementing fallback tab dragging for Wayland in Chromium
Fallback tab dragging, which has been in the works for the last three years, finally shipped in Chromium 133 on February 4th! Let’s use this opportunity to learn why it’s needed, a bit about how it works, and why it took so long to ship.
This is also a post about Wayland, the display server protocol that’s slowly but surely replacing X11 on Linux. It does things very differently, for good reason, but that sometimes also means that apps have to do things differently, or have to push for extensions to the protocol. I’ll go over some of the differences between Wayland and X11 below, and then over the client-side changes in Chromium to accommodate to these differences. Tab dragging also ended up being the motivation for a protocol extension, which I’ll briefly mention as well.
To give you some additional context, in September 2024 ca. 28 % of Chrome users on Linux were
already using the native Wayland backend – even though it’s still opt-in (set
chrome://flags/#ozone-platform-hint
to “Wayland” or “Auto”)!
Regular tab dragging
Let’s start by taking a look at how tab dragging usually looks, for example on Windows (please excuse the shabby recording from a VM):
When dragging a tab out of a window, it detaches into a new window and continues to be dragged by the mouse. You can also merge it back into a window, or release the mouse to end the drag and have the window stay there.
Now, let’s see how the same implementation looks on Wayland (GNOME, to be specific):
We still get the new window when detaching, but it’s at the wrong position and doesn’t follow the mouse. You can still merge with other windows, but only if they’re not obscured by the dragged window.
This is theoretically functional (although there used to be some crashes, but these have been fixed since), but in practice it doesn’t feel smooth. Also, in my experience the dragged window usually obscures the window you’d like to merge with, so you first have to drag the tab out, release the mouse, reposition the window, and then drag the tab into the target window (as shown in the video above).
Now, why doesn’t this work on Wayland? This is due to two big differences in Wayland compared to X11:
- Clients receive all mouse events with surface-local coordinates, i.e. relative to their window’s top-left corner, instead of with global coordinates, i.e. relative to the leftmost screen’s top-left corner.
- Clients don’t know where exactly their window is positioned, and can’t control the position on their own; they can only ask the compositor to move the window for them.
For tab dragging, these two points mean that when detaching a dragged tab into a window we can’t make it follow the mouse. (For technical reasons, asking the compositor to move the window doesn’t work in this case.)
As hinted at before, it’s possible to get the regular tab dragging experience even on Wayland by
extending the protocol. This isn’t unusual, there are already lots of protocol
extensions. The one we need for tab dragging is called xdg-toplevel-drag
,
and is based on my colleague Nick Diego’s work. However, the crux of protocol extensions is that
they need to be supported by both the client and the compositor. Chromium already supports it, but
as of this post’s writing only two compositors (KDE Plasma and Jay) support it - notably,
GNOME/Mutter and sway don’t. You can check the current list of compositors supporting
xdg-toplevel-drag
here.
Fallback tab dragging
I hope it became clear in the previous section that we need some alternative to Chromium’s regular
tab dragging when running under a compositor that doesn’t support xdg-toplevel-drag
. This
alternative is what this post is about: fallback tab dragging.
The basic idea is simple: we use a regular DnD (drag and drop) session with a drag icon showing a snapshot of the dragged tab. This is how it looks:
Note that this nice drag icon will only ship in Chromium 134 on March 4th. The initial release of fallback tab dragging in Chromium 133 will just show the Chromium/Chrome logo as the drag icon.
The actual implementation wasn’t as simple as the idea, as the original tab dragging handling code made many assumptions that don’t hold any more with fallback tab dragging. But we’ve been able to make both tab dragging implementations more and more similar over time, decreasing the complexity needed to support fallback tab dragging.
History
Before taking a glance at the implementation, let’s follow the evolution of tab dragging on Wayland. Tab dragging support for Wayland has been tracked since October 2018, and work on it began in February 2019. The linked tracking issue is a good resource if you’re interested in the low-level details, which I’ll skip in this post.
Since around August 2020 Chromium has had the basic but unsatisfying tab dragging support we saw in the earlier video. Then around November 2021 I started working on the initial fallback tab dragging implementation as part of my coding experience at Igalia (roughly similar to a paid internship).
This initial implementation was functional, but behind a feature flag that was disabled unless users opted into it. This was due to underlying testing issues on Wayland that made end-to-end testing difficult and prevented us from verifying whether fallback tab dragging was fixing more issues than it introduced. Having fallback tab dragging being opt-in was disappointing at the time, but it was the right choice – both in principle and in hindsight, as we ended up reworking the implementation after fixing the testing issues and having a test failure reveal something we initially overlooked (more on that later).
When I joined Igalia in September 2022 I started right away to work on the testing issues, which took another year to fix (I hope to write another blog post about them soon-ish). After that I didn’t always have time to work on fallback tab dragging. Over the next year I slowly but steadily continued to clean up the implementation, along with fixing the (not too many!) tests that were failing and polishing the UI/UX a bit. Finally, in November 2024 all tests were passing and all the bugs we were aware of had been fixed, and I sent a CL to enable fallback tab dragging by default. This change made its way to Chromium Stable in version 133 on February 4th 2025.
Architecture
I’d like to end this section by mentioning a few details of the actual implementation. If you’d like to know more, you can of course always browse the Chromium code yourself. There’s also a design doc for the initial implementation. As I’ve mentioned, some things have changed since then, but it should still be a good starting point.
The main class driving tab dragging is TabDragController
. As mentioned,
fallback tab dragging uses a regular DnD session, so we need to somehow let the controller receive
the DnD events, i.e. “the user dragged into/inside/out of the tab strip”.
The internal routing of DnD events is handled by Chromium’s internal “Views” UI toolkit. We need to
pick a view to consume the DnD events and forward them to TabDragController
. The obvious choice
would be TabStrip
, but that doesn’t include the empty space to the right of the currently open
tabs, meaning that we couldn’t drag new tabs into that empty space. Instead, we can use TabStrip
’s
parent view, TabStripRegionView
, which holds the TabStrip
, the “New Tab” button, and the empty
space for new tabs.
To let the Views toolkit know that the TabStripRegionView
wants to handle DnD events, we make its
CanDrop()
method return true if the TabDragController
has an active session and if the dragged
data contains the custom chromium/x-window-drag
MIME type that the controller sets when initiating
the DnD session. This way we only handle DnD events for fallback tab dragging and don’t interfere
with the regular DnD like dragging an image from the file manager into the tab strip to open it in a
new tab.
bool
To forward the DnD events, we call into a static method of TabDragController
that takes care of
updating the drag:
void
// OnDragUpdated() and OnDragExited() look similar.
void
One other interesting part of TabDragController
is what happens behind the scenes with the tabs
while they’re being dragged. The initial implementation detached them from the original window and
kept them in this state until they were either dragged into and attached to another window, or were
dropped and attached to a newly created window.
This mostly works, but tabs aren’t designed to be detached for longer periods of time. One thing that doesn’t work for detached tabs is closing them, because the implementation relies on having a context (browser window) to handle the closing. This clashes with the requirement that when a tab that’s being dragged closes (e.g. from JavaScript) the tab dragging session should automatically end.
This is a rare scenario, and so the fact that it didn’t work as it should with fallback tab dragging was only discovered after the testing issues on Wayland were fixed and the test case checking this exact scenario could finally be run.
The fix for this luckily wasn’t complex: instead of keeping dragged tabs detached, we always create a new hidden window when starting the drag and attach the tabs to that. Having dragged tabs be attached to a hidden window was already supported, because it’s the only possible way of handling things when there’s only one window open and we start dragging all of its tabs – if you detach the tabs the window closes, and as soon as the last window closes Chromium will shut down completely.
Always attaching dragged tabs to a hidden window made things a lot simpler, because it made fallback tab dragging more similar to regular tab dragging, which also always moves dragged tabs to a new window (although that one doesn’t stay hidden).
The rest of the implementation is basically just gluing things together the right way – integrating
this new logic into the existing tab dragging code, and also making sure that the high-level
TabDragController
and the low-level Wayland platform code work well together.
Closing thoughts
I’m really happy to have fallback tab dragging enabled by default, hopefully improving the lives of
users on Wayland compositors that don’t support xdg-toplevel-drag
!
Of course it wasn’t just me working on it, so I’d like to thank:
- Scott Violett (sky@) for bearing with me when reviewing the initial implementation,
- Taylor Bergquist for his work on tab dragging and reviewing all the improvements for fallback tab dragging,
- my colleague Nick Diego for the initial tab dragging support on Wayland and helping me with my implementation,
- Google for sponsoring parts of this work.
We at Igalia continue to help improve Chromium’s Wayland support – my colleagues Nick and Orko have been doing awesome work recently! Feel free to reach out if you have further questions about fallback tab dragging, Wayland, or Chromium in general.