Files
test/intern/ghost/intern/GHOST_SystemWayland.cpp

3560 lines
119 KiB
C++

/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup GHOST
*/
#include "GHOST_SystemWayland.h"
#include "GHOST_Event.h"
#include "GHOST_EventButton.h"
#include "GHOST_EventCursor.h"
#include "GHOST_EventDragnDrop.h"
#include "GHOST_EventKey.h"
#include "GHOST_EventWheel.h"
#include "GHOST_TimerManager.h"
#include "GHOST_WindowManager.h"
#include "GHOST_ContextEGL.h"
#include <EGL/egl.h>
#include <wayland-egl.h>
#include <algorithm>
#include <atomic>
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <wayland-cursor.h>
#include "GHOST_WaylandCursorSettings.h"
#include <xkbcommon/xkbcommon.h>
/* Generated by `wayland-scanner`. */
#include <pointer-constraints-unstable-v1-client-protocol.h>
#include <relative-pointer-unstable-v1-client-protocol.h>
#include <tablet-unstable-v2-client-protocol.h>
#include <xdg-output-unstable-v1-client-protocol.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstring>
#include <mutex>
static void keyboard_handle_key_repeat_cancel(struct input_t *input);
static void output_handle_done(void *data, struct wl_output *wl_output);
/**
* GNOME (mutter 42.2 had a bug with confine not respecting scale - Hi-DPI), See: T98793.
* Even though this has been fixed, at time of writing it's not yet in a release.
* Workaround the problem by implementing confine with a software cursor.
* While this isn't ideal, it's not adding a lot of overhead as software
* cursors are already used for warping (which WAYLAND doesn't support).
*/
#define USE_GNOME_CONFINE_HACK
/**
* Always use software confine (not just in GNOME).
* Useful for developing with compositors that don't need this workaround.
*/
// #define USE_GNOME_CONFINE_HACK_ALWAYS_ON
#ifdef USE_GNOME_CONFINE_HACK
static bool use_gnome_confine_hack = false;
#endif
/* -------------------------------------------------------------------- */
/** \name Inline Event Codes
*
* Selected input event code defines from `linux/input-event-codes.h`
* We include some of the button input event codes here, since the header is
* only available in more recent kernel versions.
* \{ */
/**
* The event codes are used to
* to differentiate from which mouse button an event comes from.
*/
#define BTN_LEFT 0x110
#define BTN_RIGHT 0x111
#define BTN_MIDDLE 0x112
#define BTN_SIDE 0x113
#define BTN_EXTRA 0x114
#define BTN_FORWARD 0x115
#define BTN_BACK 0x116
// #define BTN_TASK 0x117 /* UNUSED. */
/**
* Tablet events.
*/
#define BTN_STYLUS 0x14b /* Use as right-mouse. */
#define BTN_STYLUS2 0x14c /* Use as middle-mouse. */
/* NOTE(@campbellbarton): Map to an additional button (not sure which hardware uses this). */
#define BTN_STYLUS3 0x149
/**
* Keyboard scan-codes.
*/
#define KEY_GRAVE 41
/** \} */
/* -------------------------------------------------------------------- */
/** \name Private Types & Defines
* \{ */
/**
* From XKB internals, use for converting a scan-code from WAYLAND to a #xkb_keycode_t.
* Ideally this wouldn't need a local define.
*/
#define EVDEV_OFFSET 8
struct buffer_t {
void *data = nullptr;
size_t size = 0;
};
struct cursor_t {
bool visible = false;
/**
* When false, hide the hardware cursor, while the cursor is still considered to be `visible`,
* since the grab-mode determines the state of the software cursor,
* this may change - removing the need for a software cursor and in this case it's important
* the hardware cursor is used.
*/
bool is_hardware = true;
bool is_custom = false;
struct wl_surface *wl_surface = nullptr;
struct wl_buffer *wl_buffer = nullptr;
struct wl_cursor_image wl_image = {0};
struct wl_cursor_theme *wl_theme = nullptr;
struct buffer_t *file_buffer = nullptr;
int size = 0;
std::string theme_name;
/** Outputs on which the cursor is visible. */
std::unordered_set<const output_t *> outputs;
int theme_scale = 1;
int custom_scale = 1;
};
/**
* A single tablet can have multiple tools (pen, eraser, brush... etc).
* WAYLAND exposes tools via #zwp_tablet_tool_v2.
* Since are no API's to access properties of the tool, store the values here.
*/
struct tablet_tool_input_t {
struct input_t *input = nullptr;
struct wl_surface *cursor_surface = nullptr;
/** Used to delay clearing tablet focused surface until the frame is handled. */
bool proximity = false;
GHOST_TabletData data = GHOST_TABLET_DATA_NONE;
};
struct data_offer_t {
std::unordered_set<std::string> types;
uint32_t source_actions = 0;
uint32_t dnd_action = 0;
struct wl_data_offer *id = nullptr;
std::atomic<bool> in_use = false;
struct {
/** Compatible with #input_t.xy coordinates. */
wl_fixed_t xy[2] = {0, 0};
} dnd;
};
struct data_source_t {
struct wl_data_source *data_source = nullptr;
char *buffer_out = nullptr;
};
/**
* Data used to implement client-side key-repeat.
*
* \note it's important not to store the target window here
* as it can be closed while the key is repeating,
* instead use the focused keyboard from #intput_t which is cleared when windows are closed.
* Therefor keyboard events must always check the window has not been cleared.
*/
struct key_repeat_payload_t {
struct input_t *input = nullptr;
xkb_keycode_t key_code;
/**
* Don't cache the `utf8_buf` as this changes based on modifiers which may be pressed
* while key repeat is enabled.
*/
struct {
GHOST_TKey gkey;
} key_data;
};
/** Internal variables used to track grab-state. */
struct input_grab_state_t {
bool use_lock = false;
bool use_confine = false;
};
/**
* State of the pointing device (tablet or mouse).
*/
struct input_state_pointer_t {
/**
* High precision coordinates.
*
* Mapping to pixels requires the window scale.
* The following example converts these values to screen coordinates.
* \code{.cc}
* const wl_fixed_t scale = win->scale();
* const int event_xy[2] = {
* wl_fixed_to_int(scale * input_state->xy[0]),
* wl_fixed_to_int(scale * input_state->xy[1]),
* };
* \endcode
*/
wl_fixed_t xy[2] = {0, 0};
/** The serial of the last used pointer or tablet. */
uint32_t serial = 0;
/**
* The surface last used with this pointing device
* (events with this pointing device will be sent here).
*/
struct wl_surface *wl_surface = nullptr;
GHOST_Buttons buttons = GHOST_Buttons();
};
/**
* State of the keyboard.
*/
struct input_state_keyboard_t {
/** The serial of the last used pointer or tablet. */
uint32_t serial = 0;
/**
* The surface last used with this pointing device
* (events with this pointing device will be sent here).
*/
struct wl_surface *wl_surface = nullptr;
};
struct input_t {
GHOST_SystemWayland *system = nullptr;
std::string name;
struct wl_seat *wl_seat = nullptr;
struct wl_pointer *wl_pointer = nullptr;
struct wl_keyboard *wl_keyboard = nullptr;
struct zwp_tablet_seat_v2 *tablet_seat = nullptr;
/** All currently active tablet tools (needed for changing the cursor). */
std::unordered_set<zwp_tablet_tool_v2 *> tablet_tools;
/** Use to check if the last cursor input was tablet or pointer. */
uint32_t cursor_source_serial = 0;
input_state_pointer_t pointer;
/** Mostly this can be interchanged with `pointer` however it can't be locked/confined. */
input_state_pointer_t tablet;
input_state_keyboard_t keyboard;
#ifdef USE_GNOME_CONFINE_HACK
bool use_pointer_software_confine = false;
#endif
/** The cursor location (in pixel-space) when hidden grab started (#GHOST_kGrabHide). */
wl_fixed_t grab_lock_xy[2] = {0, 0};
struct cursor_t cursor;
struct zwp_relative_pointer_v1 *relative_pointer = nullptr;
struct zwp_locked_pointer_v1 *locked_pointer = nullptr;
struct zwp_confined_pointer_v1 *confined_pointer = nullptr;
struct xkb_context *xkb_context = nullptr;
struct xkb_state *xkb_state = nullptr;
/**
* Keep a state with no modifiers active, use for symbol lookups.
*/
struct xkb_state *xkb_state_empty = nullptr;
/**
* Keep a state with number-lock enabled, use to access predictable key-pad symbols.
* If number-lock is not supported by the key-map, this is set to NULL.
*/
struct xkb_state *xkb_state_empty_with_numlock = nullptr;
struct {
/** Key repetition in character per second. */
int32_t rate = 0;
/** Time (milliseconds) after which to start repeating keys. */
int32_t delay = 0;
/** Timer for key repeats. */
GHOST_ITimerTask *timer = nullptr;
} key_repeat;
struct wl_surface *focus_dnd = nullptr;
struct wl_data_device *data_device = nullptr;
/** Drag & Drop. */
struct data_offer_t *data_offer_dnd = nullptr;
std::mutex data_offer_dnd_mutex;
/** Copy & Paste. */
struct data_offer_t *data_offer_copy_paste = nullptr;
std::mutex data_offer_copy_paste_mutex;
struct data_source_t *data_source = nullptr;
std::mutex data_source_mutex;
/** Last device that was active. */
uint32_t data_source_serial = 0;
};
struct display_t {
GHOST_SystemWayland *system = nullptr;
struct wl_display *display = nullptr;
struct wl_compositor *compositor = nullptr;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
struct libdecor *decor_context = nullptr;
#else
struct xdg_wm_base *xdg_shell = nullptr;
struct zxdg_decoration_manager_v1 *xdg_decoration_manager = nullptr;
#endif
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
struct wl_shm *shm = nullptr;
std::vector<output_t *> outputs;
std::vector<input_t *> inputs;
struct wl_data_device_manager *data_device_manager = nullptr;
struct zwp_tablet_manager_v2 *tablet_manager = nullptr;
struct zwp_relative_pointer_manager_v1 *relative_pointer_manager = nullptr;
struct zwp_pointer_constraints_v1 *pointer_constraints = nullptr;
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Private Utility Functions
* \{ */
static GHOST_WindowManager *window_manager = nullptr;
/** Check this lock before accessing `GHOST_SystemWayland::selection` from a thread. */
static std::mutex system_selection_mutex;
/**
* Callback for WAYLAND to run when there is an error.
*
* \note It's useful to set a break-point on this function as some errors are fatal
* (for all intents and purposes) but don't crash the process.
*/
static void ghost_wayland_log_handler(const char *msg, va_list arg)
{
fprintf(stderr, "GHOST/Wayland: ");
vfprintf(stderr, msg, arg); /* Includes newline. */
GHOST_TBacktraceFn backtrace_fn = GHOST_ISystem::getBacktraceFn();
if (backtrace_fn) {
backtrace_fn(stderr); /* Includes newline. */
}
}
static input_state_pointer_t *input_state_pointer_active(input_t *input)
{
if (input->pointer.serial == input->cursor_source_serial) {
return &input->pointer;
}
if (input->tablet.serial == input->cursor_source_serial) {
return &input->tablet;
}
return nullptr;
}
static void display_destroy(display_t *d)
{
if (d->data_device_manager) {
wl_data_device_manager_destroy(d->data_device_manager);
}
if (d->tablet_manager) {
zwp_tablet_manager_v2_destroy(d->tablet_manager);
}
for (output_t *output : d->outputs) {
wl_output_destroy(output->wl_output);
delete output;
}
for (input_t *input : d->inputs) {
/* First handle members that require locking.
* While highly unlikely, it's possible they are being used while this function runs. */
{
std::lock_guard lock{input->data_source_mutex};
if (input->data_source) {
free(input->data_source->buffer_out);
if (input->data_source->data_source) {
wl_data_source_destroy(input->data_source->data_source);
}
delete input->data_source;
}
}
{
std::lock_guard lock{input->data_offer_dnd_mutex};
if (input->data_offer_dnd) {
wl_data_offer_destroy(input->data_offer_dnd->id);
delete input->data_offer_dnd;
}
}
{
std::lock_guard lock{input->data_offer_copy_paste_mutex};
if (input->data_offer_copy_paste) {
wl_data_offer_destroy(input->data_offer_copy_paste->id);
delete input->data_offer_copy_paste;
}
}
if (input->data_device) {
wl_data_device_release(input->data_device);
}
if (input->wl_pointer) {
if (input->cursor.file_buffer) {
munmap(input->cursor.file_buffer->data, input->cursor.file_buffer->size);
delete input->cursor.file_buffer;
}
if (input->cursor.wl_surface) {
wl_surface_destroy(input->cursor.wl_surface);
}
if (input->cursor.wl_theme) {
wl_cursor_theme_destroy(input->cursor.wl_theme);
}
if (input->wl_pointer) {
wl_pointer_destroy(input->wl_pointer);
}
}
if (input->wl_keyboard) {
if (input->key_repeat.timer) {
keyboard_handle_key_repeat_cancel(input);
}
wl_keyboard_destroy(input->wl_keyboard);
}
/* Un-referencing checks for NULL case. */
xkb_state_unref(input->xkb_state);
xkb_state_unref(input->xkb_state_empty);
xkb_state_unref(input->xkb_state_empty_with_numlock);
xkb_context_unref(input->xkb_context);
wl_seat_destroy(input->wl_seat);
delete input;
}
if (d->shm) {
wl_shm_destroy(d->shm);
}
if (d->relative_pointer_manager) {
zwp_relative_pointer_manager_v1_destroy(d->relative_pointer_manager);
}
if (d->pointer_constraints) {
zwp_pointer_constraints_v1_destroy(d->pointer_constraints);
}
if (d->compositor) {
wl_compositor_destroy(d->compositor);
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (d->decor_context) {
libdecor_unref(d->decor_context);
}
#else
if (d->xdg_decoration_manager) {
zxdg_decoration_manager_v1_destroy(d->xdg_decoration_manager);
}
if (d->xdg_shell) {
xdg_wm_base_destroy(d->xdg_shell);
}
#endif /* !WITH_GHOST_WAYLAND_LIBDECOR */
if (eglGetDisplay) {
::eglTerminate(eglGetDisplay(EGLNativeDisplayType(d->display)));
}
if (d->display) {
wl_display_disconnect(d->display);
}
delete d;
}
static GHOST_TKey xkb_map_gkey(const xkb_keysym_t sym)
{
GHOST_TKey gkey;
if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) {
gkey = GHOST_TKey(sym);
}
else if (sym >= XKB_KEY_KP_0 && sym <= XKB_KEY_KP_9) {
gkey = GHOST_TKey(GHOST_kKeyNumpad0 + sym - XKB_KEY_KP_0);
}
else if (sym >= XKB_KEY_A && sym <= XKB_KEY_Z) {
gkey = GHOST_TKey(sym);
}
else if (sym >= XKB_KEY_a && sym <= XKB_KEY_z) {
gkey = GHOST_TKey(sym - XKB_KEY_a + XKB_KEY_A);
}
else if (sym >= XKB_KEY_F1 && sym <= XKB_KEY_F24) {
gkey = GHOST_TKey(GHOST_kKeyF1 + sym - XKB_KEY_F1);
}
else {
#define GXMAP(k, x, y) \
case x: \
k = y; \
break
switch (sym) {
GXMAP(gkey, XKB_KEY_BackSpace, GHOST_kKeyBackSpace);
GXMAP(gkey, XKB_KEY_Tab, GHOST_kKeyTab);
GXMAP(gkey, XKB_KEY_Linefeed, GHOST_kKeyLinefeed);
GXMAP(gkey, XKB_KEY_Clear, GHOST_kKeyClear);
GXMAP(gkey, XKB_KEY_Return, GHOST_kKeyEnter);
GXMAP(gkey, XKB_KEY_Escape, GHOST_kKeyEsc);
GXMAP(gkey, XKB_KEY_space, GHOST_kKeySpace);
GXMAP(gkey, XKB_KEY_apostrophe, GHOST_kKeyQuote);
GXMAP(gkey, XKB_KEY_comma, GHOST_kKeyComma);
GXMAP(gkey, XKB_KEY_minus, GHOST_kKeyMinus);
GXMAP(gkey, XKB_KEY_plus, GHOST_kKeyPlus);
GXMAP(gkey, XKB_KEY_period, GHOST_kKeyPeriod);
GXMAP(gkey, XKB_KEY_slash, GHOST_kKeySlash);
GXMAP(gkey, XKB_KEY_semicolon, GHOST_kKeySemicolon);
GXMAP(gkey, XKB_KEY_equal, GHOST_kKeyEqual);
GXMAP(gkey, XKB_KEY_bracketleft, GHOST_kKeyLeftBracket);
GXMAP(gkey, XKB_KEY_bracketright, GHOST_kKeyRightBracket);
GXMAP(gkey, XKB_KEY_backslash, GHOST_kKeyBackslash);
GXMAP(gkey, XKB_KEY_grave, GHOST_kKeyAccentGrave);
GXMAP(gkey, XKB_KEY_Shift_L, GHOST_kKeyLeftShift);
GXMAP(gkey, XKB_KEY_Shift_R, GHOST_kKeyRightShift);
GXMAP(gkey, XKB_KEY_Control_L, GHOST_kKeyLeftControl);
GXMAP(gkey, XKB_KEY_Control_R, GHOST_kKeyRightControl);
GXMAP(gkey, XKB_KEY_Alt_L, GHOST_kKeyLeftAlt);
GXMAP(gkey, XKB_KEY_Alt_R, GHOST_kKeyRightAlt);
GXMAP(gkey, XKB_KEY_Super_L, GHOST_kKeyOS);
GXMAP(gkey, XKB_KEY_Super_R, GHOST_kKeyOS);
GXMAP(gkey, XKB_KEY_Menu, GHOST_kKeyApp);
GXMAP(gkey, XKB_KEY_Caps_Lock, GHOST_kKeyCapsLock);
GXMAP(gkey, XKB_KEY_Num_Lock, GHOST_kKeyNumLock);
GXMAP(gkey, XKB_KEY_Scroll_Lock, GHOST_kKeyScrollLock);
GXMAP(gkey, XKB_KEY_Left, GHOST_kKeyLeftArrow);
GXMAP(gkey, XKB_KEY_Right, GHOST_kKeyRightArrow);
GXMAP(gkey, XKB_KEY_Up, GHOST_kKeyUpArrow);
GXMAP(gkey, XKB_KEY_Down, GHOST_kKeyDownArrow);
GXMAP(gkey, XKB_KEY_Print, GHOST_kKeyPrintScreen);
GXMAP(gkey, XKB_KEY_Pause, GHOST_kKeyPause);
GXMAP(gkey, XKB_KEY_Insert, GHOST_kKeyInsert);
GXMAP(gkey, XKB_KEY_Delete, GHOST_kKeyDelete);
GXMAP(gkey, XKB_KEY_Home, GHOST_kKeyHome);
GXMAP(gkey, XKB_KEY_End, GHOST_kKeyEnd);
GXMAP(gkey, XKB_KEY_Page_Up, GHOST_kKeyUpPage);
GXMAP(gkey, XKB_KEY_Page_Down, GHOST_kKeyDownPage);
GXMAP(gkey, XKB_KEY_KP_Decimal, GHOST_kKeyNumpadPeriod);
GXMAP(gkey, XKB_KEY_KP_Enter, GHOST_kKeyNumpadEnter);
GXMAP(gkey, XKB_KEY_KP_Add, GHOST_kKeyNumpadPlus);
GXMAP(gkey, XKB_KEY_KP_Subtract, GHOST_kKeyNumpadMinus);
GXMAP(gkey, XKB_KEY_KP_Multiply, GHOST_kKeyNumpadAsterisk);
GXMAP(gkey, XKB_KEY_KP_Divide, GHOST_kKeyNumpadSlash);
GXMAP(gkey, XKB_KEY_XF86AudioPlay, GHOST_kKeyMediaPlay);
GXMAP(gkey, XKB_KEY_XF86AudioStop, GHOST_kKeyMediaStop);
GXMAP(gkey, XKB_KEY_XF86AudioPrev, GHOST_kKeyMediaFirst);
GXMAP(gkey, XKB_KEY_XF86AudioNext, GHOST_kKeyMediaLast);
default:
/* Rely on #xkb_map_gkey_or_scan_code to report when no key can be found. */
gkey = GHOST_kKeyUnknown;
}
#undef GXMAP
}
return gkey;
}
/**
* Map the keys using the users keyboard layout, if that fails fall back to physical locations.
* This is needed so users with keyboard layouts that don't expose #GHOST_kKeyAccentGrave
* (typically the key under escape) in the layout can still use this key in keyboard shortcuts.
*
* \param key: The key's scan-code, compatible with values in `linux/input-event-codes.h`.
*/
static GHOST_TKey xkb_map_gkey_or_scan_code(const xkb_keysym_t sym, const uint32_t key)
{
GHOST_TKey gkey = xkb_map_gkey(sym);
if (gkey == GHOST_kKeyUnknown) {
/* Fall back to physical location for keys that would otherwise do nothing. */
switch (key) {
case KEY_GRAVE: {
gkey = GHOST_kKeyAccentGrave;
break;
}
default: {
GHOST_PRINT(
/* Key-code. */
"unhandled key: " << std::hex << std::showbase << sym << /* Hex. */
std::dec << " (" << sym << "), " << /* Decimal. */
/* Scan-code. */
"scan-code: " << std::hex << std::showbase << key << /* Hex. */
std::dec << " (" << key << ")" << /* Decimal. */
std::endl);
break;
}
}
}
return gkey;
}
static GHOST_TTabletMode tablet_tool_map_type(enum zwp_tablet_tool_v2_type wl_tablet_tool_type)
{
switch (wl_tablet_tool_type) {
case ZWP_TABLET_TOOL_V2_TYPE_ERASER: {
return GHOST_kTabletModeEraser;
}
case ZWP_TABLET_TOOL_V2_TYPE_PEN:
case ZWP_TABLET_TOOL_V2_TYPE_BRUSH:
case ZWP_TABLET_TOOL_V2_TYPE_PENCIL:
case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH:
case ZWP_TABLET_TOOL_V2_TYPE_FINGER:
case ZWP_TABLET_TOOL_V2_TYPE_MOUSE:
case ZWP_TABLET_TOOL_V2_TYPE_LENS: {
return GHOST_kTabletModeStylus;
}
}
GHOST_PRINT("unknown tablet tool: " << wl_tablet_tool_type << std::endl);
return GHOST_kTabletModeStylus;
}
static const int default_cursor_size = 24;
static const std::unordered_map<GHOST_TStandardCursor, const char *> cursors = {
{GHOST_kStandardCursorDefault, "left_ptr"},
{GHOST_kStandardCursorRightArrow, "right_ptr"},
{GHOST_kStandardCursorLeftArrow, "left_ptr"},
{GHOST_kStandardCursorInfo, ""},
{GHOST_kStandardCursorDestroy, "pirate"},
{GHOST_kStandardCursorHelp, "question_arrow"},
{GHOST_kStandardCursorWait, "watch"},
{GHOST_kStandardCursorText, "xterm"},
{GHOST_kStandardCursorCrosshair, "crosshair"},
{GHOST_kStandardCursorCrosshairA, ""},
{GHOST_kStandardCursorCrosshairB, ""},
{GHOST_kStandardCursorCrosshairC, ""},
{GHOST_kStandardCursorPencil, "pencil"},
{GHOST_kStandardCursorUpArrow, "sb_up_arrow"},
{GHOST_kStandardCursorDownArrow, "sb_down_arrow"},
{GHOST_kStandardCursorVerticalSplit, "split_v"},
{GHOST_kStandardCursorHorizontalSplit, "split_h"},
{GHOST_kStandardCursorEraser, ""},
{GHOST_kStandardCursorKnife, ""},
{GHOST_kStandardCursorEyedropper, "color-picker"},
{GHOST_kStandardCursorZoomIn, "zoom-in"},
{GHOST_kStandardCursorZoomOut, "zoom-out"},
{GHOST_kStandardCursorMove, "move"},
{GHOST_kStandardCursorNSEWScroll, "size_all"}, /* Not an exact match. */
{GHOST_kStandardCursorNSScroll, "size_ver"}, /* Not an exact match. */
{GHOST_kStandardCursorEWScroll, "size_hor"}, /* Not an exact match. */
{GHOST_kStandardCursorStop, "not-allowed"},
{GHOST_kStandardCursorUpDown, "sb_v_double_arrow"},
{GHOST_kStandardCursorLeftRight, "sb_h_double_arrow"},
{GHOST_kStandardCursorTopSide, "top_side"},
{GHOST_kStandardCursorBottomSide, "bottom_side"},
{GHOST_kStandardCursorLeftSide, "left_side"},
{GHOST_kStandardCursorRightSide, "right_side"},
{GHOST_kStandardCursorTopLeftCorner, "top_left_corner"},
{GHOST_kStandardCursorTopRightCorner, "top_right_corner"},
{GHOST_kStandardCursorBottomRightCorner, "bottom_right_corner"},
{GHOST_kStandardCursorBottomLeftCorner, "bottom_left_corner"},
{GHOST_kStandardCursorCopy, "copy"},
};
static constexpr const char *mime_text_plain = "text/plain";
static constexpr const char *mime_text_utf8 = "text/plain;charset=utf-8";
static constexpr const char *mime_text_uri = "text/uri-list";
static const std::unordered_map<std::string, GHOST_TDragnDropTypes> mime_dnd = {
{mime_text_plain, GHOST_kDragnDropTypeString},
{mime_text_utf8, GHOST_kDragnDropTypeString},
{mime_text_uri, GHOST_kDragnDropTypeFilenames},
};
static const std::vector<std::string> mime_preference_order = {
mime_text_uri,
mime_text_utf8,
mime_text_plain,
};
static const std::vector<std::string> mime_send = {
"UTF8_STRING",
"COMPOUND_TEXT",
"TEXT",
"STRING",
"text/plain;charset=utf-8",
"text/plain",
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Relative Motion), #zwp_relative_pointer_v1_listener
*
* These callbacks are registered for Wayland interfaces and called when
* an event is received from the compositor.
* \{ */
/**
* The caller is responsible for setting the value of `input->xy`.
*/
static void relative_pointer_handle_relative_motion_impl(input_t *input,
GHOST_WindowWayland *win,
const wl_fixed_t xy[2])
{
const wl_fixed_t scale = win->scale();
input->pointer.xy[0] = xy[0];
input->pointer.xy[1] = xy[1];
#ifdef USE_GNOME_CONFINE_HACK
if (input->use_pointer_software_confine) {
GHOST_Rect bounds;
win->getClientBounds(bounds);
/* Needed or the cursor is considered outside the window and doesn't restore the location. */
bounds.m_r -= 1;
bounds.m_b -= 1;
bounds.m_l = wl_fixed_from_int(bounds.m_l) / scale;
bounds.m_t = wl_fixed_from_int(bounds.m_t) / scale;
bounds.m_r = wl_fixed_from_int(bounds.m_r) / scale;
bounds.m_b = wl_fixed_from_int(bounds.m_b) / scale;
bounds.clampPoint(input->pointer.xy[0], input->pointer.xy[1]);
}
#endif
input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(),
GHOST_kEventCursorMove,
win,
wl_fixed_to_int(scale * input->pointer.xy[0]),
wl_fixed_to_int(scale * input->pointer.xy[1]),
GHOST_TABLET_DATA_NONE));
}
static void relative_pointer_handle_relative_motion(
void *data,
struct zwp_relative_pointer_v1 * /*zwp_relative_pointer_v1*/,
const uint32_t /*utime_hi*/,
const uint32_t /*utime_lo*/,
const wl_fixed_t dx,
const wl_fixed_t dy,
const wl_fixed_t /*dx_unaccel*/,
const wl_fixed_t /*dy_unaccel*/)
{
input_t *input = static_cast<input_t *>(data);
if (wl_surface *focus_surface = input->pointer.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
const wl_fixed_t scale = win->scale();
const wl_fixed_t xy_next[2] = {
input->pointer.xy[0] + (dx / scale),
input->pointer.xy[1] + (dy / scale),
};
relative_pointer_handle_relative_motion_impl(input, win, xy_next);
}
}
static const zwp_relative_pointer_v1_listener relative_pointer_listener = {
relative_pointer_handle_relative_motion,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Data Source), #wl_data_source_listener
* \{ */
static void dnd_events(const input_t *const input, const GHOST_TEventType event)
{
/* NOTE: `input->data_offer_dnd_mutex` must already be locked. */
if (wl_surface *focus_surface = input->focus_dnd) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
const wl_fixed_t scale = win->scale();
const int event_xy[2] = {
wl_fixed_to_int(scale * input->data_offer_dnd->dnd.xy[0]),
wl_fixed_to_int(scale * input->data_offer_dnd->dnd.xy[1]),
};
const uint64_t time = input->system->getMilliSeconds();
for (const std::string &type : mime_preference_order) {
input->system->pushEvent(new GHOST_EventDragnDrop(
time, event, mime_dnd.at(type), win, event_xy[0], event_xy[1], nullptr));
}
}
}
static std::string read_pipe(data_offer_t *data_offer,
const std::string mime_receive,
std::mutex *mutex)
{
int pipefd[2];
if (pipe(pipefd) != 0) {
return {};
}
wl_data_offer_receive(data_offer->id, mime_receive.c_str(), pipefd[1]);
close(pipefd[1]);
data_offer->in_use.store(false);
if (mutex) {
mutex->unlock();
}
/* WARNING: `data_offer` may be freed from now on. */
std::string data;
ssize_t len;
char buffer[4096];
while ((len = read(pipefd[0], buffer, sizeof(buffer))) > 0) {
data.insert(data.end(), buffer, buffer + len);
}
close(pipefd[0]);
return data;
}
/**
* A target accepts an offered mime type.
*
* Sent when a target accepts pointer_focus or motion events. If
* a target does not accept any of the offered types, type is nullptr.
*/
static void data_source_handle_target(void * /*data*/,
struct wl_data_source * /*wl_data_source*/,
const char * /*mime_type*/)
{
/* pass */
}
static void data_source_handle_send(void *data,
struct wl_data_source * /*wl_data_source*/,
const char * /*mime_type*/,
const int32_t fd)
{
input_t *input = static_cast<input_t *>(data);
std::lock_guard lock{input->data_source_mutex};
const char *const buffer = input->data_source->buffer_out;
if (write(fd, buffer, strlen(buffer)) < 0) {
GHOST_PRINT("error writing to clipboard: " << std::strerror(errno) << std::endl);
}
close(fd);
}
static void data_source_handle_cancelled(void * /*data*/, struct wl_data_source *wl_data_source)
{
wl_data_source_destroy(wl_data_source);
}
/**
* The drag-and-drop operation physically finished.
*
* The user performed the drop action. This event does not
* indicate acceptance, #wl_data_source.cancelled may still be
* emitted afterwards if the drop destination does not accept any mime type.
*/
static void data_source_handle_dnd_drop_performed(void * /*data*/,
struct wl_data_source * /*wl_data_source*/)
{
/* pass */
}
/**
* The drag-and-drop operation concluded.
*
* The drop destination finished interoperating with this data
* source, so the client is now free to destroy this data source
* and free all associated data.
*/
static void data_source_handle_dnd_finished(void * /*data*/,
struct wl_data_source * /*wl_data_source*/)
{
/* pass */
}
/**
* Notify the selected action.
*
* This event indicates the action selected by the compositor
* after matching the source/destination side actions. Only one
* action (or none) will be offered here.
*/
static void data_source_handle_action(void * /*data*/,
struct wl_data_source * /*wl_data_source*/,
const uint32_t /*dnd_action*/)
{
/* pass */
}
static const struct wl_data_source_listener data_source_listener = {
data_source_handle_target,
data_source_handle_send,
data_source_handle_cancelled,
data_source_handle_dnd_drop_performed,
data_source_handle_dnd_finished,
data_source_handle_action,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Data Offer), #wl_data_offer_listener
* \{ */
static void data_offer_handle_offer(void *data,
struct wl_data_offer * /*wl_data_offer*/,
const char *mime_type)
{
static_cast<data_offer_t *>(data)->types.insert(mime_type);
}
static void data_offer_handle_source_actions(void *data,
struct wl_data_offer * /*wl_data_offer*/,
const uint32_t source_actions)
{
static_cast<data_offer_t *>(data)->source_actions = source_actions;
}
static void data_offer_handle_action(void *data,
struct wl_data_offer * /*wl_data_offer*/,
const uint32_t dnd_action)
{
static_cast<data_offer_t *>(data)->dnd_action = dnd_action;
}
static const struct wl_data_offer_listener data_offer_listener = {
data_offer_handle_offer,
data_offer_handle_source_actions,
data_offer_handle_action,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Data Device), #wl_data_device_listener
* \{ */
static void data_device_handle_data_offer(void * /*data*/,
struct wl_data_device * /*wl_data_device*/,
struct wl_data_offer *id)
{
data_offer_t *data_offer = new data_offer_t;
data_offer->id = id;
wl_data_offer_add_listener(id, &data_offer_listener, data_offer);
}
static void data_device_handle_enter(void *data,
struct wl_data_device * /*wl_data_device*/,
const uint32_t serial,
struct wl_surface *surface,
const wl_fixed_t x,
const wl_fixed_t y,
struct wl_data_offer *id)
{
input_t *input = static_cast<input_t *>(data);
std::lock_guard lock{input->data_offer_dnd_mutex};
input->data_offer_dnd = static_cast<data_offer_t *>(wl_data_offer_get_user_data(id));
data_offer_t *data_offer = input->data_offer_dnd;
data_offer->in_use.store(true);
data_offer->dnd.xy[0] = x;
data_offer->dnd.xy[1] = y;
wl_data_offer_set_actions(id,
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY |
WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE,
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
for (const std::string &type : mime_preference_order) {
wl_data_offer_accept(id, serial, type.c_str());
}
input->focus_dnd = surface;
dnd_events(input, GHOST_kEventDraggingEntered);
}
static void data_device_handle_leave(void *data, struct wl_data_device * /*wl_data_device*/)
{
input_t *input = static_cast<input_t *>(data);
std::lock_guard lock{input->data_offer_dnd_mutex};
dnd_events(input, GHOST_kEventDraggingExited);
input->focus_dnd = nullptr;
if (input->data_offer_dnd && !input->data_offer_dnd->in_use.load()) {
wl_data_offer_destroy(input->data_offer_dnd->id);
delete input->data_offer_dnd;
input->data_offer_dnd = nullptr;
}
}
static void data_device_handle_motion(void *data,
struct wl_data_device * /*wl_data_device*/,
const uint32_t /*time*/,
const wl_fixed_t x,
const wl_fixed_t y)
{
input_t *input = static_cast<input_t *>(data);
std::lock_guard lock{input->data_offer_dnd_mutex};
input->data_offer_dnd->dnd.xy[0] = x;
input->data_offer_dnd->dnd.xy[1] = y;
dnd_events(input, GHOST_kEventDraggingUpdated);
}
static void data_device_handle_drop(void *data, struct wl_data_device * /*wl_data_device*/)
{
input_t *input = static_cast<input_t *>(data);
std::lock_guard lock{input->data_offer_dnd_mutex};
data_offer_t *data_offer = input->data_offer_dnd;
const std::string mime_receive = *std::find_first_of(mime_preference_order.begin(),
mime_preference_order.end(),
data_offer->types.begin(),
data_offer->types.end());
auto read_uris_fn = [](input_t *const input,
data_offer_t *data_offer,
wl_surface *surface,
const std::string mime_receive) {
const wl_fixed_t xy[2] = {data_offer->dnd.xy[0], data_offer->dnd.xy[1]};
const std::string data = read_pipe(data_offer, mime_receive, nullptr);
wl_data_offer_finish(data_offer->id);
wl_data_offer_destroy(data_offer->id);
delete data_offer;
data_offer = nullptr;
GHOST_SystemWayland *const system = input->system;
if (mime_receive == mime_text_uri) {
static constexpr const char *file_proto = "file://";
static constexpr const char *crlf = "\r\n";
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_find(surface);
GHOST_ASSERT(win != nullptr, "Unable to find window for drop event from surface");
std::vector<std::string> uris;
size_t pos = 0;
while (true) {
pos = data.find(file_proto, pos);
const size_t start = pos + sizeof(file_proto) - 1;
pos = data.find(crlf, pos);
const size_t end = pos;
if (pos == std::string::npos) {
break;
}
uris.push_back(data.substr(start, end - start));
}
GHOST_TStringArray *flist = static_cast<GHOST_TStringArray *>(
malloc(sizeof(GHOST_TStringArray)));
flist->count = int(uris.size());
flist->strings = static_cast<uint8_t **>(malloc(uris.size() * sizeof(uint8_t *)));
for (size_t i = 0; i < uris.size(); i++) {
flist->strings[i] = static_cast<uint8_t *>(malloc((uris[i].size() + 1) * sizeof(uint8_t)));
memcpy(flist->strings[i], uris[i].data(), uris[i].size() + 1);
}
const wl_fixed_t scale = win->scale();
system->pushEvent(new GHOST_EventDragnDrop(system->getMilliSeconds(),
GHOST_kEventDraggingDropDone,
GHOST_kDragnDropTypeFilenames,
win,
wl_fixed_to_int(scale * xy[0]),
wl_fixed_to_int(scale * xy[1]),
flist));
}
else if (mime_receive == mime_text_plain || mime_receive == mime_text_utf8) {
/* TODO: enable use of internal functions 'txt_insert_buf' and
* 'text_update_edited' to behave like dropped text was pasted. */
}
wl_display_roundtrip(system->display());
};
/* Pass in `input->focus_dnd` instead of accessing it from `input` since the leave callback
* (#data_device_leave) will clear the value once this function starts. */
std::thread read_thread(read_uris_fn, input, data_offer, input->focus_dnd, mime_receive);
read_thread.detach();
}
static void data_device_handle_selection(void *data,
struct wl_data_device * /*wl_data_device*/,
struct wl_data_offer *id)
{
input_t *input = static_cast<input_t *>(data);
std::lock_guard lock{input->data_offer_copy_paste_mutex};
data_offer_t *data_offer = input->data_offer_copy_paste;
/* Delete old data offer. */
if (data_offer != nullptr) {
wl_data_offer_destroy(data_offer->id);
delete data_offer;
data_offer = nullptr;
}
if (id == nullptr) {
return;
}
/* Get new data offer. */
data_offer = static_cast<data_offer_t *>(wl_data_offer_get_user_data(id));
input->data_offer_copy_paste = data_offer;
auto read_selection_fn = [](input_t *input) {
GHOST_SystemWayland *const system = input->system;
input->data_offer_copy_paste_mutex.lock();
data_offer_t *data_offer = input->data_offer_copy_paste;
std::string mime_receive;
for (const std::string type : {mime_text_utf8, mime_text_plain}) {
if (data_offer->types.count(type)) {
mime_receive = type;
break;
}
}
const std::string data = read_pipe(
data_offer, mime_receive, &input->data_offer_copy_paste_mutex);
{
std::lock_guard lock{system_selection_mutex};
system->selection_set(data);
}
};
std::thread read_thread(read_selection_fn, input);
read_thread.detach();
}
static const struct wl_data_device_listener data_device_listener = {
data_device_handle_data_offer,
data_device_handle_enter,
data_device_handle_leave,
data_device_handle_motion,
data_device_handle_drop,
data_device_handle_selection,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Buffer), #wl_buffer_listener
* \{ */
static void cursor_buffer_handle_release(void *data, struct wl_buffer *wl_buffer)
{
cursor_t *cursor = static_cast<cursor_t *>(data);
wl_buffer_destroy(wl_buffer);
if (wl_buffer == cursor->wl_buffer) {
/* The mapped buffer was from a custom cursor. */
cursor->wl_buffer = nullptr;
}
}
const struct wl_buffer_listener cursor_buffer_listener = {
cursor_buffer_handle_release,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Surface), #wl_surface_listener
* \{ */
static bool update_cursor_scale(cursor_t &cursor, wl_shm *shm)
{
int scale = 0;
for (const output_t *output : cursor.outputs) {
if (output->scale > scale) {
scale = output->scale;
}
}
if (scale > 0 && cursor.theme_scale != scale) {
cursor.theme_scale = scale;
if (!cursor.is_custom) {
wl_surface_set_buffer_scale(cursor.wl_surface, scale);
}
wl_cursor_theme_destroy(cursor.wl_theme);
cursor.wl_theme = wl_cursor_theme_load(cursor.theme_name.c_str(), scale * cursor.size, shm);
return true;
}
return false;
}
static void cursor_surface_handle_enter(void *data,
struct wl_surface * /*wl_surface*/,
struct wl_output *output)
{
input_t *input = static_cast<input_t *>(data);
for (const output_t *reg_output : input->system->outputs()) {
if (reg_output->wl_output == output) {
input->cursor.outputs.insert(reg_output);
}
}
update_cursor_scale(input->cursor, input->system->shm());
}
static void cursor_surface_handle_leave(void *data,
struct wl_surface * /*wl_surface*/,
struct wl_output *output)
{
input_t *input = static_cast<input_t *>(data);
for (const output_t *reg_output : input->system->outputs()) {
if (reg_output->wl_output == output) {
input->cursor.outputs.erase(reg_output);
}
}
update_cursor_scale(input->cursor, input->system->shm());
}
struct wl_surface_listener cursor_surface_listener = {
cursor_surface_handle_enter,
cursor_surface_handle_leave,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Pointer), #wl_pointer_listener
* \{ */
static void pointer_handle_enter(void *data,
struct wl_pointer * /*wl_pointer*/,
const uint32_t serial,
struct wl_surface *surface,
const wl_fixed_t surface_x,
const wl_fixed_t surface_y)
{
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(surface);
win->activate();
input_t *input = static_cast<input_t *>(data);
input->cursor_source_serial = serial;
input->pointer.serial = serial;
input->pointer.xy[0] = surface_x;
input->pointer.xy[1] = surface_y;
input->pointer.wl_surface = surface;
win->setCursorShape(win->getCursorShape());
const wl_fixed_t scale = win->scale();
input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(),
GHOST_kEventCursorMove,
win,
wl_fixed_to_int(scale * input->pointer.xy[0]),
wl_fixed_to_int(scale * input->pointer.xy[1]),
GHOST_TABLET_DATA_NONE));
}
static void pointer_handle_leave(void *data,
struct wl_pointer * /*wl_pointer*/,
const uint32_t /*serial*/,
struct wl_surface *surface)
{
/* First clear the `pointer.wl_surface`, since the window won't exist when closing the window. */
static_cast<input_t *>(data)->pointer.wl_surface = nullptr;
if (surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(surface);
win->deactivate();
}
}
static void pointer_handle_motion(void *data,
struct wl_pointer * /*wl_pointer*/,
const uint32_t /*time*/,
const wl_fixed_t surface_x,
const wl_fixed_t surface_y)
{
input_t *input = static_cast<input_t *>(data);
input->pointer.xy[0] = surface_x;
input->pointer.xy[1] = surface_y;
if (wl_surface *focus_surface = input->pointer.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
const wl_fixed_t scale = win->scale();
input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(),
GHOST_kEventCursorMove,
win,
wl_fixed_to_int(scale * input->pointer.xy[0]),
wl_fixed_to_int(scale * input->pointer.xy[1]),
GHOST_TABLET_DATA_NONE));
}
}
static void pointer_handle_button(void *data,
struct wl_pointer * /*wl_pointer*/,
const uint32_t serial,
const uint32_t /*time*/,
const uint32_t button,
const uint32_t state)
{
input_t *input = static_cast<input_t *>(data);
GHOST_TEventType etype = GHOST_kEventUnknown;
switch (state) {
case WL_POINTER_BUTTON_STATE_RELEASED:
etype = GHOST_kEventButtonUp;
break;
case WL_POINTER_BUTTON_STATE_PRESSED:
etype = GHOST_kEventButtonDown;
break;
}
GHOST_TButton ebutton = GHOST_kButtonMaskLeft;
switch (button) {
case BTN_LEFT:
ebutton = GHOST_kButtonMaskLeft;
break;
case BTN_MIDDLE:
ebutton = GHOST_kButtonMaskMiddle;
break;
case BTN_RIGHT:
ebutton = GHOST_kButtonMaskRight;
break;
case BTN_SIDE:
ebutton = GHOST_kButtonMaskButton4;
break;
case BTN_EXTRA:
ebutton = GHOST_kButtonMaskButton5;
break;
case BTN_FORWARD:
ebutton = GHOST_kButtonMaskButton6;
break;
case BTN_BACK:
ebutton = GHOST_kButtonMaskButton7;
break;
}
input->data_source_serial = serial;
input->pointer.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED);
if (wl_surface *focus_surface = input->pointer.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventButton(
input->system->getMilliSeconds(), etype, win, ebutton, GHOST_TABLET_DATA_NONE));
}
}
static void pointer_handle_axis(void *data,
struct wl_pointer * /*wl_pointer*/,
const uint32_t /*time*/,
const uint32_t axis,
const wl_fixed_t value)
{
input_t *input = static_cast<input_t *>(data);
if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) {
return;
}
if (wl_surface *focus_surface = input->pointer.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventWheel(
input->system->getMilliSeconds(), win, std::signbit(value) ? +1 : -1));
}
}
static const struct wl_pointer_listener pointer_listener = {
pointer_handle_enter,
pointer_handle_leave,
pointer_handle_motion,
pointer_handle_button,
pointer_handle_axis,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Tablet Tool), #zwp_tablet_tool_v2_listener
* \{ */
static void tablet_tool_handle_type(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t tool_type)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
tool_input->data.Active = tablet_tool_map_type((enum zwp_tablet_tool_v2_type)tool_type);
}
static void tablet_tool_handle_hardware_serial(void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*hardware_serial_hi*/,
const uint32_t /*hardware_serial_lo*/)
{
}
static void tablet_tool_handle_hardware_id_wacom(
void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*hardware_id_hi*/,
const uint32_t /*hardware_id_lo*/)
{
}
static void tablet_tool_handle_capability(void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*capability*/)
{
}
static void tablet_tool_handle_done(void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
{
}
static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
if (tool_input->cursor_surface) {
wl_surface_destroy(tool_input->cursor_surface);
}
input->tablet_tools.erase(zwp_tablet_tool_v2);
delete tool_input;
}
static void tablet_tool_handle_proximity_in(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t serial,
struct zwp_tablet_v2 * /*tablet*/,
struct wl_surface *surface)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
tool_input->proximity = true;
input_t *input = tool_input->input;
input->cursor_source_serial = serial;
input->tablet.wl_surface = surface;
input->tablet.serial = serial;
input->data_source_serial = serial;
/* Update #GHOST_TabletData. */
GHOST_TabletData &td = tool_input->data;
/* Reset, to avoid using stale tilt/pressure. */
td.Xtilt = 0.0f;
td.Ytilt = 0.0f;
/* In case pressure isn't supported. */
td.Pressure = 1.0f;
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(input->tablet.wl_surface);
win->activate();
win->setCursorShape(win->getCursorShape());
}
static void tablet_tool_handle_proximity_out(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
/* Defer clearing the surface until the frame is handled.
* Without this, the frame can not access the surface. */
tool_input->proximity = false;
}
static void tablet_tool_handle_down(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t serial)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
const GHOST_TButton ebutton = GHOST_kButtonMaskLeft;
const GHOST_TEventType etype = GHOST_kEventButtonDown;
input->data_source_serial = serial;
input->tablet.buttons.set(ebutton, true);
if (wl_surface *focus_surface = input->tablet.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventButton(
input->system->getMilliSeconds(), etype, win, ebutton, tool_input->data));
}
}
static void tablet_tool_handle_up(void *data, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
const GHOST_TButton ebutton = GHOST_kButtonMaskLeft;
const GHOST_TEventType etype = GHOST_kEventButtonUp;
input->tablet.buttons.set(ebutton, false);
if (wl_surface *focus_surface = input->tablet.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventButton(
input->system->getMilliSeconds(), etype, win, ebutton, tool_input->data));
}
}
static void tablet_tool_handle_motion(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t x,
const wl_fixed_t y)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
input->tablet.xy[0] = x;
input->tablet.xy[1] = y;
/* NOTE: #tablet_tool_handle_frame generates the event (with updated pressure, tilt... etc). */
}
static void tablet_tool_handle_pressure(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t pressure)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
GHOST_TabletData &td = tool_input->data;
td.Pressure = (float)pressure / 65535;
}
static void tablet_tool_handle_distance(void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*distance*/)
{
}
static void tablet_tool_handle_tilt(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t tilt_x,
const wl_fixed_t tilt_y)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
GHOST_TabletData &td = tool_input->data;
/* Map degrees to `-1.0..1.0`. */
td.Xtilt = wl_fixed_to_double(tilt_x) / 90.0f;
td.Ytilt = wl_fixed_to_double(tilt_y) / 90.0f;
td.Xtilt = td.Xtilt < -1.0f ? -1.0f : (td.Xtilt > 1.0f ? 1.0f : td.Xtilt);
td.Ytilt = td.Ytilt < -1.0f ? -1.0f : (td.Ytilt > 1.0f ? 1.0f : td.Ytilt);
}
static void tablet_tool_handle_rotation(void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t /*degrees*/)
{
/* Pass. */
}
static void tablet_tool_handle_slider(void * /*data*/,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const int32_t /*position*/)
{
/* Pass. */
}
static void tablet_tool_handle_wheel(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t /*degrees*/,
const int32_t clicks)
{
if (clicks == 0) {
return;
}
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
if (wl_surface *focus_surface = input->tablet.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventWheel(input->system->getMilliSeconds(), win, clicks));
}
}
static void tablet_tool_handle_button(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t serial,
const uint32_t button,
const uint32_t state)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
GHOST_TEventType etype = GHOST_kEventUnknown;
switch (state) {
case WL_POINTER_BUTTON_STATE_RELEASED:
etype = GHOST_kEventButtonUp;
break;
case WL_POINTER_BUTTON_STATE_PRESSED:
etype = GHOST_kEventButtonDown;
break;
}
GHOST_TButton ebutton = GHOST_kButtonMaskLeft;
switch (button) {
case BTN_STYLUS:
ebutton = GHOST_kButtonMaskRight;
break;
case BTN_STYLUS2:
ebutton = GHOST_kButtonMaskMiddle;
break;
case BTN_STYLUS3:
ebutton = GHOST_kButtonMaskButton4;
break;
}
input->data_source_serial = serial;
input->tablet.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED);
if (wl_surface *focus_surface = input->tablet.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventButton(
input->system->getMilliSeconds(), etype, win, ebutton, tool_input->data));
}
}
static void tablet_tool_handle_frame(void *data,
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*time*/)
{
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data);
input_t *input = tool_input->input;
if (wl_surface *focus_surface = input->tablet.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
const wl_fixed_t scale = win->scale();
input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(),
GHOST_kEventCursorMove,
win,
wl_fixed_to_int(scale * input->tablet.xy[0]),
wl_fixed_to_int(scale * input->tablet.xy[1]),
tool_input->data));
if (tool_input->proximity == false) {
win->setCursorShape(win->getCursorShape());
}
}
if (tool_input->proximity == false) {
input->tablet.wl_surface = nullptr;
}
}
static const struct zwp_tablet_tool_v2_listener tablet_tool_listner = {
tablet_tool_handle_type,
tablet_tool_handle_hardware_serial,
tablet_tool_handle_hardware_id_wacom,
tablet_tool_handle_capability,
tablet_tool_handle_done,
tablet_tool_handle_removed,
tablet_tool_handle_proximity_in,
tablet_tool_handle_proximity_out,
tablet_tool_handle_down,
tablet_tool_handle_up,
tablet_tool_handle_motion,
tablet_tool_handle_pressure,
tablet_tool_handle_distance,
tablet_tool_handle_tilt,
tablet_tool_handle_rotation,
tablet_tool_handle_slider,
tablet_tool_handle_wheel,
tablet_tool_handle_button,
tablet_tool_handle_frame,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Table Seat), #zwp_tablet_seat_v2_listener
* \{ */
static void tablet_seat_handle_tablet_added(void * /*data*/,
struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
struct zwp_tablet_v2 * /*id*/)
{
/* Pass. */
}
static void tablet_seat_handle_tool_added(void *data,
struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
struct zwp_tablet_tool_v2 *id)
{
input_t *input = static_cast<input_t *>(data);
tablet_tool_input_t *tool_input = new tablet_tool_input_t();
tool_input->input = input;
/* Every tool has it's own cursor surface. */
tool_input->cursor_surface = wl_compositor_create_surface(input->system->compositor());
wl_surface_add_listener(tool_input->cursor_surface, &cursor_surface_listener, (void *)input);
zwp_tablet_tool_v2_add_listener(id, &tablet_tool_listner, tool_input);
input->tablet_tools.insert(id);
}
static void tablet_seat_handle_pad_added(void * /*data*/,
struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
struct zwp_tablet_pad_v2 * /*id*/)
{
/* Pass. */
}
const struct zwp_tablet_seat_v2_listener tablet_seat_listener = {
tablet_seat_handle_tablet_added,
tablet_seat_handle_tool_added,
tablet_seat_handle_pad_added,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Keyboard), #wl_keyboard_listener
* \{ */
static void keyboard_handle_keymap(void *data,
struct wl_keyboard * /*wl_keyboard*/,
const uint32_t format,
const int32_t fd,
const uint32_t size)
{
input_t *input = static_cast<input_t *>(data);
if ((!data) || (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)) {
close(fd);
return;
}
char *map_str = static_cast<char *>(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0));
if (map_str == MAP_FAILED) {
close(fd);
throw std::runtime_error("keymap mmap failed: " + std::string(std::strerror(errno)));
}
struct xkb_keymap *keymap = xkb_keymap_new_from_string(
input->xkb_context, map_str, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(map_str, size);
close(fd);
if (!keymap) {
return;
}
/* In practice we can assume `xkb_state_new` always succeeds. */
xkb_state_unref(input->xkb_state);
input->xkb_state = xkb_state_new(keymap);
xkb_state_unref(input->xkb_state_empty);
input->xkb_state_empty = xkb_state_new(keymap);
xkb_state_unref(input->xkb_state_empty_with_numlock);
input->xkb_state_empty_with_numlock = nullptr;
{
const xkb_mod_index_t mod2 = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM);
const xkb_mod_index_t num = xkb_keymap_mod_get_index(keymap, "NumLock");
if (num != XKB_MOD_INVALID && mod2 != XKB_MOD_INVALID) {
input->xkb_state_empty_with_numlock = xkb_state_new(keymap);
xkb_state_update_mask(
input->xkb_state_empty_with_numlock, (1 << mod2), 0, (1 << num), 0, 0, 0);
}
}
xkb_keymap_unref(keymap);
}
/**
* Enter event.
*
* Notification that this seat's keyboard focus is on a certain
* surface.
*/
static void keyboard_handle_enter(void *data,
struct wl_keyboard * /*wl_keyboard*/,
const uint32_t serial,
struct wl_surface *surface,
struct wl_array * /*keys*/)
{
input_t *input = static_cast<input_t *>(data);
input->keyboard.serial = serial;
input->keyboard.wl_surface = surface;
}
/**
* Leave event.
*
* Notification that this seat's keyboard focus is no longer on a
* certain surface.
*/
static void keyboard_handle_leave(void *data,
struct wl_keyboard * /*wl_keyboard*/,
const uint32_t /*serial*/,
struct wl_surface * /*surface*/)
{
input_t *input = static_cast<input_t *>(data);
input->keyboard.wl_surface = nullptr;
/* Losing focus must stop repeating text. */
if (input->key_repeat.timer) {
keyboard_handle_key_repeat_cancel(input);
}
}
/**
* A version of #xkb_state_key_get_one_sym which returns the key without any modifiers pressed.
* Needed because #GHOST_TKey uses these values as key-codes.
*/
static xkb_keysym_t xkb_state_key_get_one_sym_without_modifiers(
struct xkb_state *xkb_state_empty,
struct xkb_state *xkb_state_empty_with_numlock,
const xkb_keycode_t key)
{
/* Use an empty keyboard state to access key symbol without modifiers. */
xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state_empty, key);
/* NOTE(@campbellbarton): Only perform the number-locked lookup as a fallback
* when a number-pad key has been pressed. This is important as some key-maps use number lock
* for switching other layers (in particular `de(neo_qwertz)` turns on layer-4), see: T96170.
* Alternative solutions could be to inspect the layout however this could get involved
* and turning on the number-lock is only needed for a limited set of keys. */
/* Accounts for key-pad keys typically swapped for numbers when number-lock is enabled:
* `Home Left Up Right Down Prior Page_Up Next Page_Dow End Begin Insert Delete`. */
if (xkb_state_empty_with_numlock && (sym >= XKB_KEY_KP_Home && sym <= XKB_KEY_KP_Delete)) {
const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(xkb_state_empty_with_numlock, key);
if (sym_test != XKB_KEY_NoSymbol) {
sym = sym_test;
}
}
return sym;
}
static void keyboard_handle_key_repeat_cancel(input_t *input)
{
GHOST_ASSERT(input->key_repeat.timer != nullptr, "Caller much check for timer");
delete static_cast<key_repeat_payload_t *>(input->key_repeat.timer->getUserData());
input->system->removeTimer(input->key_repeat.timer);
input->key_repeat.timer = nullptr;
}
/**
* Restart the key-repeat timer.
* \param use_delay: When false, use the interval
* (prevents pause when the setting changes while the key is held).
*/
static void keyboard_handle_key_repeat_reset(input_t *input, const bool use_delay)
{
GHOST_ASSERT(input->key_repeat.timer != nullptr, "Caller much check for timer");
GHOST_SystemWayland *system = input->system;
GHOST_ITimerTask *timer = input->key_repeat.timer;
GHOST_TimerProcPtr key_repeat_fn = timer->getTimerProc();
GHOST_TUserDataPtr payload = input->key_repeat.timer->getUserData();
input->system->removeTimer(input->key_repeat.timer);
const uint64_t time_step = 1000 / input->key_repeat.rate;
const uint64_t time_start = use_delay ? input->key_repeat.delay : time_step;
input->key_repeat.timer = system->installTimer(time_start, time_step, key_repeat_fn, payload);
}
static void keyboard_handle_key(void *data,
struct wl_keyboard * /*wl_keyboard*/,
const uint32_t serial,
const uint32_t /*time*/,
const uint32_t key,
const uint32_t state)
{
input_t *input = static_cast<input_t *>(data);
const xkb_keycode_t key_code = key + EVDEV_OFFSET;
const xkb_keysym_t sym = xkb_state_key_get_one_sym_without_modifiers(
input->xkb_state_empty, input->xkb_state_empty_with_numlock, key_code);
if (sym == XKB_KEY_NoSymbol) {
return;
}
GHOST_TEventType etype = GHOST_kEventUnknown;
switch (state) {
case WL_KEYBOARD_KEY_STATE_RELEASED:
etype = GHOST_kEventKeyUp;
break;
case WL_KEYBOARD_KEY_STATE_PRESSED:
etype = GHOST_kEventKeyDown;
break;
}
struct key_repeat_payload_t *key_repeat_payload = nullptr;
/* Delete previous timer. */
if (input->key_repeat.timer) {
enum { NOP = 1, RESET, CANCEL } timer_action = NOP;
key_repeat_payload = static_cast<key_repeat_payload_t *>(
input->key_repeat.timer->getUserData());
if (input->key_repeat.rate == 0) {
/* Repeat was disabled (unlikely but possible). */
timer_action = CANCEL;
}
else if (key_code == key_repeat_payload->key_code) {
/* Releasing the key that was held always cancels. */
timer_action = CANCEL;
}
else if (xkb_keymap_key_repeats(xkb_state_get_keymap(input->xkb_state), key_code)) {
if (etype == GHOST_kEventKeyDown) {
/* Any other key-down always cancels (and may start it's own repeat timer). */
timer_action = CANCEL;
}
else {
/* Key-up from keys that were not repeating cause the repeat timer to pause.
*
* NOTE(@campbellbarton): This behavior isn't universal, some text input systems will
* stop the repeat entirely. Choose to pause repeat instead as this is what GTK/WIN32 do,
* and it fits better for keyboard input that isn't related to text entry. */
timer_action = RESET;
}
}
switch (timer_action) {
case NOP: {
/* Don't add a new timer, leave the existing timer owning this `key_repeat_payload`. */
key_repeat_payload = nullptr;
break;
}
case RESET: {
/* The payload will be added again. */
input->system->removeTimer(input->key_repeat.timer);
input->key_repeat.timer = nullptr;
break;
}
case CANCEL: {
delete key_repeat_payload;
key_repeat_payload = nullptr;
input->system->removeTimer(input->key_repeat.timer);
input->key_repeat.timer = nullptr;
break;
}
}
}
const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, key);
char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'};
if (etype == GHOST_kEventKeyDown) {
xkb_state_key_get_utf8(input->xkb_state, key_code, utf8_buf, sizeof(utf8_buf));
}
input->data_source_serial = serial;
if (wl_surface *focus_surface = input->keyboard.wl_surface) {
GHOST_IWindow *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
input->system->pushEvent(new GHOST_EventKey(
input->system->getMilliSeconds(), etype, win, gkey, '\0', utf8_buf, false));
}
/* An existing payload means the key repeat timer is reset and will be added again. */
if (key_repeat_payload == nullptr) {
/* Start timer for repeating key, if applicable. */
if ((input->key_repeat.rate > 0) && (etype == GHOST_kEventKeyDown) &&
xkb_keymap_key_repeats(xkb_state_get_keymap(input->xkb_state), key_code)) {
key_repeat_payload = new key_repeat_payload_t({
.input = input,
.key_code = key_code,
.key_data = {.gkey = gkey},
});
}
}
if (key_repeat_payload) {
auto key_repeat_fn = [](GHOST_ITimerTask *task, uint64_t /*time*/) {
struct key_repeat_payload_t *payload = static_cast<key_repeat_payload_t *>(
task->getUserData());
input_t *input = payload->input;
if (wl_surface *focus_surface = input->keyboard.wl_surface) {
GHOST_IWindow *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
GHOST_SystemWayland *system = input->system;
/* Calculate this value every time in case modifier keys are pressed. */
char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'};
xkb_state_key_get_utf8(input->xkb_state, payload->key_code, utf8_buf, sizeof(utf8_buf));
system->pushEvent(new GHOST_EventKey(system->getMilliSeconds(),
GHOST_kEventKeyDown,
win,
payload->key_data.gkey,
'\0',
utf8_buf,
true));
}
};
input->key_repeat.timer = input->system->installTimer(
input->key_repeat.delay, 1000 / input->key_repeat.rate, key_repeat_fn, key_repeat_payload);
}
}
static void keyboard_handle_modifiers(void *data,
struct wl_keyboard * /*wl_keyboard*/,
const uint32_t /*serial*/,
const uint32_t mods_depressed,
const uint32_t mods_latched,
const uint32_t mods_locked,
const uint32_t group)
{
input_t *input = static_cast<input_t *>(data);
xkb_state_update_mask(input->xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
/* A modifier changed so reset the timer,
* see comment in #keyboard_handle_key regarding this behavior. */
if (input->key_repeat.timer) {
keyboard_handle_key_repeat_reset(input, true);
}
}
static void keyboard_repeat_handle_info(void *data,
struct wl_keyboard * /*wl_keyboard*/,
const int32_t rate,
const int32_t delay)
{
input_t *input = static_cast<input_t *>(data);
input->key_repeat.rate = rate;
input->key_repeat.delay = delay;
/* Unlikely possible this setting changes while repeating. */
if (input->key_repeat.timer) {
keyboard_handle_key_repeat_reset(input, false);
}
}
static const struct wl_keyboard_listener keyboard_listener = {
keyboard_handle_keymap,
keyboard_handle_enter,
keyboard_handle_leave,
keyboard_handle_key,
keyboard_handle_modifiers,
keyboard_repeat_handle_info,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Seat), #wl_seat_listener
* \{ */
static void seat_handle_capabilities(void *data,
struct wl_seat *wl_seat,
const uint32_t capabilities)
{
input_t *input = static_cast<input_t *>(data);
input->wl_pointer = nullptr;
input->wl_keyboard = nullptr;
if (capabilities & WL_SEAT_CAPABILITY_POINTER) {
input->wl_pointer = wl_seat_get_pointer(wl_seat);
input->cursor.wl_surface = wl_compositor_create_surface(input->system->compositor());
input->cursor.visible = true;
input->cursor.wl_buffer = nullptr;
input->cursor.file_buffer = new buffer_t;
if (!get_cursor_settings(input->cursor.theme_name, input->cursor.size)) {
input->cursor.theme_name = std::string();
input->cursor.size = default_cursor_size;
}
wl_pointer_add_listener(input->wl_pointer, &pointer_listener, data);
wl_surface_add_listener(input->cursor.wl_surface, &cursor_surface_listener, data);
}
if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
input->wl_keyboard = wl_seat_get_keyboard(wl_seat);
wl_keyboard_add_listener(input->wl_keyboard, &keyboard_listener, data);
}
}
static void seat_handle_name(void *data, struct wl_seat * /*wl_seat*/, const char *name)
{
static_cast<input_t *>(data)->name = std::string(name);
}
static const struct wl_seat_listener seat_listener = {
seat_handle_capabilities,
seat_handle_name,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (XDG Output), #zxdg_output_v1_listener
* \{ */
static void xdg_output_handle_logical_position(void *data,
struct zxdg_output_v1 * /*xdg_output*/,
const int32_t x,
const int32_t y)
{
output_t *output = static_cast<output_t *>(data);
output->position_logical[0] = x;
output->position_logical[1] = y;
output->has_position_logical = true;
}
static void xdg_output_handle_logical_size(void *data,
struct zxdg_output_v1 * /*xdg_output*/,
const int32_t width,
const int32_t height)
{
output_t *output = static_cast<output_t *>(data);
if (output->size_logical[0] != 0 && output->size_logical[1] != 0) {
/* Original comment from SDL. */
/* FIXME(@flibit): GNOME has a bug where the logical size does not account for
* scale, resulting in bogus viewport sizes.
*
* Until this is fixed, validate that _some_ kind of scaling is being
* done (we can't match exactly because fractional scaling can't be
* detected otherwise), then override if necessary. */
if ((output->size_logical[0] == width) && (output->scale_fractional == wl_fixed_from_int(1))) {
GHOST_PRINT("xdg_output scale did not match, overriding with wl_output scale");
#ifdef USE_GNOME_CONFINE_HACK
/* Use a bug in GNOME to check GNOME is in use. If the bug is fixed this won't cause an issue
* as T98793 has been fixed up-stream too, but not in a release at time of writing. */
use_gnome_confine_hack = true;
#endif
return;
}
}
output->size_logical[0] = width;
output->size_logical[1] = height;
output->has_size_logical = true;
}
static void xdg_output_handle_done(void *data, struct zxdg_output_v1 * /*xdg_output*/)
{
/* NOTE: `xdg-output.done` events are deprecated and only apply below version 3 of the protocol.
* `wl-output.done` event will be emitted in version 3 or higher. */
output_t *output = static_cast<output_t *>(data);
if (zxdg_output_v1_get_version(output->xdg_output) < 3) {
output_handle_done(data, output->wl_output);
}
}
static void xdg_output_handle_name(void * /*data*/,
struct zxdg_output_v1 * /*xdg_output*/,
const char * /*name*/)
{
/* Pass. */
}
static void xdg_output_handle_description(void * /*data*/,
struct zxdg_output_v1 * /*xdg_output*/,
const char * /*description*/)
{
/* Pass. */
}
static const struct zxdg_output_v1_listener xdg_output_listener = {
xdg_output_handle_logical_position,
xdg_output_handle_logical_size,
xdg_output_handle_done,
xdg_output_handle_name,
xdg_output_handle_description,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Output), #wl_output_listener
* \{ */
static void output_handle_geometry(void *data,
struct wl_output * /*wl_output*/,
const int32_t /*x*/,
const int32_t /*y*/,
const int32_t physical_width,
const int32_t physical_height,
const int32_t /*subpixel*/,
const char *make,
const char *model,
const int32_t transform)
{
output_t *output = static_cast<output_t *>(data);
output->transform = transform;
output->make = std::string(make);
output->model = std::string(model);
output->size_mm[0] = physical_width;
output->size_mm[1] = physical_height;
}
static void output_handle_mode(void *data,
struct wl_output * /*wl_output*/,
const uint32_t flags,
const int32_t width,
const int32_t height,
const int32_t /*refresh*/)
{
output_t *output = static_cast<output_t *>(data);
if (flags & WL_OUTPUT_MODE_CURRENT) {
output->size_native[0] = width;
output->size_native[1] = height;
/* Don't rotate this yet, `wl-output` coordinates are transformed in
* handle_done and `xdg-output` coordinates are pre-transformed. */
if (!output->has_size_logical) {
output->size_logical[0] = width;
output->size_logical[1] = height;
}
}
}
/**
* Sent all information about output.
*
* This event is sent after all other properties have been sent
* after binding to the output object and after any other property
* changes done after that. This allows changes to the output
* properties to be seen as atomic, even if they happen via multiple events.
*/
static void output_handle_done(void *data, struct wl_output * /*wl_output*/)
{
output_t *output = static_cast<output_t *>(data);
int32_t size_native[2];
if (output->transform & WL_OUTPUT_TRANSFORM_90) {
size_native[0] = output->size_native[1];
size_native[1] = output->size_native[0];
}
else {
size_native[0] = output->size_native[0];
size_native[1] = output->size_native[1];
}
/* If `xdg-output` is present, calculate the true scale of the desktop */
if (output->has_size_logical) {
/* NOTE: it's not necessary to divide these values by their greatest-common-denominator
* as even a 64k screen resolution doesn't approach overflowing an `int32_t`. */
GHOST_ASSERT(size_native[0] && output->size_logical[0],
"Screen size values were not set when they were expected to be.");
output->scale_fractional = wl_fixed_from_int(size_native[0]) / output->size_logical[0];
output->has_scale_fractional = true;
}
}
static void output_handle_scale(void *data, struct wl_output * /*wl_output*/, const int32_t factor)
{
static_cast<output_t *>(data)->scale = factor;
}
static const struct wl_output_listener output_listener = {
output_handle_geometry,
output_handle_mode,
output_handle_done,
output_handle_scale,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (XDG WM Base), #xdg_wm_base_listener
* \{ */
#ifndef WITH_GHOST_WAYLAND_LIBDECOR
static void shell_handle_ping(void * /*data*/,
struct xdg_wm_base *xdg_wm_base,
const uint32_t serial)
{
xdg_wm_base_pong(xdg_wm_base, serial);
}
static const struct xdg_wm_base_listener shell_listener = {
shell_handle_ping,
};
#endif /* !WITH_GHOST_WAYLAND_LIBDECOR. */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (LibDecor), #libdecor_interface
* \{ */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
static void decor_handle_error(struct libdecor * /*context*/,
enum libdecor_error error,
const char *message)
{
(void)(error);
(void)(message);
GHOST_PRINT("decoration error (" << error << "): " << message << std::endl);
exit(EXIT_FAILURE);
}
static struct libdecor_interface libdecor_interface = {
decor_handle_error,
};
#endif /* WITH_GHOST_WAYLAND_LIBDECOR. */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Registry), #wl_registry_listener
* \{ */
static void global_handle_add(void *data,
struct wl_registry *wl_registry,
const uint32_t name,
const char *interface,
const uint32_t /*version*/)
{
struct display_t *display = static_cast<struct display_t *>(data);
if (!strcmp(interface, wl_compositor_interface.name)) {
display->compositor = static_cast<wl_compositor *>(
wl_registry_bind(wl_registry, name, &wl_compositor_interface, 3));
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/* Pass. */
#else
else if (!strcmp(interface, xdg_wm_base_interface.name)) {
display->xdg_shell = static_cast<xdg_wm_base *>(
wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, 1));
xdg_wm_base_add_listener(display->xdg_shell, &shell_listener, nullptr);
}
else if (!strcmp(interface, zxdg_decoration_manager_v1_interface.name)) {
display->xdg_decoration_manager = static_cast<zxdg_decoration_manager_v1 *>(
wl_registry_bind(wl_registry, name, &zxdg_decoration_manager_v1_interface, 1));
}
#endif /* !WITH_GHOST_WAYLAND_LIBDECOR. */
else if (!strcmp(interface, zxdg_output_manager_v1_interface.name)) {
display->xdg_output_manager = static_cast<zxdg_output_manager_v1 *>(
wl_registry_bind(wl_registry, name, &zxdg_output_manager_v1_interface, 2));
for (output_t *output : display->outputs) {
output->xdg_output = zxdg_output_manager_v1_get_xdg_output(display->xdg_output_manager,
output->wl_output);
zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output);
}
}
else if (!strcmp(interface, wl_output_interface.name)) {
output_t *output = new output_t;
output->wl_output = static_cast<wl_output *>(
wl_registry_bind(wl_registry, name, &wl_output_interface, 2));
display->outputs.push_back(output);
wl_output_add_listener(output->wl_output, &output_listener, output);
if (display->xdg_output_manager) {
output->xdg_output = zxdg_output_manager_v1_get_xdg_output(display->xdg_output_manager,
output->wl_output);
zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output);
}
}
else if (!strcmp(interface, wl_seat_interface.name)) {
input_t *input = new input_t;
input->system = display->system;
input->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
input->data_source = new data_source_t;
input->wl_seat = static_cast<wl_seat *>(
wl_registry_bind(wl_registry, name, &wl_seat_interface, 4));
display->inputs.push_back(input);
wl_seat_add_listener(input->wl_seat, &seat_listener, input);
}
else if (!strcmp(interface, wl_shm_interface.name)) {
display->shm = static_cast<wl_shm *>(
wl_registry_bind(wl_registry, name, &wl_shm_interface, 1));
}
else if (!strcmp(interface, wl_data_device_manager_interface.name)) {
display->data_device_manager = static_cast<wl_data_device_manager *>(
wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, 3));
}
else if (!strcmp(interface, zwp_tablet_manager_v2_interface.name)) {
display->tablet_manager = static_cast<zwp_tablet_manager_v2 *>(
wl_registry_bind(wl_registry, name, &zwp_tablet_manager_v2_interface, 1));
}
else if (!strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)) {
display->relative_pointer_manager = static_cast<zwp_relative_pointer_manager_v1 *>(
wl_registry_bind(wl_registry, name, &zwp_relative_pointer_manager_v1_interface, 1));
}
else if (!strcmp(interface, zwp_pointer_constraints_v1_interface.name)) {
display->pointer_constraints = static_cast<zwp_pointer_constraints_v1 *>(
wl_registry_bind(wl_registry, name, &zwp_pointer_constraints_v1_interface, 1));
}
}
/**
* Announce removal of global object.
*
* Notify the client of removed global objects.
*
* This event notifies the client that the global identified by
* name is no longer available. If the client bound to the global
* using the bind request, the client should now destroy that object.
*/
static void global_handle_remove(void * /*data*/,
struct wl_registry * /*wl_registry*/,
const uint32_t /*name*/)
{
}
static const struct wl_registry_listener registry_listener = {
global_handle_add,
global_handle_remove,
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name GHOST Implementation
*
* WAYLAND specific implementation of the #GHOST_System interface.
* \{ */
GHOST_SystemWayland::GHOST_SystemWayland() : GHOST_System(), d(new display_t)
{
wl_log_set_handler_client(ghost_wayland_log_handler);
d->system = this;
/* Connect to the Wayland server. */
d->display = wl_display_connect(nullptr);
if (!d->display) {
display_destroy(d);
throw std::runtime_error("Wayland: unable to connect to display!");
}
/* Register interfaces. */
struct wl_registry *registry = wl_display_get_registry(d->display);
wl_registry_add_listener(registry, &registry_listener, d);
/* Call callback for registry listener. */
wl_display_roundtrip(d->display);
/* Call callbacks for registered listeners. */
wl_display_roundtrip(d->display);
wl_registry_destroy(registry);
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
d->decor_context = libdecor_new(d->display, &libdecor_interface);
if (!d->decor_context) {
display_destroy(d);
throw std::runtime_error("Wayland: unable to create window decorations!");
}
#else
if (!d->xdg_shell) {
display_destroy(d);
throw std::runtime_error("Wayland: unable to access xdg_shell!");
}
#endif
/* Register data device per seat for IPC between Wayland clients. */
if (d->data_device_manager) {
for (input_t *input : d->inputs) {
input->data_device = wl_data_device_manager_get_data_device(d->data_device_manager,
input->wl_seat);
wl_data_device_add_listener(input->data_device, &data_device_listener, input);
}
}
if (d->tablet_manager) {
for (input_t *input : d->inputs) {
input->tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(d->tablet_manager,
input->wl_seat);
zwp_tablet_seat_v2_add_listener(input->tablet_seat, &tablet_seat_listener, input);
}
}
}
GHOST_SystemWayland::~GHOST_SystemWayland()
{
display_destroy(d);
}
bool GHOST_SystemWayland::processEvents(bool waitForEvent)
{
const bool fired = getTimerManager()->fireTimers(getMilliSeconds());
if (waitForEvent) {
wl_display_dispatch(d->display);
}
else {
wl_display_roundtrip(d->display);
}
return fired || (getEventManager()->getNumEvents() > 0);
}
int GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*action*/)
{
return 0;
}
GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
static const xkb_state_component mods_all = xkb_state_component(
XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED | XKB_STATE_MODS_LOCKED |
XKB_STATE_MODS_EFFECTIVE);
bool val;
/* NOTE: XKB doesn't seem to differentiate between left/right modifiers. */
val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_SHIFT, mods_all) == 1;
keys.set(GHOST_kModifierKeyLeftShift, val);
keys.set(GHOST_kModifierKeyRightShift, val);
val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_ALT, mods_all) == 1;
keys.set(GHOST_kModifierKeyLeftAlt, val);
keys.set(GHOST_kModifierKeyRightAlt, val);
val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_CTRL, mods_all) == 1;
keys.set(GHOST_kModifierKeyLeftControl, val);
keys.set(GHOST_kModifierKeyRightControl, val);
val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_LOGO, mods_all) == 1;
keys.set(GHOST_kModifierKeyOS, val);
val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_NUM, mods_all) == 1;
keys.set(GHOST_kModifierKeyNum, val);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
input_t *input = d->inputs[0];
input_state_pointer_t *input_state = input_state_pointer_active(input);
if (!input_state) {
return GHOST_kFailure;
}
buttons = input_state->buttons;
return GHOST_kSuccess;
}
char *GHOST_SystemWayland::getClipboard(bool /*selection*/) const
{
char *clipboard = static_cast<char *>(malloc((selection.size() + 1)));
memcpy(clipboard, selection.data(), selection.size() + 1);
return clipboard;
}
void GHOST_SystemWayland::putClipboard(const char *buffer, bool /*selection*/) const
{
if (!d->data_device_manager || d->inputs.empty()) {
return;
}
input_t *input = d->inputs[0];
std::lock_guard lock{input->data_source_mutex};
data_source_t *data_source = input->data_source;
/* Copy buffer. */
free(data_source->buffer_out);
const size_t buffer_size = strlen(buffer) + 1;
data_source->buffer_out = static_cast<char *>(malloc(buffer_size));
std::memcpy(data_source->buffer_out, buffer, buffer_size);
data_source->data_source = wl_data_device_manager_create_data_source(d->data_device_manager);
wl_data_source_add_listener(data_source->data_source, &data_source_listener, input);
for (const std::string &type : mime_send) {
wl_data_source_offer(data_source->data_source, type.c_str());
}
if (input->data_device) {
wl_data_device_set_selection(
input->data_device, data_source->data_source, input->data_source_serial);
}
}
uint8_t GHOST_SystemWayland::getNumDisplays() const
{
return d ? uint8_t(d->outputs.size()) : 0;
}
static GHOST_TSuccess getCursorPositionClientRelative_impl(
const input_state_pointer_t *input_state,
const GHOST_WindowWayland *win,
int32_t &x,
int32_t &y)
{
const wl_fixed_t scale = win->scale();
x = wl_fixed_to_int(scale * input_state->xy[0]);
y = wl_fixed_to_int(scale * input_state->xy[1]);
return GHOST_kSuccess;
}
static GHOST_TSuccess setCursorPositionClientRelative_impl(input_t *input,
GHOST_WindowWayland *win,
const int32_t x,
const int32_t y)
{
/* NOTE: WAYLAND doesn't support warping the cursor.
* However when grab is enabled, we already simulate a cursor location
* so that can be set to a new location. */
if (!input->relative_pointer) {
return GHOST_kFailure;
}
const wl_fixed_t scale = win->scale();
const wl_fixed_t xy_next[2] = {
wl_fixed_from_int(x) / scale,
wl_fixed_from_int(y) / scale,
};
/* As the cursor was "warped" generate an event at the new location. */
relative_pointer_handle_relative_motion_impl(input, win, xy_next);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::getCursorPositionClientRelative(const GHOST_IWindow *window,
int32_t &x,
int32_t &y) const
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
input_t *input = d->inputs[0];
input_state_pointer_t *input_state = input_state_pointer_active(input);
if (!input_state || !input_state->wl_surface) {
return GHOST_kFailure;
}
const GHOST_WindowWayland *win = static_cast<const GHOST_WindowWayland *>(window);
return getCursorPositionClientRelative_impl(input_state, win, x, y);
}
GHOST_TSuccess GHOST_SystemWayland::setCursorPositionClientRelative(GHOST_IWindow *window,
const int32_t x,
const int32_t y)
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
input_t *input = d->inputs[0];
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(window);
return setCursorPositionClientRelative_impl(input, win, x, y);
}
GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
input_t *input = d->inputs[0];
input_state_pointer_t *input_state = input_state_pointer_active(input);
if (!input_state) {
return GHOST_kFailure;
}
if (wl_surface *focus_surface = input_state->wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
return getCursorPositionClientRelative_impl(input_state, win, x, y);
}
return GHOST_kFailure;
}
GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(const int32_t x, const int32_t y)
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
input_t *input = d->inputs[0];
/* Intentionally different from `getCursorPosition` which supports both tablet & pointer.
* In the case of setting the cursor location, tablets don't support this. */
if (wl_surface *focus_surface = input->pointer.wl_surface) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(focus_surface);
return setCursorPositionClientRelative_impl(input, win, x, y);
}
return GHOST_kFailure;
}
void GHOST_SystemWayland::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const
{
if (getNumDisplays() == 0) {
return;
}
/* We assume first output as main. */
width = uint32_t(d->outputs[0]->size_native[0]);
height = uint32_t(d->outputs[0]->size_native[1]);
}
void GHOST_SystemWayland::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const
{
int32_t xy_min[2] = {INT32_MAX, INT32_MAX};
int32_t xy_max[2] = {INT32_MIN, INT32_MIN};
for (const output_t *output : d->outputs) {
int32_t xy[2] = {0, 0};
if (output->has_position_logical) {
xy[0] = output->position_logical[0];
xy[1] = output->position_logical[1];
}
xy_min[0] = std::min(xy_min[0], xy[0]);
xy_min[1] = std::min(xy_min[1], xy[1]);
xy_max[0] = std::max(xy_max[0], xy[0] + output->size_native[0]);
xy_max[1] = std::max(xy_max[1], xy[1] + output->size_native[1]);
}
width = xy_max[0] - xy_min[0];
height = xy_max[1] - xy_min[1];
}
static GHOST_Context *createOffscreenContext_impl(GHOST_SystemWayland *system,
struct wl_display *wl_display,
wl_egl_window *egl_window)
{
GHOST_Context *context;
for (int minor = 6; minor >= 0; --minor) {
context = new GHOST_ContextEGL(system,
false,
EGLNativeWindowType(egl_window),
EGLNativeDisplayType(wl_display),
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
4,
minor,
GHOST_OPENGL_EGL_CONTEXT_FLAGS,
GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
EGL_OPENGL_API);
if (context->initializeDrawingContext()) {
return context;
}
delete context;
}
context = new GHOST_ContextEGL(system,
false,
EGLNativeWindowType(egl_window),
EGLNativeDisplayType(wl_display),
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
3,
3,
GHOST_OPENGL_EGL_CONTEXT_FLAGS,
GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
EGL_OPENGL_API);
if (context->initializeDrawingContext()) {
return context;
}
delete context;
return nullptr;
}
GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GLSettings /*glSettings*/)
{
/* Create new off-screen window. */
wl_surface *wl_surface = wl_compositor_create_surface(compositor());
wl_egl_window *egl_window = wl_surface ? wl_egl_window_create(wl_surface, 1, 1) : nullptr;
GHOST_Context *context = createOffscreenContext_impl(this, d->display, egl_window);
if (!context) {
GHOST_PRINT("Cannot create off-screen EGL context" << std::endl);
if (wl_surface) {
wl_surface_destroy(wl_surface);
}
if (egl_window) {
wl_egl_window_destroy(egl_window);
}
return nullptr;
}
wl_surface_set_user_data(wl_surface, egl_window);
context->setUserData(wl_surface);
return context;
}
GHOST_TSuccess GHOST_SystemWayland::disposeContext(GHOST_IContext *context)
{
struct wl_surface *wl_surface = (struct wl_surface *)((GHOST_Context *)context)->getUserData();
wl_egl_window *egl_window = (wl_egl_window *)wl_surface_get_user_data(wl_surface);
wl_egl_window_destroy(egl_window);
wl_surface_destroy(wl_surface);
delete context;
return GHOST_kSuccess;
}
GHOST_IWindow *GHOST_SystemWayland::createWindow(const char *title,
const int32_t left,
const int32_t top,
const uint32_t width,
const uint32_t height,
const GHOST_TWindowState state,
const GHOST_TDrawingContextType type,
const GHOST_GLSettings glSettings,
const bool exclusive,
const bool is_dialog,
const GHOST_IWindow *parentWindow)
{
/* Globally store pointer to window manager. */
if (!window_manager) {
window_manager = getWindowManager();
}
GHOST_WindowWayland *window = new GHOST_WindowWayland(
this,
title,
left,
top,
width,
height,
state,
parentWindow,
type,
is_dialog,
((glSettings.flags & GHOST_glStereoVisual) != 0),
exclusive);
if (window) {
if (window->getValid()) {
m_windowManager->addWindow(window);
m_windowManager->setActiveWindow(window);
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowSize, window));
}
else {
delete window;
window = nullptr;
}
}
return window;
}
/**
* Show the buffer defined by #cursor_buffer_set without changing anything else,
* so #cursor_buffer_hide can be used to display it again.
*
* The caller is responsible for setting `input->cursor.visible`.
*/
static void cursor_buffer_show(const input_t *input)
{
const cursor_t *c = &input->cursor;
const int scale = c->is_custom ? c->custom_scale : c->theme_scale;
const int32_t hotspot_x = int32_t(c->wl_image.hotspot_x) / scale;
const int32_t hotspot_y = int32_t(c->wl_image.hotspot_y) / scale;
wl_pointer_set_cursor(
input->wl_pointer, input->pointer.serial, c->wl_surface, hotspot_x, hotspot_y);
for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) {
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(
zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2,
input->tablet.serial,
tool_input->cursor_surface,
hotspot_x,
hotspot_y);
}
}
/**
* Hide the buffer defined by #cursor_buffer_set without changing anything else,
* so #cursor_buffer_show can be used to display it again.
*
* The caller is responsible for setting `input->cursor.visible`.
*/
static void cursor_buffer_hide(const input_t *input)
{
wl_pointer_set_cursor(input->wl_pointer, input->pointer.serial, nullptr, 0, 0);
for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) {
zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, input->tablet.serial, nullptr, 0, 0);
}
}
static void cursor_buffer_set(const input_t *input, wl_buffer *buffer)
{
const cursor_t *c = &input->cursor;
const int scale = c->is_custom ? c->custom_scale : c->theme_scale;
const bool visible = (c->visible && c->is_hardware);
const int32_t image_size_x = int32_t(c->wl_image.width);
const int32_t image_size_y = int32_t(c->wl_image.height);
/* This is a requirement of WAYLAND, when this isn't the case,
* it causes Blender's window to close intermittently. */
GHOST_ASSERT((image_size_x % scale) == 0 && (image_size_y % scale) == 0,
"The size must be a multiple of the scale!");
const int32_t hotspot_x = int32_t(c->wl_image.hotspot_x) / scale;
const int32_t hotspot_y = int32_t(c->wl_image.hotspot_y) / scale;
wl_surface_set_buffer_scale(c->wl_surface, scale);
wl_surface_attach(c->wl_surface, buffer, 0, 0);
wl_surface_damage(c->wl_surface, 0, 0, image_size_x, image_size_y);
wl_surface_commit(c->wl_surface);
wl_pointer_set_cursor(input->wl_pointer,
input->pointer.serial,
visible ? c->wl_surface : nullptr,
hotspot_x,
hotspot_y);
/* Set the cursor for all tablet tools as well. */
for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) {
tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(
zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
/* FIXME: for some reason cursor scale is applied twice (when the scale isn't 1x),
* this happens both in gnome-shell & KDE. Setting the surface scale here doesn't help. */
wl_surface_set_buffer_scale(tool_input->cursor_surface, scale);
wl_surface_attach(tool_input->cursor_surface, buffer, 0, 0);
wl_surface_damage(tool_input->cursor_surface, 0, 0, image_size_x, image_size_y);
wl_surface_commit(tool_input->cursor_surface);
zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2,
input->tablet.serial,
visible ? tool_input->cursor_surface : nullptr,
hotspot_x,
hotspot_y);
}
}
enum eCursorSetMode {
CURSOR_VISIBLE_ALWAYS_SET = 1,
CURSOR_VISIBLE_ONLY_HIDE,
CURSOR_VISIBLE_ONLY_SHOW,
};
static void cursor_visible_set(input_t *input,
const bool visible,
const bool is_hardware,
const enum eCursorSetMode set_mode)
{
cursor_t *cursor = &input->cursor;
const bool was_visible = cursor->is_hardware && cursor->visible;
const bool use_visible = is_hardware && visible;
if (set_mode == CURSOR_VISIBLE_ALWAYS_SET) {
/* Pass. */
}
else if ((set_mode == CURSOR_VISIBLE_ONLY_SHOW)) {
if (!use_visible) {
return;
}
}
else if ((set_mode == CURSOR_VISIBLE_ONLY_HIDE)) {
if (use_visible) {
return;
}
}
if (use_visible) {
if (!was_visible) {
cursor_buffer_show(input);
}
}
else {
if (was_visible) {
cursor_buffer_hide(input);
}
}
cursor->visible = visible;
cursor->is_hardware = is_hardware;
}
static bool cursor_is_software(const GHOST_TGrabCursorMode mode, const bool use_software_confine)
{
if (mode == GHOST_kGrabWrap) {
return true;
}
#ifdef USE_GNOME_CONFINE_HACK
if (mode == GHOST_kGrabNormal) {
if (use_software_confine) {
return true;
}
}
#else
(void)use_software_confine;
#endif
return false;
}
GHOST_TSuccess GHOST_SystemWayland::setCursorShape(const GHOST_TStandardCursor shape)
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
auto cursor_find = cursors.find(shape);
const char *cursor_name = (cursor_find == cursors.end()) ?
cursors.at(GHOST_kStandardCursorDefault) :
(*cursor_find).second;
input_t *input = d->inputs[0];
cursor_t *c = &input->cursor;
if (!c->wl_theme) {
/* The cursor surface hasn't entered an output yet. Initialize theme with scale 1. */
c->wl_theme = wl_cursor_theme_load(
c->theme_name.c_str(), c->size, d->inputs[0]->system->shm());
}
wl_cursor *cursor = wl_cursor_theme_get_cursor(c->wl_theme, cursor_name);
if (!cursor) {
GHOST_PRINT("cursor '" << cursor_name << "' does not exist" << std::endl);
return GHOST_kFailure;
}
struct wl_cursor_image *image = cursor->images[0];
struct wl_buffer *buffer = wl_cursor_image_get_buffer(image);
if (!buffer) {
return GHOST_kFailure;
}
c->visible = true;
c->is_custom = false;
c->wl_buffer = buffer;
c->wl_image = *image;
cursor_buffer_set(input, buffer);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::hasCursorShape(const GHOST_TStandardCursor cursorShape)
{
auto cursor_find = cursors.find(cursorShape);
if (cursor_find == cursors.end()) {
return GHOST_kFailure;
}
const char *value = (*cursor_find).second;
if (*value == '\0') {
return GHOST_kFailure;
}
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::setCustomCursorShape(uint8_t *bitmap,
uint8_t *mask,
const int sizex,
const int sizey,
const int hotX,
const int hotY,
const bool /*canInvertColor*/)
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
cursor_t *cursor = &d->inputs[0]->cursor;
static const int32_t stride = sizex * 4; /* ARGB */
cursor->file_buffer->size = (size_t)stride * sizey;
#ifdef HAVE_MEMFD_CREATE
const int fd = memfd_create("blender-cursor-custom", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd >= 0) {
fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL);
}
#else
char *path = getenv("XDG_RUNTIME_DIR");
if (!path) {
errno = ENOENT;
return GHOST_kFailure;
}
char *tmpname;
asprintf(&tmpname, "%s/%s", path, "blender-XXXXXX");
const int fd = mkostemp(tmpname, O_CLOEXEC);
if (fd >= 0) {
unlink(tmpname);
}
free(tmpname);
#endif
if (fd < 0) {
return GHOST_kFailure;
}
if (posix_fallocate(fd, 0, int32_t(cursor->file_buffer->size)) != 0) {
return GHOST_kFailure;
}
cursor->file_buffer->data = mmap(
nullptr, cursor->file_buffer->size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (cursor->file_buffer->data == MAP_FAILED) {
cursor->file_buffer->data = nullptr;
close(fd);
return GHOST_kFailure;
}
struct wl_shm_pool *pool = wl_shm_create_pool(d->shm, fd, int32_t(cursor->file_buffer->size));
wl_buffer *buffer = wl_shm_pool_create_buffer(
pool, 0, sizex, sizey, stride, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool);
close(fd);
wl_buffer_add_listener(buffer, &cursor_buffer_listener, cursor);
static constexpr uint32_t black = 0xFF000000;
static constexpr uint32_t white = 0xFFFFFFFF;
static constexpr uint32_t transparent = 0x00000000;
uint8_t datab = 0, maskb = 0;
uint32_t *pixel;
for (int y = 0; y < sizey; ++y) {
pixel = &static_cast<uint32_t *>(cursor->file_buffer->data)[y * sizex];
for (int x = 0; x < sizex; ++x) {
if ((x % 8) == 0) {
datab = *bitmap++;
maskb = *mask++;
/* Reverse bit order. */
datab = uint8_t((datab * 0x0202020202ULL & 0x010884422010ULL) % 1023);
maskb = uint8_t((maskb * 0x0202020202ULL & 0x010884422010ULL) % 1023);
}
if (maskb & 0x80) {
*pixel++ = (datab & 0x80) ? white : black;
}
else {
*pixel++ = (datab & 0x80) ? white : transparent;
}
datab <<= 1;
maskb <<= 1;
}
}
cursor->visible = true;
cursor->is_custom = true;
cursor->custom_scale = 1; /* TODO: support Hi-DPI custom cursors. */
cursor->wl_buffer = buffer;
cursor->wl_image.width = uint32_t(sizex);
cursor->wl_image.height = uint32_t(sizey);
cursor->wl_image.hotspot_x = uint32_t(hotX);
cursor->wl_image.hotspot_y = uint32_t(hotY);
cursor_buffer_set(d->inputs[0], buffer);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::getCursorBitmap(GHOST_CursorBitmapRef *bitmap)
{
cursor_t *cursor = &d->inputs[0]->cursor;
if (cursor->file_buffer->data == nullptr) {
return GHOST_kFailure;
}
if (!cursor->is_custom) {
return GHOST_kFailure;
}
bitmap->data_size[0] = cursor->wl_image.width;
bitmap->data_size[1] = cursor->wl_image.height;
bitmap->hot_spot[0] = cursor->wl_image.hotspot_x;
bitmap->hot_spot[1] = cursor->wl_image.hotspot_y;
bitmap->data = (uint8_t *)static_cast<void *>(cursor->file_buffer->data);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::setCursorVisibility(const bool visible)
{
if (d->inputs.empty()) {
return GHOST_kFailure;
}
input_t *input = d->inputs[0];
cursor_visible_set(input, visible, input->cursor.is_hardware, CURSOR_VISIBLE_ALWAYS_SET);
return GHOST_kSuccess;
}
bool GHOST_SystemWayland::supportsCursorWarp()
{
/* WAYLAND doesn't support setting the cursor position directly,
* this is an intentional choice, forcing us to use a software cursor in this case. */
return false;
}
bool GHOST_SystemWayland::supportsWindowPosition()
{
/* WAYLAND doesn't support accessing the window position. */
return false;
}
bool GHOST_SystemWayland::getCursorGrabUseSoftwareDisplay(const GHOST_TGrabCursorMode mode)
{
if (d->inputs.empty()) {
return false;
}
#ifdef USE_GNOME_CONFINE_HACK
input_t *input = d->inputs[0];
const bool use_software_confine = input->use_pointer_software_confine;
#else
const bool use_software_confine = false;
#endif
return cursor_is_software(mode, use_software_confine);
}
#ifdef USE_GNOME_CONFINE_HACK
static bool setCursorGrab_use_software_confine(const GHOST_TGrabCursorMode mode,
wl_surface *surface)
{
# ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON
if (use_gnome_confine_hack == false) {
return false;
}
# endif
if (mode != GHOST_kGrabNormal) {
return false;
}
const GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface(surface);
if (!win) {
return false;
}
# ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON
if (win->scale() <= 1) {
return false;
}
# endif
return true;
}
#endif
static input_grab_state_t input_grab_state_from_mode(const GHOST_TGrabCursorMode mode,
const bool use_software_confine)
{
/* Initialize all members. */
const struct input_grab_state_t grab_state = {
/* Warping happens to require software cursor which also hides. */
.use_lock = (mode == GHOST_kGrabWrap || mode == GHOST_kGrabHide) || use_software_confine,
.use_confine = (mode == GHOST_kGrabNormal) && (use_software_confine == false),
};
return grab_state;
}
GHOST_TSuccess GHOST_SystemWayland::setCursorGrab(const GHOST_TGrabCursorMode mode,
const GHOST_TGrabCursorMode mode_current,
int32_t init_grab_xy[2],
wl_surface *surface)
{
/* Ignore, if the required protocols are not supported. */
if (!d->relative_pointer_manager || !d->pointer_constraints) {
return GHOST_kFailure;
}
if (d->inputs.empty()) {
return GHOST_kFailure;
}
/* No change, success. */
if (mode == mode_current) {
return GHOST_kSuccess;
}
input_t *input = d->inputs[0];
#ifdef USE_GNOME_CONFINE_HACK
const bool was_software_confine = input->use_pointer_software_confine;
const bool use_software_confine = setCursorGrab_use_software_confine(mode, surface);
#else
const bool was_software_confine = false;
const bool use_software_confine = false;
#endif
const struct input_grab_state_t grab_state_prev = input_grab_state_from_mode(
mode_current, was_software_confine);
const struct input_grab_state_t grab_state_next = input_grab_state_from_mode(
mode, use_software_confine);
/* Check for wrap as #supportsCursorWarp isn't supported. */
const bool use_visible = !(((mode == GHOST_kGrabHide) || (mode == GHOST_kGrabWrap)) ||
use_software_confine);
const bool is_hardware_cursor = !cursor_is_software(mode, use_software_confine);
/* Only hide so the cursor is not made visible before it's location is restored.
* This function is called again at the end of this function which only shows. */
cursor_visible_set(input, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_HIDE);
/* Switching from one grab mode to another,
* in this case disable the current locks as it makes logic confusing,
* postpone changing the cursor to avoid flickering. */
if (!grab_state_next.use_lock) {
if (input->relative_pointer) {
zwp_relative_pointer_v1_destroy(input->relative_pointer);
input->relative_pointer = nullptr;
}
if (input->locked_pointer) {
/* Request location to restore to. */
if (mode_current == GHOST_kGrabWrap) {
/* Since this call is initiated by Blender, we can be sure the window wasn't closed
* by logic outside this function - as the window was needed to make this call. */
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(surface);
GHOST_ASSERT(win, "could not find window from surface when un-grabbing!");
GHOST_Rect bounds;
int32_t xy_new[2] = {input->pointer.xy[0], input->pointer.xy[1]};
/* Fallback to window bounds. */
if (win->getCursorGrabBounds(bounds) == GHOST_kFailure) {
win->getClientBounds(bounds);
}
const int scale = win->scale();
bounds.m_l = wl_fixed_from_int(bounds.m_l) / scale;
bounds.m_t = wl_fixed_from_int(bounds.m_t) / scale;
bounds.m_r = wl_fixed_from_int(bounds.m_r) / scale;
bounds.m_b = wl_fixed_from_int(bounds.m_b) / scale;
bounds.wrapPoint(xy_new[0], xy_new[1], 0, win->getCursorGrabAxis());
/* Push an event so the new location is registered. */
if ((xy_new[0] != input->pointer.xy[0]) || (xy_new[1] != input->pointer.xy[1])) {
input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(),
GHOST_kEventCursorMove,
win,
wl_fixed_to_int(scale * xy_new[0]),
wl_fixed_to_int(scale * xy_new[1]),
GHOST_TABLET_DATA_NONE));
}
input->pointer.xy[0] = xy_new[0];
input->pointer.xy[1] = xy_new[1];
zwp_locked_pointer_v1_set_cursor_position_hint(
input->locked_pointer, xy_new[0], xy_new[1]);
wl_surface_commit(surface);
}
else if (mode_current == GHOST_kGrabHide) {
if ((init_grab_xy[0] != input->grab_lock_xy[0]) ||
(init_grab_xy[1] != input->grab_lock_xy[1])) {
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(surface);
const int scale = win->scale();
const wl_fixed_t xy_next[2] = {
wl_fixed_from_int(init_grab_xy[0]) / scale,
wl_fixed_from_int(init_grab_xy[1]) / scale,
};
zwp_locked_pointer_v1_set_cursor_position_hint(
input->locked_pointer, xy_next[0], xy_next[1]);
wl_surface_commit(surface);
}
}
#ifdef USE_GNOME_CONFINE_HACK
else if (mode_current == GHOST_kGrabNormal) {
if (was_software_confine) {
zwp_locked_pointer_v1_set_cursor_position_hint(
input->locked_pointer, input->pointer.xy[0], input->pointer.xy[1]);
wl_surface_commit(surface);
}
}
#endif
zwp_locked_pointer_v1_destroy(input->locked_pointer);
input->locked_pointer = nullptr;
}
}
if (!grab_state_next.use_confine) {
if (input->confined_pointer) {
zwp_confined_pointer_v1_destroy(input->confined_pointer);
input->confined_pointer = nullptr;
}
}
if (mode != GHOST_kGrabDisable) {
if (grab_state_next.use_lock) {
if (!grab_state_prev.use_lock) {
/* TODO(@campbellbarton): As WAYLAND does not support warping the pointer it may not be
* possible to support #GHOST_kGrabWrap by pragmatically settings it's coordinates.
* An alternative could be to draw the cursor in software (and hide the real cursor),
* or just accept a locked cursor on WAYLAND. */
input->relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(
d->relative_pointer_manager, input->wl_pointer);
zwp_relative_pointer_v1_add_listener(
input->relative_pointer, &relative_pointer_listener, input);
input->locked_pointer = zwp_pointer_constraints_v1_lock_pointer(
d->pointer_constraints,
surface,
input->wl_pointer,
nullptr,
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
}
if (mode == GHOST_kGrabHide) {
/* Set the initial position to detect any changes when un-grabbing,
* otherwise the unlocked cursor defaults to un-locking in-place. */
GHOST_WindowWayland *win = GHOST_WindowWayland::from_surface_mut(surface);
const int scale = win->scale();
init_grab_xy[0] = wl_fixed_to_int(scale * input->pointer.xy[0]);
init_grab_xy[1] = wl_fixed_to_int(scale * input->pointer.xy[1]);
input->grab_lock_xy[0] = init_grab_xy[0];
input->grab_lock_xy[1] = init_grab_xy[1];
}
}
else if (grab_state_next.use_confine) {
if (!grab_state_prev.use_confine) {
input->confined_pointer = zwp_pointer_constraints_v1_confine_pointer(
d->pointer_constraints,
surface,
input->wl_pointer,
nullptr,
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
}
}
}
/* Only show so the cursor is made visible as the last step. */
cursor_visible_set(input, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_SHOW);
#ifdef USE_GNOME_CONFINE_HACK
input->use_pointer_software_confine = use_software_confine;
#endif
return GHOST_kSuccess;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Direct Data Access
*
* Expose some members via methods.
* \{ */
wl_display *GHOST_SystemWayland::display()
{
return d->display;
}
wl_compositor *GHOST_SystemWayland::compositor()
{
return d->compositor;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
libdecor *GHOST_SystemWayland::decor_context()
{
return d->decor_context;
}
#else /* WITH_GHOST_WAYLAND_LIBDECOR */
xdg_wm_base *GHOST_SystemWayland::xdg_shell()
{
return d->xdg_shell;
}
zxdg_decoration_manager_v1 *GHOST_SystemWayland::xdg_decoration_manager()
{
return d->xdg_decoration_manager;
}
#endif /* !WITH_GHOST_WAYLAND_LIBDECOR */
const std::vector<output_t *> &GHOST_SystemWayland::outputs() const
{
return d->outputs;
}
wl_shm *GHOST_SystemWayland::shm() const
{
return d->shm;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Query Access
* \{ */
output_t *GHOST_SystemWayland::output_find_by_wl(const struct wl_output *output) const
{
for (output_t *reg_output : this->outputs()) {
if (reg_output->wl_output == output) {
return reg_output;
}
}
return nullptr;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Utility Functions
*
* Functionality only used for the WAYLAND implementation.
* \{ */
void GHOST_SystemWayland::selection_set(const std::string &selection)
{
this->selection = selection;
}
void GHOST_SystemWayland::window_surface_unref(const wl_surface *surface)
{
#define SURFACE_CLEAR_PTR(surface_test) \
if (surface_test == surface) { \
surface_test = nullptr; \
} \
((void)0);
/* Only clear window surfaces (not cursors, off-screen surfaces etc). */
for (input_t *input : d->inputs) {
SURFACE_CLEAR_PTR(input->pointer.wl_surface);
SURFACE_CLEAR_PTR(input->tablet.wl_surface);
SURFACE_CLEAR_PTR(input->keyboard.wl_surface);
SURFACE_CLEAR_PTR(input->focus_dnd);
}
#undef SURFACE_CLEAR_PTR
}
/** \} */