/* 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_PathUtils.h" #include "GHOST_TimerManager.h" #include "GHOST_WaylandUtils.h" #include "GHOST_WindowManager.h" #include "GHOST_utildefines.h" #include "GHOST_ContextEGL.h" #ifdef WITH_INPUT_NDOF # include "GHOST_NDOFManagerUnix.h" #endif #ifdef WITH_GHOST_WAYLAND_DYNLOAD # include /* For `ghost_wl_dynload_libraries`. */ #endif #ifdef WITH_GHOST_WAYLAND_DYNLOAD # include #endif #include #include #include #include #include #include #include #ifdef WITH_GHOST_WAYLAND_DYNLOAD # include #endif #include #include "GHOST_WaylandCursorSettings.h" #include /* Generated by `wayland-scanner`. */ #include #include #include #include #include #include #include #include #include /* Logging, use `ghost.wl.*` prefix. */ #include "CLG_log.h" 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 #define XKB_STATE_MODS_ALL \ (enum xkb_state_component)(XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED | \ XKB_STATE_MODS_LOCKED | XKB_STATE_MODS_EFFECTIVE) /* -------------------------------------------------------------------- */ /** \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 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; void *custom_data = nullptr; size_t custom_data_size = 0; int size = 0; std::string theme_name; 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 types; uint32_t source_actions = 0; uint32_t dnd_action = 0; struct wl_data_offer *id = nullptr; std::atomic 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}; /** Outputs on which the cursor is visible. */ std::unordered_set outputs; int theme_scale = 1; /** 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 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; /** * Cache result of `xkb_keymap_mod_get_index` * so every time a modifier is accessed a string lookup isn't required. * Be sure to check for #XKB_MOD_INVALID before using. */ struct { xkb_mod_index_t shift; /* #XKB_MOD_NAME_SHIFT */ xkb_mod_index_t caps; /* #XKB_MOD_NAME_CAPS */ xkb_mod_index_t ctrl; /* #XKB_MOD_NAME_CTRL */ xkb_mod_index_t alt; /* #XKB_MOD_NAME_ALT */ xkb_mod_index_t num; /* #XKB_MOD_NAME_NUM */ xkb_mod_index_t logo; /* #XKB_MOD_NAME_LOGO */ } xkb_keymap_mod_index; 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 outputs; std::vector 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; }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \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 input_state_pointer_t *input_state_pointer_from_cursor_surface(input_t *input, const wl_surface *wl_surface) { if (ghost_wl_surface_own_cursor_pointer(wl_surface)) { return &input->pointer; } if (ghost_wl_surface_own_cursor_tablet(wl_surface)) { return &input->tablet; } GHOST_ASSERT(0, "Surface found without pointer/tablet tag"); 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->cursor.custom_data) { munmap(input->cursor.custom_data, input->cursor.custom_data_size); } if (input->wl_pointer) { 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 (UNLIKELY(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 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 mime_dnd = { {mime_text_plain, GHOST_kDragnDropTypeString}, {mime_text_utf8, GHOST_kDragnDropTypeString}, {mime_text_uri, GHOST_kDragnDropTypeFilenames}, }; static const std::vector mime_preference_order = { mime_text_uri, mime_text_utf8, mime_text_plain, }; static const std::vector mime_send = { "UTF8_STRING", "COMPOUND_TEXT", "TEXT", "STRING", "text/plain;charset=utf-8", "text/plain", }; static int memfd_create_sealed(const char *name) { #ifdef HAVE_MEMFD_CREATE const int fd = memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING); if (fd >= 0) { fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL); } return fd; #else /* HAVE_MEMFD_CREATE */ char *path = getenv("XDG_RUNTIME_DIR"); if (!path) { errno = ENOENT; return -1; } char *tmpname; asprintf(&tmpname, "%s/%s-XXXXXX", path, name); const int fd = mkostemp(tmpname, O_CLOEXEC); if (fd >= 0) { unlink(tmpname); } free(tmpname); return fd; #endif /* !HAVE_MEMFD_CREATE */ } static size_t ghost_wl_shm_format_as_size(enum wl_shm_format format) { switch (format) { case WL_SHM_FORMAT_ARGB8888: { return 4; } default: { /* Support other formats as needed. */ GHOST_ASSERT(0, "Unexpected format passed in!"); return 4; } } } /** * Return a #wl_buffer, ready to have it's data filled in or NULL in case of failure. * The caller is responsible for calling `unmap(buffer_data, buffer_size)`. * * \param r_buffer_data: The buffer to be filled. * \param r_buffer_data_size: The size of `r_buffer_data` in bytes. */ static wl_buffer *ghost_wl_buffer_create_for_image(struct wl_shm *shm, const int32_t size_xy[2], enum wl_shm_format format, void **r_buffer_data, size_t *r_buffer_data_size) { const int fd = memfd_create_sealed("ghost-wl-buffer"); wl_buffer *buffer = nullptr; if (fd >= 0) { const int32_t buffer_stride = size_xy[0] * ghost_wl_shm_format_as_size(format); const int32_t buffer_size = buffer_stride * size_xy[1]; if (posix_fallocate(fd, 0, buffer_size) == 0) { void *buffer_data = mmap(nullptr, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (buffer_data != MAP_FAILED) { struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, buffer_size); buffer = wl_shm_pool_create_buffer(pool, 0, UNPACK2(size_xy), buffer_stride, format); wl_shm_pool_destroy(pool); if (buffer) { *r_buffer_data = buffer_data; *r_buffer_data_size = (size_t)buffer_size; } else { /* Highly unlikely. */ munmap(buffer_data, buffer_size); } } } close(fd); } return buffer; } /** \} */ /* -------------------------------------------------------------------- */ /** \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. * \{ */ static CLG_LogRef LOG_WL_RELATIVE_POINTER = {"ghost.wl.handle.relative_pointer"}; #define LOG (&LOG_WL_RELATIVE_POINTER) /** * 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(UNPACK2(input->pointer.xy)); } #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(data); if (wl_surface *focus_surface = input->pointer.wl_surface) { CLOG_INFO(LOG, 2, "relative_motion"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(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); } else { CLOG_INFO(LOG, 2, "relative_motion (skipped)"); } } static const zwp_relative_pointer_v1_listener relative_pointer_listener = { relative_pointer_handle_relative_motion, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Data Source), #wl_data_source_listener * \{ */ static CLG_LogRef LOG_WL_DATA_SOURCE = {"ghost.wl.handle.data_source"}; #define LOG (&LOG_WL_DATA_SOURCE) 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_wl_surface_user_data(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, UNPACK2(event_xy), nullptr)); } } } static std::string read_pipe(data_offer_t *data_offer, const std::string mime_receive, std::mutex *mutex) { int pipefd[2]; if (UNLIKELY(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*/) { CLOG_INFO(LOG, 2, "target"); } 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(data); std::lock_guard lock{input->data_source_mutex}; CLOG_INFO(LOG, 2, "send"); 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) { CLOG_INFO(LOG, 2, "cancelled"); 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*/) { CLOG_INFO(LOG, 2, "dnd_drop_performed"); } /** * 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*/) { CLOG_INFO(LOG, 2, "dnd_finished"); } /** * 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) { CLOG_INFO(LOG, 2, "handle_action (dnd_action=%u)", dnd_action); } 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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Data Offer), #wl_data_offer_listener * \{ */ static CLG_LogRef LOG_WL_DATA_OFFER = {"ghost.wl.handle.data_offer"}; #define LOG (&LOG_WL_DATA_OFFER) static void data_offer_handle_offer(void *data, struct wl_data_offer * /*wl_data_offer*/, const char *mime_type) { CLOG_INFO(LOG, 2, "offer (mime_type=%s)", mime_type); static_cast(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) { CLOG_INFO(LOG, 2, "source_actions (%u)", source_actions); static_cast(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) { CLOG_INFO(LOG, 2, "actions (%u)", dnd_action); static_cast(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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Data Device), #wl_data_device_listener * \{ */ static CLG_LogRef LOG_WL_DATA_DEVICE = {"ghost.wl.handle.data_device"}; #define LOG (&LOG_WL_DATA_DEVICE) static void data_device_handle_data_offer(void * /*data*/, struct wl_data_device * /*wl_data_device*/, struct wl_data_offer *id) { CLOG_INFO(LOG, 2, "data_offer"); 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) { if (!ghost_wl_surface_own(surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); input_t *input = static_cast(data); std::lock_guard lock{input->data_offer_dnd_mutex}; delete input->data_offer_dnd; input->data_offer_dnd = static_cast(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(data); std::lock_guard lock{input->data_offer_dnd_mutex}; CLOG_INFO(LOG, 2, "leave"); 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(data); std::lock_guard lock{input->data_offer_dnd_mutex}; CLOG_INFO(LOG, 2, "motion"); 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(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()); CLOG_INFO(LOG, 2, "drop mime_recieve=%s", mime_receive.c_str()); 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] = {UNPACK2(data_offer->dnd.xy)}; const std::string data = read_pipe(data_offer, mime_receive, nullptr); CLOG_INFO( LOG, 2, "drop_read_uris mime_receive=%s, data=%s", mime_receive.c_str(), data.c_str()); wl_data_offer_finish(data_offer->id); wl_data_offer_destroy(data_offer->id); if (input->data_offer_dnd == data_offer) { input->data_offer_dnd = nullptr; } 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://"; /* NOTE: some applications CRLF (`\r\n`) GTK3 for e.g. & others don't `pcmanfm-qt`. * So support both, once `\n` is found, strip the preceding `\r` if found. */ static constexpr const char *lf = "\n"; GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); std::vector 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(lf, pos); if (pos == std::string::npos) { break; } /* Account for 'CRLF' case. */ size_t end = pos; if (data[end - 1] == '\r') { end -= 1; } uris.push_back(data.substr(start, end - start)); CLOG_INFO(LOG, 2, "drop_read_uris pos=%zu, text_uri=\"%s\"", start, uris.back().c_str()); } GHOST_TStringArray *flist = static_cast( malloc(sizeof(GHOST_TStringArray))); flist->count = int(uris.size()); flist->strings = static_cast(malloc(uris.size() * sizeof(uint8_t *))); for (size_t i = 0; i < uris.size(); i++) { flist->strings[i] = (uint8_t *)GHOST_URL_decode_alloc(uris[i].c_str()); } CLOG_INFO(LOG, 2, "drop_read_uris_fn file_count=%d", flist->count); 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 (ELEM(mime_receive, mime_text_plain, mime_text_utf8)) { /* TODO: enable use of internal functions 'txt_insert_buf' and * 'text_update_edited' to behave like dropped text was pasted. */ CLOG_INFO(LOG, 2, "drop_read_uris_fn (text_plain, text_utf8), unhandled!"); } wl_display_roundtrip(system->display()); }; /* Pass in `input->focus_dnd` instead of accessing it from `input` since the leave callback * (#data_device_handle_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(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; input->data_offer_copy_paste = nullptr; } if (id == nullptr) { CLOG_INFO(LOG, 2, "selection: (skipped)"); return; } CLOG_INFO(LOG, 2, "selection"); /* Get new data offer. */ data_offer = static_cast(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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Buffer), #wl_buffer_listener * \{ */ static CLG_LogRef LOG_WL_CURSOR_BUFFER = {"ghost.wl.handle.cursor_buffer"}; #define LOG (&LOG_WL_CURSOR_BUFFER) static void cursor_buffer_handle_release(void *data, struct wl_buffer *wl_buffer) { CLOG_INFO(LOG, 2, "release"); cursor_t *cursor = static_cast(data); wl_buffer_destroy(wl_buffer); if (wl_buffer == cursor->wl_buffer) { /* The mapped buffer was from a custom cursor. */ cursor->wl_buffer = nullptr; } } static const struct wl_buffer_listener cursor_buffer_listener = { cursor_buffer_handle_release, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Surface), #wl_surface_listener * \{ */ static CLG_LogRef LOG_WL_CURSOR_SURFACE = {"ghost.wl.handle.cursor_surface"}; #define LOG (&LOG_WL_CURSOR_SURFACE) static bool update_cursor_scale(cursor_t &cursor, wl_shm *shm, input_state_pointer_t *input_state, wl_surface *cursor_surface) { int scale = 0; for (const output_t *output : input_state->outputs) { if (output->scale > scale) { scale = output->scale; } } if (scale > 0 && input_state->theme_scale != scale) { input_state->theme_scale = scale; if (!cursor.is_custom) { wl_surface_set_buffer_scale(cursor_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) { if (!ghost_wl_output_own(output)) { CLOG_INFO(LOG, 2, "handle_enter (skipped)"); return; } CLOG_INFO(LOG, 2, "handle_enter"); input_t *input = static_cast(data); input_state_pointer_t *input_state = input_state_pointer_from_cursor_surface(input, wl_surface); const output_t *reg_output = ghost_wl_output_user_data(output); input_state->outputs.insert(reg_output); update_cursor_scale(input->cursor, input->system->shm(), input_state, wl_surface); } static void cursor_surface_handle_leave(void *data, struct wl_surface *wl_surface, struct wl_output *output) { if (!(output && ghost_wl_output_own(output))) { CLOG_INFO(LOG, 2, "handle_leave (skipped)"); return; } CLOG_INFO(LOG, 2, "handle_leave"); input_t *input = static_cast(data); input_state_pointer_t *input_state = input_state_pointer_from_cursor_surface(input, wl_surface); const output_t *reg_output = ghost_wl_output_user_data(output); input_state->outputs.erase(reg_output); update_cursor_scale(input->cursor, input->system->shm(), input_state, wl_surface); } static const struct wl_surface_listener cursor_surface_listener = { cursor_surface_handle_enter, cursor_surface_handle_leave, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer), #wl_pointer_listener * \{ */ static CLG_LogRef LOG_WL_POINTER = {"ghost.wl.handle.pointer"}; #define LOG (&LOG_WL_POINTER) 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) { if (!ghost_wl_surface_own(surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); win->activate(); input_t *input = static_cast(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(data)->pointer.wl_surface = nullptr; if (surface && ghost_wl_surface_own(surface)) { CLOG_INFO(LOG, 2, "leave"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); win->deactivate(); } else { CLOG_INFO(LOG, 2, "leave (skipped)"); } } 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(data); input->pointer.xy[0] = surface_x; input->pointer.xy[1] = surface_y; if (wl_surface *focus_surface = input->pointer.wl_surface) { CLOG_INFO(LOG, 2, "motion"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(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)); } else { CLOG_INFO(LOG, 2, "motion (skipped)"); } } 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) { CLOG_INFO(LOG, 2, "button (button=%u, state=%u)", button, state); input_t *input = static_cast(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_wl_surface_user_data(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) { CLOG_INFO(LOG, 2, "axis (axis=%u, value=%d)", axis, value); } static void pointer_handle_frame(void * /*data*/, struct wl_pointer * /*wl_pointer*/) { CLOG_INFO(LOG, 2, "frame"); } static void pointer_handle_axis_source(void * /*data*/, struct wl_pointer * /*wl_pointer*/, uint32_t axis_source) { CLOG_INFO(LOG, 2, "axis_source (axis_source=%u)", axis_source); } static void pointer_handle_axis_stop(void * /*data*/, struct wl_pointer * /*wl_pointer*/, uint32_t /*time*/, uint32_t axis) { CLOG_INFO(LOG, 2, "axis_stop (axis=%u)", axis); } static void pointer_handle_axis_discrete(void *data, struct wl_pointer * /*wl_pointer*/, uint32_t axis, int32_t discrete) { CLOG_INFO(LOG, 2, "axis_discrete (axis=%u, discrete=%d)", axis, discrete); input_t *input = static_cast(data); if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) { return; } if (wl_surface *focus_surface = input->pointer.wl_surface) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); input->system->pushEvent(new GHOST_EventWheel( input->system->getMilliSeconds(), win, std::signbit(discrete) ? +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, pointer_handle_frame, pointer_handle_axis_source, pointer_handle_axis_stop, pointer_handle_axis_discrete, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Tablet Tool), #zwp_tablet_tool_v2_listener * \{ */ static CLG_LogRef LOG_WL_TABLET_TOOL = {"ghost.wl.handle.tablet_tool"}; #define LOG (&LOG_WL_TABLET_TOOL) static void tablet_tool_handle_type(void *data, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t tool_type) { CLOG_INFO(LOG, 2, "type (type=%u)", tool_type); tablet_tool_input_t *tool_input = static_cast(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*/) { CLOG_INFO(LOG, 2, "hardware_serial"); } 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*/) { CLOG_INFO(LOG, 2, "hardware_id_wacom"); } static void tablet_tool_handle_capability(void * /*data*/, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t capability) { CLOG_INFO(LOG, 2, "capability (tilt=%d, distance=%d, rotation=%d, slider=%d, wheel=%d)", (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_TILT) != 0, (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_DISTANCE) != 0, (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_ROTATION) != 0, (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_SLIDER) != 0, (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_WHEEL) != 0); } static void tablet_tool_handle_done(void * /*data*/, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) { CLOG_INFO(LOG, 2, "done"); } static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { CLOG_INFO(LOG, 2, "removed"); tablet_tool_input_t *tool_input = static_cast(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) { if (!ghost_wl_surface_own(surface)) { CLOG_INFO(LOG, 2, "proximity_in (skipped)"); return; } CLOG_INFO(LOG, 2, "proximity_in"); tablet_tool_input_t *tool_input = static_cast(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_wl_surface_user_data(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*/) { CLOG_INFO(LOG, 2, "proximity_out"); tablet_tool_input_t *tool_input = static_cast(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) { CLOG_INFO(LOG, 2, "down"); tablet_tool_input_t *tool_input = static_cast(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_wl_surface_user_data(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*/) { CLOG_INFO(LOG, 2, "up"); tablet_tool_input_t *tool_input = static_cast(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_wl_surface_user_data(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) { CLOG_INFO(LOG, 2, "motion"); tablet_tool_input_t *tool_input = static_cast(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) { const float pressure_unit = (float)pressure / 65535; CLOG_INFO(LOG, 2, "pressure (%.4f)", pressure_unit); tablet_tool_input_t *tool_input = static_cast(data); GHOST_TabletData &td = tool_input->data; td.Pressure = pressure_unit; } static void tablet_tool_handle_distance(void * /*data*/, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t distance) { CLOG_INFO(LOG, 2, "distance (distance=%u)", 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) { /* Map degrees to `-1.0..1.0`. */ const float tilt_unit[2] = { (float)(wl_fixed_to_double(tilt_x) / 90.0), (float)(wl_fixed_to_double(tilt_y) / 90.0), }; CLOG_INFO(LOG, 2, "tilt (x=%.4f, y=%.4f)", UNPACK2(tilt_unit)); tablet_tool_input_t *tool_input = static_cast(data); GHOST_TabletData &td = tool_input->data; td.Xtilt = tilt_unit[0]; td.Ytilt = tilt_unit[1]; CLAMP(td.Xtilt, -1.0f, 1.0f); CLAMP(td.Ytilt, -1.0f, 1.0f); } static void tablet_tool_handle_rotation(void * /*data*/, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const wl_fixed_t degrees) { CLOG_INFO(LOG, 2, "rotation (degrees=%.4f)", wl_fixed_to_double(degrees)); } static void tablet_tool_handle_slider(void * /*data*/, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const int32_t position) { CLOG_INFO(LOG, 2, "slider (position=%d)", position); } 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; } CLOG_INFO(LOG, 2, "wheel (clicks=%d)", clicks); tablet_tool_input_t *tool_input = static_cast(data); input_t *input = tool_input->input; if (wl_surface *focus_surface = input->tablet.wl_surface) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(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) { CLOG_INFO(LOG, 2, "button (button=%u, state=%u)", button, state); tablet_tool_input_t *tool_input = static_cast(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_wl_surface_user_data(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*/) { CLOG_INFO(LOG, 2, "frame"); tablet_tool_input_t *tool_input = static_cast(data); input_t *input = tool_input->input; /* No need to check the surfaces origin, it's already known to be owned by GHOST. */ if (wl_surface *focus_surface = input->tablet.wl_surface) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Table Seat), #zwp_tablet_seat_v2_listener * \{ */ static CLG_LogRef LOG_WL_TABLET_SEAT = {"ghost.wl.handle.tablet_seat"}; #define LOG (&LOG_WL_TABLET_SEAT) static void tablet_seat_handle_tablet_added(void * /*data*/, struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, struct zwp_tablet_v2 *id) { CLOG_INFO(LOG, 2, "tablet_added (id=%p)", id); } static void tablet_seat_handle_tool_added(void *data, struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, struct zwp_tablet_tool_v2 *id) { CLOG_INFO(LOG, 2, "tool_added (id=%p)", id); input_t *input = static_cast(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()); ghost_wl_surface_tag_cursor_tablet(tool_input->cursor_surface); 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) { CLOG_INFO(LOG, 2, "pad_added (id=%p)", id); } static 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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Keyboard), #wl_keyboard_listener * \{ */ static CLG_LogRef LOG_WL_KEYBOARD = {"ghost.wl.handle.keyboard"}; #define LOG (&LOG_WL_KEYBOARD) 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(data); if ((!data) || (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)) { CLOG_INFO(LOG, 2, "keymap (no data or wrong version)"); close(fd); return; } char *map_str = static_cast(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) { CLOG_INFO(LOG, 2, "keymap (not found)"); return; } CLOG_INFO(LOG, 2, "keymap"); /* 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); } } input->xkb_keymap_mod_index.shift = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_SHIFT); input->xkb_keymap_mod_index.caps = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_CAPS); input->xkb_keymap_mod_index.ctrl = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_CTRL); input->xkb_keymap_mod_index.alt = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_ALT); input->xkb_keymap_mod_index.num = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM); input->xkb_keymap_mod_index.logo = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_LOGO); 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) { if (!ghost_wl_surface_own(surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); input_t *input = static_cast(data); input->keyboard.serial = serial; input->keyboard.wl_surface = surface; if (keys->size != 0) { /* If there are any keys held when activating the window, * modifiers will be compared against the input state, * only enabling modifiers that were previously disabled. */ const xkb_mod_mask_t state = xkb_state_serialize_mods(input->xkb_state, XKB_STATE_MODS_ALL); uint32_t *key; WL_ARRAY_FOR_EACH (key, keys) { const xkb_keycode_t key_code = *key + EVDEV_OFFSET; const xkb_keysym_t sym = xkb_state_key_get_one_sym(input->xkb_state, key_code); GHOST_TKey gkey = GHOST_kKeyUnknown; #define MOD_TEST(state, mod) ((mod != XKB_MOD_INVALID) && (state & (1 << mod))) #define MOD_TEST_CASE(xkb_key, ghost_key, mod_index) \ case xkb_key: \ if (!MOD_TEST(state, input->xkb_keymap_mod_index.mod_index)) { \ gkey = ghost_key; \ } \ break switch (sym) { MOD_TEST_CASE(XKB_KEY_Shift_L, GHOST_kKeyLeftShift, shift); MOD_TEST_CASE(XKB_KEY_Shift_R, GHOST_kKeyRightShift, shift); MOD_TEST_CASE(XKB_KEY_Control_L, GHOST_kKeyLeftControl, ctrl); MOD_TEST_CASE(XKB_KEY_Control_R, GHOST_kKeyRightControl, ctrl); MOD_TEST_CASE(XKB_KEY_Alt_L, GHOST_kKeyLeftAlt, alt); MOD_TEST_CASE(XKB_KEY_Alt_R, GHOST_kKeyRightAlt, alt); MOD_TEST_CASE(XKB_KEY_Super_L, GHOST_kKeyOS, logo); MOD_TEST_CASE(XKB_KEY_Super_R, GHOST_kKeyOS, logo); } #undef MOD_TEST #undef MOD_TEST_CASE if (gkey != GHOST_kKeyUnknown) { GHOST_IWindow *win = ghost_wl_surface_user_data(surface); GHOST_SystemWayland *system = input->system; input->system->pushEvent( new GHOST_EventKey(system->getMilliSeconds(), GHOST_kEventKeyDown, win, gkey, false)); } } } } /** * 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) { if (!(surface && ghost_wl_surface_own(surface))) { CLOG_INFO(LOG, 2, "leave (skipped)"); return; } CLOG_INFO(LOG, 2, "leave"); input_t *input = static_cast(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(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(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) { CLOG_INFO(LOG, 2, "key (no symbol, skipped)"); return; } CLOG_INFO(LOG, 2, "key"); 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( 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_wl_surface_user_data(focus_surface); input->system->pushEvent( new GHOST_EventKey(input->system->getMilliSeconds(), etype, win, gkey, false, utf8_buf)); } /* 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( task->getUserData()); input_t *input = payload->input; if (wl_surface *focus_surface = input->keyboard.wl_surface) { GHOST_IWindow *win = ghost_wl_surface_user_data(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, true, utf8_buf)); } }; 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) { CLOG_INFO(LOG, 2, "modifiers (depressed=%u, latched=%u, locked=%u, group=%u)", mods_depressed, mods_latched, mods_locked, group); input_t *input = static_cast(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) { CLOG_INFO(LOG, 2, "info (rate=%d, delay=%d)", rate, delay); input_t *input = static_cast(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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Seat), #wl_seat_listener * \{ */ static CLG_LogRef LOG_WL_SEAT = {"ghost.wl.handle.seat"}; #define LOG (&LOG_WL_SEAT) static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, const uint32_t capabilities) { CLOG_INFO(LOG, 2, "capabilities (pointer=%d, keyboard=%d, touch=%d)", (capabilities & WL_SEAT_CAPABILITY_POINTER) != 0, (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0, (capabilities & WL_SEAT_CAPABILITY_TOUCH) != 0); input_t *input = static_cast(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; 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); ghost_wl_surface_tag_cursor_pointer(input->cursor.wl_surface); } 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) { CLOG_INFO(LOG, 2, "name (name=\"%s\")", name); static_cast(data)->name = std::string(name); } static const struct wl_seat_listener seat_listener = { seat_handle_capabilities, seat_handle_name, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (XDG Output), #zxdg_output_v1_listener * \{ */ static CLG_LogRef LOG_WL_XDG_OUTPUT = {"ghost.wl.handle.xdg_output"}; #define LOG (&LOG_WL_XDG_OUTPUT) static void xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 * /*xdg_output*/, const int32_t x, const int32_t y) { CLOG_INFO(LOG, 2, "logical_position [%d, %d]", x, y); output_t *output = static_cast(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) { CLOG_INFO(LOG, 2, "logical_size [%d, %d]", width, height); output_t *output = static_cast(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\n"); #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*/) { CLOG_INFO(LOG, 2, "done"); /* 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(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) { CLOG_INFO(LOG, 2, "name (name=\"%s\")", name); } static void xdg_output_handle_description(void * /*data*/, struct zxdg_output_v1 * /*xdg_output*/, const char *description) { CLOG_INFO(LOG, 2, "description (description=\"%s\")", description); } 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, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Output), #wl_output_listener * \{ */ static CLG_LogRef LOG_WL_OUTPUT = {"ghost.wl.handle.output"}; #define LOG (&LOG_WL_OUTPUT) 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) { CLOG_INFO(LOG, 2, "geometry (make=\"%s\", model=\"%s\", transform=%d, size=[%d, %d])", make, model, transform, physical_width, physical_height); output_t *output = static_cast(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*/) { if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) { CLOG_INFO(LOG, 2, "mode (skipped)"); return; } CLOG_INFO(LOG, 2, "mode (size=[%d, %d], flags=%u)", width, height, flags); output_t *output = static_cast(data); 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*/) { CLOG_INFO(LOG, 2, "done"); output_t *output = static_cast(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) { CLOG_INFO(LOG, 2, "scale"); static_cast(data)->scale = factor; if (window_manager) { for (GHOST_IWindow *iwin : window_manager->getWindows()) { GHOST_WindowWayland *win = static_cast(iwin); win->outputs_changed_update_scale(); } } } static const struct wl_output_listener output_listener = { output_handle_geometry, output_handle_mode, output_handle_done, output_handle_scale, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (XDG WM Base), #xdg_wm_base_listener * \{ */ #ifndef WITH_GHOST_WAYLAND_LIBDECOR static CLG_LogRef LOG_WL_XDG_WM_BASE = {"ghost.wl.handle.output"}; # define LOG (&LOG_WL_XDG_WM_BASE) static void shell_handle_ping(void * /*data*/, struct xdg_wm_base *xdg_wm_base, const uint32_t serial) { CLOG_INFO(LOG, 2, "ping"); xdg_wm_base_pong(xdg_wm_base, serial); } static const struct xdg_wm_base_listener shell_listener = { shell_handle_ping, }; # undef LOG #endif /* !WITH_GHOST_WAYLAND_LIBDECOR. */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (LibDecor), #libdecor_interface * \{ */ #ifdef WITH_GHOST_WAYLAND_LIBDECOR static CLG_LogRef LOG_WL_LIBDECOR = {"ghost.wl.handle.libdecor"}; # define LOG (&LOG_WL_LIBDECOR) static void decor_handle_error(struct libdecor * /*context*/, enum libdecor_error error, const char *message) { CLOG_INFO(LOG, 2, "error (id=%d, message=%s)", error, 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, }; # undef LOG #endif /* WITH_GHOST_WAYLAND_LIBDECOR. */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Registry), #wl_registry_listener * \{ */ static CLG_LogRef LOG_WL_REGISTRY = {"ghost.wl.handle.registry"}; #define LOG (&LOG_WL_REGISTRY) static void global_handle_add(void *data, struct wl_registry *wl_registry, const uint32_t name, const char *interface, const uint32_t version) { /* Log last since it can be noted if the interface was handled or not. */ bool found = true; struct display_t *display = static_cast(data); if (!strcmp(interface, wl_compositor_interface.name)) { display->compositor = static_cast( 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( 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( 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( 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_registry_bind(wl_registry, name, &wl_output_interface, 2)); ghost_wl_output_tag(output->wl_output); wl_output_set_user_data(output->wl_output, output); 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_registry_bind(wl_registry, name, &wl_seat_interface, 5)); 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_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_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( 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( 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( wl_registry_bind(wl_registry, name, &zwp_pointer_constraints_v1_interface, 1)); } else { found = false; } CLOG_INFO(LOG, 2, "add %s(interface=%s, version=%u, name=%u)", found ? "" : "(skipped), ", interface, version, name); } /** * 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) { CLOG_INFO(LOG, 2, "remove (name=%u)", name); } static const struct wl_registry_listener registry_listener = { global_handle_add, global_handle_remove, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \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, ®istry_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); } GHOST_TSuccess GHOST_SystemWayland::init() { GHOST_TSuccess success = GHOST_System::init(); if (success) { #ifdef WITH_INPUT_NDOF m_ndofManager = new GHOST_NDOFManagerUnix(*this); #endif return GHOST_kSuccess; } return GHOST_kFailure; } bool GHOST_SystemWayland::processEvents(bool waitForEvent) { bool any_processed = false; if (getTimerManager()->fireTimers(getMilliSeconds())) { any_processed = true; } #ifdef WITH_INPUT_NDOF if (static_cast(m_ndofManager)->processEvents()) { /* As NDOF bypasses WAYLAND event handling, * never wait for an event when an NDOF event was found. */ waitForEvent = false; any_processed = true; } #endif /* WITH_INPUT_NDOF */ if (waitForEvent) { wl_display_dispatch(d->display); } else { wl_display_roundtrip(d->display); } if ((getEventManager()->getNumEvents() > 0)) { any_processed = true; } return any_processed; } bool GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*action*/) { return false; } GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const { if (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } input_t *input = d->inputs[0]; bool val; /* NOTE: XKB doesn't differentiate between left/right modifiers * for it's internal modifier state storage. */ const xkb_mod_mask_t state = xkb_state_serialize_mods(input->xkb_state, XKB_STATE_MODS_ALL); #define MOD_TEST(state, mod) ((mod != XKB_MOD_INVALID) && (state & (1 << mod))) val = MOD_TEST(state, input->xkb_keymap_mod_index.shift); keys.set(GHOST_kModifierKeyLeftShift, val); keys.set(GHOST_kModifierKeyRightShift, val); val = MOD_TEST(state, input->xkb_keymap_mod_index.alt); keys.set(GHOST_kModifierKeyLeftAlt, val); keys.set(GHOST_kModifierKeyRightAlt, val); val = MOD_TEST(state, input->xkb_keymap_mod_index.ctrl); keys.set(GHOST_kModifierKeyLeftControl, val); keys.set(GHOST_kModifierKeyRightControl, val); val = MOD_TEST(state, input->xkb_keymap_mod_index.logo); keys.set(GHOST_kModifierKeyOS, val); val = MOD_TEST(state, input->xkb_keymap_mod_index.num); keys.set(GHOST_kModifierKeyNum, val); #undef MOD_TEST return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const { if (UNLIKELY(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(malloc((selection.size() + 1))); memcpy(clipboard, selection.data(), selection.size() + 1); return clipboard; } void GHOST_SystemWayland::putClipboard(const char *buffer, bool /*selection*/) const { if (UNLIKELY(!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(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 (UNLIKELY(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(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 (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } input_t *input = d->inputs[0]; GHOST_WindowWayland *win = static_cast(window); return setCursorPositionClientRelative_impl(input, win, x, y); } GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const { if (UNLIKELY(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_wl_surface_user_data(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 (UNLIKELY(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_wl_surface_user_data(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; if (input->wl_pointer) { const int scale = c->is_custom ? c->custom_scale : input->pointer.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; if (input->wl_pointer) { wl_pointer_set_cursor( input->wl_pointer, input->pointer.serial, c->wl_surface, hotspot_x, hotspot_y); } } if (!input->tablet_tools.empty()) { const int scale = c->is_custom ? c->custom_scale : input->tablet.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; for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) { tablet_tool_input_t *tool_input = static_cast( 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); } } /** * Needed to ensure the cursor size is always a multiple of scale. */ static int cursor_buffer_compatible_scale_from_image(const struct wl_cursor_image *wl_image, int scale) { const int32_t image_size_x = int32_t(wl_image->width); const int32_t image_size_y = int32_t(wl_image->height); while (scale > 1) { if ((image_size_x % scale) == 0 && (image_size_y % scale) == 0) { break; } scale -= 1; } return scale; } static void cursor_buffer_set_surface_impl(const input_t *input, wl_buffer *buffer, struct wl_surface *wl_surface, const int scale) { const wl_cursor_image *wl_image = &input->cursor.wl_image; const int32_t image_size_x = int32_t(wl_image->width); const int32_t image_size_y = int32_t(wl_image->height); GHOST_ASSERT((image_size_x % scale) == 0 && (image_size_y % scale) == 0, "The size must be a multiple of the scale!"); wl_surface_set_buffer_scale(wl_surface, scale); wl_surface_attach(wl_surface, buffer, 0, 0); wl_surface_damage(wl_surface, 0, 0, image_size_x, image_size_y); wl_surface_commit(wl_surface); } static void cursor_buffer_set(const input_t *input, wl_buffer *buffer) { const cursor_t *c = &input->cursor; const wl_cursor_image *wl_image = &input->cursor.wl_image; const bool visible = (c->visible && c->is_hardware); /* This is a requirement of WAYLAND, when this isn't the case, * it causes Blender's window to close intermittently. */ if (input->wl_pointer) { const int scale = cursor_buffer_compatible_scale_from_image( wl_image, c->is_custom ? c->custom_scale : input->pointer.theme_scale); const int32_t hotspot_x = int32_t(wl_image->hotspot_x) / scale; const int32_t hotspot_y = int32_t(wl_image->hotspot_y) / scale; cursor_buffer_set_surface_impl(input, buffer, c->wl_surface, scale); 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. */ if (!input->tablet_tools.empty()) { const int scale = cursor_buffer_compatible_scale_from_image( wl_image, c->is_custom ? c->custom_scale : input->tablet.theme_scale); const int32_t hotspot_x = int32_t(wl_image->hotspot_x) / scale; const int32_t hotspot_y = int32_t(wl_image->hotspot_y) / scale; for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) { tablet_tool_input_t *tool_input = static_cast( zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); cursor_buffer_set_surface_impl(input, buffer, tool_input->cursor_surface, scale); 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 (UNLIKELY(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 (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } cursor_t *cursor = &d->inputs[0]->cursor; if (cursor->custom_data) { munmap(cursor->custom_data, cursor->custom_data_size); cursor->custom_data = nullptr; cursor->custom_data_size = 0; /* Not needed, but the value is no longer meaningful. */ } const int32_t size_xy[2] = {sizex, sizey}; wl_buffer *buffer = ghost_wl_buffer_create_for_image( d->shm, size_xy, WL_SHM_FORMAT_ARGB8888, &cursor->custom_data, &cursor->custom_data_size); if (buffer == nullptr) { return GHOST_kFailure; } 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(cursor->custom_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->custom_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(cursor->custom_data); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::setCursorVisibility(const bool visible) { if (UNLIKELY(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 (UNLIKELY(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_wl_surface_user_data(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 = ELEM(mode, GHOST_kGrabWrap, GHOST_kGrabHide) || use_software_confine, .use_confine = (mode == GHOST_kGrabNormal) && (use_software_confine == false), }; return grab_state; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Proxy Ownership API * \{ */ static const char *ghost_wl_output_tag_id = "GHOST-output"; static const char *ghost_wl_surface_tag_id = "GHOST-window"; static const char *ghost_wl_surface_cursor_pointer_tag_id = "GHOST-cursor-pointer"; static const char *ghost_wl_surface_cursor_tablet_tag_id = "GHOST-cursor-tablet"; bool ghost_wl_output_own(const struct wl_output *output) { return wl_proxy_get_tag((struct wl_proxy *)output) == &ghost_wl_output_tag_id; } bool ghost_wl_surface_own(const struct wl_surface *surface) { return wl_proxy_get_tag((struct wl_proxy *)surface) == &ghost_wl_surface_tag_id; } bool ghost_wl_surface_own_cursor_pointer(const struct wl_surface *surface) { return wl_proxy_get_tag((struct wl_proxy *)surface) == &ghost_wl_surface_cursor_pointer_tag_id; } bool ghost_wl_surface_own_cursor_tablet(const struct wl_surface *surface) { return wl_proxy_get_tag((struct wl_proxy *)surface) == &ghost_wl_surface_cursor_tablet_tag_id; } void ghost_wl_output_tag(struct wl_output *output) { wl_proxy_set_tag((struct wl_proxy *)output, &ghost_wl_output_tag_id); } void ghost_wl_surface_tag(struct wl_surface *surface) { wl_proxy_set_tag((struct wl_proxy *)surface, &ghost_wl_surface_tag_id); } void ghost_wl_surface_tag_cursor_pointer(struct wl_surface *surface) { wl_proxy_set_tag((struct wl_proxy *)surface, &ghost_wl_surface_cursor_pointer_tag_id); } void ghost_wl_surface_tag_cursor_tablet(struct wl_surface *surface) { wl_proxy_set_tag((struct wl_proxy *)surface, &ghost_wl_surface_cursor_tablet_tag_id); } /** \} */ /* -------------------------------------------------------------------- */ /** \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 &GHOST_SystemWayland::outputs() const { return d->outputs; } wl_shm *GHOST_SystemWayland::shm() const { return d->shm; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Query Access * \{ */ struct output_t *ghost_wl_output_user_data(struct wl_output *wl_output) { GHOST_ASSERT(wl_output, "output must not be NULL"); GHOST_ASSERT(ghost_wl_output_own(wl_output), "output is not owned by GHOST"); output_t *output = static_cast(wl_output_get_user_data(wl_output)); return output; } GHOST_WindowWayland *ghost_wl_surface_user_data(struct wl_surface *surface) { GHOST_ASSERT(surface, "surface must not be NULL"); GHOST_ASSERT(ghost_wl_surface_own(surface), "surface is not owned by GHOST"); GHOST_WindowWayland *win = static_cast(wl_surface_get_user_data(surface)); return win; } /** \} */ /* -------------------------------------------------------------------- */ /** \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 } bool GHOST_SystemWayland::window_cursor_grab_set(const GHOST_TGrabCursorMode mode, const GHOST_TGrabCursorMode mode_current, int32_t init_grab_xy[2], const GHOST_Rect *wrap_bounds, const GHOST_TAxisFlag wrap_axis, wl_surface *surface, const int scale) { /* Ignore, if the required protocols are not supported. */ if (UNLIKELY(!d->relative_pointer_manager || !d->pointer_constraints)) { return GHOST_kFailure; } if (UNLIKELY(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 = !(ELEM(mode, GHOST_kGrabHide, 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) { /* Potentially add a motion event so the application has updated X/Y coordinates. */ int32_t xy_motion[2] = {0, 0}; bool xy_motion_create_event = false; /* 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. */ int32_t xy_next[2] = {UNPACK2(input->pointer.xy)}; GHOST_Rect bounds_scale; bounds_scale.m_l = wl_fixed_from_int(wrap_bounds->m_l) / scale; bounds_scale.m_t = wl_fixed_from_int(wrap_bounds->m_t) / scale; bounds_scale.m_r = wl_fixed_from_int(wrap_bounds->m_r) / scale; bounds_scale.m_b = wl_fixed_from_int(wrap_bounds->m_b) / scale; bounds_scale.wrapPoint(UNPACK2(xy_next), 0, wrap_axis); /* Push an event so the new location is registered. */ if ((xy_next[0] != input->pointer.xy[0]) || (xy_next[1] != input->pointer.xy[1])) { xy_motion[0] = xy_next[0]; xy_motion[1] = xy_next[1]; xy_motion_create_event = true; } input->pointer.xy[0] = xy_next[0]; input->pointer.xy[1] = xy_next[1]; zwp_locked_pointer_v1_set_cursor_position_hint(input->locked_pointer, UNPACK2(xy_next)); 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])) { 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, UNPACK2(xy_next)); wl_surface_commit(surface); /* NOTE(@campbellbarton): The new cursor position is a hint, * it's possible the hint is ignored. It doesn't seem like there is a good way to * know if the hint will be used or not, at least not immediately. */ xy_motion[0] = xy_next[0]; xy_motion[1] = xy_next[1]; xy_motion_create_event = true; } } #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, UNPACK2(input->pointer.xy)); wl_surface_commit(surface); } } #endif if (xy_motion_create_event) { input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), GHOST_kEventCursorMove, ghost_wl_surface_user_data(surface), wl_fixed_to_int(scale * xy_motion[0]), wl_fixed_to_int(scale * xy_motion[1]), GHOST_TABLET_DATA_NONE)); } 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. */ 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; } #ifdef WITH_GHOST_WAYLAND_DYNLOAD bool ghost_wl_dynload_libraries() { /* Only report when `libwayland-client` is not found when building without X11, * which will be used as a fallback. */ # ifdef WITH_GHOST_X11 bool verbose = false; # else bool verbose = true; # endif if (wayland_dynload_client_init(verbose) && /* `libwayland-client`. */ wayland_dynload_cursor_init(verbose) && /* `libwayland-cursor`. */ wayland_dynload_egl_init(verbose) && /* `libwayland-egl`. */ # ifdef WITH_GHOST_WAYLAND_LIBDECOR wayland_dynload_libdecor_init(verbose) && /* `libdecor-0`. */ # endif true) { return true; } # ifdef WITH_GHOST_WAYLAND_LIBDECOR wayland_dynload_libdecor_exit(); # endif wayland_dynload_client_exit(); wayland_dynload_cursor_exit(); wayland_dynload_egl_exit(); return false; } #endif /* WITH_GHOST_WAYLAND_DYNLOAD */ /** \} */