/* 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_EventTrackpad.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 /* Decorations `xdg_decor`. */ #include #include /* End `xdg_decor`. */ #include #include #include #include #include /* Logging, use `ghost.wl.*` prefix. */ #include "CLG_log.h" #ifdef WITH_GHOST_WAYLAND_LIBDECOR static bool use_libdecor = true; # ifdef WITH_GHOST_WAYLAND_DYNLOAD static bool has_libdecor = false; # else static bool has_libdecor = true; # endif #endif static void keyboard_handle_key_repeat_cancel(struct GWL_Seat *seat); static void output_handle_done(void *data, struct wl_output *wl_output); static void gwl_seat_capability_pointer_disable(GWL_Seat *seat); static void gwl_seat_capability_keyboard_disable(GWL_Seat *seat); static void gwl_seat_capability_touch_disable(GWL_Seat *seat); static bool gwl_registry_entry_remove_by_name(GWL_Display *display, uint32_t name, int *r_interface_slot); static void gwl_registry_entry_remove_all(GWL_Display *display); struct GWL_RegistryHandler; static int gwl_registry_handler_interface_slot_max(); static int gwl_registry_handler_interface_slot_from_string(const char *interface); static const struct GWL_RegistryHandler *gwl_registry_handler_from_interface_slot( int interface_slot); /* -------------------------------------------------------------------- */ /** \name Workaround Compositor Specific Bugs * \{ */ /** * 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 /** * GNOME (mutter 42.5) doesn't follow the WAYLAND spec regarding keyboard handling, * unlike (other compositors: KDE-plasma, River & Sway which work without problems). * * This means GNOME can't know which modifiers are held when activating windows, * so we guess the left modifiers are held. * * This define could be removed without changing any functionality, * it just means GNOME users will see verbose warning messages that alert them about * a known problem that needs to be fixed up-stream. * * This has been fixed for GNOME 43. Keep the workaround until support for gnome 42 is dropped. * See: https://gitlab.gnome.org/GNOME/mutter/-/issues/2457 */ #define USE_GNOME_KEYBOARD_SUPPRESS_WARNING /** * KDE (plasma 5.26.1) has a bug where the cursor surface needs to be committed * (via `wl_surface_commit`) when it was hidden and is being set to visible again, see: T102048. * See: https://bugs.kde.org/show_bug.cgi?id=461001 */ #define USE_KDE_TABLET_HIDDEN_CURSOR_HACK /** * When GNOME is found, require `libdecor`. * This is a hack because it seems there is no way to check if the compositor supports * server side decorations when initializing WAYLAND. */ #if defined(WITH_GHOST_WAYLAND_LIBDECOR) && defined(WITH_GHOST_X11) # define USE_GNOME_NEEDS_LIBDECOR_HACK #endif /* -------------------------------------------------------------------- */ /** \name Local Defines * * Control local functionality, compositors specific workarounds. * \{ */ /** * Fix short-cut part of keyboard reading code not properly handling some keys, see: T102194. * \note This is similar to X11 workaround by the same name, see: T47228. */ #define USE_NON_LATIN_KB_WORKAROUND #define WL_NAME_UNSET uint32_t(-1) /** \} */ /* -------------------------------------------------------------------- */ /** \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 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. * * \note Gnome/GTK swap middle/right, where the same application in X11 will swap the middle/right * mouse button when running under WAYLAND. KDE doesn't do this, and according to artists * at the Blender studio, having the button closest to the nib be MMB is preferable, * so use this as a default. If needs be - swapping these could be a preference. */ #define BTN_STYLUS 0x14b /* Use as middle-mouse. */ #define BTN_STYLUS2 0x14c /* Use as right-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 #ifdef USE_NON_LATIN_KB_WORKAROUND # define KEY_1 2 # define KEY_2 3 # define KEY_3 4 # define KEY_4 5 # define KEY_5 6 # define KEY_6 7 # define KEY_7 8 # define KEY_8 9 # define KEY_9 10 # define KEY_0 11 #endif /** \} */ /* -------------------------------------------------------------------- */ /** \name Modifier Table * * Convenient access to modifier key values, allow looping over modifier keys. * \{ */ enum { MOD_INDEX_SHIFT = 0, MOD_INDEX_ALT = 1, MOD_INDEX_CTRL = 2, MOD_INDEX_OS = 3, }; #define MOD_INDEX_NUM (MOD_INDEX_OS + 1) struct GWL_ModifierInfo { /** Only for printing messages. */ const char *display_name; const char *xkb_id; GHOST_TKey key_l, key_r; GHOST_TModifierKey mod_l, mod_r; }; static const GWL_ModifierInfo g_modifier_info_table[MOD_INDEX_NUM] = { /* MOD_INDEX_SHIFT */ { /* display_name */ "Shift", /* xkb_id */ XKB_MOD_NAME_SHIFT, /* key_l */ GHOST_kKeyLeftShift, /* key_r */ GHOST_kKeyRightShift, /* mod_l */ GHOST_kModifierKeyLeftShift, /* mod_r */ GHOST_kModifierKeyRightShift, }, /* MOD_INDEX_ALT */ { /* display_name */ "Alt", /* xkb_id */ XKB_MOD_NAME_ALT, /* key_l */ GHOST_kKeyLeftAlt, /* key_r */ GHOST_kKeyRightAlt, /* mod_l */ GHOST_kModifierKeyLeftAlt, /* mod_r */ GHOST_kModifierKeyRightAlt, }, /* MOD_INDEX_CTRL */ { /* display_name */ "Control", /* xkb_id */ XKB_MOD_NAME_CTRL, /* key_l */ GHOST_kKeyLeftControl, /* key_r */ GHOST_kKeyRightControl, /* mod_l */ GHOST_kModifierKeyLeftControl, /* mod_r */ GHOST_kModifierKeyRightControl, }, /* MOD_INDEX_OS */ { /* display_name */ "OS", /* xkb_id */ XKB_MOD_NAME_LOGO, /* key_l */ GHOST_kKeyLeftOS, /* key_r */ GHOST_kKeyRightOS, /* mod_l */ GHOST_kModifierKeyLeftOS, /* mod_r */ GHOST_kModifierKeyRightOS, }, }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_SimpleBuffer Type * \{ */ struct GWL_SimpleBuffer { /** Constant data, but may be freed. */ const char *data = nullptr; size_t data_size = 0; }; static void gwl_simple_buffer_free_data(GWL_SimpleBuffer *buffer) { free(const_cast(buffer->data)); buffer->data = nullptr; buffer->data_size = 0; } static void gwl_simple_buffer_set_from_string(GWL_SimpleBuffer *buffer, const char *str) { free(const_cast(buffer->data)); buffer->data_size = strlen(str); char *data = static_cast(malloc(buffer->data_size)); std::memcpy(data, str, buffer->data_size); buffer->data = data; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_Cursor Type * \{ */ /** * 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 GWL_Cursor { 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; /** When true, a custom image is used to display the cursor (stored in `wl_image`). */ bool is_custom = false; struct wl_surface *wl_surface_cursor = 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; /** The size of `custom_data` in bytes. */ size_t custom_data_size = 0; /** * The name of the theme (loaded by DBUS, depends on #WITH_GHOST_WAYLAND_DBUS). * When disabled, leave as an empty string and the default theme will be used. */ std::string theme_name; /** * The size of the cursor (when looking up a cursor theme). * This must be scaled by the maximum output scale when passing to wl_cursor_theme_load. * See #update_cursor_scale. * */ int theme_size = 0; 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 GWL_TabletTool { struct GWL_Seat *seat = nullptr; /** Tablets have a separate cursor to the 'pointer', this surface is used for cursor drawing. */ struct wl_surface *wl_surface_cursor = nullptr; /** Used to delay clearing tablet focused wl_surface until the frame is handled. */ bool proximity = false; GHOST_TabletData data = GHOST_TABLET_DATA_NONE; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_DataOffer Type * \{ */ /** * Data storage used for clipboard paste & drag-and-drop. */ struct GWL_DataOffer { struct wl_data_offer *id = nullptr; std::unordered_set types; struct { /** * Prevents freeing after #wl_data_device_listener.leave, * before #wl_data_device_listener.drop. */ bool in_use = false; /** * Bit-mask with available drop options. * #WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, #WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE.. etc. * The application that initializes the drag may set these depending on modifiers held * \note when dragging begins. Currently ghost doesn't make use of these. */ enum wl_data_device_manager_dnd_action source_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; enum wl_data_device_manager_dnd_action action = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; /** Compatible with #GWL_Seat.xy coordinates. */ wl_fixed_t xy[2] = {0, 0}; } dnd; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_DataSource Type * \{ */ struct GWL_DataSource { struct wl_data_source *wl_source = nullptr; GWL_SimpleBuffer buffer_out; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_Seat Type (#wl_seat wrapper & associated types) * \{ */ /** * 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 #GWL_Seat which is cleared when windows are closed. * Therefor keyboard events must always check the window has not been cleared. */ struct GWL_KeyRepeatPlayload { struct GWL_Seat *seat = 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 GWL_SeatStateGrab { bool use_lock = false; bool use_confine = false; }; /** * State of the pointing device (tablet or mouse). */ struct GWL_SeatStatePointer { /** * 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 * seat_state_pointer->xy[0]), * wl_fixed_to_int(scale * seat_state_pointer->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 wl_surface last used with this pointing device * (events with this pointing device will be sent here). */ struct wl_surface *wl_surface_window = nullptr; GHOST_Buttons buttons = GHOST_Buttons(); }; /** * Scroll state, applying to pointer (not tablet) events. * Otherwise this would be part of #GWL_SeatStatePointer. */ struct GWL_SeatStatePointerScroll { /** Smooth scrolling (handled & reset with pointer "frame" callback). */ wl_fixed_t smooth_xy[2] = {0, 0}; /** Discrete scrolling (handled & reset with pointer "frame" callback). */ int32_t discrete_xy[2] = {0, 0}; /** The source of scroll event. */ enum wl_pointer_axis_source axis_source = WL_POINTER_AXIS_SOURCE_WHEEL; }; /** * Utility struct to access rounded values from a scaled `wl_fixed_t`, * without loosing information. * * As the rounded result is rounded to a lower precision integer, * the high precision value is accumulated and converted to an integer to * prevent the accumulation of rounded values giving an inaccurate result. * * \note This is simple but doesn't read well when expanded multiple times inline. */ struct GWL_ScaledFixedT { wl_fixed_t value = 0; wl_fixed_t factor = 1; }; static int gwl_scaled_fixed_t_add_and_calc_rounded_delta(GWL_ScaledFixedT *sf, const wl_fixed_t add) { const int result_prev = wl_fixed_to_int(sf->value * sf->factor); sf->value += add; const int result_curr = wl_fixed_to_int(sf->value * sf->factor); return result_curr - result_prev; } /** * Gesture state. * This is needed so the gesture values can be converted to deltas. */ struct GWL_SeatStatePointerGesture_Pinch { GWL_ScaledFixedT scale; GWL_ScaledFixedT rotation; }; /** * State of the keyboard (in #GWL_Seat). */ struct GWL_SeatStateKeyboard { /** The serial of the last used pointer or tablet. */ uint32_t serial = 0; /** * The wl_surface last used with this pointing device * (events with this pointing device will be sent here). */ struct wl_surface *wl_surface_window = nullptr; }; /** * Store held keys (only modifiers), could store other keys in the future. * * Needed as #GWL_Seat.xkb_state doesn't store which modifier keys are held. */ struct GWL_KeyboardDepressedState { int16_t mods[GHOST_KEY_MODIFIER_NUM] = {0}; }; #ifdef WITH_GHOST_WAYLAND_LIBDECOR struct GWL_LibDecor_System { struct libdecor *context = nullptr; }; static void gwl_libdecor_system_destroy(GWL_LibDecor_System *decor) { if (decor->context) { libdecor_unref(decor->context); decor->context = nullptr; } delete decor; } #endif struct GWL_XDG_Decor_System { struct xdg_wm_base *shell = nullptr; uint32_t shell_name = WL_NAME_UNSET; struct zxdg_decoration_manager_v1 *manager = nullptr; uint32_t manager_name = WL_NAME_UNSET; }; static void gwl_xdg_decor_system_destroy(struct GWL_Display *display, GWL_XDG_Decor_System *decor) { if (decor->manager) { gwl_registry_entry_remove_by_name(display, decor->manager_name, nullptr); GHOST_ASSERT(decor->manager == nullptr, "Internal registry error"); } if (decor->shell) { gwl_registry_entry_remove_by_name(display, decor->shell_name, nullptr); GHOST_ASSERT(decor->shell == nullptr, "Internal registry error"); } delete decor; } struct GWL_PrimarySelection_DataOffer { struct zwp_primary_selection_offer_v1 *id = nullptr; std::unordered_set types; }; struct GWL_PrimarySelection_DataSource { struct zwp_primary_selection_source_v1 *wp_source = nullptr; GWL_SimpleBuffer buffer_out; }; /** Primary selection support. */ struct GWL_PrimarySelection { GWL_PrimarySelection_DataSource *data_source = nullptr; std::mutex data_source_mutex; GWL_PrimarySelection_DataOffer *data_offer = nullptr; std::mutex data_offer_mutex; }; static void gwl_primary_selection_discard_offer(GWL_PrimarySelection *primary) { if (primary->data_offer == nullptr) { return; } zwp_primary_selection_offer_v1_destroy(primary->data_offer->id); delete primary->data_offer; primary->data_offer = nullptr; } static void gwl_primary_selection_discard_source(GWL_PrimarySelection *primary) { GWL_PrimarySelection_DataSource *data_source = primary->data_source; if (data_source == nullptr) { return; } gwl_simple_buffer_free_data(&data_source->buffer_out); if (data_source->wp_source) { zwp_primary_selection_source_v1_destroy(data_source->wp_source); } delete primary->data_source; primary->data_source = nullptr; } struct GWL_Seat { GHOST_SystemWayland *system = nullptr; std::string name; struct wl_seat *wl_seat = nullptr; struct wl_pointer *wl_pointer = nullptr; struct wl_touch *wl_touch = nullptr; struct wl_keyboard *wl_keyboard = nullptr; struct zwp_tablet_seat_v2 *wp_tablet_seat = nullptr; #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE struct zwp_pointer_gesture_hold_v1 *wp_pointer_gesture_hold = nullptr; #endif #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch = nullptr; #endif #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE struct zwp_pointer_gesture_swipe_v1 *wp_pointer_gesture_swipe = nullptr; #endif /** 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; GWL_SeatStatePointer pointer; GWL_SeatStatePointerScroll pointer_scroll; GWL_SeatStatePointerGesture_Pinch pointer_gesture_pinch; /** Mostly this can be interchanged with `pointer` however it can't be locked/confined. */ GWL_SeatStatePointer tablet; GWL_SeatStateKeyboard 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 GWL_Cursor cursor; struct zwp_relative_pointer_v1 *wp_relative_pointer = nullptr; struct zwp_locked_pointer_v1 *wp_locked_pointer = nullptr; struct zwp_confined_pointer_v1 *wp_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 shift enabled, use to access predictable number access for AZERTY keymaps. * If shift is not supported by the key-map, this is set to NULL. */ struct xkb_state *xkb_state_empty_with_shift = 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; #ifdef USE_NON_LATIN_KB_WORKAROUND bool xkb_use_non_latin_workaround = false; #endif /** Keys held matching `xkb_state`. */ struct GWL_KeyboardDepressedState key_depressed; #ifdef USE_GNOME_KEYBOARD_SUPPRESS_WARNING struct { bool any_mod_held = false; bool any_keys_held_on_enter = false; } key_depressed_suppress_warning; #endif /** * 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. */ xkb_mod_index_t xkb_keymap_mod_index[MOD_INDEX_NUM]; 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 *wl_surface_window_focus_dnd = nullptr; struct wl_data_device *wl_data_device = nullptr; /** Drag & Drop. */ struct GWL_DataOffer *data_offer_dnd = nullptr; std::mutex data_offer_dnd_mutex; /** Copy & Paste. */ struct GWL_DataOffer *data_offer_copy_paste = nullptr; std::mutex data_offer_copy_paste_mutex; struct GWL_DataSource *data_source = nullptr; std::mutex data_source_mutex; struct zwp_primary_selection_device_v1 *wp_primary_selection_device = nullptr; struct GWL_PrimarySelection primary_selection; /** Last device that was active. */ uint32_t data_source_serial = 0; }; static GWL_SeatStatePointer *gwl_seat_state_pointer_active(GWL_Seat *seat) { if (seat->pointer.serial == seat->cursor_source_serial) { return &seat->pointer; } if (seat->tablet.serial == seat->cursor_source_serial) { return &seat->tablet; } return nullptr; } static GWL_SeatStatePointer *gwl_seat_state_pointer_from_cursor_surface( GWL_Seat *seat, const wl_surface *wl_surface) { if (ghost_wl_surface_own_cursor_pointer(wl_surface)) { return &seat->pointer; } if (ghost_wl_surface_own_cursor_tablet(wl_surface)) { return &seat->tablet; } GHOST_ASSERT(0, "Surface found without pointer/tablet tag"); return nullptr; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_Display Type (#wl_display & #wl_compositor wrapper) * \{ */ struct GWL_RegistryEntry; struct GWL_Display { GHOST_SystemWayland *system = nullptr; /** * True when initializing registration, while updating all other entries wont cause problems, * it will preform many redundant update calls. */ bool registry_skip_update_all = false; /** Registry entries, kept to allow updating & removal at run-time. */ struct GWL_RegistryEntry *registry_entry = nullptr; struct wl_registry *wl_registry = nullptr; struct wl_display *wl_display = nullptr; struct wl_compositor *wl_compositor = nullptr; #ifdef WITH_GHOST_WAYLAND_LIBDECOR GWL_LibDecor_System *libdecor = nullptr; bool libdecor_required = false; #endif GWL_XDG_Decor_System *xdg_decor = nullptr; struct zxdg_output_manager_v1 *xdg_output_manager = nullptr; struct wl_shm *wl_shm = nullptr; std::vector outputs; std::vector seats; /** * Support a single active seat at once, this isn't an exact or correct mapping from WAYLAND. * Only allow input from different seats, not full concurrent multi-seat support. * * The main purpose of having an active seat is an alternative from always using the first * seat which prevents events from any other seat. * * NOTE(@campbellbarton): This could be extended and developed further extended to support * an active seat per window (for e.g.), basic support is sufficient for now as currently isn't * a widely used feature. */ int seats_active_index = 0; /* Managers. */ struct wl_data_device_manager *wl_data_device_manager = nullptr; struct zwp_tablet_manager_v2 *wp_tablet_manager = nullptr; struct zwp_relative_pointer_manager_v1 *wp_relative_pointer_manager = nullptr; struct zwp_primary_selection_device_manager_v1 *wp_primary_selection_device_manager = nullptr; struct zwp_pointer_constraints_v1 *wp_pointer_constraints = nullptr; struct zwp_pointer_gestures_v1 *wp_pointer_gestures = nullptr; }; /** * Free the #GWL_Display and it's related members. * * \note This may run on a partially initialized struct, * so it can't be assumed all members are set. */ static void gwl_display_destroy(GWL_Display *display) { /* For typical WAYLAND use this will always be set. * However when WAYLAND isn't running, this will early-exit and be null. */ if (display->wl_registry) { wl_registry_destroy(display->wl_registry); display->wl_registry = nullptr; } /* Unregister items in reverse order. */ gwl_registry_entry_remove_all(display); #ifdef WITH_GHOST_WAYLAND_LIBDECOR if (use_libdecor) { if (display->libdecor) { gwl_libdecor_system_destroy(display->libdecor); display->libdecor = nullptr; } } else #endif { if (display->xdg_decor) { gwl_xdg_decor_system_destroy(display, display->xdg_decor); display->xdg_decor = nullptr; } } if (eglGetDisplay) { ::eglTerminate(eglGetDisplay(EGLNativeDisplayType(display->wl_display))); } if (display->wl_display) { wl_display_disconnect(display->wl_display); } delete display; } static int gwl_display_seat_index(GWL_Display *display, const GWL_Seat *seat) { std::vector::iterator iter = std::find( display->seats.begin(), display->seats.end(), seat); const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) : -1; GHOST_ASSERT(index != -1, "invalid internal state"); return index; } static GWL_Seat *gwl_display_seat_active_get(const GWL_Display *display) { if (UNLIKELY(display->seats.empty())) { return nullptr; } return display->seats[display->seats_active_index]; } static bool gwl_display_seat_active_set(GWL_Display *display, const GWL_Seat *seat) { if (UNLIKELY(display->seats.empty())) { return false; } const int index = gwl_display_seat_index(display, seat); if (index == display->seats_active_index) { return false; } display->seats_active_index = index; return true; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_RegistryHandler * \{ */ struct GWL_RegisteryAdd_Params { uint32_t name = 0; /** Index within `gwl_registry_handlers`. */ int interface_slot = 0; uint32_t version = 0; }; /** * Add callback for object registry. * \note Any operations that depend on other interfaces being registered must be performed in the * #GWL_RegistryHandler_UpdateFn callback as the order interfaces are added is out of our control. * * \param display: The display which holes a reference to the global object. * \param params: Various arguments needed for registration. */ using GWL_RegistryHandler_AddFn = void (*)(GWL_Display *display, const GWL_RegisteryAdd_Params *params); struct GWL_RegisteryUpdate_Params { uint32_t name = 0; /** Index within `gwl_registry_handlers`. */ int interface_slot = 0; uint32_t version = 0; /** Set to #GWL_RegistryEntry.user_data. */ void *user_data = nullptr; }; /** * Optional update callback to refresh internal data when another interface has been added/removed. * * \param display: The display which holes a reference to the global object. * \param params: Various arguments needed for updating. */ using GWL_RegistryHandler_UpdateFn = void (*)(GWL_Display *display, const GWL_RegisteryUpdate_Params *params); /** * Remove callback for object registry. * \param display: The display which holes a reference to the global object. * \param user_data: Optional reference to a sub element of `display`, * use for outputs or seats for e.g. when the display may hold multiple references. * \param on_exit: Enabled when freeing on exit. * When true the consistency of references between objects should be kept valid. * Otherwise it can be assumed that all objects will be freed and none will be used again, * so there is no need to ensure a valid state. */ using GWL_RegistryEntry_RemoveFn = void (*)(GWL_Display *display, void *user_data, bool on_exit); struct GWL_RegistryHandler { /** Pointer to the name (not the name it's self), needed as the values aren't set on startup. */ const char *const *interface_p = nullptr; /** Add the interface. */ GWL_RegistryHandler_AddFn add_fn = nullptr; /** Optional update the interface (when other interfaces have been added/removed). */ GWL_RegistryHandler_UpdateFn update_fn = nullptr; /** Remove the interface. */ GWL_RegistryEntry_RemoveFn remove_fn = nullptr; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_RegistryEntry * \{ */ /** * Registered global objects can be removed by the compositor, * these entries are a registry of objects and callbacks to properly remove them. * These are also used to remove all registered objects before exiting. */ struct GWL_RegistryEntry { GWL_RegistryEntry *next = nullptr; /** * Optional pointer passed to `remove_fn`, typically the container in #GWL_Display * in cases multiple instances of the same interface are supported. */ void *user_data = nullptr; /** * A unique identifier used as a handle by `wl_registry_listener.global_remove`. */ uint32_t name = WL_NAME_UNSET; /** * Version passed by the add callback. */ uint32_t version; /** * The index in `gwl_registry_handlers`, * useful for accessing the interface name (for logging for example). */ int interface_slot = 0; }; static void gwl_registry_entry_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params, void *user_data) { GWL_RegistryEntry *reg = new GWL_RegistryEntry; reg->interface_slot = params->interface_slot; reg->name = params->name; reg->version = params->version; reg->user_data = user_data; reg->next = display->registry_entry; display->registry_entry = reg; } static bool gwl_registry_entry_remove_by_name(GWL_Display *display, uint32_t name, int *r_interface_slot) { GWL_RegistryEntry *reg = display->registry_entry; GWL_RegistryEntry **reg_link_p = &display->registry_entry; bool found = false; if (r_interface_slot) { *r_interface_slot = -1; } while (reg) { if (reg->name == name) { GWL_RegistryEntry *reg_next = reg->next; const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot( reg->interface_slot); handler->remove_fn(display, reg->user_data, false); if (r_interface_slot) { *r_interface_slot = reg->interface_slot; } delete reg; *reg_link_p = reg_next; found = true; break; } reg_link_p = ®->next; reg = reg->next; } return found; } static bool gwl_registry_entry_remove_by_interface_slot(GWL_Display *display, int interface_slot, bool on_exit) { GWL_RegistryEntry *reg = display->registry_entry; GWL_RegistryEntry **reg_link_p = &display->registry_entry; bool found = false; while (reg) { if (reg->interface_slot == interface_slot) { GWL_RegistryEntry *reg_next = reg->next; const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot( interface_slot); handler->remove_fn(display, reg->user_data, on_exit); delete reg; *reg_link_p = reg_next; reg = reg_next; found = true; continue; } reg_link_p = ®->next; reg = reg->next; } return found; } /** * Remove all global objects (on exit). */ static void gwl_registry_entry_remove_all(GWL_Display *display) { const bool on_exit = true; /* NOTE(@campbellbarton): Free by slot instead of simply looping over * `display->registry_entry` so the order of freeing is always predictable. * Otherwise global objects would be feed in the order they are registered. * While this works in my tests, it could cause difficult to reproduce bugs * where lesser used compositors or changes to existing compositors could * crash on exit based on the order of freeing objects is out of our control. * * To give a concrete example of how this could fail, it's possible removing * a tablet interface could reference the pointer interface, or the output interface. * Even though references between interfaces shouldn't be necessary in most cases * when `on_exit` is enabled. */ int interface_slot = gwl_registry_handler_interface_slot_max(); while (interface_slot--) { gwl_registry_entry_remove_by_interface_slot(display, interface_slot, on_exit); } GHOST_ASSERT(display->registry_entry == nullptr, "Failed to remove all entries!"); display->registry_entry = nullptr; } /** * Run GWL_RegistryHandler.update_fn an all registered interface instances. * This is needed to refresh the state of interfaces that may reference other interfaces. * Called when interfaces are added/removed. * * \param interface_slot_exclude: Skip updating slots of this type. * Note that while harmless dependencies only exist between different types, * so there is no reason to update all other outputs that an output was removed (for e.g.). * Pass as -1 to update all slots. * * NOTE(@campbellbarton): Updating all other items on a single change is typically worth avoiding. * In practice this isn't a problem as so there are so few elements in `display->registry_entry`, * so few use update functions and adding/removal at runtime is rarely called (plugging/unplugging) * hardware for e.g. So while it's possible to store dependency links to avoid unnecessary * looping over data - it ends up being a non issue. */ static void gwl_registry_entry_update_all(GWL_Display *display, const int interface_slot_exclude) { GHOST_ASSERT(interface_slot_exclude == -1 || (uint(interface_slot_exclude) < uint(gwl_registry_handler_interface_slot_max())), "Invalid exclude slot"); for (GWL_RegistryEntry *reg = display->registry_entry; reg; reg = reg->next) { if (reg->interface_slot == interface_slot_exclude) { continue; } const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot( reg->interface_slot); if (handler->update_fn == nullptr) { continue; } GWL_RegisteryUpdate_Params params = { .name = reg->name, .interface_slot = reg->interface_slot, .version = reg->version, .user_data = reg->user_data, }; handler->update_fn(display, ¶ms); } } /** \} */ /* -------------------------------------------------------------------- */ /** \name Private Utility Functions * \{ */ static void ghost_wl_display_report_error(struct wl_display *display) { int ecode = wl_display_get_error(display); GHOST_ASSERT(ecode, "Error not set!"); if ((ecode == EPIPE || ecode == ECONNRESET)) { fprintf(stderr, "The Wayland connection broke. Did the Wayland compositor die?\n"); } else { fprintf(stderr, "The Wayland connection experienced a fatal error: %s\n", strerror(ecode)); } } /** * 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 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_kKeyLeftOS); GXMAP(gkey, XKB_KEY_Super_R, GHOST_kKeyRightOS); 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); /* Additional keys for non US layouts. */ /* Uses the same physical key as #XKB_KEY_KP_Decimal for QWERTZ layout, see: T102287. */ GXMAP(gkey, XKB_KEY_KP_Separator, GHOST_kKeyNumpadPeriod); 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 int pointer_axis_as_index(const uint32_t axis) { switch (axis) { case WL_POINTER_AXIS_HORIZONTAL_SCROLL: { return 0; } case WL_POINTER_AXIS_VERTICAL_SCROLL: { return 1; } default: { return -1; } } } static GHOST_TTabletMode tablet_tool_map_type(enum zwp_tablet_tool_v2_type wp_tablet_tool_type) { switch (wp_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: " << wp_tablet_tool_type << std::endl); return GHOST_kTabletModeStylus; } static const int default_cursor_size = 24; static const std::unordered_map ghost_wl_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 *ghost_wl_mime_text_plain = "text/plain"; static constexpr const char *ghost_wl_mime_text_utf8 = "text/plain;charset=utf-8"; static constexpr const char *ghost_wl_mime_text_uri = "text/uri-list"; static const char *ghost_wl_mime_preference_order[] = { ghost_wl_mime_text_uri, ghost_wl_mime_text_utf8, ghost_wl_mime_text_plain, }; /* Aligned to `ghost_wl_mime_preference_order`. */ static const GHOST_TDragnDropTypes ghost_wl_mime_preference_order_type[] = { GHOST_kDragnDropTypeString, GHOST_kDragnDropTypeString, GHOST_kDragnDropTypeFilenames, }; static const char *ghost_wl_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 Private Keyboard Depressed Key Tracking * * Don't track physical key-codes because there may be multiple keyboards connected. * Instead, count the number of #GHOST_kKey are pressed. * This may seem susceptible to bugs with sticky-keys however XKB works this way internally. * \{ */ static CLG_LogRef LOG_WL_KEYBOARD_DEPRESSED_STATE = {"ghost.wl.keyboard.depressed"}; #define LOG (&LOG_WL_KEYBOARD_DEPRESSED_STATE) static void keyboard_depressed_state_reset(GWL_Seat *seat) { for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) { seat->key_depressed.mods[i] = 0; } } static void keyboard_depressed_state_key_event(GWL_Seat *seat, const GHOST_TKey gkey, const GHOST_TEventType etype) { if (GHOST_KEY_MODIFIER_CHECK(gkey)) { const int index = GHOST_KEY_MODIFIER_TO_INDEX(gkey); int16_t &value = seat->key_depressed.mods[index]; if (etype == GHOST_kEventKeyUp) { value -= 1; if (UNLIKELY(value < 0)) { CLOG_WARN(LOG, "modifier (%d) has negative keys held (%d)!", index, value); value = 0; } } else { value += 1; } } } static void keyboard_depressed_state_push_events_from_change( GWL_Seat *seat, const GWL_KeyboardDepressedState &key_depressed_prev) { GHOST_IWindow *win = ghost_wl_surface_user_data(seat->keyboard.wl_surface_window); GHOST_SystemWayland *system = seat->system; /* Separate key up and down into separate passes so key down events always come after key up. * Do this so users of GHOST can use the last pressed or released modifier to check * if the modifier is held instead of counting modifiers pressed as is done here, * this isn't perfect but works well enough in practice. */ for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) { for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d < 0; d++) { const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i); seat->system->pushEvent( new GHOST_EventKey(system->getMilliSeconds(), GHOST_kEventKeyUp, win, gkey, false)); CLOG_INFO(LOG, 2, "modifier (%d) up", i); } } for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) { for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d > 0; d--) { const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i); seat->system->pushEvent( new GHOST_EventKey(system->getMilliSeconds(), GHOST_kEventKeyDown, win, gkey, false)); CLOG_INFO(LOG, 2, "modifier (%d) down", i); } } } #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \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 `seat->xy`. */ static void relative_pointer_handle_relative_motion_impl(GWL_Seat *seat, GHOST_WindowWayland *win, const wl_fixed_t xy[2]) { const wl_fixed_t scale = win->scale(); seat->pointer.xy[0] = xy[0]; seat->pointer.xy[1] = xy[1]; #ifdef USE_GNOME_CONFINE_HACK if (seat->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(seat->pointer.xy)); } #endif seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(), GHOST_kEventCursorMove, win, wl_fixed_to_int(scale * seat->pointer.xy[0]), wl_fixed_to_int(scale * seat->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*/) { GWL_Seat *seat = static_cast(data); if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { CLOG_INFO(LOG, 2, "relative_motion"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const wl_fixed_t scale = win->scale(); const wl_fixed_t xy_next[2] = { seat->pointer.xy[0] + (dx / scale), seat->pointer.xy[1] + (dy / scale), }; relative_pointer_handle_relative_motion_impl(seat, 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 GWL_Seat *const seat, const GHOST_TEventType event) { /* NOTE: `seat->data_offer_dnd_mutex` must already be locked. */ if (wl_surface *wl_surface_focus = seat->wl_surface_window_focus_dnd) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const wl_fixed_t scale = win->scale(); const int event_xy[2] = { wl_fixed_to_int(scale * seat->data_offer_dnd->dnd.xy[0]), wl_fixed_to_int(scale * seat->data_offer_dnd->dnd.xy[1]), }; const uint64_t time = seat->system->getMilliSeconds(); for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order_type); i++) { const GHOST_TDragnDropTypes type = ghost_wl_mime_preference_order_type[i]; seat->system->pushEvent( new GHOST_EventDragnDrop(time, event, type, win, UNPACK2(event_xy), nullptr)); } } } /** * Read from `fd` into a buffer which is returned. * \return the buffer or null on failure. */ static char *read_file_as_buffer(const int fd, const bool nil_terminate, size_t *r_len) { struct ByteChunk { ByteChunk *next; char data[4096 - sizeof(ByteChunk *)]; }; ByteChunk *chunk_first = nullptr, **chunk_link_p = &chunk_first; bool ok = true; size_t len = 0; while (true) { ByteChunk *chunk = static_cast(malloc(sizeof(*chunk))); if (UNLIKELY(chunk == nullptr)) { CLOG_WARN(LOG, "unable to allocate chunk for file buffer"); ok = false; break; } chunk->next = nullptr; const ssize_t len_chunk = read(fd, chunk->data, sizeof(chunk->data)); if (len_chunk <= 0) { if (UNLIKELY(len_chunk < 0)) { CLOG_WARN(LOG, "error reading from pipe: %s", std::strerror(errno)); ok = false; } free(chunk); break; } if (chunk_first == nullptr) { chunk_first = chunk; } *chunk_link_p = chunk; chunk_link_p = &chunk->next; len += len_chunk; } char *buf = nullptr; if (ok) { buf = static_cast(malloc(len + (nil_terminate ? 1 : 0))); if (UNLIKELY(buf == nullptr)) { CLOG_WARN(LOG, "unable to allocate file buffer: %zu bytes", len); ok = false; } } if (ok) { *r_len = len; if (nil_terminate) { buf[len] = '\0'; } } else { *r_len = 0; } char *buf_stride = buf; while (chunk_first) { if (ok) { const size_t len_chunk = std::min(len, sizeof(chunk_first->data)); memcpy(buf_stride, chunk_first->data, len_chunk); buf_stride += len_chunk; len -= len_chunk; } ByteChunk *chunk = chunk_first->next; free(chunk_first); chunk_first = chunk; } return buf; } static char *read_buffer_from_data_offer(GWL_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, const bool nil_terminate, size_t *r_len) { int pipefd[2]; const bool pipefd_ok = pipe(pipefd) == 0; if (pipefd_ok) { wl_data_offer_receive(data_offer->id, mime_receive, pipefd[1]); close(pipefd[1]); } else { CLOG_WARN(LOG, "error creating pipe: %s", std::strerror(errno)); } /* Only for DND (A no-op to disable for clipboard data-offer). */ data_offer->dnd.in_use = false; if (mutex) { mutex->unlock(); } /* WARNING: `data_offer` may be freed from now on. */ char *buf = nullptr; if (pipefd_ok) { buf = read_file_as_buffer(pipefd[0], nil_terminate, r_len); close(pipefd[0]); } return buf; } static char *read_buffer_from_primary_selection_offer(GWL_PrimarySelection_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, const bool nil_terminate, size_t *r_len) { int pipefd[2]; const bool pipefd_ok = pipe(pipefd) == 0; if (pipefd_ok) { zwp_primary_selection_offer_v1_receive(data_offer->id, mime_receive, pipefd[1]); close(pipefd[1]); } else { CLOG_WARN(LOG, "error creating pipe: %s", std::strerror(errno)); } if (mutex) { mutex->unlock(); } /* WARNING: `data_offer` may be freed from now on. */ char *buf = nullptr; if (pipefd_ok) { buf = read_file_as_buffer(pipefd[0], nil_terminate, r_len); close(pipefd[0]); } return buf; } /** * 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) { GWL_Seat *seat = static_cast(data); CLOG_INFO(LOG, 2, "send"); auto write_file_fn = [](GWL_Seat *seat, const int fd) { if (UNLIKELY(write(fd, seat->data_source->buffer_out.data, seat->data_source->buffer_out.data_size) < 0)) { CLOG_WARN(LOG, "error writing to clipboard: %s", std::strerror(errno)); } close(fd); seat->data_source_mutex.unlock(); }; seat->data_source_mutex.lock(); std::thread write_thread(write_file_fn, seat, fd); write_thread.detach(); } static void data_source_handle_cancelled(void *data, struct wl_data_source *wl_data_source) { CLOG_INFO(LOG, 2, "cancelled"); GWL_Seat *seat = static_cast(data); GWL_DataSource *data_source = seat->data_source; if (seat->data_source->wl_source == wl_data_source) { data_source->wl_source = nullptr; } 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); GWL_DataOffer *data_offer = static_cast(data); data_offer->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); GWL_DataOffer *data_offer = static_cast(data); data_offer->dnd.source_actions = (enum wl_data_device_manager_dnd_action)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); GWL_DataOffer *data_offer = static_cast(data); data_offer->dnd.action = (enum wl_data_device_manager_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"); GWL_DataOffer *data_offer = new GWL_DataOffer; 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 *wl_surface, const wl_fixed_t x, const wl_fixed_t y, struct wl_data_offer *id) { if (!ghost_wl_surface_own(wl_surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_dnd_mutex}; delete seat->data_offer_dnd; seat->data_offer_dnd = static_cast(wl_data_offer_get_user_data(id)); GWL_DataOffer *data_offer = seat->data_offer_dnd; data_offer->dnd.in_use = 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 (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order); i++) { const char *type = ghost_wl_mime_preference_order[i]; wl_data_offer_accept(id, serial, type); } seat->wl_surface_window_focus_dnd = wl_surface; seat->system->seat_active_set(seat); dnd_events(seat, GHOST_kEventDraggingEntered); } static void data_device_handle_leave(void *data, struct wl_data_device * /*wl_data_device*/) { GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_dnd_mutex}; CLOG_INFO(LOG, 2, "leave"); dnd_events(seat, GHOST_kEventDraggingExited); seat->wl_surface_window_focus_dnd = nullptr; if (seat->data_offer_dnd && !seat->data_offer_dnd->dnd.in_use) { wl_data_offer_destroy(seat->data_offer_dnd->id); delete seat->data_offer_dnd; seat->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) { GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_dnd_mutex}; CLOG_INFO(LOG, 2, "motion"); seat->data_offer_dnd->dnd.xy[0] = x; seat->data_offer_dnd->dnd.xy[1] = y; dnd_events(seat, GHOST_kEventDraggingUpdated); } static void data_device_handle_drop(void *data, struct wl_data_device * /*wl_data_device*/) { GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_dnd_mutex}; GWL_DataOffer *data_offer = seat->data_offer_dnd; /* Use a blank string for `mime_receive` to prevent crashes, although could also be `nullptr`. * Failure to set this to a known type just means the file won't have any special handling. * GHOST still generates a dropped file event. * NOTE: this string can be compared with `mime_text_plain`, `mime_text_uri` etc... * as the this always points to the same values. */ const char *mime_receive = ""; for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order); i++) { const char *type = ghost_wl_mime_preference_order[i]; if (data_offer->types.count(type)) { mime_receive = type; break; } } CLOG_INFO(LOG, 2, "drop mime_recieve=%s", mime_receive); auto read_uris_fn = [](GWL_Seat *const seat, GWL_DataOffer *data_offer, wl_surface *wl_surface_window, const char *mime_receive) { const wl_fixed_t xy[2] = {UNPACK2(data_offer->dnd.xy)}; size_t data_buf_len = 0; const char *data_buf = read_buffer_from_data_offer( data_offer, mime_receive, nullptr, false, &data_buf_len); std::string data = data_buf ? std::string(data_buf, data_buf_len) : ""; free(const_cast(data_buf)); CLOG_INFO(LOG, 2, "drop_read_uris mime_receive=%s, data=%s", mime_receive, data.c_str()); wl_data_offer_finish(data_offer->id); wl_data_offer_destroy(data_offer->id); if (seat->data_offer_dnd == data_offer) { seat->data_offer_dnd = nullptr; } delete data_offer; data_offer = nullptr; GHOST_SystemWayland *const system = seat->system; if (mime_receive == ghost_wl_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(wl_surface_window); 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, ghost_wl_mime_text_plain, ghost_wl_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->wl_display()); }; /* Pass in `seat->wl_surface_window_focus_dnd` instead of accessing it from `seat` since the * leave callback (#data_device_handle_leave) will clear the value once this function starts. */ std::thread read_thread( read_uris_fn, seat, data_offer, seat->wl_surface_window_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) { GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_copy_paste_mutex}; GWL_DataOffer *data_offer = seat->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; seat->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)); seat->data_offer_copy_paste = data_offer; } 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"); GWL_Cursor *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(GWL_Cursor &cursor, wl_shm *shm, GWL_SeatStatePointer *seat_state_pointer, wl_surface *wl_surface_cursor) { int scale = 0; for (const GWL_Output *output : seat_state_pointer->outputs) { if (output->scale > scale) { scale = output->scale; } } if (scale > 0 && seat_state_pointer->theme_scale != scale) { seat_state_pointer->theme_scale = scale; if (!cursor.is_custom) { wl_surface_set_buffer_scale(wl_surface_cursor, scale); } wl_cursor_theme_destroy(cursor.wl_theme); cursor.wl_theme = wl_cursor_theme_load( cursor.theme_name.c_str(), scale * cursor.theme_size, shm); return true; } return false; } static void cursor_surface_handle_enter(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { if (!ghost_wl_output_own(wl_output)) { CLOG_INFO(LOG, 2, "handle_enter (skipped)"); return; } CLOG_INFO(LOG, 2, "handle_enter"); GWL_Seat *seat = static_cast(data); GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_from_cursor_surface( seat, wl_surface); const GWL_Output *reg_output = ghost_wl_output_user_data(wl_output); seat_state_pointer->outputs.insert(reg_output); update_cursor_scale(seat->cursor, seat->system->wl_shm(), seat_state_pointer, wl_surface); } static void cursor_surface_handle_leave(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { if (!(wl_output && ghost_wl_output_own(wl_output))) { CLOG_INFO(LOG, 2, "handle_leave (skipped)"); return; } CLOG_INFO(LOG, 2, "handle_leave"); GWL_Seat *seat = static_cast(data); GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_from_cursor_surface( seat, wl_surface); const GWL_Output *reg_output = ghost_wl_output_user_data(wl_output); seat_state_pointer->outputs.erase(reg_output); update_cursor_scale(seat->cursor, seat->system->wl_shm(), seat_state_pointer, 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 *wl_surface, const wl_fixed_t surface_x, const wl_fixed_t surface_y) { if (!ghost_wl_surface_own(wl_surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface); win->activate(); GWL_Seat *seat = static_cast(data); seat->cursor_source_serial = serial; seat->pointer.serial = serial; seat->pointer.xy[0] = surface_x; seat->pointer.xy[1] = surface_y; /* Resetting scroll events is likely unnecessary, * do this to avoid any possible problems as it's harmless. */ seat->pointer_scroll.smooth_xy[0] = 0; seat->pointer_scroll.smooth_xy[1] = 0; seat->pointer_scroll.discrete_xy[0] = 0; seat->pointer_scroll.discrete_xy[1] = 0; seat->pointer_scroll.axis_source = WL_POINTER_AXIS_SOURCE_WHEEL; seat->pointer.wl_surface_window = wl_surface; seat->system->seat_active_set(seat); win->setCursorShape(win->getCursorShape()); const wl_fixed_t scale = win->scale(); seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(), GHOST_kEventCursorMove, win, wl_fixed_to_int(scale * seat->pointer.xy[0]), wl_fixed_to_int(scale * seat->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 *wl_surface) { /* First clear the `pointer.wl_surface`, since the window won't exist when closing the window. */ static_cast(data)->pointer.wl_surface_window = nullptr; if (wl_surface && ghost_wl_surface_own(wl_surface)) { CLOG_INFO(LOG, 2, "leave"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_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) { GWL_Seat *seat = static_cast(data); seat->pointer.xy[0] = surface_x; seat->pointer.xy[1] = surface_y; if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { CLOG_INFO(LOG, 2, "motion"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const wl_fixed_t scale = win->scale(); seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(), GHOST_kEventCursorMove, win, wl_fixed_to_int(scale * seat->pointer.xy[0]), wl_fixed_to_int(scale * seat->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); GWL_Seat *seat = 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; } seat->data_source_serial = serial; seat->pointer.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED); if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent(new GHOST_EventButton( seat->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) { /* NOTE: this is used for touch based scrolling - or other input that doesn't scroll with * discrete "steps". This allows supporting smooth-scrolling without "touch" gesture support. */ CLOG_INFO(LOG, 2, "axis (axis=%u, value=%d)", axis, value); const int index = pointer_axis_as_index(axis); if (UNLIKELY(index == -1)) { return; } GWL_Seat *seat = static_cast(data); seat->pointer_scroll.smooth_xy[index] = value; } static void pointer_handle_frame(void *data, struct wl_pointer * /*wl_pointer*/) { CLOG_INFO(LOG, 2, "frame"); GWL_Seat *seat = static_cast(data); /* Both discrete and smooth events may be set at once, never generate events for both * as this will be handling the same event in to different ways. * Prioritize discrete axis events for the mouse wheel, otherwise smooth scroll. */ if (seat->pointer_scroll.axis_source == WL_POINTER_AXIS_SOURCE_WHEEL) { if (seat->pointer_scroll.discrete_xy[0]) { seat->pointer_scroll.smooth_xy[0] = 0; } if (seat->pointer_scroll.discrete_xy[1]) { seat->pointer_scroll.smooth_xy[1] = 0; } } else { if (seat->pointer_scroll.smooth_xy[0]) { seat->pointer_scroll.discrete_xy[0] = 0; } if (seat->pointer_scroll.smooth_xy[1]) { seat->pointer_scroll.discrete_xy[1] = 0; } } /* Discrete X axis currently unsupported. */ if (seat->pointer_scroll.discrete_xy[1]) { if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const int32_t discrete = seat->pointer_scroll.discrete_xy[1]; seat->system->pushEvent(new GHOST_EventWheel( seat->system->getMilliSeconds(), win, std::signbit(discrete) ? +1 : -1)); } seat->pointer_scroll.discrete_xy[0] = 0; seat->pointer_scroll.discrete_xy[1] = 0; } if (seat->pointer_scroll.smooth_xy[0] || seat->pointer_scroll.smooth_xy[1]) { if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const wl_fixed_t scale = win->scale(); seat->system->pushEvent(new GHOST_EventTrackpad( seat->system->getMilliSeconds(), win, GHOST_kTrackpadEventScroll, wl_fixed_to_int(scale * seat->pointer.xy[0]), wl_fixed_to_int(scale * seat->pointer.xy[1]), /* NOTE: scaling the delta doesn't seem necessary. * NOTE: inverting delta gives correct results, see: QTBUG-85767. * NOTE: the preference to invert scrolling (in GNOME at least) * has already been applied so there is no need to read this preference. */ -wl_fixed_to_int(seat->pointer_scroll.smooth_xy[0]), -wl_fixed_to_int(seat->pointer_scroll.smooth_xy[1]), false)); } seat->pointer_scroll.smooth_xy[0] = 0; seat->pointer_scroll.smooth_xy[1] = 0; } seat->pointer_scroll.axis_source = WL_POINTER_AXIS_SOURCE_WHEEL; } 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); GWL_Seat *seat = static_cast(data); seat->pointer_scroll.axis_source = (enum wl_pointer_axis_source)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) { /* NOTE: a discrete axis are typically mouse wheel events. * The non-discrete version of this function is used for touch-pad. */ CLOG_INFO(LOG, 2, "axis_discrete (axis=%u, discrete=%d)", axis, discrete); const int index = pointer_axis_as_index(axis); if (UNLIKELY(index == -1)) { return; } GWL_Seat *seat = static_cast(data); seat->pointer_scroll.discrete_xy[index] = discrete; } 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 (Pointer Gesture: Hold), #zwp_pointer_gesture_hold_v1_listener * \{ */ #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE static CLG_LogRef LOG_WL_POINTER_GESTURE_HOLD = {"ghost.wl.handle.pointer_gesture.hold"}; # define LOG (&LOG_WL_POINTER_GESTURE_HOLD) static void gesture_hold_handle_begin( void * /*data*/, struct zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/, uint32_t /*serial*/, uint32_t /*time*/, struct wl_surface * /*surface*/, uint32_t fingers) { CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers); } static void gesture_hold_handle_end( void * /*data*/, struct zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t cancelled) { CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled); } static const struct zwp_pointer_gesture_hold_v1_listener gesture_hold_listener = { gesture_hold_handle_begin, gesture_hold_handle_end, }; # undef LOG #endif /* ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer Gesture: Pinch), #zwp_pointer_gesture_pinch_v1_listener * \{ */ #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE static CLG_LogRef LOG_WL_POINTER_GESTURE_PINCH = {"ghost.wl.handle.pointer_gesture.pinch"}; # define LOG (&LOG_WL_POINTER_GESTURE_PINCH) static void gesture_pinch_handle_begin(void *data, struct zwp_pointer_gesture_pinch_v1 * /*pinch*/, uint32_t /*serial*/, uint32_t /*time*/, struct wl_surface * /*surface*/, uint32_t fingers) { CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers); GWL_Seat *seat = static_cast(data); /* Reset defaults. */ seat->pointer_gesture_pinch = GWL_SeatStatePointerGesture_Pinch{}; GHOST_WindowWayland *win = nullptr; if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { win = ghost_wl_surface_user_data(wl_surface_focus); } /* NOTE(@campbellbarton): Blender's use of track-pad coordinates is inconsistent and needs work. * This isn't specific to WAYLAND, in practice they tend to work well enough in most cases. * Some operators scale by the UI scale, some don't. * Even this window scale is not correct because it doesn't account for: * 1) Fractional window scale. * 2) Blender's UI scale preference (which GHOST doesn't know about). * * If support for this were all that was needed it could be handled in GHOST, * however as the operators are not even using coordinates compatible with each other, * it would be better to resolve this by passing rotation & zoom levels directly, * instead of attempting to handle them as cursor coordinates. */ const wl_fixed_t win_scale = win ? win->scale() : 1; /* NOTE(@campbellbarton): Scale factors match Blender's operators & default preferences. * For these values to work correctly, operator logic will need to be changed not to scale input * by the region size (as with 3D view zoom) or preference for 3D view orbit sensitivity. * * By working "correctly" I mean that a rotation action where the users fingers rotate to * opposite locations should always rotate the viewport 180d, since users will expect the * physical location of their fingers to match the viewport. * Similarly with zoom, the scale value from the pinch action can be mapped to a zoom level * although unlike rotation, an inexact mapping is less noticeable. * Users may even prefer the zoom level to be scaled - which could be a preference. */ seat->pointer_gesture_pinch.scale.value = wl_fixed_from_int(1); /* The value 300 matches a value used in clip & image zoom operators. * It seems OK for the 3D view too. */ seat->pointer_gesture_pinch.scale.factor = 300 * win_scale; /* The value 5 is used on macOS and roughly maps 1:1 with turntable rotation, * although preferences can scale the sensitivity (which would be skipped ideally). */ seat->pointer_gesture_pinch.rotation.factor = 5 * win_scale; } static void gesture_pinch_handle_update(void *data, struct zwp_pointer_gesture_pinch_v1 * /*pinch*/, uint32_t /*time*/, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) { CLOG_INFO(LOG, 2, "update (dx=%.3f, dy=%.3f, scale=%.3f, rotation=%.3f)", wl_fixed_to_double(dx), wl_fixed_to_double(dy), wl_fixed_to_double(scale), wl_fixed_to_double(rotation)); GWL_Seat *seat = static_cast(data); GHOST_WindowWayland *win = nullptr; if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) { win = ghost_wl_surface_user_data(wl_surface_focus); } /* Scale defaults to `wl_fixed_from_int(1)` which may change while pinching. * This needs to be converted to a delta. */ const wl_fixed_t scale_delta = scale - seat->pointer_gesture_pinch.scale.value; const int scale_as_delta_px = gwl_scaled_fixed_t_add_and_calc_rounded_delta( &seat->pointer_gesture_pinch.scale, scale_delta); /* Rotation in degrees, unlike scale this is a delta. */ const int rotation_as_delta_px = gwl_scaled_fixed_t_add_and_calc_rounded_delta( &seat->pointer_gesture_pinch.rotation, rotation); if (win) { const wl_fixed_t win_scale = win->scale(); const int32_t event_xy[2] = { wl_fixed_to_int(win_scale * seat->pointer.xy[0]), wl_fixed_to_int(win_scale * seat->pointer.xy[1]), }; if (scale_as_delta_px) { seat->system->pushEvent(new GHOST_EventTrackpad(seat->system->getMilliSeconds(), win, GHOST_kTrackpadEventMagnify, event_xy[0], event_xy[1], scale_as_delta_px, 0, false)); } if (rotation_as_delta_px) { seat->system->pushEvent(new GHOST_EventTrackpad(seat->system->getMilliSeconds(), win, GHOST_kTrackpadEventRotate, event_xy[0], event_xy[1], rotation_as_delta_px, 0, false)); } } } static void gesture_pinch_handle_end(void * /*data*/, struct zwp_pointer_gesture_pinch_v1 * /*pinch*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t cancelled) { CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled); } static const struct zwp_pointer_gesture_pinch_v1_listener gesture_pinch_listener = { gesture_pinch_handle_begin, gesture_pinch_handle_update, gesture_pinch_handle_end, }; # undef LOG #endif /* ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer Gesture: Swipe), #zwp_pointer_gesture_swipe_v1 * * \note In both Gnome-Shell & KDE this gesture isn't emitted at time of writing, * instead, high resolution 2D #wl_pointer_listener.axis data is generated which works well. * There may be some situations where WAYLAND compositors generate this gesture * (swiping with 3+ fingers, for e.g.). So keep this to allow logging & testing gestures. * \{ */ #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE static CLG_LogRef LOG_WL_POINTER_GESTURE_SWIPE = {"ghost.wl.handle.pointer_gesture.swipe"}; # define LOG (&LOG_WL_POINTER_GESTURE_SWIPE) static void gesture_swipe_handle_begin( void * /*data*/, struct zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/, uint32_t /*serial*/, uint32_t /*time*/, struct wl_surface * /*surface*/, uint32_t fingers) { CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers); } static void gesture_swipe_handle_update( void * /*data*/, struct zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/, uint32_t /*time*/, wl_fixed_t dx, wl_fixed_t dy) { CLOG_INFO(LOG, 2, "update (dx=%.3f, dy=%.3f)", wl_fixed_to_double(dx), wl_fixed_to_double(dy)); } static void gesture_swipe_handle_end( void * /*data*/, struct zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t cancelled) { CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled); } static const struct zwp_pointer_gesture_swipe_v1_listener gesture_swipe_listener = { gesture_swipe_handle_begin, gesture_swipe_handle_update, gesture_swipe_handle_end, }; # undef LOG #endif /* ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Touch Seat), #wl_touch_listener * * NOTE(@campbellbarton): It's not clear if this interface is used by popular compositors. * It looks like GNOME/KDE only support `zwp_pointer_gestures_v1_interface`. * If this isn't used anywhere, it could be removed. * \{ */ static CLG_LogRef LOG_WL_TOUCH = {"ghost.wl.handle.touch"}; #define LOG (&LOG_WL_TOUCH) static void touch_seat_handle_down(void * /*data*/, struct wl_touch * /*wl_touch*/, uint32_t /*serial*/, uint32_t /*time*/, struct wl_surface * /*wl_surface*/, int32_t /*id*/, wl_fixed_t /*x*/, wl_fixed_t /*y*/) { CLOG_INFO(LOG, 2, "down"); } static void touch_seat_handle_up(void * /*data*/, struct wl_touch * /*wl_touch*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t /*id*/) { CLOG_INFO(LOG, 2, "up"); } static void touch_seat_handle_motion(void * /*data*/, struct wl_touch * /*wl_touch*/, uint32_t /*time*/, int32_t /*id*/, wl_fixed_t /*x*/, wl_fixed_t /*y*/) { CLOG_INFO(LOG, 2, "motion"); } static void touch_seat_handle_frame(void * /*data*/, struct wl_touch * /*wl_touch*/) { CLOG_INFO(LOG, 2, "frame"); } static void touch_seat_handle_cancel(void * /*data*/, struct wl_touch * /*wl_touch*/) { CLOG_INFO(LOG, 2, "cancel"); } static void touch_seat_handle_shape(void * /*data*/, struct wl_touch * /*touch*/, int32_t /*id*/, wl_fixed_t /*major*/, wl_fixed_t /*minor*/) { CLOG_INFO(LOG, 2, "shape"); } static void touch_seat_handle_orientation(void * /*data*/, struct wl_touch * /*touch*/, int32_t /*id*/, wl_fixed_t /*orientation*/) { CLOG_INFO(LOG, 2, "orientation"); } static const struct wl_touch_listener touch_seat_listener = { touch_seat_handle_down, touch_seat_handle_up, touch_seat_handle_motion, touch_seat_handle_frame, touch_seat_handle_cancel, touch_seat_handle_shape, touch_seat_handle_orientation, }; #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); GWL_TabletTool *tablet_tool = static_cast(data); tablet_tool->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"); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; if (tablet_tool->wl_surface_cursor) { wl_surface_destroy(tablet_tool->wl_surface_cursor); } seat->tablet_tools.erase(zwp_tablet_tool_v2); delete tablet_tool; } 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 *wl_surface) { if (!ghost_wl_surface_own(wl_surface)) { CLOG_INFO(LOG, 2, "proximity_in (skipped)"); return; } CLOG_INFO(LOG, 2, "proximity_in"); GWL_TabletTool *tablet_tool = static_cast(data); tablet_tool->proximity = true; GWL_Seat *seat = tablet_tool->seat; seat->cursor_source_serial = serial; seat->tablet.wl_surface_window = wl_surface; seat->tablet.serial = serial; seat->data_source_serial = serial; seat->system->seat_active_set(seat); /* Update #GHOST_TabletData. */ GHOST_TabletData &td = tablet_tool->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(seat->tablet.wl_surface_window); 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"); GWL_TabletTool *tablet_tool = static_cast(data); /* Defer clearing the wl_surface until the frame is handled. * Without this, the frame can not access the wl_surface. */ tablet_tool->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"); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; const GHOST_TButton ebutton = GHOST_kButtonMaskLeft; const GHOST_TEventType etype = GHOST_kEventButtonDown; seat->data_source_serial = serial; seat->tablet.buttons.set(ebutton, true); if (wl_surface *wl_surface_focus = seat->tablet.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent(new GHOST_EventButton( seat->system->getMilliSeconds(), etype, win, ebutton, tablet_tool->data)); } } static void tablet_tool_handle_up(void *data, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) { CLOG_INFO(LOG, 2, "up"); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; const GHOST_TButton ebutton = GHOST_kButtonMaskLeft; const GHOST_TEventType etype = GHOST_kEventButtonUp; seat->tablet.buttons.set(ebutton, false); if (wl_surface *wl_surface_focus = seat->tablet.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent(new GHOST_EventButton( seat->system->getMilliSeconds(), etype, win, ebutton, tablet_tool->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"); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; seat->tablet.xy[0] = x; seat->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); GWL_TabletTool *tablet_tool = static_cast(data); GHOST_TabletData &td = tablet_tool->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)); GWL_TabletTool *tablet_tool = static_cast(data); GHOST_TabletData &td = tablet_tool->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); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; if (wl_surface *wl_surface_focus = seat->tablet.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent(new GHOST_EventWheel(seat->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); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; 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_kButtonMaskMiddle; break; case BTN_STYLUS2: ebutton = GHOST_kButtonMaskRight; break; case BTN_STYLUS3: ebutton = GHOST_kButtonMaskButton4; break; } seat->data_source_serial = serial; seat->tablet.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED); if (wl_surface *wl_surface_focus = seat->tablet.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent(new GHOST_EventButton( seat->system->getMilliSeconds(), etype, win, ebutton, tablet_tool->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"); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; /* No need to check the surfaces origin, it's already known to be owned by GHOST. */ if (wl_surface *wl_surface_focus = seat->tablet.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const wl_fixed_t scale = win->scale(); seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(), GHOST_kEventCursorMove, win, wl_fixed_to_int(scale * seat->tablet.xy[0]), wl_fixed_to_int(scale * seat->tablet.xy[1]), tablet_tool->data)); if (tablet_tool->proximity == false) { win->setCursorShape(win->getCursorShape()); } } if (tablet_tool->proximity == false) { seat->tablet.wl_surface_window = 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); GWL_Seat *seat = static_cast(data); GWL_TabletTool *tablet_tool = new GWL_TabletTool(); tablet_tool->seat = seat; /* Every tool has it's own cursor wl_surface. */ tablet_tool->wl_surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor()); ghost_wl_surface_tag_cursor_tablet(tablet_tool->wl_surface_cursor); wl_surface_add_listener(tablet_tool->wl_surface_cursor, &cursor_surface_listener, (void *)seat); zwp_tablet_tool_v2_add_listener(id, &tablet_tool_listner, tablet_tool); seat->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) { GWL_Seat *seat = 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( seat->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(seat->xkb_state); seat->xkb_state = xkb_state_new(keymap); xkb_state_unref(seat->xkb_state_empty); seat->xkb_state_empty = xkb_state_new(keymap); for (int i = 0; i < MOD_INDEX_NUM; i++) { const GWL_ModifierInfo &mod_info = g_modifier_info_table[i]; seat->xkb_keymap_mod_index[i] = xkb_keymap_mod_get_index(keymap, mod_info.xkb_id); } xkb_state_unref(seat->xkb_state_empty_with_shift); seat->xkb_state_empty_with_shift = nullptr; { const xkb_mod_index_t mod_shift = seat->xkb_keymap_mod_index[MOD_INDEX_SHIFT]; if (mod_shift != XKB_MOD_INVALID) { seat->xkb_state_empty_with_shift = xkb_state_new(keymap); xkb_state_update_mask(seat->xkb_state_empty_with_shift, (1 << mod_shift), 0, 0, 0, 0, 0); } } xkb_state_unref(seat->xkb_state_empty_with_numlock); seat->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) { seat->xkb_state_empty_with_numlock = xkb_state_new(keymap); xkb_state_update_mask( seat->xkb_state_empty_with_numlock, (1 << mod2), 0, (1 << num), 0, 0, 0); } } #ifdef USE_NON_LATIN_KB_WORKAROUND seat->xkb_use_non_latin_workaround = false; if (seat->xkb_state_empty_with_shift) { seat->xkb_use_non_latin_workaround = true; for (xkb_keycode_t key_code = KEY_1 + EVDEV_OFFSET; key_code <= KEY_0 + EVDEV_OFFSET; key_code++) { const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(seat->xkb_state_empty_with_shift, key_code); if (!(sym_test >= XKB_KEY_0 && sym_test <= XKB_KEY_9)) { seat->xkb_use_non_latin_workaround = false; break; } } } #endif keyboard_depressed_state_reset(seat); xkb_keymap_unref(keymap); } /** * Enter event. * * Notification that this seat's keyboard focus is on a certain wl_surface. */ static void keyboard_handle_enter(void *data, struct wl_keyboard * /*wl_keyboard*/, const uint32_t serial, struct wl_surface *wl_surface, struct wl_array *keys) { if (!ghost_wl_surface_own(wl_surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); GWL_Seat *seat = static_cast(data); seat->keyboard.serial = serial; seat->keyboard.wl_surface_window = wl_surface; seat->system->seat_active_set(seat); /* If there are any keys held when activating the window, * modifiers will be compared against the seat state, * only enabling modifiers that were previously disabled. */ GWL_KeyboardDepressedState key_depressed_prev = seat->key_depressed; keyboard_depressed_state_reset(seat); uint32_t *key; WL_ARRAY_FOR_EACH (key, keys) { const xkb_keycode_t key_code = *key + EVDEV_OFFSET; CLOG_INFO(LOG, 2, "enter (key_held=%d)", int(key_code)); const xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->xkb_state, key_code); const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, *key); if (gkey != GHOST_kKeyUnknown) { keyboard_depressed_state_key_event(seat, gkey, GHOST_kEventKeyDown); } } keyboard_depressed_state_push_events_from_change(seat, key_depressed_prev); #ifdef USE_GNOME_KEYBOARD_SUPPRESS_WARNING seat->key_depressed_suppress_warning.any_keys_held_on_enter = keys->size != 0; #endif } /** * Leave event. * * Notification that this seat's keyboard focus is no longer on a certain wl_surface. */ static void keyboard_handle_leave(void *data, struct wl_keyboard * /*wl_keyboard*/, const uint32_t /*serial*/, struct wl_surface *wl_surface) { if (!(wl_surface && ghost_wl_surface_own(wl_surface))) { CLOG_INFO(LOG, 2, "leave (skipped)"); return; } CLOG_INFO(LOG, 2, "leave"); GWL_Seat *seat = static_cast(data); seat->keyboard.wl_surface_window = nullptr; /* Losing focus must stop repeating text. */ if (seat->key_repeat.timer) { keyboard_handle_key_repeat_cancel(seat); } #ifdef USE_GNOME_KEYBOARD_SUPPRESS_WARNING seat->key_depressed_suppress_warning.any_mod_held = false; seat->key_depressed_suppress_warning.any_keys_held_on_enter = false; #endif } /** * 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, struct xkb_state *xkb_state_empty_with_shift, const bool xkb_use_non_latin_workaround, 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 (sym >= XKB_KEY_KP_Home && sym <= XKB_KEY_KP_Delete) { if (xkb_state_empty_with_numlock) { 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; } } } else { #ifdef USE_NON_LATIN_KB_WORKAROUND if (key >= (KEY_1 + EVDEV_OFFSET) && key <= (KEY_0 + EVDEV_OFFSET)) { if (xkb_state_empty_with_shift && xkb_use_non_latin_workaround) { const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(xkb_state_empty_with_shift, key); if (sym_test != XKB_KEY_NoSymbol) { /* Should never happen as enabling `xkb_use_non_latin_workaround` checks this. */ GHOST_ASSERT(sym_test >= XKB_KEY_0 && sym_test <= XKB_KEY_9, "Unexpected key"); sym = sym_test; } } } #else (void)xkb_state_empty_with_shift; (void)xkb_use_non_latin_workaround; #endif } return sym; } static void keyboard_handle_key_repeat_cancel(GWL_Seat *seat) { GHOST_ASSERT(seat->key_repeat.timer != nullptr, "Caller much check for timer"); delete static_cast(seat->key_repeat.timer->getUserData()); seat->system->removeTimer(seat->key_repeat.timer); seat->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(GWL_Seat *seat, const bool use_delay) { GHOST_ASSERT(seat->key_repeat.timer != nullptr, "Caller much check for timer"); GHOST_SystemWayland *system = seat->system; GHOST_ITimerTask *timer = seat->key_repeat.timer; GHOST_TimerProcPtr key_repeat_fn = timer->getTimerProc(); GHOST_TUserDataPtr payload = seat->key_repeat.timer->getUserData(); seat->system->removeTimer(seat->key_repeat.timer); const uint64_t time_step = 1000 / seat->key_repeat.rate; const uint64_t time_start = use_delay ? seat->key_repeat.delay : time_step; seat->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) { GWL_Seat *seat = 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( seat->xkb_state_empty, seat->xkb_state_empty_with_numlock, seat->xkb_state_empty_with_shift, #ifdef USE_NON_LATIN_KB_WORKAROUND seat->xkb_use_non_latin_workaround, #else false, #endif key_code); if (sym == XKB_KEY_NoSymbol) { CLOG_INFO(LOG, 2, "key (code=%d, state=%u, no symbol, skipped)", int(key_code), state); return; } CLOG_INFO(LOG, 2, "key (code=%d, state=%u)", int(key_code), state); 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 GWL_KeyRepeatPlayload *key_repeat_payload = nullptr; /* Delete previous timer. */ if (seat->key_repeat.timer) { enum { NOP = 1, RESET, CANCEL } timer_action = NOP; key_repeat_payload = static_cast( seat->key_repeat.timer->getUserData()); if (seat->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(seat->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. */ seat->system->removeTimer(seat->key_repeat.timer); seat->key_repeat.timer = nullptr; break; } case CANCEL: { delete key_repeat_payload; key_repeat_payload = nullptr; seat->system->removeTimer(seat->key_repeat.timer); seat->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(seat->xkb_state, key_code, utf8_buf, sizeof(utf8_buf)); } seat->data_source_serial = serial; keyboard_depressed_state_key_event(seat, gkey, etype); if (wl_surface *wl_surface_focus = seat->keyboard.wl_surface_window) { GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent( new GHOST_EventKey(seat->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 ((seat->key_repeat.rate > 0) && (etype == GHOST_kEventKeyDown) && xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb_state), key_code)) { key_repeat_payload = new GWL_KeyRepeatPlayload(); key_repeat_payload->seat = seat; key_repeat_payload->key_code = key_code; key_repeat_payload->key_data.gkey = gkey; } } if (key_repeat_payload) { auto key_repeat_fn = [](GHOST_ITimerTask *task, uint64_t /*time*/) { struct GWL_KeyRepeatPlayload *payload = static_cast( task->getUserData()); GWL_Seat *seat = payload->seat; if (wl_surface *wl_surface_focus = seat->keyboard.wl_surface_window) { GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus); GHOST_SystemWayland *system = seat->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(seat->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)); } }; seat->key_repeat.timer = seat->system->installTimer( seat->key_repeat.delay, 1000 / seat->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); GWL_Seat *seat = static_cast(data); xkb_state_update_mask(seat->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 (seat->key_repeat.timer) { keyboard_handle_key_repeat_reset(seat, true); } #ifdef USE_GNOME_KEYBOARD_SUPPRESS_WARNING seat->key_depressed_suppress_warning.any_mod_held = mods_depressed != 0; #endif } 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); GWL_Seat *seat = static_cast(data); seat->key_repeat.rate = rate; seat->key_repeat.delay = delay; /* Unlikely possible this setting changes while repeating. */ if (seat->key_repeat.timer) { keyboard_handle_key_repeat_reset(seat, 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 (Primary Selection Offer), #zwp_primary_selection_offer_v1_listener * \{ */ static CLG_LogRef LOG_WL_PRIMARY_SELECTION_OFFER = {"ghost.wl.handle.primary_selection_offer"}; #define LOG (&LOG_WL_PRIMARY_SELECTION_OFFER) static void primary_selection_offer_offer(void *data, struct zwp_primary_selection_offer_v1 *id, const char *type) { GWL_PrimarySelection_DataOffer *data_offer = static_cast(data); if (data_offer->id != id) { CLOG_INFO(LOG, 2, "offer: %p: offer for unknown selection %p of %s (skipped)", data, id, type); return; } data_offer->types.insert(std::string(type)); } static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { primary_selection_offer_offer, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Primary Selection Device), #zwp_primary_selection_device_v1_listener * \{ */ static CLG_LogRef LOG_WL_PRIMARY_SELECTION_DEVICE = {"ghost.wl.handle.primary_selection_device"}; #define LOG (&LOG_WL_PRIMARY_SELECTION_DEVICE) static void primary_selection_device_handle_data_offer( void * /*data*/, struct zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/, struct zwp_primary_selection_offer_v1 *id) { CLOG_INFO(LOG, 2, "data_offer"); GWL_PrimarySelection_DataOffer *data_offer = new GWL_PrimarySelection_DataOffer; data_offer->id = id; zwp_primary_selection_offer_v1_add_listener(id, &primary_selection_offer_listener, data_offer); } static void primary_selection_device_handle_selection( void *data, struct zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/, struct zwp_primary_selection_offer_v1 *id) { GWL_PrimarySelection *primary = static_cast(data); std::lock_guard lock{primary->data_offer_mutex}; /* Delete old data offer. */ if (primary->data_offer != nullptr) { gwl_primary_selection_discard_offer(primary); } if (id == nullptr) { CLOG_INFO(LOG, 2, "selection: (skipped)"); return; } CLOG_INFO(LOG, 2, "selection"); /* Get new data offer. */ GWL_PrimarySelection_DataOffer *data_offer = static_cast( zwp_primary_selection_offer_v1_get_user_data(id)); primary->data_offer = data_offer; } static const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = { primary_selection_device_handle_data_offer, primary_selection_device_handle_selection, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Primary Selection Source), #zwp_primary_selection_source_v1_listener * \{ */ static CLG_LogRef LOG_WL_PRIMARY_SELECTION_SOURCE = {"ghost.wl.handle.primary_selection_source"}; #define LOG (&LOG_WL_PRIMARY_SELECTION_SOURCE) static void primary_selection_source_send(void *data, struct zwp_primary_selection_source_v1 * /*source*/, const char * /*mime_type*/, int32_t fd) { CLOG_INFO(LOG, 2, "send"); GWL_PrimarySelection *primary = static_cast(data); auto write_file_fn = [](GWL_PrimarySelection *primary, const int fd) { if (UNLIKELY(write(fd, primary->data_source->buffer_out.data, primary->data_source->buffer_out.data_size) < 0)) { CLOG_WARN(LOG, "error writing to primary clipboard: %s", std::strerror(errno)); } close(fd); primary->data_source_mutex.unlock(); }; primary->data_source_mutex.lock(); std::thread write_thread(write_file_fn, primary, fd); write_thread.detach(); } static void primary_selection_source_cancelled(void *data, struct zwp_primary_selection_source_v1 *source) { CLOG_INFO(LOG, 2, "cancelled"); GWL_PrimarySelection *primary = static_cast(data); if (source == primary->data_source->wp_source) { gwl_primary_selection_discard_source(primary); } } static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = { primary_selection_source_send, primary_selection_source_cancelled, }; #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 gwl_seat_capability_pointer_enable(GWL_Seat *seat) { if (seat->wl_pointer) { return; } seat->wl_pointer = wl_seat_get_pointer(seat->wl_seat); seat->cursor.wl_surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor()); seat->cursor.visible = true; seat->cursor.wl_buffer = nullptr; if (!get_cursor_settings(seat->cursor.theme_name, seat->cursor.theme_size)) { seat->cursor.theme_name = std::string(); seat->cursor.theme_size = default_cursor_size; } wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); wl_surface_add_listener(seat->cursor.wl_surface_cursor, &cursor_surface_listener, seat); ghost_wl_surface_tag_cursor_pointer(seat->cursor.wl_surface_cursor); zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures(); if (pointer_gestures) { #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE { /* Hold gesture. */ struct zwp_pointer_gesture_hold_v1 *gesture = zwp_pointer_gestures_v1_get_hold_gesture( pointer_gestures, seat->wl_pointer); zwp_pointer_gesture_hold_v1_set_user_data(gesture, seat); zwp_pointer_gesture_hold_v1_add_listener(gesture, &gesture_hold_listener, seat); seat->wp_pointer_gesture_hold = gesture; } #endif #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE { /* Pinch gesture. */ struct zwp_pointer_gesture_pinch_v1 *gesture = zwp_pointer_gestures_v1_get_pinch_gesture( pointer_gestures, seat->wl_pointer); zwp_pointer_gesture_pinch_v1_set_user_data(gesture, seat); zwp_pointer_gesture_pinch_v1_add_listener(gesture, &gesture_pinch_listener, seat); seat->wp_pointer_gesture_pinch = gesture; } #endif #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE { /* Swipe gesture. */ struct zwp_pointer_gesture_swipe_v1 *gesture = zwp_pointer_gestures_v1_get_swipe_gesture( pointer_gestures, seat->wl_pointer); zwp_pointer_gesture_swipe_v1_set_user_data(gesture, seat); zwp_pointer_gesture_swipe_v1_add_listener(gesture, &gesture_swipe_listener, seat); seat->wp_pointer_gesture_swipe = gesture; } #endif } } static void gwl_seat_capability_pointer_disable(GWL_Seat *seat) { if (!seat->wl_pointer) { return; } zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures(); if (pointer_gestures) { #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE { /* Hold gesture. */ struct zwp_pointer_gesture_hold_v1 **gesture_p = &seat->wp_pointer_gesture_hold; if (*gesture_p) { zwp_pointer_gesture_hold_v1_destroy(*gesture_p); *gesture_p = nullptr; } } #endif #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE { /* Pinch gesture. */ struct zwp_pointer_gesture_pinch_v1 **gesture_p = &seat->wp_pointer_gesture_pinch; if (*gesture_p) { zwp_pointer_gesture_pinch_v1_destroy(*gesture_p); *gesture_p = nullptr; } } #endif #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE { /* Swipe gesture. */ struct zwp_pointer_gesture_swipe_v1 **gesture_p = &seat->wp_pointer_gesture_swipe; if (*gesture_p) { zwp_pointer_gesture_swipe_v1_destroy(*gesture_p); *gesture_p = nullptr; } } #endif } if (seat->cursor.wl_surface_cursor) { wl_surface_destroy(seat->cursor.wl_surface_cursor); seat->cursor.wl_surface_cursor = nullptr; } if (seat->cursor.wl_theme) { wl_cursor_theme_destroy(seat->cursor.wl_theme); seat->cursor.wl_theme = nullptr; } wl_pointer_destroy(seat->wl_pointer); seat->wl_pointer = nullptr; } static void gwl_seat_capability_keyboard_enable(GWL_Seat *seat) { if (seat->wl_keyboard) { return; } seat->wl_keyboard = wl_seat_get_keyboard(seat->wl_seat); wl_keyboard_add_listener(seat->wl_keyboard, &keyboard_listener, seat); } static void gwl_seat_capability_keyboard_disable(GWL_Seat *seat) { if (!seat->wl_keyboard) { return; } if (seat->key_repeat.timer) { keyboard_handle_key_repeat_cancel(seat); } wl_keyboard_destroy(seat->wl_keyboard); seat->wl_keyboard = nullptr; } static void gwl_seat_capability_touch_enable(GWL_Seat *seat) { if (seat->wl_touch) { return; } seat->wl_touch = wl_seat_get_touch(seat->wl_seat); wl_touch_set_user_data(seat->wl_touch, seat); wl_touch_add_listener(seat->wl_touch, &touch_seat_listener, seat); } static void gwl_seat_capability_touch_disable(GWL_Seat *seat) { if (!seat->wl_touch) { return; } wl_touch_destroy(seat->wl_touch); seat->wl_touch = nullptr; } static void seat_handle_capabilities(void *data, /* Only used in an assert. */ [[maybe_unused]] 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); GWL_Seat *seat = static_cast(data); GHOST_ASSERT(seat->wl_seat == wl_seat, "Seat mismatch"); if (capabilities & WL_SEAT_CAPABILITY_POINTER) { gwl_seat_capability_pointer_enable(seat); } else { gwl_seat_capability_pointer_disable(seat); } if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { gwl_seat_capability_keyboard_enable(seat); } else { gwl_seat_capability_keyboard_disable(seat); } if (capabilities & WL_SEAT_CAPABILITY_TOUCH) { gwl_seat_capability_touch_enable(seat); } else { gwl_seat_capability_touch_disable(seat); } } 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); GWL_Output *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); GWL_Output *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. */ GWL_Output *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); GWL_Output *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); GWL_Output *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"); GWL_Output *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"); GWL_Output *output = static_cast(data); output->scale = factor; GHOST_WindowManager *window_manager = output->system->getWindowManager(); if (window_manager) { for (GHOST_IWindow *iwin : window_manager->getWindows()) { GHOST_WindowWayland *win = static_cast(iwin); const std::vector &outputs = win->outputs(); if (std::find(outputs.begin(), outputs.end(), output) == outputs.cend()) { continue; } 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 * \{ */ static CLG_LogRef LOG_WL_XDG_WM_BASE = {"ghost.wl.handle.xdg_wm_base"}; #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 /** \} */ /* -------------------------------------------------------------------- */ /** \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) /* #GWL_Display.wl_compositor */ static void gwl_registry_compositor_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wl_compositor = static_cast( wl_registry_bind(display->wl_registry, params->name, &wl_compositor_interface, 3)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_compositor_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct wl_compositor **value_p = &display->wl_compositor; wl_compositor_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.xdg_decor.shell */ static void gwl_registry_xdg_wm_base_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_XDG_Decor_System &decor = *display->xdg_decor; decor.shell = static_cast( wl_registry_bind(display->wl_registry, params->name, &xdg_wm_base_interface, 1)); xdg_wm_base_add_listener(decor.shell, &shell_listener, nullptr); decor.shell_name = params->name; gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_wm_base_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { GWL_XDG_Decor_System &decor = *display->xdg_decor; struct xdg_wm_base **value_p = &decor.shell; uint32_t *name_p = &decor.shell_name; xdg_wm_base_destroy(*value_p); *value_p = nullptr; *name_p = WL_NAME_UNSET; } /* #GWL_Display.xdg_decor.manager */ static void gwl_registry_xdg_decoration_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_XDG_Decor_System &decor = *display->xdg_decor; decor.manager = static_cast(wl_registry_bind( display->wl_registry, params->name, &zxdg_decoration_manager_v1_interface, 1)); decor.manager_name = params->name; gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_decoration_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { GWL_XDG_Decor_System &decor = *display->xdg_decor; struct zxdg_decoration_manager_v1 **value_p = &decor.manager; uint32_t *name_p = &decor.manager_name; zxdg_decoration_manager_v1_destroy(*value_p); *value_p = nullptr; *name_p = WL_NAME_UNSET; } /* #GWL_Display.xdg_output_manager */ static void gwl_registry_xdg_output_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->xdg_output_manager = static_cast( wl_registry_bind(display->wl_registry, params->name, &zxdg_output_manager_v1_interface, 2)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_output_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct zxdg_output_manager_v1 **value_p = &display->xdg_output_manager; zxdg_output_manager_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wl_output */ static void gwl_registry_wl_output_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_Output *output = new GWL_Output; output->system = display->system; output->wl_output = static_cast( wl_registry_bind(display->wl_registry, params->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); gwl_registry_entry_add(display, params, static_cast(output)); } static void gwl_registry_wl_output_update(GWL_Display *display, const GWL_RegisteryUpdate_Params *params) { GWL_Output *output = static_cast(params->user_data); if (display->xdg_output_manager) { if (output->xdg_output == nullptr) { 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 { output->xdg_output = nullptr; } } static void gwl_registry_wl_output_remove(GWL_Display *display, void *user_data, const bool /*on_exit*/) { /* While windows & cursors hold references to outputs, there is no need to manually remove * these references as the compositor will remove references via #wl_surface_listener.leave. */ GWL_Output *output = static_cast(user_data); wl_output_destroy(output->wl_output); std::vector::iterator iter = std::find( display->outputs.begin(), display->outputs.end(), output); const int index = (iter != display->outputs.cend()) ? std::distance(display->outputs.begin(), iter) : -1; GHOST_ASSERT(index != -1, "invalid internal state"); /* NOTE: always erase even when `on_exit` because `output->xdg_output` is cleared later. */ display->outputs.erase(display->outputs.begin() + index); delete output; } /* #GWL_Display.seats */ static void gwl_registry_wl_seat_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_Seat *seat = new GWL_Seat; seat->system = display->system; seat->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); seat->data_source = new GWL_DataSource; seat->wl_seat = static_cast( wl_registry_bind(display->wl_registry, params->name, &wl_seat_interface, 5)); display->seats.push_back(seat); wl_seat_add_listener(seat->wl_seat, &seat_listener, seat); gwl_registry_entry_add(display, params, static_cast(seat)); } static void gwl_registry_wl_seat_update(GWL_Display *display, const GWL_RegisteryUpdate_Params *params) { GWL_Seat *seat = static_cast(params->user_data); /* Register data device per seat for IPC between WAYLAND clients. */ if (display->wl_data_device_manager) { if (seat->wl_data_device == nullptr) { seat->wl_data_device = wl_data_device_manager_get_data_device( display->wl_data_device_manager, seat->wl_seat); wl_data_device_add_listener(seat->wl_data_device, &data_device_listener, seat); } } else { seat->wl_data_device = nullptr; } if (display->wp_tablet_manager) { if (seat->wp_tablet_seat == nullptr) { seat->wp_tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(display->wp_tablet_manager, seat->wl_seat); zwp_tablet_seat_v2_add_listener(seat->wp_tablet_seat, &tablet_seat_listener, seat); } } else { seat->wp_tablet_seat = nullptr; } if (display->wp_primary_selection_device_manager) { if (seat->wp_primary_selection_device == nullptr) { seat->wp_primary_selection_device = zwp_primary_selection_device_manager_v1_get_device( display->wp_primary_selection_device_manager, seat->wl_seat); zwp_primary_selection_device_v1_add_listener(seat->wp_primary_selection_device, &primary_selection_device_listener, &seat->primary_selection); } } else { seat->wp_primary_selection_device = nullptr; } } static void gwl_registry_wl_seat_remove(GWL_Display *display, void *user_data, const bool on_exit) { GWL_Seat *seat = static_cast(user_data); /* First handle members that require locking. * While highly unlikely, it's possible they are being used while this function runs. */ { std::lock_guard lock{seat->data_source_mutex}; if (seat->data_source) { gwl_simple_buffer_free_data(&seat->data_source->buffer_out); if (seat->data_source->wl_source) { wl_data_source_destroy(seat->data_source->wl_source); } delete seat->data_source; } } { std::lock_guard lock{seat->data_offer_dnd_mutex}; if (seat->data_offer_dnd) { wl_data_offer_destroy(seat->data_offer_dnd->id); delete seat->data_offer_dnd; } } { std::lock_guard lock{seat->data_offer_copy_paste_mutex}; if (seat->data_offer_copy_paste) { wl_data_offer_destroy(seat->data_offer_copy_paste->id); delete seat->data_offer_copy_paste; } } { GWL_PrimarySelection *primary = &seat->primary_selection; std::lock_guard lock{primary->data_offer_mutex}; gwl_primary_selection_discard_offer(primary); } { GWL_PrimarySelection *primary = &seat->primary_selection; std::lock_guard lock{primary->data_source_mutex}; gwl_primary_selection_discard_source(primary); } if (seat->wp_primary_selection_device) { zwp_primary_selection_device_v1_destroy(seat->wp_primary_selection_device); } if (seat->wl_data_device) { wl_data_device_release(seat->wl_data_device); } if (seat->cursor.custom_data) { munmap(seat->cursor.custom_data, seat->cursor.custom_data_size); } /* Disable all capabilities as a way to free: * - `seat.wl_pointer` (and related cursor variables). * - `seat.wl_touch`. * - `seat.wl_keyboard`. */ gwl_seat_capability_pointer_disable(seat); gwl_seat_capability_keyboard_disable(seat); gwl_seat_capability_touch_disable(seat); /* Un-referencing checks for NULL case. */ xkb_state_unref(seat->xkb_state); xkb_state_unref(seat->xkb_state_empty); xkb_state_unref(seat->xkb_state_empty_with_shift); xkb_state_unref(seat->xkb_state_empty_with_numlock); xkb_context_unref(seat->xkb_context); /* Remove the seat. */ wl_seat_destroy(seat->wl_seat); std::vector::iterator iter = std::find( display->seats.begin(), display->seats.end(), seat); const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) : -1; GHOST_ASSERT(index != -1, "invalid internal state"); if (!on_exit) { if (display->seats_active_index >= index) { display->seats_active_index -= 1; } display->seats.erase(display->seats.begin() + index); } delete seat; } /* #GWL_Display.wl_shm */ static void gwl_registry_wl_shm_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wl_shm = static_cast( wl_registry_bind(display->wl_registry, params->name, &wl_shm_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wl_shm_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct wl_shm **value_p = &display->wl_shm; wl_shm_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wl_data_device_manager */ static void gwl_registry_wl_data_device_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wl_data_device_manager = static_cast( wl_registry_bind(display->wl_registry, params->name, &wl_data_device_manager_interface, 3)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wl_data_device_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct wl_data_device_manager **value_p = &display->wl_data_device_manager; wl_data_device_manager_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_tablet_manager */ static void gwl_registry_wp_tablet_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp_tablet_manager = static_cast( wl_registry_bind(display->wl_registry, params->name, &zwp_tablet_manager_v2_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_tablet_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct zwp_tablet_manager_v2 **value_p = &display->wp_tablet_manager; zwp_tablet_manager_v2_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_relative_pointer_manager */ static void gwl_registry_wp_relative_pointer_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp_relative_pointer_manager = static_cast( wl_registry_bind( display->wl_registry, params->name, &zwp_relative_pointer_manager_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_relative_pointer_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct zwp_relative_pointer_manager_v1 **value_p = &display->wp_relative_pointer_manager; zwp_relative_pointer_manager_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_pointer_constraints */ static void gwl_registry_wp_pointer_constraints_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp_pointer_constraints = static_cast(wl_registry_bind( display->wl_registry, params->name, &zwp_pointer_constraints_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_pointer_constraints_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct zwp_pointer_constraints_v1 **value_p = &display->wp_pointer_constraints; zwp_pointer_constraints_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_pointer_gestures */ static void gwl_registry_wp_pointer_gestures_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp_pointer_gestures = static_cast( wl_registry_bind(display->wl_registry, params->name, &zwp_pointer_gestures_v1_interface, 3)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_pointer_gestures_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct zwp_pointer_gestures_v1 **value_p = &display->wp_pointer_gestures; zwp_pointer_gestures_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_primary_selection_device_manager */ static void gwl_registry_wp_primary_selection_device_manager_add( struct GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp_primary_selection_device_manager = static_cast( wl_registry_bind(display->wl_registry, params->name, &zwp_primary_selection_device_manager_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_primary_selection_device_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { struct zwp_primary_selection_device_manager_v1 **value_p = &display->wp_primary_selection_device_manager; zwp_primary_selection_device_manager_v1_destroy(*value_p); *value_p = nullptr; } /** * Map interfaces to initialization functions. * * \note This list also defines the order interfaces are removed. * On exit interface removal runs from last to first to avoid potential bugs * caused by undefined order of removal. * * In general fundamental, low level objects such as the compositor and shared memory * should be declared earlier and other interfaces that may use them should be declared later. */ static const GWL_RegistryHandler gwl_registry_handlers[] = { /* Low level interfaces. */ { &wl_compositor_interface.name, gwl_registry_compositor_add, nullptr, gwl_registry_compositor_remove, }, { &wl_shm_interface.name, gwl_registry_wl_shm_add, nullptr, gwl_registry_wl_shm_remove, }, { &xdg_wm_base_interface.name, gwl_registry_xdg_wm_base_add, nullptr, gwl_registry_xdg_wm_base_remove, }, /* Managers. */ { &zxdg_decoration_manager_v1_interface.name, gwl_registry_xdg_decoration_manager_add, nullptr, gwl_registry_xdg_decoration_manager_remove, }, { &zxdg_output_manager_v1_interface.name, gwl_registry_xdg_output_manager_add, nullptr, gwl_registry_xdg_output_manager_remove, }, { &wl_data_device_manager_interface.name, gwl_registry_wl_data_device_manager_add, nullptr, gwl_registry_wl_data_device_manager_remove, }, { &zwp_primary_selection_device_manager_v1_interface.name, gwl_registry_wp_primary_selection_device_manager_add, nullptr, gwl_registry_wp_primary_selection_device_manager_remove, }, { &zwp_tablet_manager_v2_interface.name, gwl_registry_wp_tablet_manager_add, nullptr, gwl_registry_wp_tablet_manager_remove, }, { &zwp_relative_pointer_manager_v1_interface.name, gwl_registry_wp_relative_pointer_manager_add, nullptr, gwl_registry_wp_relative_pointer_manager_remove, }, /* Higher level interfaces. */ { &zwp_pointer_constraints_v1_interface.name, gwl_registry_wp_pointer_constraints_add, nullptr, gwl_registry_wp_pointer_constraints_remove, }, { &zwp_pointer_gestures_v1_interface.name, gwl_registry_wp_pointer_gestures_add, nullptr, gwl_registry_wp_pointer_gestures_remove, }, /* Display outputs. */ { &wl_output_interface.name, gwl_registry_wl_output_add, gwl_registry_wl_output_update, gwl_registry_wl_output_remove, }, /* Seats. * Keep the seat near the end to ensure other types are created first. * as the seat creates data based on other interfaces. */ { &wl_seat_interface.name, gwl_registry_wl_seat_add, gwl_registry_wl_seat_update, gwl_registry_wl_seat_remove, }, {nullptr, nullptr, nullptr}, }; /** * Workaround for `gwl_registry_handlers` order of declaration, * preventing `ARRAY_SIZE(gwl_registry_handlers) - 1` being used. */ static int gwl_registry_handler_interface_slot_max() { return ARRAY_SIZE(gwl_registry_handlers) - 1; } static int gwl_registry_handler_interface_slot_from_string(const char *interface) { for (const GWL_RegistryHandler *handler = gwl_registry_handlers; handler->interface_p != nullptr; handler++) { if (STREQ(interface, *handler->interface_p)) { return int(handler - gwl_registry_handlers); } } return -1; } static const GWL_RegistryHandler *gwl_registry_handler_from_interface_slot(int interface_slot) { GHOST_ASSERT(uint32_t(interface_slot) < uint32_t(gwl_registry_handler_interface_slot_max()), "Index out of range"); return &gwl_registry_handlers[interface_slot]; } static void global_handle_add(void *data, [[maybe_unused]] struct wl_registry *wl_registry, const uint32_t name, const char *interface, const uint32_t version) { /* Log last since it's useful to know if the interface was handled or not. */ GWL_Display *display = static_cast(data); GHOST_ASSERT(display->wl_registry == wl_registry, "Registry argument must match!"); const int interface_slot = gwl_registry_handler_interface_slot_from_string(interface); bool added = false; if (interface_slot != -1) { const GWL_RegistryHandler *handler = &gwl_registry_handlers[interface_slot]; const GWL_RegistryEntry *registry_entry_prev = display->registry_entry; /* The interface name that is ensured not to be freed. */ GWL_RegisteryAdd_Params params = { .name = name, .interface_slot = interface_slot, .version = version, }; handler->add_fn(display, ¶ms); added = display->registry_entry != registry_entry_prev; } else { /* Not found. */ #ifdef USE_GNOME_NEEDS_LIBDECOR_HACK if (STRPREFIX(interface, "gtk_shell")) { /* `gtk_shell1` at time of writing. */ /* Only require `libdecor` when built with X11 support, * otherwise there is nothing to fall back on. */ display->libdecor_required = true; } #endif } CLOG_INFO(LOG, 2, "add %s(interface=%s, version=%u, name=%u)", (interface_slot != -1) ? (added ? "" : "(found but not added)") : "(skipped), ", interface, version, name); /* Initialization avoids excessive calls by calling update after all have been initialized. */ if (added) { if (display->registry_skip_update_all == false) { /* See doc-string for rationale on updating all on add/removal. */ gwl_registry_entry_update_all(display, interface_slot); } } } /** * 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, [[maybe_unused]] struct wl_registry *wl_registry, const uint32_t name) { GWL_Display *display = static_cast(data); GHOST_ASSERT(display->wl_registry == wl_registry, "Registry argument must match!"); int interface_slot = 0; const bool removed = gwl_registry_entry_remove_by_name(display, name, &interface_slot); CLOG_INFO(LOG, 2, "remove (name=%u, interface=%s)", name, removed ? *gwl_registry_handlers[interface_slot].interface_p : "(unknown)"); if (removed) { if (display->registry_skip_update_all == false) { /* See doc-string for rationale on updating all on add/removal. */ gwl_registry_entry_update_all(display, interface_slot); } } } static const struct wl_registry_listener registry_listener = { global_handle_add, global_handle_remove, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Display), #wl_display_listener * \{ */ static CLG_LogRef LOG_WL_DISPLAY = {"ghost.wl.handle.display"}; #define LOG (&LOG_WL_DISPLAY) static void display_handle_error( void *data, struct wl_display *wl_display, void *object_id, uint32_t code, const char *message) { GWL_Display *display = static_cast(data); GHOST_ASSERT(display->wl_display == wl_display, "Invalid internal state"); (void)display; /* NOTE: code is #wl_display_error, there isn't a convenient way to convert to an ID. */ CLOG_INFO(LOG, 2, "error (code=%u, object_id=%p, message=%s)", code, object_id, message); } static void display_handle_delete_id(void *data, struct wl_display *wl_display, uint32_t id) { GWL_Display *display = static_cast(data); GHOST_ASSERT(display->wl_display == wl_display, "Invalid internal state"); (void)display; CLOG_INFO(LOG, 2, "delete_id (id=%u)", id); } static const struct wl_display_listener display_listener = { display_handle_error, display_handle_delete_id, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name GHOST Implementation * * WAYLAND specific implementation of the #GHOST_System interface. * \{ */ GHOST_SystemWayland::GHOST_SystemWayland(bool background) : GHOST_System(), display_(new GWL_Display) { wl_log_set_handler_client(ghost_wayland_log_handler); display_->system = this; /* Connect to the Wayland server. */ display_->wl_display = wl_display_connect(nullptr); if (!display_->wl_display) { gwl_display_destroy(display_); throw std::runtime_error("Wayland: unable to connect to display!"); } /* This may be removed later if decorations are required, needed as part of registration. */ display_->xdg_decor = new GWL_XDG_Decor_System; wl_display_add_listener(display_->wl_display, &display_listener, display_); /* Register interfaces. */ { display_->registry_skip_update_all = true; struct wl_registry *registry = wl_display_get_registry(display_->wl_display); display_->wl_registry = registry; wl_registry_add_listener(registry, ®istry_listener, display_); /* First round-trip to receive all registry objects. */ wl_display_roundtrip(display_->wl_display); /* Second round-trip to receive all output events. */ wl_display_roundtrip(display_->wl_display); /* Account for dependencies between interfaces. */ gwl_registry_entry_update_all(display_, -1); display_->registry_skip_update_all = false; } #ifdef WITH_GHOST_WAYLAND_LIBDECOR /* Ignore windowing requirements when running in background mode, * as it doesn't make sense to fall back to X11 because of windowing functionality * in background mode, also LIBDECOR is crashing in background mode `blender -b -f 1` * for e.g. while it could be fixed, requiring the library at all makes no sense . */ if (background) { display_->libdecor_required = false; } if (display_->libdecor_required) { gwl_xdg_decor_system_destroy(display_, display_->xdg_decor); display_->xdg_decor = nullptr; if (!has_libdecor) { # ifdef WITH_GHOST_X11 /* LIBDECOR was the only reason X11 was used, let the user know they need it installed. */ fprintf(stderr, "WAYLAND found but libdecor was not, install libdecor for Wayland support, " "falling back to X11\n"); # endif gwl_display_destroy(display_); throw std::runtime_error("Wayland: unable to find libdecor!"); use_libdecor = true; } } else { use_libdecor = false; } #endif /* WITH_GHOST_WAYLAND_LIBDECOR */ #ifdef WITH_GHOST_WAYLAND_LIBDECOR if (use_libdecor) { display_->libdecor = new GWL_LibDecor_System; GWL_LibDecor_System &decor = *display_->libdecor; decor.context = libdecor_new(display_->wl_display, &libdecor_interface); if (!decor.context) { gwl_display_destroy(display_); throw std::runtime_error("Wayland: unable to create window decorations!"); } } else #else (void)background; #endif { GWL_XDG_Decor_System &decor = *display_->xdg_decor; if (!decor.shell) { gwl_display_destroy(display_); throw std::runtime_error("Wayland: unable to access xdg_shell!"); } } } GHOST_SystemWayland::~GHOST_SystemWayland() { gwl_display_destroy(display_); } 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) { if (wl_display_dispatch(display_->wl_display) == -1) { ghost_wl_display_report_error(display_->wl_display); } } else { if (wl_display_roundtrip(display_->wl_display) == -1) { ghost_wl_display_report_error(display_->wl_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 { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } const xkb_mod_mask_t state = xkb_state_serialize_mods(seat->xkb_state, XKB_STATE_MODS_DEPRESSED); bool show_warning = true; #ifdef USE_GNOME_KEYBOARD_SUPPRESS_WARNING if ((seat->key_depressed_suppress_warning.any_mod_held == true) && (seat->key_depressed_suppress_warning.any_keys_held_on_enter == false)) { /* The compositor gave us invalid information, don't show a warning. */ show_warning = false; } #endif /* Use local #GWL_KeyboardDepressedState to check which key is pressed. * Use XKB as the source of truth, if there is any discrepancy. */ for (int i = 0; i < MOD_INDEX_NUM; i++) { if (UNLIKELY(seat->xkb_keymap_mod_index[i] == XKB_MOD_INVALID)) { continue; } const GWL_ModifierInfo &mod_info = g_modifier_info_table[i]; const bool val = (state & (1 << seat->xkb_keymap_mod_index[i])) != 0; bool val_l = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_l)] > 0; bool val_r = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_r)] > 0; /* This shouldn't be needed, but guard against any possibility of modifiers being stuck. * Warn so if this happens it can be investigated. */ if (val) { if (UNLIKELY(!(val_l || val_r))) { if (show_warning) { CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE, "modifier (%s) state is inconsistent (GHOST held keys do not match XKB)", mod_info.display_name); } /* Picking the left is arbitrary. */ val_l = true; } } else { if (UNLIKELY(val_l || val_r)) { if (show_warning) { CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE, "modifier (%s) state is inconsistent (GHOST released keys do not match XKB)", mod_info.display_name); } val_l = false; val_r = false; } } keys.set(mod_info.mod_l, val_l); keys.set(mod_info.mod_r, val_r); } return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat); if (!seat_state_pointer) { return GHOST_kFailure; } buttons = seat_state_pointer->buttons; return GHOST_kSuccess; } /** * Return a mime type which is supported by GHOST and exists in `types` * (defined by the data offer). */ static const char *system_clipboard_text_mime_type( const std::unordered_set &data_offer_types) { const char *ghost_supported_types[] = {ghost_wl_mime_text_utf8, ghost_wl_mime_text_plain}; for (size_t i = 0; i < ARRAY_SIZE(ghost_supported_types); i++) { if (data_offer_types.count(ghost_supported_types[i])) { return ghost_supported_types[i]; } } return nullptr; } static char *system_clipboard_get_primary_selection(GWL_Display *display) { GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return nullptr; } GWL_PrimarySelection *primary = &seat->primary_selection; std::mutex &mutex = primary->data_offer_mutex; mutex.lock(); bool mutex_locked = true; char *data = nullptr; GWL_PrimarySelection_DataOffer *data_offer = primary->data_offer; if (data_offer != nullptr) { const char *mime_receive = system_clipboard_text_mime_type(data_offer->types); if (mime_receive) { /* Receive the clipboard in a thread, performing round-trips while waiting. * This is needed so pasting contents from our own `primary->data_source` doesn't hang. */ struct ThreadResult { char *data = nullptr; std::atomic done = false; } thread_result; auto read_clipboard_fn = [](GWL_PrimarySelection_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, struct ThreadResult *thread_result) { size_t data_len = 0; thread_result->data = read_buffer_from_primary_selection_offer( data_offer, mime_receive, mutex, true, &data_len); thread_result->done = true; }; std::thread read_thread(read_clipboard_fn, data_offer, mime_receive, &mutex, &thread_result); read_thread.detach(); while (!thread_result.done) { wl_display_roundtrip(display->wl_display); } data = thread_result.data; /* Reading the data offer unlocks the mutex. */ mutex_locked = false; } } if (mutex_locked) { mutex.unlock(); } return data; } static char *system_clipboard_get(GWL_Display *display) { GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return nullptr; } std::mutex &mutex = seat->data_offer_copy_paste_mutex; mutex.lock(); bool mutex_locked = true; char *data = nullptr; GWL_DataOffer *data_offer = seat->data_offer_copy_paste; if (data_offer != nullptr) { const char *mime_receive = system_clipboard_text_mime_type(data_offer->types); if (mime_receive) { /* Receive the clipboard in a thread, performing round-trips while waiting. * This is needed so pasting contents from our own `seat->data_source` doesn't hang. */ struct ThreadResult { char *data = nullptr; std::atomic done = false; } thread_result; auto read_clipboard_fn = [](GWL_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, struct ThreadResult *thread_result) { size_t data_len = 0; thread_result->data = read_buffer_from_data_offer( data_offer, mime_receive, mutex, true, &data_len); thread_result->done = true; }; std::thread read_thread(read_clipboard_fn, data_offer, mime_receive, &mutex, &thread_result); read_thread.detach(); while (!thread_result.done) { wl_display_roundtrip(display->wl_display); } data = thread_result.data; /* Reading the data offer unlocks the mutex. */ mutex_locked = false; } } if (mutex_locked) { mutex.unlock(); } return data; } char *GHOST_SystemWayland::getClipboard(bool selection) const { char *data = nullptr; if (selection) { data = system_clipboard_get_primary_selection(display_); } else { data = system_clipboard_get(display_); } return data; } static void system_clipboard_put_primary_selection(GWL_Display *display, const char *buffer) { if (!display->wp_primary_selection_device_manager) { return; } GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return; } GWL_PrimarySelection *primary = &seat->primary_selection; std::lock_guard lock{primary->data_source_mutex}; gwl_primary_selection_discard_source(primary); GWL_PrimarySelection_DataSource *data_source = new GWL_PrimarySelection_DataSource; primary->data_source = data_source; /* Copy buffer. */ gwl_simple_buffer_set_from_string(&data_source->buffer_out, buffer); data_source->wp_source = zwp_primary_selection_device_manager_v1_create_source( display->wp_primary_selection_device_manager); zwp_primary_selection_source_v1_add_listener( data_source->wp_source, &primary_selection_source_listener, primary); for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_send); i++) { zwp_primary_selection_source_v1_offer(data_source->wp_source, ghost_wl_mime_send[i]); } if (seat->wp_primary_selection_device) { zwp_primary_selection_device_v1_set_selection( seat->wp_primary_selection_device, data_source->wp_source, seat->data_source_serial); } } static void system_clipboard_put(GWL_Display *display, const char *buffer) { if (!display->wl_data_device_manager) { return; } GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return; } std::lock_guard lock{seat->data_source_mutex}; GWL_DataSource *data_source = seat->data_source; /* Copy buffer. */ gwl_simple_buffer_set_from_string(&data_source->buffer_out, buffer); data_source->wl_source = wl_data_device_manager_create_data_source( display->wl_data_device_manager); wl_data_source_add_listener(data_source->wl_source, &data_source_listener, seat); for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_send); i++) { wl_data_source_offer(data_source->wl_source, ghost_wl_mime_send[i]); } if (seat->wl_data_device) { wl_data_device_set_selection( seat->wl_data_device, data_source->wl_source, seat->data_source_serial); } } void GHOST_SystemWayland::putClipboard(const char *buffer, bool selection) const { if (selection) { system_clipboard_put_primary_selection(display_, buffer); } else { system_clipboard_put(display_, buffer); } } uint8_t GHOST_SystemWayland::getNumDisplays() const { return display_ ? uint8_t(display_->outputs.size()) : 0; } static GHOST_TSuccess getCursorPositionClientRelative_impl( const GWL_SeatStatePointer *seat_state_pointer, const GHOST_WindowWayland *win, int32_t &x, int32_t &y) { const wl_fixed_t scale = win->scale(); x = wl_fixed_to_int(scale * seat_state_pointer->xy[0]); y = wl_fixed_to_int(scale * seat_state_pointer->xy[1]); return GHOST_kSuccess; } static GHOST_TSuccess setCursorPositionClientRelative_impl(GWL_Seat *seat, 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 (!seat->wp_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(seat, win, xy_next); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getCursorPositionClientRelative(const GHOST_IWindow *window, int32_t &x, int32_t &y) const { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat); if (!seat_state_pointer || !seat_state_pointer->wl_surface_window) { return GHOST_kFailure; } const GHOST_WindowWayland *win = static_cast(window); return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y); } GHOST_TSuccess GHOST_SystemWayland::setCursorPositionClientRelative(GHOST_IWindow *window, const int32_t x, const int32_t y) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GHOST_WindowWayland *win = static_cast(window); return setCursorPositionClientRelative_impl(seat, win, x, y); } GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat); if (!seat_state_pointer) { return GHOST_kFailure; } if (wl_surface *wl_surface_focus = seat_state_pointer->wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y); } return GHOST_kFailure; } GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(const int32_t x, const int32_t y) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } /* 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 *wl_surface_focus = seat->pointer.wl_surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); return setCursorPositionClientRelative_impl(seat, 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(display_->outputs[0]->size_native[0]); height = uint32_t(display_->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 GWL_Output *output : display_->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(wl_compositor()); wl_egl_window *egl_window = wl_surface ? wl_egl_window_create(wl_surface, 1, 1) : nullptr; GHOST_Context *context = createOffscreenContext_impl(this, display_->wl_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_GLSettings glSettings, const bool exclusive, const bool is_dialog, const GHOST_IWindow *parentWindow) { /* Globally store pointer to window manager. */ GHOST_WindowWayland *window = new GHOST_WindowWayland( this, title, left, top, width, height, state, parentWindow, glSettings.context_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 `seat->cursor.visible`. */ static void cursor_buffer_show(const GWL_Seat *seat) { const GWL_Cursor *cursor = &seat->cursor; if (seat->wl_pointer) { const int scale = cursor->is_custom ? cursor->custom_scale : seat->pointer.theme_scale; const int32_t hotspot_x = int32_t(cursor->wl_image.hotspot_x) / scale; const int32_t hotspot_y = int32_t(cursor->wl_image.hotspot_y) / scale; if (seat->wl_pointer) { wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, cursor->wl_surface_cursor, hotspot_x, hotspot_y); } } if (!seat->tablet_tools.empty()) { const int scale = cursor->is_custom ? cursor->custom_scale : seat->tablet.theme_scale; const int32_t hotspot_x = int32_t(cursor->wl_image.hotspot_x) / scale; const int32_t hotspot_y = int32_t(cursor->wl_image.hotspot_y) / scale; for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->tablet_tools) { GWL_TabletTool *tablet_tool = static_cast( zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, seat->tablet.serial, tablet_tool->wl_surface_cursor, hotspot_x, hotspot_y); #ifdef USE_KDE_TABLET_HIDDEN_CURSOR_HACK wl_surface_commit(tablet_tool->wl_surface_cursor); #endif } } } /** * 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 `seat->cursor.visible`. */ static void cursor_buffer_hide(const GWL_Seat *seat) { wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, nullptr, 0, 0); for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->tablet_tools) { zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, seat->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 GWL_Seat *seat, wl_buffer *buffer, struct wl_surface *wl_surface, const int scale) { const wl_cursor_image *wl_image = &seat->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 GWL_Seat *seat, wl_buffer *buffer) { const GWL_Cursor *cursor = &seat->cursor; const wl_cursor_image *wl_image = &seat->cursor.wl_image; const bool visible = (cursor->visible && cursor->is_hardware); /* This is a requirement of WAYLAND, when this isn't the case, * it causes Blender's window to close intermittently. */ if (seat->wl_pointer) { const int scale = cursor_buffer_compatible_scale_from_image( wl_image, cursor->is_custom ? cursor->custom_scale : seat->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(seat, buffer, cursor->wl_surface_cursor, scale); wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, visible ? cursor->wl_surface_cursor : nullptr, hotspot_x, hotspot_y); } /* Set the cursor for all tablet tools as well. */ if (!seat->tablet_tools.empty()) { const int scale = cursor_buffer_compatible_scale_from_image( wl_image, cursor->is_custom ? cursor->custom_scale : seat->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 : seat->tablet_tools) { GWL_TabletTool *tablet_tool = static_cast( zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); cursor_buffer_set_surface_impl(seat, buffer, tablet_tool->wl_surface_cursor, scale); zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, seat->tablet.serial, visible ? tablet_tool->wl_surface_cursor : 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(GWL_Seat *seat, const bool visible, const bool is_hardware, const enum eCursorSetMode set_mode) { GWL_Cursor *cursor = &seat->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(seat); } } else { if (was_visible) { cursor_buffer_hide(seat); } } 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) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } auto cursor_find = ghost_wl_cursors.find(shape); const char *cursor_name = (cursor_find == ghost_wl_cursors.end()) ? ghost_wl_cursors.at(GHOST_kStandardCursorDefault) : (*cursor_find).second; GWL_Cursor *cursor = &seat->cursor; if (!cursor->wl_theme) { /* The cursor wl_surface hasn't entered an output yet. Initialize theme with scale 1. */ cursor->wl_theme = wl_cursor_theme_load( cursor->theme_name.c_str(), cursor->theme_size, wl_shm()); } wl_cursor *wl_cursor = wl_cursor_theme_get_cursor(cursor->wl_theme, cursor_name); if (!wl_cursor) { GHOST_PRINT("cursor '" << cursor_name << "' does not exist" << std::endl); return GHOST_kFailure; } struct wl_cursor_image *image = wl_cursor->images[0]; struct wl_buffer *buffer = wl_cursor_image_get_buffer(image); if (!buffer) { return GHOST_kFailure; } cursor->visible = true; cursor->is_custom = false; cursor->wl_buffer = buffer; cursor->wl_image = *image; cursor_buffer_set(seat, buffer); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::hasCursorShape(const GHOST_TStandardCursor cursorShape) { auto cursor_find = ghost_wl_cursors.find(cursorShape); if (cursor_find == ghost_wl_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*/) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_Cursor *cursor = &seat->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(display_->wl_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(seat, buffer); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getCursorBitmap(GHOST_CursorBitmapRef *bitmap) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_Cursor *cursor = &seat->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) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } cursor_visible_set(seat, visible, seat->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) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return false; } #ifdef USE_GNOME_CONFINE_HACK const bool use_software_confine = seat->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 *wl_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(wl_surface); if (!win) { return false; } # ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON if (win->scale() <= 1) { return false; } # endif return true; } #endif static GWL_SeatStateGrab seat_grab_state_from_mode(const GHOST_TGrabCursorMode mode, const bool use_software_confine) { /* Initialize all members. */ GWL_SeatStateGrab grab_state; /* Warping happens to require software cursor which also hides. */ grab_state.use_lock = ELEM(mode, GHOST_kGrabWrap, GHOST_kGrabHide) || use_software_confine; grab_state.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 *wl_output) { return wl_proxy_get_tag((struct wl_proxy *)wl_output) == &ghost_wl_output_tag_id; } bool ghost_wl_surface_own(const struct wl_surface *wl_surface) { return wl_proxy_get_tag((struct wl_proxy *)wl_surface) == &ghost_wl_surface_tag_id; } bool ghost_wl_surface_own_cursor_pointer(const struct wl_surface *wl_surface) { return wl_proxy_get_tag((struct wl_proxy *)wl_surface) == &ghost_wl_surface_cursor_pointer_tag_id; } bool ghost_wl_surface_own_cursor_tablet(const struct wl_surface *wl_surface) { return wl_proxy_get_tag((struct wl_proxy *)wl_surface) == &ghost_wl_surface_cursor_tablet_tag_id; } void ghost_wl_output_tag(struct wl_output *wl_output) { wl_proxy_set_tag((struct wl_proxy *)wl_output, &ghost_wl_output_tag_id); } void ghost_wl_surface_tag(struct wl_surface *wl_surface) { wl_proxy_set_tag((struct wl_proxy *)wl_surface, &ghost_wl_surface_tag_id); } void ghost_wl_surface_tag_cursor_pointer(struct wl_surface *wl_surface) { wl_proxy_set_tag((struct wl_proxy *)wl_surface, &ghost_wl_surface_cursor_pointer_tag_id); } void ghost_wl_surface_tag_cursor_tablet(struct wl_surface *wl_surface) { wl_proxy_set_tag((struct wl_proxy *)wl_surface, &ghost_wl_surface_cursor_tablet_tag_id); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Direct Data Access * * Expose some members via methods. * \{ */ wl_display *GHOST_SystemWayland::wl_display() { return display_->wl_display; } wl_compositor *GHOST_SystemWayland::wl_compositor() { return display_->wl_compositor; } struct zwp_primary_selection_device_manager_v1 *GHOST_SystemWayland::wp_primary_selection_manager() { return display_->wp_primary_selection_device_manager; } struct zwp_pointer_gestures_v1 *GHOST_SystemWayland::wp_pointer_gestures() { return display_->wp_pointer_gestures; } #ifdef WITH_GHOST_WAYLAND_LIBDECOR libdecor *GHOST_SystemWayland::libdecor_context() { return display_->libdecor->context; } #endif /* !WITH_GHOST_WAYLAND_LIBDECOR */ xdg_wm_base *GHOST_SystemWayland::xdg_decor_shell() { return display_->xdg_decor->shell; } zxdg_decoration_manager_v1 *GHOST_SystemWayland::xdg_decor_manager() { return display_->xdg_decor->manager; } /* End `xdg_decor`. */ const std::vector &GHOST_SystemWayland::outputs() const { return display_->outputs; } struct wl_shm *GHOST_SystemWayland::wl_shm() const { return display_->wl_shm; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Query Access * \{ */ struct GWL_Output *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"); GWL_Output *output = static_cast(wl_output_get_user_data(wl_output)); return output; } GHOST_WindowWayland *ghost_wl_surface_user_data(struct wl_surface *wl_surface) { GHOST_ASSERT(wl_surface, "wl_surface must not be NULL"); GHOST_ASSERT(ghost_wl_surface_own(wl_surface), "wl_surface is not owned by GHOST"); GHOST_WindowWayland *win = static_cast( wl_surface_get_user_data(wl_surface)); return win; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Utility Functions * * Functionality only used for the WAYLAND implementation. * \{ */ void GHOST_SystemWayland::seat_active_set(const struct GWL_Seat *seat) { gwl_display_seat_active_set(display_, seat); } void GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface) { #define SURFACE_CLEAR_PTR(surface_test) \ if (surface_test == wl_surface) { \ surface_test = nullptr; \ } \ ((void)0); /* Only clear window surfaces (not cursors, off-screen surfaces etc). */ for (GWL_Seat *seat : display_->seats) { SURFACE_CLEAR_PTR(seat->pointer.wl_surface_window); SURFACE_CLEAR_PTR(seat->tablet.wl_surface_window); SURFACE_CLEAR_PTR(seat->keyboard.wl_surface_window); SURFACE_CLEAR_PTR(seat->wl_surface_window_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 *wl_surface, const int scale) { /* Ignore, if the required protocols are not supported. */ if (UNLIKELY(!display_->wp_relative_pointer_manager || !display_->wp_pointer_constraints)) { return GHOST_kFailure; } GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } /* No change, success. */ if (mode == mode_current) { return GHOST_kSuccess; } #ifdef USE_GNOME_CONFINE_HACK const bool was_software_confine = seat->use_pointer_software_confine; const bool use_software_confine = setCursorGrab_use_software_confine(mode, wl_surface); #else const bool was_software_confine = false; const bool use_software_confine = false; #endif const struct GWL_SeatStateGrab grab_state_prev = seat_grab_state_from_mode(mode_current, was_software_confine); const struct GWL_SeatStateGrab grab_state_next = seat_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(seat, 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 (seat->wp_relative_pointer) { zwp_relative_pointer_v1_destroy(seat->wp_relative_pointer); seat->wp_relative_pointer = nullptr; } if (seat->wp_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(seat->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] != seat->pointer.xy[0]) || (xy_next[1] != seat->pointer.xy[1])) { xy_motion[0] = xy_next[0]; xy_motion[1] = xy_next[1]; xy_motion_create_event = true; } seat->pointer.xy[0] = xy_next[0]; seat->pointer.xy[1] = xy_next[1]; zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp_locked_pointer, UNPACK2(xy_next)); wl_surface_commit(wl_surface); } else if (mode_current == GHOST_kGrabHide) { if ((init_grab_xy[0] != seat->grab_lock_xy[0]) || (init_grab_xy[1] != seat->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(seat->wp_locked_pointer, UNPACK2(xy_next)); wl_surface_commit(wl_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(seat->wp_locked_pointer, UNPACK2(seat->pointer.xy)); wl_surface_commit(wl_surface); } } #endif if (xy_motion_create_event) { seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(), GHOST_kEventCursorMove, ghost_wl_surface_user_data(wl_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(seat->wp_locked_pointer); seat->wp_locked_pointer = nullptr; } } if (!grab_state_next.use_confine) { if (seat->wp_confined_pointer) { zwp_confined_pointer_v1_destroy(seat->wp_confined_pointer); seat->wp_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. */ seat->wp_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer( display_->wp_relative_pointer_manager, seat->wl_pointer); zwp_relative_pointer_v1_add_listener( seat->wp_relative_pointer, &relative_pointer_listener, seat); seat->wp_locked_pointer = zwp_pointer_constraints_v1_lock_pointer( display_->wp_pointer_constraints, wl_surface, seat->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 * seat->pointer.xy[0]); init_grab_xy[1] = wl_fixed_to_int(scale * seat->pointer.xy[1]); seat->grab_lock_xy[0] = init_grab_xy[0]; seat->grab_lock_xy[1] = init_grab_xy[1]; } } else if (grab_state_next.use_confine) { if (!grab_state_prev.use_confine) { seat->wp_confined_pointer = zwp_pointer_constraints_v1_confine_pointer( display_->wp_pointer_constraints, wl_surface, seat->wl_pointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); } } } /* Only show so the cursor is made visible as the last step. */ cursor_visible_set(seat, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_SHOW); #ifdef USE_GNOME_CONFINE_HACK seat->use_pointer_software_confine = use_software_confine; #endif return GHOST_kSuccess; } #ifdef WITH_GHOST_WAYLAND_LIBDECOR bool GHOST_SystemWayland::use_libdecor_runtime() { return use_libdecor; } #endif #ifdef WITH_GHOST_WAYLAND_DYNLOAD bool ghost_wl_dynload_libraries_init() { # ifdef WITH_GHOST_X11 /* When running in WAYLAND, let the user know when a missing library is the only reason * WAYLAND could not be used. Otherwise it's not obvious why X11 is used as a fallback. * Otherwise when X11 is used, reporting WAYLAND library warnings is unwelcome noise. */ bool verbose = getenv("WAYLAND_DISPLAY") != nullptr; # else bool verbose = true; # endif /* !WITH_GHOST_X11 */ 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 has_libdecor = wayland_dynload_libdecor_init(verbose); /* `libdecor-0`. */ # endif return true; } wayland_dynload_client_exit(); wayland_dynload_cursor_exit(); wayland_dynload_egl_exit(); return false; } void ghost_wl_dynload_libraries_exit() { wayland_dynload_client_exit(); wayland_dynload_cursor_exit(); wayland_dynload_egl_exit(); # ifdef WITH_GHOST_WAYLAND_LIBDECOR wayland_dynload_libdecor_exit(); # endif } #endif /* WITH_GHOST_WAYLAND_DYNLOAD */ /** \} */