Custom client-side window decorations in Qt 5.15

This is just a quick update about a new feature in Qt 5.15 that I'm really excited
about.

Traditionally, window decorations have been a pretty boring thing. Title bar,
border, minimize, maximize, resize and quit... and that's it.

In recent times, however, applications more and more tend to include
application specific UI and theming in their decorations. Just a couple of screenshots to
explain what I'm talking about:

ios-preferences window decorationsMacOS has been doing this for quite a while.

chromium...and so has Chrome, and about every other web browser.

vscodeEmbedding menus in the decorations can save a lot of screen space.

slack

...or it can be important for branding or design purposes.

Unfortunately, these kinds of things were not previously possible with Qt.

It was, however, possible to remove the decorations on a window i.e.:

Window {
flags: Qt.FramelessWindowHint
}

But that just left you with a window with no decorations. So it couldn't be moved or resized. If you then were to try implementing window movement or resizing yourself by grabbing the mouse and manually setting the window size and position, you would quickly discover that it didn't really feel good. Window managers generally tend to have very specific behavior for how windows are moved or resized. Common conventions are dragging to the top to maximize, dragging to the left/right to tile, snapping to other windows or the task bar, resizing two windows simultaneously if they are tiled next to each other and so on.

In fairness, we did provide one helper for this previously: QSizeGrip. It lets you resize any given corner of a window, but it only works on corners, not window edges, and it's only available for widget applications.

QSizeGrip

In Qt 5.15, we've added two new methods to QWindow: startSystemMove and startSystemResize. These methods ask the window manager to take over and start a native resize or move operation. This means snapping, tiling, etc. should just magically work and implementing a titlebar in QML becomes almost a one-liner:

DragHandler {
onActiveChanged: if (active) window.startSystemMove();
target: null
}

Putting this this piece of code inside a QtQuick Item, will make any drag operations trigger a native window move operation.

startSystemResize works similarly, except it takes a Qt::Edges argument, which is a bit field of the window edges you grabbed. i.e. for the bottom, right corner, you would call

startSystemResize(Qt.RightEdge | Qt.BottomEdge)

This is also really convenient as you can easily have one handler for all four window edges and just build up the edges argument like this:

DragHandler {
id: resizeHandler
grabPermissions: TapHandler.TakeOverForbidden
target: null
onActiveChanged: if (active) {
const p = resizeHandler.centroid.position;
let e = 0;
if (p.x < border) e |= Qt.LeftEdge;
if (p.x >= width - border) e |= Qt.RightEdge;
if (p.y < border) e |= Qt.TopEdge;
if (p.y >= height - border) e |= Qt.BottomEdge;
window.startSystemResize(e);
}
}

If you want a full example of how this can be used, I made a mockup of a web browser using the new API.

Note that although this is a cross-platform API, not all platforms support it. startSystemMove is currently supported on Wayland, X11, macOS and Windows, while startSystemResize, on the other hand, is supported on Wayland, X11 and Windows, but not on macOS.

In order to deal with this, both methods return a boolean which indicates whether the operation was supported or not. This means that if you want to implement resizing on macOS as well, you will have to check the return value of startSystemResize and try your best to implement a fallback in case it fails, i.e.

if (!window.startSystemResize(edges)) {
// your fallback code for setting window.width/height manually
}

Further work in Qt would be to provide fallbacks or abstractions on top that would do this work for you, but at least nothing should stop you from doing it yourself.

Another area of improvement is the negotiations with the window manager on whether client-side or server-side window decorations should be used. Some applications might want to support both modes and let the window manager decide, but this is not currently possible. Once the FramelessWindowHint is set, there will be no server-side decorations.

A third area is the window shadow. At least on Wayland, the shadow should be drawn as part of the window decoration. And while we could definitely draw shadows with QtQuick, there is currently no way to tell the QPA plugin which part of the surface is the shadow and which part is the window frame, which means if you try to draw shadows, the window manager will currently consider the shadow part of the window and this will mess up tiling and snapping with other windows. On other platforms, shadows are generally drawn by the window manager, even for client-side decorated windows, so this is a tricky issue to solve.

That's it. A big thanks to everyone that helped test the API on various platforms! I really hope people will build something cool with it.


Blog Topics:

Comments