From 577d4e22441b1fd9f5a1be1a3c85ae9d7ad40ef3 Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Sun, 16 Jun 2024 19:26:08 +0200 Subject: [PATCH] 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 --- .../platforms/wayland/qwaylandcallback_p.h | 31 ++++ .../wayland/qwaylandcursorsurface_p.h | 81 ++++++++ .../platforms/wayland/qwaylandinputdevice.cpp | 82 +------- .../platforms/wayland/qwaylandinputdevice_p.h | 5 +- .../platforms/wayland/qwaylandtabletv2.cpp | 175 ++++++++++++++++++ .../platforms/wayland/qwaylandtabletv2_p.h | 44 +++++ 6 files changed, 341 insertions(+), 77 deletions(-) create mode 100644 src/plugins/platforms/wayland/qwaylandcallback_p.h create mode 100644 src/plugins/platforms/wayland/qwaylandcursorsurface_p.h diff --git a/src/plugins/platforms/wayland/qwaylandcallback_p.h b/src/plugins/platforms/wayland/qwaylandcallback_p.h new file mode 100644 index 00000000000..b9afa18c6a0 --- /dev/null +++ b/src/plugins/platforms/wayland/qwaylandcallback_p.h @@ -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 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 m_fn; +}; + +} // namespace QtWaylandClient + +QT_END_NAMESPACE + +#endif // QWAYLANDCALLBACK_H diff --git a/src/plugins/platforms/wayland/qwaylandcursorsurface_p.h b/src/plugins/platforms/wayland/qwaylandcursorsurface_p.h new file mode 100644 index 00000000000..3e011ead28f --- /dev/null +++ b/src/plugins/platforms/wayland/qwaylandcursorsurface_p.h @@ -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 +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 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 diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp index 9bfa2b0f4af..e2b788171a0 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp +++ b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp @@ -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 #include @@ -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 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 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 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::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 diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h index 4e74df1e182..0d79ef93376 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h +++ b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h @@ -65,6 +65,7 @@ class QWaylandTextInputMethod; #if QT_CONFIG(cursor) class QWaylandCursorTheme; class QWaylandCursorShape; +template class CursorSurface; #endif @@ -294,7 +295,7 @@ public: void updateCursor(); void cursorTimerCallback(); void cursorFrameCallback(); - CursorSurface *getOrCreateCursorSurface(); + CursorSurface *getOrCreateCursorSurface(); #endif QWaylandInputDevice *seat() const { return mParent; } @@ -336,7 +337,7 @@ public: QScopedPointer shape; QWaylandCursorTheme *theme = nullptr; int themeBufferScale = 0; - QScopedPointer surface; + QScopedPointer> surface; QTimer frameTimer; bool gotFrameCallback = false; bool gotTimerCallback = false; diff --git a/src/plugins/platforms/wayland/qwaylandtabletv2.cpp b/src/plugins/platforms/wayland/qwaylandtabletv2.cpp index 627f8746f88..6a8c84c44a8 100644 --- a/src/plugins/platforms/wayland/qwaylandtabletv2.cpp +++ b/src/plugins/platforms/wayland/qwaylandtabletv2.cpp @@ -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 #include +#include + +#include 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::getOrCreateCursorSurface() +{ + if (!mCursor.surface) + mCursor.surface.reset( + new CursorSurface(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() diff --git a/src/plugins/platforms/wayland/qwaylandtabletv2_p.h b/src/plugins/platforms/wayland/qwaylandtabletv2_p.h index b101826e4ac..94b687ee3ea 100644 --- a/src/plugins/platforms/wayland/qwaylandtabletv2_p.h +++ b/src/plugins/platforms/wayland/qwaylandtabletv2_p.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,13 @@ class QWaylandTabletV2; class QWaylandTabletToolV2; class QWaylandTabletPadV2; +#if QT_CONFIG(cursor) +class QWaylandCursorTheme; +class QWaylandCursorShape; +template +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 *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 shape; + QWaylandCursorTheme *theme = nullptr; + int themeBufferScale = 0; + QScopedPointer> 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 + friend class CursorSurface; }; class Q_WAYLANDCLIENT_EXPORT QWaylandTabletPadV2 : public QPointingDevice, public QtWayland::zwp_tablet_pad_v2