Files
test/intern/ghost/intern/GHOST_SystemWayland.cpp
Campbell Barton ed66321996 GHOST/Wayland: simplify logic for disconnecting monitors under Wayland
The T103586 fix effectively ran the wl_surface_listener.leave callback
to as WLROOTS based compositors doesn't run them. Remove the workaround
since it's an error in WLROOTS to be fixed upstream.

Temporarily using the wrong window scale when disconnecting a monitor
on configurations that use different DPI per monitor is a minor enough
issue that I don't think it makes sense to workaround in GHOST.
2023-01-10 17:50:29 +11:00

7073 lines
237 KiB
C++

/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup GHOST
*/
#include "GHOST_SystemWayland.h"
#include "GHOST_Event.h"
#include "GHOST_EventButton.h"
#include "GHOST_EventCursor.h"
#include "GHOST_EventDragnDrop.h"
#include "GHOST_EventKey.h"
#include "GHOST_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_VULKAN_BACKEND
# include "GHOST_ContextVK.h"
#endif
#ifdef WITH_INPUT_NDOF
# include "GHOST_NDOFManagerUnix.h"
#endif
#ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_API.h> /* For `ghost_wl_dynload_libraries`. */
#endif
#ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_egl.h>
#endif
#include <wayland-egl.h>
#include <algorithm>
#include <atomic>
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_cursor.h>
#endif
#include <wayland-cursor.h>
#include "GHOST_WaylandCursorSettings.h"
#include <xkbcommon/xkbcommon.h>
/* Generated by `wayland-scanner`. */
#include <pointer-constraints-unstable-v1-client-protocol.h>
#include <pointer-gestures-unstable-v1-client-protocol.h>
#include <primary-selection-unstable-v1-client-protocol.h>
#include <relative-pointer-unstable-v1-client-protocol.h>
#include <tablet-unstable-v2-client-protocol.h>
#include <xdg-output-unstable-v1-client-protocol.h>
/* Decorations `xdg_decor`. */
#include <xdg-decoration-unstable-v1-client-protocol.h>
#include <xdg-shell-client-protocol.h>
/* End `xdg_decor`. */
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstdlib> /* For `exit`. */
#include <cstring>
#include <mutex>
#ifdef HAVE_POLL
# include <poll.h>
#endif
/* Logging, use `ghost.wl.*` prefix. */
#include "CLG_log.h"
#ifdef USE_EVENT_BACKGROUND_THREAD
# include <pthread.h>
#endif
#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
/* -------------------------------------------------------------------- */
/** \name Forward Declarations
* \{ */
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);
#ifdef USE_EVENT_BACKGROUND_THREAD
static void gwl_display_event_thread_destroy(GWL_Display *display);
static void ghost_wl_display_lock_without_input(struct wl_display *wl_display,
std::mutex *server_mutex);
/** Default size for pending event vector. */
constexpr size_t events_pending_default_size = 4096 / sizeof(void *);
#endif /* USE_EVENT_BACKGROUND_THREAD */
/** In nearly all cases use `pushEvent_maybe_pending`
* at least when called from WAYLAND callbacks. */
#define pushEvent DONT_USE
/** \} */
/* -------------------------------------------------------------------- */
/** \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<char *>(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<char *>(buffer->data));
buffer->data_size = strlen(str);
char *data = static_cast<char *>(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<std::string> 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<const GWL_Output *> 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<std::string> 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<zwp_tablet_tool_v2 *> 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;
}
static bool gwl_seat_key_depressed_suppress_warning(const GWL_Seat *seat)
{
bool suppress_warning = false;
#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. */
suppress_warning = true;
}
#endif
return suppress_warning;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \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<GWL_Output *> outputs;
std::vector<GWL_Seat *> 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;
/* Threaded event handling. */
#ifdef USE_EVENT_BACKGROUND_THREAD
/**
* Run a thread that consumes events in the background.
* Use `pthread` because `std::thread` leaks memory.
*/
pthread_t events_pthread = 0;
/** Use to exit the event reading loop. */
bool events_pthread_is_active = false;
/**
* Events added from the event reading thread.
* Added into the main event queue when on #GHOST_SystemWayland::processEvents.
*/
std::vector<GHOST_IEvent *> events_pending;
/** Guard against multiple threads accessing `events_pending` at once. */
std::mutex events_pending_mutex;
#endif /* USE_EVENT_BACKGROUND_THREAD */
};
/**
* 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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
if (display->events_pthread) {
ghost_wl_display_lock_without_input(display->wl_display, display->system->server_mutex);
display->events_pthread_is_active = false;
}
#endif
/* 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)));
}
#ifdef USE_EVENT_BACKGROUND_THREAD
if (display->events_pthread) {
gwl_display_event_thread_destroy(display);
display->system->server_mutex->unlock();
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
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<GWL_Seat *>::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 = &reg->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 = &reg->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{};
params.name = reg->name;
params.interface_slot = reg->interface_slot;
params.version = reg->version;
params.user_data = reg->user_data;
handler->update_fn(display, &params);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \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 (ELEM(ecode, EPIPE, 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));
}
/* NOTE(@campbellbarton): The application is running,
* however an error closes all windows and most importantly:
* shuts down the GPU context (loosing all GPU state - shaders, bind codes etc),
* so recovering from this effectively involves restarting.
*
* Keeping the GPU state alive doesn't seem to be supported as windows EGL context must use the
* same connection as the used for all other WAYLAND interactions (see #wl_display_connect).
* So in practice re-connecting to the display server isn't an option.
*
* Exit since leaving the process open will simply flood the output and do nothing.
* Although as the process is in a valid state, auto-save for e.g. is possible, see: T100855. */
::exit(-1);
}
/**
* 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_TStandardCursor, const char *> 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 */
}
enum {
GWL_IOR_READ = 1 << 0,
GWL_IOR_WRITE = 1 << 1,
GWL_IOR_NO_RETRY = 1 << 2,
};
static int file_descriptor_is_io_ready(int fd, const int flags, const int timeout_ms)
{
int result;
GHOST_ASSERT(flags & (GWL_IOR_READ | GWL_IOR_WRITE), "X");
/* Note: We don't bother to account for elapsed time if we get EINTR */
do {
#ifdef HAVE_POLL
struct pollfd info;
info.fd = fd;
info.events = 0;
if (flags & GWL_IOR_READ) {
info.events |= POLLIN | POLLPRI;
}
if (flags & GWL_IOR_WRITE) {
info.events |= POLLOUT;
}
result = poll(&info, 1, timeout_ms);
#else
fd_set rfdset, *rfdp = nullptr;
fd_set wfdset, *wfdp = nullptr;
struct timeval tv, *tvp = nullptr;
/* If this assert triggers we'll corrupt memory here */
GHOST_ASSERT(fd >= 0 && fd < FD_SETSIZE, "X");
if (flags & GWL_IOR_READ) {
FD_ZERO(&rfdset);
FD_SET(fd, &rfdset);
rfdp = &rfdset;
}
if (flags & GWL_IOR_WRITE) {
FD_ZERO(&wfdset);
FD_SET(fd, &wfdset);
wfdp = &wfdset;
}
if (timeout_ms >= 0) {
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
tvp = &tv;
}
result = select(fd + 1, rfdp, wfdp, nullptr, tvp);
#endif /* !HAVE_POLL */
} while (result < 0 && errno == EINTR && !(flags & GWL_IOR_NO_RETRY));
return result;
}
static int ghost_wl_display_event_pump(struct wl_display *wl_display)
{
/* Based on SDL's `Wayland_PumpEvents`. */
int err;
/* NOTE: Without this, interactions with window borders via LIBDECOR doesn't function. */
wl_display_flush(wl_display);
if (wl_display_prepare_read(wl_display) == 0) {
/* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */
if (file_descriptor_is_io_ready(
wl_display_get_fd(wl_display), GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0) {
err = wl_display_read_events(wl_display);
}
else {
wl_display_cancel_read(wl_display);
err = 0;
}
}
else {
err = wl_display_dispatch_pending(wl_display);
}
return err;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
static void ghost_wl_display_lock_without_input(struct wl_display *wl_display,
std::mutex *server_mutex)
{
const int fd = wl_display_get_fd(wl_display);
int state;
do {
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
/* Re-check `state` with a lock held, needed to avoid holding the lock. */
if (state == 0) {
server_mutex->lock();
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
if (state == 0) {
break;
}
}
} while (state == 0);
}
static int ghost_wl_display_event_pump_from_thread(struct wl_display *wl_display,
const int fd,
std::mutex *server_mutex)
{
/* Based on SDL's `Wayland_PumpEvents`. */
server_mutex->lock();
int err = 0;
if (wl_display_prepare_read(wl_display) == 0) {
/* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */
if (file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0) {
err = wl_display_read_events(wl_display);
}
else {
wl_display_cancel_read(wl_display);
}
}
else {
int state;
do {
server_mutex->unlock();
/* Wait for input (unlocked, so as not to block other threads). */
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX);
server_mutex->lock();
/* Re-check `state` with a lock held, needed to avoid holding the lock. */
if (state > 0) {
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
if (state > 0) {
err = wl_display_dispatch_pending(wl_display);
}
}
} while (state > 0);
}
server_mutex->unlock();
return err;
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
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)
{
const bool show_warning = !gwl_seat_key_depressed_suppress_warning(seat);
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)) {
if (show_warning) {
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_maybe_pending(
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_maybe_pending(
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_maybe_pending(
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<GWL_Seat *>(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_maybe_pending(
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<typeof(chunk)>(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<char *>(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_DataOffer *>(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<GWL_DataOffer *>(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<GWL_DataOffer *>(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<GWL_Seat *>(data);
std::lock_guard lock{seat->data_offer_dnd_mutex};
delete seat->data_offer_dnd;
seat->data_offer_dnd = static_cast<GWL_DataOffer *>(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Seat *>(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<char *>(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<std::string> uris;
size_t pos = 0;
while (true) {
pos = data.find(file_proto, pos);
const size_t start = pos + sizeof(file_proto) - 1;
pos = data.find(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<GHOST_TStringArray *>(
malloc(sizeof(GHOST_TStringArray)));
flist->count = int(uris.size());
flist->strings = static_cast<uint8_t **>(malloc(uris.size() * sizeof(uint8_t *)));
for (size_t i = 0; i < uris.size(); i++) {
flist->strings[i] = (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_maybe_pending(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<GWL_Seat *>(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<GWL_DataOffer *>(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<GWL_Cursor *>(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Seat *>(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);
seat->system->cursor_shape_set(win->getCursorShape());
const wl_fixed_t scale = win->scale();
seat->system->pushEvent_maybe_pending(
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<GWL_Seat *>(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<GWL_Seat *>(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_maybe_pending(
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<GWL_Seat *>(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_maybe_pending(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<GWL_Seat *>(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<GWL_Seat *>(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_maybe_pending(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_maybe_pending(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Seat *>(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_maybe_pending(
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_maybe_pending(
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<GWL_TabletTool *>(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<GWL_TabletTool *>(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<GWL_TabletTool *>(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();
seat->system->cursor_shape_set(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<GWL_TabletTool *>(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<GWL_TabletTool *>(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_maybe_pending(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<GWL_TabletTool *>(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_maybe_pending(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<GWL_TabletTool *>(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<GWL_TabletTool *>(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<GWL_TabletTool *>(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<GWL_TabletTool *>(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_maybe_pending(
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<GWL_TabletTool *>(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_maybe_pending(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<GWL_TabletTool *>(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_maybe_pending(
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) {
seat->system->cursor_shape_set(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<GWL_Seat *>(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<GWL_Seat *>(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<char *>(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0));
if (map_str == MAP_FAILED) {
close(fd);
throw std::runtime_error("keymap mmap failed: " + std::string(std::strerror(errno)));
}
struct xkb_keymap *keymap = xkb_keymap_new_from_string(
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<GWL_Seat *>(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<GWL_Seat *>(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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
GHOST_ASSERT(seat->key_repeat.timer != nullptr, "Caller much check for timer");
delete static_cast<GWL_KeyRepeatPlayload *>(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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
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<GWL_Seat *>(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<GWL_KeyRepeatPlayload *>(
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: {
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
/* The payload will be added again. */
seat->system->removeTimer(seat->key_repeat.timer);
seat->key_repeat.timer = nullptr;
break;
}
case CANCEL: {
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
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_maybe_pending(
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<GWL_KeyRepeatPlayload *>(
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_maybe_pending(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_PrimarySelection_DataOffer *>(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<GWL_PrimarySelection *>(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<GWL_PrimarySelection_DataOffer *>(
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<GWL_PrimarySelection *>(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<GWL_PrimarySelection *>(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Output *>(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<GWL_Output *>(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<GWL_Output *>(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<GWL_Output *>(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<GWL_Output *>(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<GWL_Output *>(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<GWL_Output *>(data);
output->scale = factor;
output->system->output_scale_update(output);
}
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_compositor *>(
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<xdg_wm_base *>(
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<zxdg_decoration_manager_v1 *>(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<zxdg_output_manager_v1 *>(
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_output *>(
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<void *>(output));
}
static void gwl_registry_wl_output_update(GWL_Display *display,
const GWL_RegisteryUpdate_Params *params)
{
GWL_Output *output = static_cast<GWL_Output *>(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.
*
* WARNING: this is not the case for WLROOTS based compositors which have a (bug?)
* where surface leave events don't run. So `system->output_leave(..)` is needed
* until the issue is resolved in WLROOTS. */
GWL_Output *output = static_cast<GWL_Output *>(user_data);
if (!on_exit) {
/* Needed for WLROOTS, does nothing if surface leave callbacks have already run. */
if (output->system->output_unref(output->wl_output)) {
CLOG_WARN(LOG,
"mis-behaving compositor failed to call \"surface_listener.leave\" "
"window scale may be invalid!");
}
}
if (output->xdg_output) {
zxdg_output_v1_destroy(output->xdg_output);
}
wl_output_destroy(output->wl_output);
std::vector<GWL_Output *>::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_seat *>(
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<void *>(seat));
}
static void gwl_registry_wl_seat_update(GWL_Display *display,
const GWL_RegisteryUpdate_Params *params)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Seat *>::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_shm *>(
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_data_device_manager *>(
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<zwp_tablet_manager_v2 *>(
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<zwp_relative_pointer_manager_v1 *>(
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<zwp_pointer_constraints_v1 *>(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<zwp_pointer_gestures_v1 *>(
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<zwp_primary_selection_device_manager_v1 *>(
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<GWL_Display *>(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{};
params.name = name;
params.interface_slot = interface_slot;
params.version = version;
handler->add_fn(display, &params);
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<GWL_Display *>(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 Event Thread
* \{ */
#ifdef USE_EVENT_BACKGROUND_THREAD
static void *gwl_display_event_thread_fn(void *display_voidp)
{
GWL_Display *display = static_cast<GWL_Display *>(display_voidp);
const int fd = wl_display_get_fd(display->wl_display);
while (display->events_pthread_is_active) {
/* Wait for an event, this thread is dedicated to event handling. */
if (ghost_wl_display_event_pump_from_thread(
display->wl_display, fd, display->system->server_mutex) == -1) {
break;
}
}
return nullptr;
}
/* Event reading thread. */
static void gwl_display_event_thread_create(GWL_Display *display)
{
GHOST_ASSERT(display->events_pthread == 0, "Only call once");
display->events_pending.reserve(events_pending_default_size);
display->events_pthread_is_active = true;
pthread_create(&display->events_pthread, nullptr, gwl_display_event_thread_fn, display);
pthread_detach(display->events_pthread);
}
static void gwl_display_event_thread_destroy(GWL_Display *display)
{
pthread_cancel(display->events_pthread);
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
/** \} */
/* -------------------------------------------------------------------- */
/** \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);
#ifdef USE_EVENT_BACKGROUND_THREAD
server_mutex = new std::mutex;
timer_mutex = new std::mutex;
main_thread_id = std::this_thread::get_id();
#endif
display_->system = this;
/* Connect to the Wayland server. */
display_->wl_display = wl_display_connect(nullptr);
if (!display_->wl_display) {
this->~GHOST_SystemWayland();
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;
/* 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, &registry_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
this->~GHOST_SystemWayland();
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) {
this->~GHOST_SystemWayland();
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) {
this->~GHOST_SystemWayland();
throw std::runtime_error("Wayland: unable to access xdg_shell!");
}
}
#ifdef USE_EVENT_BACKGROUND_THREAD
gwl_display_event_thread_create(display_);
#endif
}
GHOST_SystemWayland::~GHOST_SystemWayland()
{
gwl_display_destroy(display_);
#ifdef USE_EVENT_BACKGROUND_THREAD
delete server_mutex;
delete timer_mutex;
#endif
}
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;
}
#undef pushEvent
bool GHOST_SystemWayland::processEvents(bool waitForEvent)
{
bool any_processed = false;
#ifdef USE_EVENT_BACKGROUND_THREAD
if (UNLIKELY(has_pending_actions_for_window.exchange(false))) {
std::lock_guard lock_server_guard{*server_mutex};
for (GHOST_IWindow *iwin : getWindowManager()->getWindows()) {
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
win->pending_actions_handle();
}
}
{
std::lock_guard lock{display_->events_pending_mutex};
for (GHOST_IEvent *event : display_->events_pending) {
/* Perform actions that aren't handled in a thread. */
switch (event->getType()) {
case GHOST_kEventWindowActivate: {
getWindowManager()->setActiveWindow(event->getWindow());
break;
}
case GHOST_kEventWindowDeactivate: {
getWindowManager()->setWindowInactive(event->getWindow());
break;
}
default: {
break;
}
}
pushEvent(event);
}
display_->events_pending.clear();
if (UNLIKELY(display_->events_pending.capacity() > events_pending_default_size)) {
/* Avoid over allocation in the case of occasional delay between processing events
* causing many events to be collected and making this into a large array. */
display_->events_pending.shrink_to_fit();
display_->events_pending.reserve(events_pending_default_size);
}
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*display_->system->timer_mutex};
#endif
if (getTimerManager()->fireTimers(getMilliSeconds())) {
any_processed = true;
}
}
#ifdef WITH_INPUT_NDOF
if (static_cast<GHOST_NDOFManagerUnix *>(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) {
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (wl_display_dispatch(display_->wl_display) == -1) {
ghost_wl_display_report_error(display_->wl_display);
}
}
else {
#ifdef USE_EVENT_BACKGROUND_THREAD
/* NOTE: this works, but as the events are being read in a thread,
* this could be removed and event handling still works.. */
if (server_mutex->try_lock()) {
if (ghost_wl_display_event_pump(display_->wl_display) == -1) {
ghost_wl_display_report_error(display_->wl_display);
}
server_mutex->unlock();
}
#else
if (ghost_wl_display_event_pump(display_->wl_display) == -1) {
ghost_wl_display_report_error(display_->wl_display);
}
#endif /* !USE_EVENT_BACKGROUND_THREAD */
}
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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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);
const bool show_warning = !gwl_seat_key_depressed_suppress_warning(seat);
/* 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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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<std::string> &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<bool> 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<bool> 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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (selection) {
system_clipboard_put_primary_selection(display_, buffer);
}
else {
system_clipboard_put(display_, buffer);
}
}
uint8_t GHOST_SystemWayland::getNumDisplays() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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();
if (win->getCursorGrabModeIsWarp()) {
/* As the cursor is restored at the warped location,
* apply warping when requesting the cursor location. */
GHOST_Rect wrap_bounds{};
if (win->getCursorGrabModeIsWarp()) {
if (win->getCursorGrabBounds(wrap_bounds) == GHOST_kFailure) {
win->getClientBounds(wrap_bounds);
}
}
int xy_wrap[2] = {
seat_state_pointer->xy[0],
seat_state_pointer->xy[1],
};
GHOST_Rect wrap_bounds_scale;
wrap_bounds_scale.m_l = wl_fixed_from_int(wrap_bounds.m_l) / scale;
wrap_bounds_scale.m_t = wl_fixed_from_int(wrap_bounds.m_t) / scale;
wrap_bounds_scale.m_r = wl_fixed_from_int(wrap_bounds.m_r) / scale;
wrap_bounds_scale.m_b = wl_fixed_from_int(wrap_bounds.m_b) / scale;
wrap_bounds_scale.wrapPoint(UNPACK2(xy_wrap), 0, win->getCursorGrabAxis());
x = wl_fixed_to_int(scale * xy_wrap[0]);
y = wl_fixed_to_int(scale * xy_wrap[1]);
}
else {
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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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<const GHOST_WindowWayland *>(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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(window);
return setCursorPositionClientRelative_impl(seat, win, x, y);
}
GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (display_->outputs.empty()) {
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
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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)
{
/* Caller must lock `system->server_mutex`. */
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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
/* Create new off-screen window. */
wl_surface *wl_surface = wl_compositor_create_surface(wl_compositor());
#ifdef WITH_VULKAN_BACKEND
const bool debug_context = (glSettings.flags & GHOST_glDebugContext) != 0;
if (glSettings.context_type == GHOST_kDrawingContextTypeVulkan) {
GHOST_Context *context = new GHOST_ContextVK(false,
GHOST_kVulkanPlatformWayland,
0,
NULL,
wl_surface,
display_->wl_display,
1,
0,
debug_context);
if (!context->initializeDrawingContext()) {
delete context;
return nullptr;
}
return context;
}
#else
(void)glSettings;
#endif
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)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
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<GWL_TabletTool *>(
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<GWL_TabletTool *>(
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::cursor_shape_set(const GHOST_TStandardCursor shape)
{
/* Caller must lock `server_mutex`. */
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::cursor_shape_check(const GHOST_TStandardCursor cursorShape)
{
/* No need to lock `server_mutex`. */
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::cursor_shape_custom_set(uint8_t *bitmap,
uint8_t *mask,
const int sizex,
const int sizey,
const int hotX,
const int hotY,
const bool /*canInvertColor*/)
{
/* Caller needs to lock `server_mutex`. */
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<uint32_t *>(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::cursor_bitmap_get(GHOST_CursorBitmapRef *bitmap)
{
/* Caller must lock `server_mutex`. */
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<void *>(cursor->custom_data);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::cursor_visibility_set(const bool visible)
{
/* Caller must lock `server_mutex`. */
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::cursor_grab_use_software_display_get(const GHOST_TGrabCursorMode mode)
{
/* Caller must lock `server_mutex`. */
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<GWL_Output *> &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<GWL_Output *>(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<GHOST_WindowWayland *>(
wl_surface_get_user_data(wl_surface));
return win;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Utility Functions
*
* Functionality only used for the WAYLAND implementation.
* \{ */
GHOST_TSuccess GHOST_SystemWayland::pushEvent_maybe_pending(GHOST_IEvent *event)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
if (main_thread_id != std::this_thread::get_id()) {
std::lock_guard lock{display_->events_pending_mutex};
display_->events_pending.push_back(event);
return GHOST_kSuccess;
}
#endif
return pushEvent(event);
}
void GHOST_SystemWayland::seat_active_set(const struct GWL_Seat *seat)
{
gwl_display_seat_active_set(display_, seat);
}
bool GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface)
{
bool changed = false;
#define SURFACE_CLEAR_PTR(surface_test) \
if (surface_test == wl_surface) { \
surface_test = nullptr; \
changed = true; \
} \
((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
return changed;
}
bool GHOST_SystemWayland::output_unref(wl_output *wl_output)
{
bool changed = false;
if (!ghost_wl_output_own(wl_output)) {
return changed;
}
/* NOTE: keep in sync with `output_scale_update`. */
GWL_Output *output = ghost_wl_output_user_data(wl_output);
GHOST_WindowManager *window_manager = getWindowManager();
if (window_manager) {
for (GHOST_IWindow *iwin : window_manager->getWindows()) {
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
if (win->outputs_leave(output)) {
changed = true;
}
}
}
for (GWL_Seat *seat : display_->seats) {
if (seat->pointer.outputs.erase(output)) {
changed = true;
}
if (seat->tablet.outputs.erase(output)) {
changed = true;
}
}
return changed;
}
void GHOST_SystemWayland::output_scale_update(GWL_Output *output)
{
/* NOTE: keep in sync with `output_unref`. */
GHOST_WindowManager *window_manager = getWindowManager();
if (window_manager) {
for (GHOST_IWindow *iwin : window_manager->getWindows()) {
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
const std::vector<GWL_Output *> &outputs = win->outputs();
if (!(std::find(outputs.begin(), outputs.end(), output) == outputs.cend())) {
win->outputs_changed_update_scale();
}
}
}
for (GWL_Seat *seat : display_->seats) {
if (seat->pointer.outputs.count(output)) {
if (seat->cursor.wl_surface_cursor != nullptr) {
update_cursor_scale(
seat->cursor, seat->system->wl_shm(), &seat->pointer, seat->cursor.wl_surface_cursor);
}
}
if (seat->tablet.outputs.count(output)) {
for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->tablet_tools) {
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(
zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
if (tablet_tool->wl_surface_cursor != nullptr) {
update_cursor_scale(seat->cursor,
seat->system->wl_shm(),
&seat->pointer,
tablet_tool->wl_surface_cursor);
}
}
}
}
}
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)
{
/* Caller must lock `server_mutex`. */
/* 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_maybe_pending(
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 */
/** \} */