Fancy Compositor

Fancy Compositor is an example that demonstrates how to write a fancy Wayland compositor in pure QML.

Introduction

Fancy Compositor is a small desktop-style Wayland compositor example that demonstrates the power and ease of the Qt Wayland Compositor QML APIs.

The Fancy Compositor example is similar to the Minimal QML example, in that it is a full-blown Wayland compositor, implemented only using QML code.

Initializing the Compositor

Like the Minimal QML example, Fancy Compositor supports the main shell extensions that are supported by Qt.

// Shell surface extension. Needed to provide a window concept for Wayland clients.
// I.e. requests and events for maximization, minimization, resizing, closing etc.
XdgShell {
    onToplevelCreated: (toplevel, xdgSurface) => screen.handleShellSurface(xdgSurface)
}

// Minimalistic shell extension. Mainly used for embedded applications.
IviApplication {
    onIviSurfaceCreated: (iviSurface) => screen.handleShellSurface(iviSurface)
}

// Deprecated shell extension, still used by some clients
WlShell {
    onWlShellSurfaceCreated: (shellSurface) => screen.handleShellSurface(shellSurface)
}

These are instantiated as children of the WaylandCompositor which automatically adds them to the list of supported interfaces which is broadcasted to clients from the server.

When a connected client creates a surface and binds it to one of the shell extensions, the corresponding signal is emitted. This then calls a method inside our custom WaylandOutput class, which appends the ShellSurface to a ListModel.

function handleShellSurface(shellSurface) {
    shellSurfaces.append({shellSurface: shellSurface});
}

This model is used as the source for a Repeater which creates ShellSurfaceItems inside the compositor's WaylandOutput. This adds a view of the surface in the Qt Quick scene. Since it is a ShellSurfaceItem, it also has certain interaction options for the user of the compositor, depending on which shell extension is in use.

Repeater {
    model: output.shellSurfaces
    // Chrome displays a shell surface on the screen (See Chrome.qml)
    Chrome {
        shellSurface: modelData
        onDestroyAnimationFinished: output.shellSurfaces.remove(index)
    }
}

Keyboard

In addition to the basic windowing system functions, the Fancy Compositor also supports an optional on-screen keyboard running in-process. This uses the Qt Virtual Keyboard module, and will be enabled if the module is available.

import QtQuick
import QtQuick.VirtualKeyboard

InputPanel {
    visible: active
    y: active ? parent.height - height : parent.height
    anchors.left: parent.left
    anchors.right: parent.right
}

The code is simple. We instantiate an InputPanel in the bottom of the output, and make sure it is visible if and only if it is currently active.

Loader {
    anchors.fill: parent
    source: "Keyboard.qml"
}

The keyboard is then added to the WaylandOutput using a Loader element. The Loader is used here to avoid having a hard dependency on the Qt Virtual Keyboard module. If loading fails, then the compositor will continue operating normally, but without support for an on-screen keyboard.

Finally, we need a way for the compositor to communicate the text input to its clients. This is done via a text-input extension. The Fancy Compositor example supports both the text_input_unstable_v2 protocol as well as Qt's qt_text_input_method_unstable_v1 protocol.

The qt_text_input_method_unstable_v1 extension is added to the compositor by instantiating the QtTextInputMethodManager as a child of the WaylandCompositor, and TextInputManager adds text_input_unstable_v2.

Newer Qt applications will pick qt_text_input_method_unstable_v1 when it is available, while other clients can use text_input_unstable_v2.

Transitions

In addition to the basic functionality, the Fancy Compositor example also demonstrates animated transitions between states.

The first of these is the activation transition. This is only supported on the XdgShell, since this is the only shell extension which has an activated state.

Connections {
    target: shellSurface.toplevel !== undefined ? shellSurface.toplevel : null

    // some signals are not available on wl_shell, so let's ignore them
    ignoreUnknownSignals: true

    function onActivatedChanged() { // xdg_shell only
        if (shellSurface.toplevel.activated) {
            receivedFocusAnimation.start();
        }
    }
}

SequentialAnimation {
    id: receivedFocusAnimation

    ParallelAnimation {
        NumberAnimation { target: scaleTransform; property: "yScale"; to: 1.02; duration: 100; easing.type: Easing.OutQuad }
        NumberAnimation { target: scaleTransform; property: "xScale"; to: 1.02; duration: 100; easing.type: Easing.OutQuad }
    }
    ParallelAnimation {
        NumberAnimation { target: scaleTransform; property: "yScale"; to: 1; duration: 100; easing.type: Easing.InOutQuad }
        NumberAnimation { target: scaleTransform; property: "xScale"; to: 1; duration: 100; easing.type: Easing.InOutQuad }
    }
}

When a client window becomes activated under the XdgShell protocol, we trigger an animation which makes the window "pop out" for 200 ms.

The Fancy Compositor also supports a destruction animation. This triggers whenever the window closes and surface is destroyed, whether this was because the client gracefully closed its window, or even if it crashes.

onSurfaceDestroyed: {
    bufferLocked = true;
    destroyAnimation.start();
}

SequentialAnimation {
    id: destroyAnimation

    ParallelAnimation {
        NumberAnimation { target: scaleTransform; property: "yScale"; to: 2/height; duration: 150 }
        NumberAnimation { target: scaleTransform; property: "xScale"; to: 0.4; duration: 150 }
        NumberAnimation { target: chrome; property: "opacity"; to: chrome.isChild ? 0 : 1; duration: 150 }
    }
    NumberAnimation { target: scaleTransform; property: "xScale"; to: 0; duration: 150 }
    ScriptAction { script: destroyAnimationFinished() }
}

To ensure that the content exists for the duration of the animation, we start by locking the buffer. This means the final frame rendered by the client will remain in memory until we are done with it.

Again, we trigger an animation on the scale of the item. The animation in question imitates turning off the power on a CRT screen, giving a visual clue to the user that the window is closing, and didn't just vanish into thin air.

Any sort of animated effect may be used for state changes such as these, with the full range of Qt Quick at your disposal.

Example project @ code.qt.io

© 2024 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.