1// Copyright (C) 2016 The Qt Company Ltd.
2// Copyright (C) 2023 David Edmundson <davidedmundson@kde.org>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qwaylandcursor_p.h"
6
7#include "qwaylanddisplay_p.h"
8#include "qwaylandinputdevice_p.h"
9#include "qwaylandshmbackingstore_p.h"
10#include "qwayland-pointer-warp-v1.h"
11
12#include <QtGui/private/qguiapplication_p.h>
13#include <qpa/qplatformtheme.h>
14
15#include <QtGui/QImageReader>
16#include <QDebug>
17
18#include <wayland-cursor.h>
19
20#include <algorithm>
21
22QT_BEGIN_NAMESPACE
23
24namespace QtWaylandClient {
25
26std::unique_ptr<QWaylandCursorTheme> QWaylandCursorTheme::create(QWaylandShm *shm, int size, const QString &themeName)
27{
28 QByteArray nameBytes = themeName.toLocal8Bit();
29 struct ::wl_cursor_theme *theme = wl_cursor_theme_load(nameBytes.constData(), size, shm->object());
30
31 if (!theme) {
32 qCWarning(lcQpaWayland) << "Could not load cursor theme" << themeName << "size" << size;
33 return nullptr;
34 }
35
36 return std::unique_ptr<QWaylandCursorTheme>{new QWaylandCursorTheme(theme)};
37}
38
39QWaylandCursorTheme::~QWaylandCursorTheme()
40{
41 wl_cursor_theme_destroy(theme: m_theme);
42}
43
44wl_cursor *QWaylandCursorTheme::requestCursor(WaylandCursor shape)
45{
46 if (struct wl_cursor *cursor = m_cursors[shape])
47 return cursor;
48
49 static Q_CONSTEXPR struct ShapeAndName {
50 WaylandCursor shape;
51 const char name[33];
52 } cursorNamesMap[] = {
53 {.shape: ArrowCursor, .name: "left_ptr"},
54 {.shape: ArrowCursor, .name: "default"},
55 {.shape: ArrowCursor, .name: "top_left_arrow"},
56 {.shape: ArrowCursor, .name: "left_arrow"},
57
58 {.shape: UpArrowCursor, .name: "up_arrow"},
59
60 {.shape: CrossCursor, .name: "cross"},
61
62 {.shape: WaitCursor, .name: "wait"},
63 {.shape: WaitCursor, .name: "watch"},
64 {.shape: WaitCursor, .name: "0426c94ea35c87780ff01dc239897213"},
65
66 {.shape: IBeamCursor, .name: "ibeam"},
67 {.shape: IBeamCursor, .name: "text"},
68 {.shape: IBeamCursor, .name: "xterm"},
69
70 {.shape: SizeVerCursor, .name: "size_ver"},
71 {.shape: SizeVerCursor, .name: "ns-resize"},
72 {.shape: SizeVerCursor, .name: "v_double_arrow"},
73 {.shape: SizeVerCursor, .name: "00008160000006810000408080010102"},
74
75 {.shape: SizeHorCursor, .name: "size_hor"},
76 {.shape: SizeHorCursor, .name: "ew-resize"},
77 {.shape: SizeHorCursor, .name: "h_double_arrow"},
78 {.shape: SizeHorCursor, .name: "028006030e0e7ebffc7f7070c0600140"},
79
80 {.shape: SizeBDiagCursor, .name: "size_bdiag"},
81 {.shape: SizeBDiagCursor, .name: "nesw-resize"},
82 {.shape: SizeBDiagCursor, .name: "50585d75b494802d0151028115016902"},
83 {.shape: SizeBDiagCursor, .name: "fcf1c3c7cd4491d801f1e1c78f100000"},
84
85 {.shape: SizeFDiagCursor, .name: "size_fdiag"},
86 {.shape: SizeFDiagCursor, .name: "nwse-resize"},
87 {.shape: SizeFDiagCursor, .name: "38c5dff7c7b8962045400281044508d2"},
88 {.shape: SizeFDiagCursor, .name: "c7088f0f3e6c8088236ef8e1e3e70000"},
89
90 {.shape: SizeAllCursor, .name: "size_all"},
91
92 {.shape: BlankCursor, .name: "blank"},
93
94 {.shape: SplitVCursor, .name: "split_v"},
95 {.shape: SplitVCursor, .name: "row-resize"},
96 {.shape: SplitVCursor, .name: "sb_v_double_arrow"},
97 {.shape: SplitVCursor, .name: "2870a09082c103050810ffdffffe0204"},
98 {.shape: SplitVCursor, .name: "c07385c7190e701020ff7ffffd08103c"},
99
100 {.shape: SplitHCursor, .name: "split_h"},
101 {.shape: SplitHCursor, .name: "col-resize"},
102 {.shape: SplitHCursor, .name: "sb_h_double_arrow"},
103 {.shape: SplitHCursor, .name: "043a9f68147c53184671403ffa811cc5"},
104 {.shape: SplitHCursor, .name: "14fef782d02440884392942c11205230"},
105
106 {.shape: PointingHandCursor, .name: "pointing_hand"},
107 {.shape: PointingHandCursor, .name: "pointer"},
108 {.shape: PointingHandCursor, .name: "hand1"},
109 {.shape: PointingHandCursor, .name: "e29285e634086352946a0e7090d73106"},
110
111 {.shape: ForbiddenCursor, .name: "forbidden"},
112 {.shape: ForbiddenCursor, .name: "not-allowed"},
113 {.shape: ForbiddenCursor, .name: "crossed_circle"},
114 {.shape: ForbiddenCursor, .name: "circle"},
115 {.shape: ForbiddenCursor, .name: "03b6e0fcb3499374a867c041f52298f0"},
116
117 {.shape: WhatsThisCursor, .name: "whats_this"},
118 {.shape: WhatsThisCursor, .name: "help"},
119 {.shape: WhatsThisCursor, .name: "question_arrow"},
120 {.shape: WhatsThisCursor, .name: "5c6cd98b3f3ebcb1f9c7f1c204630408"},
121 {.shape: WhatsThisCursor, .name: "d9ce0ab605698f320427677b458ad60b"},
122
123 {.shape: BusyCursor, .name: "left_ptr_watch"},
124 {.shape: BusyCursor, .name: "half-busy"},
125 {.shape: BusyCursor, .name: "progress"},
126 {.shape: BusyCursor, .name: "00000000000000020006000e7e9ffc3f"},
127 {.shape: BusyCursor, .name: "08e8e1c95fe2fc01f976f1e063a24ccd"},
128
129 {.shape: OpenHandCursor, .name: "openhand"},
130 {.shape: OpenHandCursor, .name: "fleur"},
131 {.shape: OpenHandCursor, .name: "5aca4d189052212118709018842178c0"},
132 {.shape: OpenHandCursor, .name: "9d800788f1b08800ae810202380a0822"},
133
134 {.shape: ClosedHandCursor, .name: "closedhand"},
135 {.shape: ClosedHandCursor, .name: "grabbing"},
136 {.shape: ClosedHandCursor, .name: "208530c400c041818281048008011002"},
137
138 {.shape: DragCopyCursor, .name: "dnd-copy"},
139 {.shape: DragCopyCursor, .name: "copy"},
140
141 {.shape: DragMoveCursor, .name: "dnd-move"},
142 {.shape: DragMoveCursor, .name: "move"},
143
144 {.shape: DragLinkCursor, .name: "dnd-link"},
145 {.shape: DragLinkCursor, .name: "link"},
146
147 {.shape: ResizeNorthCursor, .name: "n-resize"},
148 {.shape: ResizeNorthCursor, .name: "top_side"},
149
150 {.shape: ResizeSouthCursor, .name: "s-resize"},
151 {.shape: ResizeSouthCursor, .name: "bottom_side"},
152
153 {.shape: ResizeEastCursor, .name: "e-resize"},
154 {.shape: ResizeEastCursor, .name: "right_side"},
155
156 {.shape: ResizeWestCursor, .name: "w-resize"},
157 {.shape: ResizeWestCursor, .name: "left_side"},
158
159 {.shape: ResizeNorthWestCursor, .name: "nw-resize"},
160 {.shape: ResizeNorthWestCursor, .name: "top_left_corner"},
161
162 {.shape: ResizeSouthEastCursor, .name: "se-resize"},
163 {.shape: ResizeSouthEastCursor, .name: "bottom_right_corner"},
164
165 {.shape: ResizeNorthEastCursor, .name: "ne-resize"},
166 {.shape: ResizeNorthEastCursor, .name: "top_right_corner"},
167
168 {.shape: ResizeSouthWestCursor, .name: "sw-resize"},
169 {.shape: ResizeSouthWestCursor, .name: "bottom_left_corner"},
170 };
171
172 const auto byShape = [](ShapeAndName lhs, ShapeAndName rhs) {
173 return lhs.shape < rhs.shape;
174 };
175 Q_ASSERT(std::is_sorted(std::begin(cursorNamesMap), std::end(cursorNamesMap), byShape));
176 const auto p = std::equal_range(first: std::begin(arr: cursorNamesMap), last: std::end(arr: cursorNamesMap),
177 val: ShapeAndName{.shape: shape, .name: ""}, comp: byShape);
178 for (auto it = p.first; it != p.second; ++it) {
179 if (wl_cursor *cursor = wl_cursor_theme_get_cursor(theme: m_theme, name: it->name)) {
180 m_cursors[shape] = cursor;
181 return cursor;
182 }
183 }
184
185 // Fallback to arrow cursor
186 if (shape != ArrowCursor)
187 return requestCursor(shape: ArrowCursor);
188
189 // Give up
190 return nullptr;
191}
192
193::wl_cursor *QWaylandCursorTheme::cursor(Qt::CursorShape shape)
194{
195 struct wl_cursor *waylandCursor = nullptr;
196
197 if (shape < Qt::BitmapCursor) {
198 waylandCursor = requestCursor(shape: WaylandCursor(shape));
199 } else if (shape == Qt::BitmapCursor) {
200 qCWarning(lcQpaWayland) << "cannot create a wl_cursor_image for a CursorShape";
201 return nullptr;
202 } else {
203 //TODO: Custom cursor logic (for resize arrows)
204 }
205
206 if (!waylandCursor) {
207 qCWarning(lcQpaWayland) << "Could not find cursor for shape" << shape;
208 return nullptr;
209 }
210
211 return waylandCursor;
212}
213
214QWaylandCursorShape::QWaylandCursorShape(::wp_cursor_shape_device_v1 *object)
215 : QtWayland::wp_cursor_shape_device_v1(object)
216{}
217
218QWaylandCursorShape::~QWaylandCursorShape()
219{
220 destroy();
221}
222
223static QtWayland::wp_cursor_shape_device_v1::shape qtCursorShapeToWaylandShape(Qt::CursorShape cursorShape)
224{
225 using QtWayland::wp_cursor_shape_device_v1;
226
227 switch (cursorShape) {
228 case Qt::BlankCursor:
229 case Qt::CustomCursor:
230 case Qt::BitmapCursor:
231 // these should have been handled separately before using the shape protocol
232 Q_ASSERT(false);
233 break;
234 case Qt::ArrowCursor:
235 return wp_cursor_shape_device_v1::shape_default;
236 case Qt::SizeVerCursor:
237 return wp_cursor_shape_device_v1::shape_ns_resize;
238 case Qt::UpArrowCursor:
239 return wp_cursor_shape_device_v1::shape_n_resize;
240 case Qt::SizeHorCursor:
241 return wp_cursor_shape_device_v1::shape_ew_resize;
242 case Qt::CrossCursor:
243 return wp_cursor_shape_device_v1::shape_crosshair;
244 case Qt::SizeBDiagCursor:
245 return wp_cursor_shape_device_v1::shape_nesw_resize;
246 case Qt::IBeamCursor:
247 return wp_cursor_shape_device_v1::shape_text;
248 case Qt::SizeFDiagCursor:
249 return wp_cursor_shape_device_v1::shape_nwse_resize;
250 case Qt::WaitCursor:
251 return wp_cursor_shape_device_v1::shape_wait;
252 case Qt::SizeAllCursor:
253 return wp_cursor_shape_device_v1::shape_all_scroll;
254 case Qt::BusyCursor:
255 return wp_cursor_shape_device_v1::shape_progress;
256 case Qt::SplitVCursor:
257 return wp_cursor_shape_device_v1::shape_row_resize;
258 case Qt::ForbiddenCursor:
259 return wp_cursor_shape_device_v1::shape_not_allowed;
260 case Qt::SplitHCursor:
261 return wp_cursor_shape_device_v1::shape_col_resize;
262 case Qt::PointingHandCursor:
263 return wp_cursor_shape_device_v1::shape_pointer;
264 case Qt::OpenHandCursor:
265 return wp_cursor_shape_device_v1::shape_grab;
266 case Qt::WhatsThisCursor:
267 return wp_cursor_shape_device_v1::shape_help;
268 case Qt::ClosedHandCursor:
269 return wp_cursor_shape_device_v1::shape_grabbing;
270 case Qt::DragMoveCursor:
271 return wp_cursor_shape_device_v1::shape_move;
272 case Qt::DragCopyCursor:
273 return wp_cursor_shape_device_v1::shape_copy;
274 case Qt::DragLinkCursor:
275 return wp_cursor_shape_device_v1::shape_alias;
276 }
277 return wp_cursor_shape_device_v1::shape_default;
278}
279
280void QWaylandCursorShape::setShape(uint32_t serial, Qt::CursorShape shape)
281{
282 set_shape(serial, qtCursorShapeToWaylandShape(shape));
283}
284
285QWaylandCursor::QWaylandCursor(QWaylandDisplay *display)
286 : mDisplay(display)
287{
288}
289
290QSharedPointer<QWaylandBuffer> QWaylandCursor::cursorBitmapBuffer(QWaylandDisplay *display, const QCursor *cursor)
291{
292 Q_ASSERT(cursor->shape() == Qt::BitmapCursor);
293 QImage img = !cursor->pixmap().isNull() ? cursor->pixmap().toImage() : cursor->bitmap().toImage();
294
295 // convert to supported format if necessary
296 if (!display->shm()->formatSupported(format: img.format())) {
297 if (cursor->mask().isNull()) {
298 img.convertTo(f: QImage::Format_RGB32);
299 } else {
300 // preserve mask
301 img.convertTo(f: QImage::Format_ARGB32);
302 QPixmap pixmap = QPixmap::fromImage(image: img);
303 pixmap.setMask(cursor->mask());
304 img = pixmap.toImage();
305 }
306 }
307
308 QSharedPointer<QWaylandShmBuffer> buffer(new QWaylandShmBuffer(display, img.size(), img.format()));
309 memcpy(dest: buffer->image()->bits(), src: img.bits(), n: size_t(img.sizeInBytes()));
310 return buffer;
311}
312
313void QWaylandCursor::changeCursor(QCursor *cursor, QWindow *window)
314{
315 if (!window)
316 return;
317 // Create the buffer here so we don't have to create one per input device
318 QSharedPointer<QWaylandBuffer> bitmapBuffer;
319 if (cursor && cursor->shape() == Qt::BitmapCursor)
320 bitmapBuffer = cursorBitmapBuffer(display: mDisplay, cursor);
321
322 QWaylandWindow *waylandWindow = static_cast<QWaylandWindow*>(window->handle());
323 if (!waylandWindow)
324 return;
325
326 if (cursor)
327 waylandWindow->setStoredCursor(*cursor);
328 else
329 waylandWindow->resetStoredCursor();
330
331 for (QWaylandInputDevice *device : mDisplay->inputDevices()) {
332 if (device->pointer() && device->pointer()->focusWindow() == waylandWindow)
333 device->setCursor(cursor, cachedBuffer: bitmapBuffer, fallbackOutputScale: qCeil(v: waylandWindow->devicePixelRatio()));
334 }
335
336 wl_display_flush(mDisplay->wl_display());
337}
338
339void QWaylandCursor::pointerEvent(const QMouseEvent &event)
340{
341 mLastPos = event.globalPosition().toPoint();
342}
343
344QPoint QWaylandCursor::pos() const
345{
346 return mLastPos;
347}
348
349void QWaylandCursor::setPos(const QPoint &pos)
350{
351 if (mDisplay->pointerWarp()) {
352 const auto seats = mDisplay->inputDevices();
353 for (auto *seat : seats) {
354 if (!seat->pointer() || !seat->pointer()->focusWindow()) {
355 continue;
356 }
357 const auto focus = seat->pointer()->focusWindow();
358 if (!focus->windowFrameGeometry().contains(pos)) {
359 continue;
360 }
361 mDisplay->pointerWarp()->warp_pointer(focus->surface(), seat->pointer()->object(), wl_fixed_from_double(pos.x() - focus->windowFrameGeometry().x()), wl_fixed_from_double(pos.y() - focus->windowFrameGeometry().y()), seat->pointer()->mEnterSerial);
362 return;
363 }
364 } else {
365 qCWarning(lcQpaWayland) << "Setting cursor position requires pointer warp v1 protocol support";
366 }
367}
368
369void QWaylandCursor::setPosFromEnterEvent(const QPoint &pos)
370{
371 mLastPos = pos;
372}
373
374QSize QWaylandCursor::size() const
375{
376 if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme())
377 return theme->themeHint(hint: QPlatformTheme::MouseCursorSize).toSize();
378 return QSize(24, 24);
379}
380
381} // namespace QtWaylandClient
382
383QT_END_NAMESPACE
384

source code of qtbase/src/plugins/platforms/wayland/qwaylandcursor.cpp