QWaylandTablet: Implement cursor

Currently we don't set a cursor for tablet devices,
so we get either a generic fallback cursor (KWin)
or no cursor at all (GNOME).

This makes sure we get the same cursor we get for
mouse input.

The code is mostly identical to the mouse cursor
handling, so refactor things a bit to share

Pick-to: 6.8
Fixes: QTBUG-105843
Fixes: QTBUG-123776
Change-Id: Ie626ff978d9b66ec422804a103699eebec85e267
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Nicolas Fella 2024-06-16 19:26:08 +02:00 committed by Shawn Rutledge
parent efd45d447e
commit 577d4e2244
6 changed files with 341 additions and 77 deletions

View File

@ -0,0 +1,31 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QWAYLANDCALLBACK_H
#define QWAYLANDCALLBACK_H
#include "qwayland-wayland.h"
QT_BEGIN_NAMESPACE
namespace QtWaylandClient {
class WlCallback : public QtWayland::wl_callback
{
public:
explicit WlCallback(::wl_callback *callback, std::function<void(uint32_t)> fn)
: QtWayland::wl_callback(callback), m_fn(fn)
{
}
~WlCallback() override { wl_callback_destroy(object()); }
void callback_done(uint32_t callback_data) override { m_fn(callback_data); }
private:
std::function<void(uint32_t)> m_fn;
};
} // namespace QtWaylandClient
QT_END_NAMESPACE
#endif // QWAYLANDCALLBACK_H

View File

@ -0,0 +1,81 @@
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QWAYLANDCURSORSURFACE_H
#define QWAYLANDCURSORSURFACE_H
#include "qwaylandsurface_p.h"
#include "qwaylandcallback_p.h"
QT_BEGIN_NAMESPACE
namespace QtWaylandClient {
#if QT_CONFIG(cursor)
template <typename InputDevice>
class CursorSurface : public QWaylandSurface
{
public:
explicit CursorSurface(InputDevice *pointer, QWaylandDisplay *display)
: QWaylandSurface(display), m_pointer(pointer)
{
connect(this, &QWaylandSurface::screensChanged, m_pointer, &InputDevice::updateCursor);
}
void reset()
{
m_setSerial = 0;
m_hotspot = QPoint();
}
// Size and hotspot are in surface coordinates
void update(wl_buffer *buffer, const QPoint &hotspot, const QSize &size, int bufferScale,
bool animated = false)
{
// Calling code needs to ensure buffer scale is supported if != 1
Q_ASSERT(bufferScale == 1 || version() >= 3);
auto enterSerial = m_pointer->mEnterSerial;
if (m_setSerial < enterSerial || m_hotspot != hotspot) {
m_pointer->set_cursor(m_pointer->mEnterSerial, object(), hotspot.x(), hotspot.y());
m_setSerial = enterSerial;
m_hotspot = hotspot;
}
if (version() >= 3)
set_buffer_scale(bufferScale);
attach(buffer, 0, 0);
damage(0, 0, size.width(), size.height());
m_frameCallback.reset();
if (animated) {
m_frameCallback.reset(new WlCallback(frame(), [this](uint32_t time) {
Q_UNUSED(time);
m_pointer->cursorFrameCallback();
}));
}
commit();
}
int outputScale() const
{
int scale = 0;
for (auto *screen : m_screens)
scale = qMax(scale, screen->scale());
return scale;
}
private:
QScopedPointer<WlCallback> m_frameCallback;
InputDevice *m_pointer = nullptr;
uint m_setSerial = 0;
QPoint m_hotspot;
};
#endif // QT_CONFIG(cursor)
} // namespace QtWaylandClient
QT_END_NAMESPACE
#endif // QWAYLANDCURSORSURFACE_H

View File

@ -29,6 +29,8 @@
#include "qwaylandtextinputinterface_p.h"
#include "qwaylandinputcontext_p.h"
#include "qwaylandinputmethodcontext_p.h"
#include "qwaylandcallback_p.h"
#include "qwaylandcursorsurface_p.h"
#include <QtGui/private/qpixmap_raster_p.h>
#include <QtGui/private/qguiapplication_p.h>
@ -152,80 +154,6 @@ QWaylandWindow *QWaylandInputDevice::Pointer::focusWindow() const
#if QT_CONFIG(cursor)
class WlCallback : public QtWayland::wl_callback {
public:
explicit WlCallback(::wl_callback *callback, std::function<void(uint32_t)> fn)
: QtWayland::wl_callback(callback)
, m_fn(fn)
{}
~WlCallback() override { wl_callback_destroy(object()); }
void callback_done(uint32_t callback_data) override {
m_fn(callback_data);
}
private:
std::function<void(uint32_t)> m_fn;
};
class CursorSurface : public QWaylandSurface
{
public:
explicit CursorSurface(QWaylandInputDevice::Pointer *pointer, QWaylandDisplay *display)
: QWaylandSurface(display)
, m_pointer(pointer)
{
connect(this, &QWaylandSurface::screensChanged,
m_pointer, &QWaylandInputDevice::Pointer::updateCursor);
}
void reset()
{
m_setSerial = 0;
m_hotspot = QPoint();
}
// Size and hotspot are in surface coordinates
void update(wl_buffer *buffer, const QPoint &hotspot, const QSize &size, int bufferScale, bool animated = false)
{
// Calling code needs to ensure buffer scale is supported if != 1
Q_ASSERT(bufferScale == 1 || version() >= 3);
auto enterSerial = m_pointer->mEnterSerial;
if (m_setSerial < enterSerial || m_hotspot != hotspot) {
m_pointer->set_cursor(m_pointer->mEnterSerial, object(), hotspot.x(), hotspot.y());
m_setSerial = enterSerial;
m_hotspot = hotspot;
}
if (version() >= 3)
set_buffer_scale(bufferScale);
attach(buffer, 0, 0);
damage(0, 0, size.width(), size.height());
m_frameCallback.reset();
if (animated) {
m_frameCallback.reset(new WlCallback(frame(), [this](uint32_t time){
Q_UNUSED(time);
m_pointer->cursorFrameCallback();
}));
}
commit();
}
int outputScale() const
{
int scale = 0;
for (auto *screen : m_screens)
scale = qMax(scale, screen->scale());
return scale;
}
private:
QScopedPointer<WlCallback> m_frameCallback;
QWaylandInputDevice::Pointer *m_pointer = nullptr;
uint m_setSerial = 0;
QPoint m_hotspot;
};
int QWaylandInputDevice::Pointer::idealCursorScale() const
{
if (seat()->mQDisplay->compositor()->version() < 3) {
@ -342,7 +270,8 @@ void QWaylandInputDevice::Pointer::updateCursor()
qCWarning(lcQpaWayland) << "Unable to change to cursor" << shape;
}
CursorSurface *QWaylandInputDevice::Pointer::getOrCreateCursorSurface()
CursorSurface<QWaylandInputDevice::Pointer> *
QWaylandInputDevice::Pointer::getOrCreateCursorSurface()
{
if (!mCursor.surface)
mCursor.surface.reset(new CursorSurface(this, seat()->mQDisplay));
@ -688,6 +617,9 @@ void QWaylandInputDevice::setCursor(const QCursor *cursor, const QSharedPointer<
if (mPointer)
mPointer->updateCursor();
if (mTabletSeat)
mTabletSeat->updateCursor();
}
#endif

View File

@ -65,6 +65,7 @@ class QWaylandTextInputMethod;
#if QT_CONFIG(cursor)
class QWaylandCursorTheme;
class QWaylandCursorShape;
template <typename T>
class CursorSurface;
#endif
@ -294,7 +295,7 @@ public:
void updateCursor();
void cursorTimerCallback();
void cursorFrameCallback();
CursorSurface *getOrCreateCursorSurface();
CursorSurface<QWaylandInputDevice::Pointer> *getOrCreateCursorSurface();
#endif
QWaylandInputDevice *seat() const { return mParent; }
@ -336,7 +337,7 @@ public:
QScopedPointer<QWaylandCursorShape> shape;
QWaylandCursorTheme *theme = nullptr;
int themeBufferScale = 0;
QScopedPointer<CursorSurface> surface;
QScopedPointer<CursorSurface<QWaylandInputDevice::Pointer>> surface;
QTimer frameTimer;
bool gotFrameCallback = false;
bool gotTimerCallback = false;

View File

@ -5,7 +5,16 @@
#include "qwaylandinputdevice_p.h"
#include "qwaylanddisplay_p.h"
#include "qwaylandsurface_p.h"
#include "qwaylandscreen_p.h"
#include "qwaylandbuffer_p.h"
#include "qwaylandcursorsurface_p.h"
#include "qwaylandcursor_p.h"
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/private/qpointingdevice_p.h>
#include <qpa/qplatformtheme.h>
#include <wayland-cursor.h>
QT_BEGIN_NAMESPACE
@ -16,6 +25,148 @@ using namespace Qt::StringLiterals;
Q_LOGGING_CATEGORY(lcQpaInputDevices, "qt.qpa.input.devices")
Q_DECLARE_LOGGING_CATEGORY(lcQpaWaylandInput)
#if QT_CONFIG(cursor)
int QWaylandTabletToolV2::idealCursorScale() const
{
if (m_tabletSeat->seat()->mQDisplay->compositor()->version() < 3) {
return 1;
}
if (auto *s = mCursor.surface.data()) {
if (s->outputScale() > 0)
return s->outputScale();
}
return m_tabletSeat->seat()->mCursor.fallbackOutputScale;
}
void QWaylandTabletToolV2::updateCursorTheme()
{
QString cursorThemeName;
QSize cursorSize;
if (const QPlatformTheme *platformTheme = QGuiApplicationPrivate::platformTheme()) {
cursorThemeName = platformTheme->themeHint(QPlatformTheme::MouseCursorTheme).toString();
cursorSize = platformTheme->themeHint(QPlatformTheme::MouseCursorSize).toSize();
}
if (cursorThemeName.isEmpty())
cursorThemeName = QStringLiteral("default");
if (cursorSize.isEmpty())
cursorSize = QSize(24, 24);
int scale = idealCursorScale();
int pixelSize = cursorSize.width() * scale;
auto *display = m_tabletSeat->seat()->mQDisplay;
mCursor.theme = display->loadCursorTheme(cursorThemeName, pixelSize);
if (!mCursor.theme)
return; // A warning has already been printed in loadCursorTheme
if (auto *arrow = mCursor.theme->cursor(Qt::ArrowCursor)) {
int arrowPixelSize = qMax(arrow->images[0]->width,
arrow->images[0]->height); // Not all cursor themes are square
while (scale > 1 && arrowPixelSize / scale < cursorSize.width())
--scale;
} else {
qCWarning(lcQpaWayland) << "Cursor theme does not support the arrow cursor";
}
mCursor.themeBufferScale = scale;
}
void QWaylandTabletToolV2::updateCursor()
{
if (mEnterSerial == 0)
return;
auto shape = m_tabletSeat->seat()->mCursor.shape;
if (shape == Qt::BlankCursor) {
if (mCursor.surface)
mCursor.surface->reset();
set_cursor(mEnterSerial, nullptr, 0, 0);
return;
}
if (shape == Qt::BitmapCursor) {
auto buffer = m_tabletSeat->seat()->mCursor.bitmapBuffer;
if (!buffer) {
qCWarning(lcQpaWayland) << "No buffer for bitmap cursor, can't set cursor";
return;
}
auto hotspot = m_tabletSeat->seat()->mCursor.hotspot;
int bufferScale = m_tabletSeat->seat()->mCursor.bitmapScale;
getOrCreateCursorSurface()->update(buffer->buffer(), hotspot, buffer->size(), bufferScale);
return;
}
if (mCursor.shape) {
if (mCursor.surface) {
mCursor.surface->reset();
}
mCursor.shape->setShape(mEnterSerial, shape);
return;
}
if (!mCursor.theme || idealCursorScale() != mCursor.themeBufferScale)
updateCursorTheme();
if (!mCursor.theme)
return;
// Set from shape using theme
uint time = m_tabletSeat->seat()->mCursor.animationTimer.elapsed();
if (struct ::wl_cursor *waylandCursor = mCursor.theme->cursor(shape)) {
uint duration = 0;
int frame = wl_cursor_frame_and_duration(waylandCursor, time, &duration);
::wl_cursor_image *image = waylandCursor->images[frame];
struct wl_buffer *buffer = wl_cursor_image_get_buffer(image);
if (!buffer) {
qCWarning(lcQpaWayland) << "Could not find buffer for cursor" << shape;
return;
}
int bufferScale = mCursor.themeBufferScale;
QPoint hotspot = QPoint(image->hotspot_x, image->hotspot_y) / bufferScale;
QSize size = QSize(image->width, image->height) / bufferScale;
bool animated = duration > 0;
if (animated) {
mCursor.gotFrameCallback = false;
mCursor.gotTimerCallback = false;
mCursor.frameTimer.start(duration);
}
getOrCreateCursorSurface()->update(buffer, hotspot, size, bufferScale, animated);
return;
}
qCWarning(lcQpaWayland) << "Unable to change to cursor" << shape;
}
CursorSurface<QWaylandTabletToolV2> *QWaylandTabletToolV2::getOrCreateCursorSurface()
{
if (!mCursor.surface)
mCursor.surface.reset(
new CursorSurface<QWaylandTabletToolV2>(this, m_tabletSeat->seat()->mQDisplay));
return mCursor.surface.get();
}
void QWaylandTabletToolV2::cursorTimerCallback()
{
mCursor.gotTimerCallback = true;
if (mCursor.gotFrameCallback)
updateCursor();
}
void QWaylandTabletToolV2::cursorFrameCallback()
{
mCursor.gotFrameCallback = true;
if (mCursor.gotTimerCallback)
updateCursor();
}
#endif // QT_CONFIG(cursor)
QWaylandTabletManagerV2::QWaylandTabletManagerV2(QWaylandDisplay *display, uint id, uint version)
: zwp_tablet_manager_v2(display->wl_registry(), id, qMin(version, uint(1)))
{
@ -127,6 +278,12 @@ void QWaylandTabletV2::zwp_tablet_v2_done()
QWindowSystemInterface::registerInputDevice(this);
}
void QWaylandTabletSeatV2::updateCursor()
{
for (auto tool : m_tools)
tool->updateCursor();
}
void QWaylandTabletSeatV2::toolRemoved(QWaylandTabletToolV2 *tool)
{
m_tools.removeOne(tool);
@ -147,8 +304,20 @@ QWaylandTabletToolV2::QWaylandTabletToolV2(QWaylandTabletSeatV2 *tabletSeat, ::z
, m_tabletSeat(tabletSeat)
{
// TODO get the number of buttons somehow?
#if QT_CONFIG(cursor)
if (auto cursorShapeManager = m_tabletSeat->seat()->mQDisplay->cursorShapeManager()) {
mCursor.shape.reset(
new QWaylandCursorShape(cursorShapeManager->get_tablet_tool_v2(object())));
}
mCursor.frameTimer.setSingleShot(true);
mCursor.frameTimer.callOnTimeout(this, [&]() { cursorTimerCallback(); });
#endif
}
QWaylandTabletToolV2::~QWaylandTabletToolV2() = default;
void QWaylandTabletToolV2::zwp_tablet_tool_v2_type(uint32_t tool_type)
{
QPointingDevicePrivate *d = QPointingDevicePrivate::get(this);
@ -250,6 +419,7 @@ void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_in(uint32_t serial, zwp_
Q_UNUSED(tablet);
m_tabletSeat->seat()->mSerial = serial;
mEnterSerial = serial;
if (Q_UNLIKELY(!surface)) {
qCDebug(lcQpaWayland) << "Ignoring zwp_tablet_tool_v2_proximity_v2 with no surface";
@ -257,6 +427,11 @@ void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_in(uint32_t serial, zwp_
}
m_pending.enteredSurface = true;
m_pending.proximitySurface = QWaylandSurface::fromWlSurface(surface);
#if QT_CONFIG(cursor)
// Depends on mEnterSerial being updated
updateCursor();
#endif
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_out()

View File

@ -22,6 +22,7 @@
#include <QtCore/QObject>
#include <QtCore/QPointer>
#include <QtCore/QPointF>
#include <QtCore/QTimer>
#include <QtGui/QPointingDevice>
#include <QtGui/QInputDevice>
@ -38,6 +39,13 @@ class QWaylandTabletV2;
class QWaylandTabletToolV2;
class QWaylandTabletPadV2;
#if QT_CONFIG(cursor)
class QWaylandCursorTheme;
class QWaylandCursorShape;
template <typename T>
class CursorSurface;
#endif
class Q_WAYLANDCLIENT_EXPORT QWaylandTabletManagerV2 : public QtWayland::zwp_tablet_manager_v2
{
public:
@ -54,6 +62,7 @@ public:
QWaylandInputDevice *seat() const { return m_seat; }
void updateCursor();
void toolRemoved(QWaylandTabletToolV2 *tool);
protected:
@ -89,6 +98,9 @@ class Q_WAYLANDCLIENT_EXPORT QWaylandTabletToolV2 : public QPointingDevice, publ
Q_OBJECT
public:
QWaylandTabletToolV2(QWaylandTabletSeatV2 *tabletSeat, ::zwp_tablet_tool_v2 *tool);
~QWaylandTabletToolV2() override;
void updateCursor();
protected:
void zwp_tablet_tool_v2_type(uint32_t tool_type) override;
@ -112,8 +124,37 @@ protected:
void zwp_tablet_tool_v2_frame(uint32_t time) override;
private:
#if QT_CONFIG(cursor)
int idealCursorScale() const;
void updateCursorTheme();
void cursorTimerCallback();
void cursorFrameCallback();
CursorSurface<QWaylandTabletToolV2> *getOrCreateCursorSurface();
#endif
QWaylandTabletSeatV2 *m_tabletSeat;
// Static state (sent before done event)
QPointingDevice::PointerType m_pointerType = QPointingDevice::PointerType::Unknown;
QInputDevice::DeviceType m_tabletDevice = QInputDevice::DeviceType::Unknown;
zwp_tablet_tool_v2::type m_toolType = type_pen;
bool m_hasRotation = false;
quint64 m_uid = 0;
uint32_t mEnterSerial = 0;
#if QT_CONFIG(cursor)
struct
{
QScopedPointer<QWaylandCursorShape> shape;
QWaylandCursorTheme *theme = nullptr;
int themeBufferScale = 0;
QScopedPointer<CursorSurface<QWaylandTabletToolV2>> surface;
QTimer frameTimer;
bool gotFrameCallback = false;
bool gotTimerCallback = false;
} mCursor;
#endif
// Accumulated state (applied on frame event)
struct State {
bool down = false;
@ -130,6 +171,9 @@ private:
//auto operator<=>(const Point&) const = default; // TODO: use this when upgrading to C++20
bool operator==(const State &o) const;
} m_pending, m_applied;
template <typename T>
friend class CursorSurface;
};
class Q_WAYLANDCLIENT_EXPORT QWaylandTabletPadV2 : public QPointingDevice, public QtWayland::zwp_tablet_pad_v2