6540 lines
221 KiB
C++
6540 lines
221 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_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 <cstring>
|
|
#include <mutex>
|
|
|
|
/* Logging, use `ghost.wl.*` prefix. */
|
|
#include "CLG_log.h"
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
static bool use_libdecor = true;
|
|
# ifdef WITH_GHOST_WAYLAND_DYNLOAD
|
|
static bool has_libdecor = false;
|
|
# else
|
|
static bool has_libdecor = true;
|
|
# endif
|
|
#endif
|
|
|
|
static void keyboard_handle_key_repeat_cancel(struct GWL_Seat *seat);
|
|
|
|
static void output_handle_done(void *data, struct wl_output *wl_output);
|
|
|
|
static void gwl_seat_capability_pointer_disable(GWL_Seat *seat);
|
|
static void gwl_seat_capability_keyboard_disable(GWL_Seat *seat);
|
|
static void gwl_seat_capability_touch_disable(GWL_Seat *seat);
|
|
|
|
static bool gwl_registry_entry_remove_by_name(GWL_Display *display,
|
|
uint32_t name,
|
|
int *r_interface_slot);
|
|
static void gwl_registry_entry_remove_all(GWL_Display *display);
|
|
|
|
struct GWL_RegistryHandler;
|
|
static int gwl_registry_handler_interface_slot_max();
|
|
static int gwl_registry_handler_interface_slot_from_string(const char *interface);
|
|
static const struct GWL_RegistryHandler *gwl_registry_handler_from_interface_slot(
|
|
int interface_slot);
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Workaround Compositor Specific Bugs
|
|
* \{ */
|
|
|
|
/**
|
|
* GNOME (mutter 42.2 had a bug with confine not respecting scale - Hi-DPI), See: T98793.
|
|
* Even though this has been fixed, at time of writing it's not yet in a release.
|
|
* Workaround the problem by implementing confine with a software cursor.
|
|
* While this isn't ideal, it's not adding a lot of overhead as software
|
|
* cursors are already used for warping (which WAYLAND doesn't support).
|
|
*/
|
|
#define USE_GNOME_CONFINE_HACK
|
|
/**
|
|
* Always use software confine (not just in GNOME).
|
|
* Useful for developing with compositors that don't need this workaround.
|
|
*/
|
|
// #define USE_GNOME_CONFINE_HACK_ALWAYS_ON
|
|
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
static bool use_gnome_confine_hack = false;
|
|
#endif
|
|
|
|
/**
|
|
* GNOME (mutter 42.5) doesn't follow the WAYLAND spec regarding keyboard handling,
|
|
* unlike (other compositors: KDE-plasma, River & Sway which work without problems).
|
|
*
|
|
* This means GNOME can't know which modifiers are held when activating windows,
|
|
* so we guess the left modifiers are held.
|
|
*
|
|
* This define could be removed without changing any functionality,
|
|
* it just means GNOME users will see verbose warning messages that alert them about
|
|
* a known problem that needs to be fixed up-stream.
|
|
*
|
|
* This has been fixed for GNOME 43. Keep the workaround until support for gnome 42 is dropped.
|
|
* See: https://gitlab.gnome.org/GNOME/mutter/-/issues/2457
|
|
*/
|
|
#define USE_GNOME_KEYBOARD_SUPPRESS_WARNING
|
|
|
|
/**
|
|
* KDE (plasma 5.26.1) has a bug where the cursor surface needs to be committed
|
|
* (via `wl_surface_commit`) when it was hidden and is being set to visible again, see: T102048.
|
|
* See: https://bugs.kde.org/show_bug.cgi?id=461001
|
|
*/
|
|
#define USE_KDE_TABLET_HIDDEN_CURSOR_HACK
|
|
|
|
/**
|
|
* When GNOME is found, require `libdecor`.
|
|
* This is a hack because it seems there is no way to check if the compositor supports
|
|
* server side decorations when initializing WAYLAND.
|
|
*/
|
|
#if defined(WITH_GHOST_WAYLAND_LIBDECOR) && defined(WITH_GHOST_X11)
|
|
# define USE_GNOME_NEEDS_LIBDECOR_HACK
|
|
#endif
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Local Defines
|
|
*
|
|
* Control local functionality, compositors specific workarounds.
|
|
* \{ */
|
|
|
|
/**
|
|
* Fix short-cut part of keyboard reading code not properly handling some keys, see: T102194.
|
|
* \note This is similar to X11 workaround by the same name, see: T47228.
|
|
*/
|
|
#define USE_NON_LATIN_KB_WORKAROUND
|
|
|
|
#define WL_NAME_UNSET uint32_t(-1)
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Inline Event Codes
|
|
*
|
|
* Selected input event code defines from `linux/input-event-codes.h`
|
|
* We include some of the button input event codes here, since the header is
|
|
* only available in more recent kernel versions.
|
|
* \{ */
|
|
|
|
/**
|
|
* The event codes are used to differentiate from which mouse button an event comes from.
|
|
*/
|
|
#define BTN_LEFT 0x110
|
|
#define BTN_RIGHT 0x111
|
|
#define BTN_MIDDLE 0x112
|
|
#define BTN_SIDE 0x113
|
|
#define BTN_EXTRA 0x114
|
|
#define BTN_FORWARD 0x115
|
|
#define BTN_BACK 0x116
|
|
// #define BTN_TASK 0x117 /* UNUSED. */
|
|
|
|
/**
|
|
* Tablet events.
|
|
*
|
|
* \note Gnome/GTK swap middle/right, where the same application in X11 will swap the middle/right
|
|
* mouse button when running under WAYLAND. KDE doesn't do this, and according to artists
|
|
* at the Blender studio, having the button closest to the nib be MMB is preferable,
|
|
* so use this as a default. If needs be - swapping these could be a preference.
|
|
*/
|
|
#define BTN_STYLUS 0x14b /* Use as middle-mouse. */
|
|
#define BTN_STYLUS2 0x14c /* Use as right-mouse. */
|
|
/* NOTE(@campbellbarton): Map to an additional button (not sure which hardware uses this). */
|
|
#define BTN_STYLUS3 0x149
|
|
|
|
/**
|
|
* Keyboard scan-codes.
|
|
*/
|
|
#define KEY_GRAVE 41
|
|
|
|
#ifdef USE_NON_LATIN_KB_WORKAROUND
|
|
# define KEY_1 2
|
|
# define KEY_2 3
|
|
# define KEY_3 4
|
|
# define KEY_4 5
|
|
# define KEY_5 6
|
|
# define KEY_6 7
|
|
# define KEY_7 8
|
|
# define KEY_8 9
|
|
# define KEY_9 10
|
|
# define KEY_0 11
|
|
#endif
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Modifier Table
|
|
*
|
|
* Convenient access to modifier key values, allow looping over modifier keys.
|
|
* \{ */
|
|
|
|
enum {
|
|
MOD_INDEX_SHIFT = 0,
|
|
MOD_INDEX_ALT = 1,
|
|
MOD_INDEX_CTRL = 2,
|
|
MOD_INDEX_OS = 3,
|
|
};
|
|
#define MOD_INDEX_NUM (MOD_INDEX_OS + 1)
|
|
|
|
struct GWL_ModifierInfo {
|
|
/** Only for printing messages. */
|
|
const char *display_name;
|
|
const char *xkb_id;
|
|
GHOST_TKey key_l, key_r;
|
|
GHOST_TModifierKey mod_l, mod_r;
|
|
};
|
|
|
|
static const GWL_ModifierInfo g_modifier_info_table[MOD_INDEX_NUM] = {
|
|
/* MOD_INDEX_SHIFT */
|
|
{
|
|
/* display_name */ "Shift",
|
|
/* xkb_id */ XKB_MOD_NAME_SHIFT,
|
|
/* key_l */ GHOST_kKeyLeftShift,
|
|
/* key_r */ GHOST_kKeyRightShift,
|
|
/* mod_l */ GHOST_kModifierKeyLeftShift,
|
|
/* mod_r */ GHOST_kModifierKeyRightShift,
|
|
},
|
|
/* MOD_INDEX_ALT */
|
|
{
|
|
/* display_name */ "Alt",
|
|
/* xkb_id */ XKB_MOD_NAME_ALT,
|
|
/* key_l */ GHOST_kKeyLeftAlt,
|
|
/* key_r */ GHOST_kKeyRightAlt,
|
|
/* mod_l */ GHOST_kModifierKeyLeftAlt,
|
|
/* mod_r */ GHOST_kModifierKeyRightAlt,
|
|
},
|
|
/* MOD_INDEX_CTRL */
|
|
{
|
|
/* display_name */ "Control",
|
|
/* xkb_id */ XKB_MOD_NAME_CTRL,
|
|
/* key_l */ GHOST_kKeyLeftControl,
|
|
/* key_r */ GHOST_kKeyRightControl,
|
|
/* mod_l */ GHOST_kModifierKeyLeftControl,
|
|
/* mod_r */ GHOST_kModifierKeyRightControl,
|
|
},
|
|
/* MOD_INDEX_OS */
|
|
{
|
|
/* display_name */ "OS",
|
|
/* xkb_id */ XKB_MOD_NAME_LOGO,
|
|
/* key_l */ GHOST_kKeyLeftOS,
|
|
/* key_r */ GHOST_kKeyRightOS,
|
|
/* mod_l */ GHOST_kModifierKeyLeftOS,
|
|
/* mod_r */ GHOST_kModifierKeyRightOS,
|
|
},
|
|
};
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Internal #GWL_SimpleBuffer Type
|
|
* \{ */
|
|
|
|
struct GWL_SimpleBuffer {
|
|
/** Constant data, but may be freed. */
|
|
const char *data = nullptr;
|
|
size_t data_size = 0;
|
|
};
|
|
|
|
static void gwl_simple_buffer_free_data(GWL_SimpleBuffer *buffer)
|
|
{
|
|
free(const_cast<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;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \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;
|
|
};
|
|
|
|
/**
|
|
* Free the #GWL_Display and it's related members.
|
|
*
|
|
* \note This may run on a partially initialized struct,
|
|
* so it can't be assumed all members are set.
|
|
*/
|
|
static void gwl_display_destroy(GWL_Display *display)
|
|
{
|
|
/* For typical WAYLAND use this will always be set.
|
|
* However when WAYLAND isn't running, this will early-exit and be null. */
|
|
if (display->wl_registry) {
|
|
wl_registry_destroy(display->wl_registry);
|
|
display->wl_registry = nullptr;
|
|
}
|
|
|
|
/* Unregister items in reverse order. */
|
|
gwl_registry_entry_remove_all(display);
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
if (use_libdecor) {
|
|
if (display->libdecor) {
|
|
gwl_libdecor_system_destroy(display->libdecor);
|
|
display->libdecor = nullptr;
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
if (display->xdg_decor) {
|
|
gwl_xdg_decor_system_destroy(display, display->xdg_decor);
|
|
display->xdg_decor = nullptr;
|
|
}
|
|
}
|
|
|
|
if (eglGetDisplay) {
|
|
::eglTerminate(eglGetDisplay(EGLNativeDisplayType(display->wl_display)));
|
|
}
|
|
|
|
if (display->wl_display) {
|
|
wl_display_disconnect(display->wl_display);
|
|
}
|
|
|
|
delete display;
|
|
}
|
|
|
|
static int gwl_display_seat_index(GWL_Display *display, const GWL_Seat *seat)
|
|
{
|
|
std::vector<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 = ®->next;
|
|
reg = reg->next;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static bool gwl_registry_entry_remove_by_interface_slot(GWL_Display *display,
|
|
int interface_slot,
|
|
bool on_exit)
|
|
{
|
|
GWL_RegistryEntry *reg = display->registry_entry;
|
|
GWL_RegistryEntry **reg_link_p = &display->registry_entry;
|
|
bool found = false;
|
|
|
|
while (reg) {
|
|
if (reg->interface_slot == interface_slot) {
|
|
GWL_RegistryEntry *reg_next = reg->next;
|
|
const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
|
|
interface_slot);
|
|
handler->remove_fn(display, reg->user_data, on_exit);
|
|
delete reg;
|
|
*reg_link_p = reg_next;
|
|
reg = reg_next;
|
|
found = true;
|
|
continue;
|
|
}
|
|
reg_link_p = ®->next;
|
|
reg = reg->next;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Remove all global objects (on exit).
|
|
*/
|
|
static void gwl_registry_entry_remove_all(GWL_Display *display)
|
|
{
|
|
const bool on_exit = true;
|
|
|
|
/* NOTE(@campbellbarton): Free by slot instead of simply looping over
|
|
* `display->registry_entry` so the order of freeing is always predictable.
|
|
* Otherwise global objects would be feed in the order they are registered.
|
|
* While this works in my tests, it could cause difficult to reproduce bugs
|
|
* where lesser used compositors or changes to existing compositors could
|
|
* crash on exit based on the order of freeing objects is out of our control.
|
|
*
|
|
* To give a concrete example of how this could fail, it's possible removing
|
|
* a tablet interface could reference the pointer interface, or the output interface.
|
|
* Even though references between interfaces shouldn't be necessary in most cases
|
|
* when `on_exit` is enabled. */
|
|
int interface_slot = gwl_registry_handler_interface_slot_max();
|
|
while (interface_slot--) {
|
|
gwl_registry_entry_remove_by_interface_slot(display, interface_slot, on_exit);
|
|
}
|
|
|
|
GHOST_ASSERT(display->registry_entry == nullptr, "Failed to remove all entries!");
|
|
display->registry_entry = nullptr;
|
|
}
|
|
|
|
/**
|
|
* Run GWL_RegistryHandler.update_fn an all registered interface instances.
|
|
* This is needed to refresh the state of interfaces that may reference other interfaces.
|
|
* Called when interfaces are added/removed.
|
|
*
|
|
* \param interface_slot_exclude: Skip updating slots of this type.
|
|
* Note that while harmless dependencies only exist between different types,
|
|
* so there is no reason to update all other outputs that an output was removed (for e.g.).
|
|
* Pass as -1 to update all slots.
|
|
*
|
|
* NOTE(@campbellbarton): Updating all other items on a single change is typically worth avoiding.
|
|
* In practice this isn't a problem as so there are so few elements in `display->registry_entry`,
|
|
* so few use update functions and adding/removal at runtime is rarely called (plugging/unplugging)
|
|
* hardware for e.g. So while it's possible to store dependency links to avoid unnecessary
|
|
* looping over data - it ends up being a non issue.
|
|
*/
|
|
static void gwl_registry_entry_update_all(GWL_Display *display, const int interface_slot_exclude)
|
|
{
|
|
GHOST_ASSERT(interface_slot_exclude == -1 || (uint(interface_slot_exclude) <
|
|
uint(gwl_registry_handler_interface_slot_max())),
|
|
"Invalid exclude slot");
|
|
|
|
for (GWL_RegistryEntry *reg = display->registry_entry; reg; reg = reg->next) {
|
|
if (reg->interface_slot == interface_slot_exclude) {
|
|
continue;
|
|
}
|
|
const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
|
|
reg->interface_slot);
|
|
if (handler->update_fn == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
GWL_RegisteryUpdate_Params params = {
|
|
.name = reg->name,
|
|
.interface_slot = reg->interface_slot,
|
|
.version = reg->version,
|
|
|
|
.user_data = reg->user_data,
|
|
};
|
|
handler->update_fn(display, ¶ms);
|
|
}
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Private Utility Functions
|
|
* \{ */
|
|
|
|
static void ghost_wl_display_report_error(struct wl_display *display)
|
|
{
|
|
int ecode = wl_display_get_error(display);
|
|
GHOST_ASSERT(ecode, "Error not set!");
|
|
if ((ecode == EPIPE || ecode == ECONNRESET)) {
|
|
fprintf(stderr, "The Wayland connection broke. Did the Wayland compositor die?\n");
|
|
}
|
|
else {
|
|
fprintf(stderr, "The Wayland connection experienced a fatal error: %s\n", strerror(ecode));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback for WAYLAND to run when there is an error.
|
|
*
|
|
* \note It's useful to set a break-point on this function as some errors are fatal
|
|
* (for all intents and purposes) but don't crash the process.
|
|
*/
|
|
static void ghost_wayland_log_handler(const char *msg, va_list arg)
|
|
{
|
|
fprintf(stderr, "GHOST/Wayland: ");
|
|
vfprintf(stderr, msg, arg); /* Includes newline. */
|
|
|
|
GHOST_TBacktraceFn backtrace_fn = GHOST_ISystem::getBacktraceFn();
|
|
if (backtrace_fn) {
|
|
backtrace_fn(stderr); /* Includes newline. */
|
|
}
|
|
}
|
|
|
|
static GHOST_TKey xkb_map_gkey(const xkb_keysym_t sym)
|
|
{
|
|
|
|
GHOST_TKey gkey;
|
|
if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) {
|
|
gkey = GHOST_TKey(sym);
|
|
}
|
|
else if (sym >= XKB_KEY_KP_0 && sym <= XKB_KEY_KP_9) {
|
|
gkey = GHOST_TKey(GHOST_kKeyNumpad0 + sym - XKB_KEY_KP_0);
|
|
}
|
|
else if (sym >= XKB_KEY_A && sym <= XKB_KEY_Z) {
|
|
gkey = GHOST_TKey(sym);
|
|
}
|
|
else if (sym >= XKB_KEY_a && sym <= XKB_KEY_z) {
|
|
gkey = GHOST_TKey(sym - XKB_KEY_a + XKB_KEY_A);
|
|
}
|
|
else if (sym >= XKB_KEY_F1 && sym <= XKB_KEY_F24) {
|
|
gkey = GHOST_TKey(GHOST_kKeyF1 + sym - XKB_KEY_F1);
|
|
}
|
|
else {
|
|
|
|
#define GXMAP(k, x, y) \
|
|
case x: \
|
|
k = y; \
|
|
break
|
|
|
|
switch (sym) {
|
|
GXMAP(gkey, XKB_KEY_BackSpace, GHOST_kKeyBackSpace);
|
|
GXMAP(gkey, XKB_KEY_Tab, GHOST_kKeyTab);
|
|
GXMAP(gkey, XKB_KEY_Linefeed, GHOST_kKeyLinefeed);
|
|
GXMAP(gkey, XKB_KEY_Clear, GHOST_kKeyClear);
|
|
GXMAP(gkey, XKB_KEY_Return, GHOST_kKeyEnter);
|
|
|
|
GXMAP(gkey, XKB_KEY_Escape, GHOST_kKeyEsc);
|
|
GXMAP(gkey, XKB_KEY_space, GHOST_kKeySpace);
|
|
GXMAP(gkey, XKB_KEY_apostrophe, GHOST_kKeyQuote);
|
|
GXMAP(gkey, XKB_KEY_comma, GHOST_kKeyComma);
|
|
GXMAP(gkey, XKB_KEY_minus, GHOST_kKeyMinus);
|
|
GXMAP(gkey, XKB_KEY_plus, GHOST_kKeyPlus);
|
|
GXMAP(gkey, XKB_KEY_period, GHOST_kKeyPeriod);
|
|
GXMAP(gkey, XKB_KEY_slash, GHOST_kKeySlash);
|
|
|
|
GXMAP(gkey, XKB_KEY_semicolon, GHOST_kKeySemicolon);
|
|
GXMAP(gkey, XKB_KEY_equal, GHOST_kKeyEqual);
|
|
|
|
GXMAP(gkey, XKB_KEY_bracketleft, GHOST_kKeyLeftBracket);
|
|
GXMAP(gkey, XKB_KEY_bracketright, GHOST_kKeyRightBracket);
|
|
GXMAP(gkey, XKB_KEY_backslash, GHOST_kKeyBackslash);
|
|
GXMAP(gkey, XKB_KEY_grave, GHOST_kKeyAccentGrave);
|
|
|
|
GXMAP(gkey, XKB_KEY_Shift_L, GHOST_kKeyLeftShift);
|
|
GXMAP(gkey, XKB_KEY_Shift_R, GHOST_kKeyRightShift);
|
|
GXMAP(gkey, XKB_KEY_Control_L, GHOST_kKeyLeftControl);
|
|
GXMAP(gkey, XKB_KEY_Control_R, GHOST_kKeyRightControl);
|
|
GXMAP(gkey, XKB_KEY_Alt_L, GHOST_kKeyLeftAlt);
|
|
GXMAP(gkey, XKB_KEY_Alt_R, GHOST_kKeyRightAlt);
|
|
GXMAP(gkey, XKB_KEY_Super_L, GHOST_kKeyLeftOS);
|
|
GXMAP(gkey, XKB_KEY_Super_R, GHOST_kKeyRightOS);
|
|
GXMAP(gkey, XKB_KEY_Menu, GHOST_kKeyApp);
|
|
|
|
GXMAP(gkey, XKB_KEY_Caps_Lock, GHOST_kKeyCapsLock);
|
|
GXMAP(gkey, XKB_KEY_Num_Lock, GHOST_kKeyNumLock);
|
|
GXMAP(gkey, XKB_KEY_Scroll_Lock, GHOST_kKeyScrollLock);
|
|
|
|
GXMAP(gkey, XKB_KEY_Left, GHOST_kKeyLeftArrow);
|
|
GXMAP(gkey, XKB_KEY_Right, GHOST_kKeyRightArrow);
|
|
GXMAP(gkey, XKB_KEY_Up, GHOST_kKeyUpArrow);
|
|
GXMAP(gkey, XKB_KEY_Down, GHOST_kKeyDownArrow);
|
|
|
|
GXMAP(gkey, XKB_KEY_Print, GHOST_kKeyPrintScreen);
|
|
GXMAP(gkey, XKB_KEY_Pause, GHOST_kKeyPause);
|
|
|
|
GXMAP(gkey, XKB_KEY_Insert, GHOST_kKeyInsert);
|
|
GXMAP(gkey, XKB_KEY_Delete, GHOST_kKeyDelete);
|
|
GXMAP(gkey, XKB_KEY_Home, GHOST_kKeyHome);
|
|
GXMAP(gkey, XKB_KEY_End, GHOST_kKeyEnd);
|
|
GXMAP(gkey, XKB_KEY_Page_Up, GHOST_kKeyUpPage);
|
|
GXMAP(gkey, XKB_KEY_Page_Down, GHOST_kKeyDownPage);
|
|
|
|
GXMAP(gkey, XKB_KEY_KP_Decimal, GHOST_kKeyNumpadPeriod);
|
|
GXMAP(gkey, XKB_KEY_KP_Enter, GHOST_kKeyNumpadEnter);
|
|
GXMAP(gkey, XKB_KEY_KP_Add, GHOST_kKeyNumpadPlus);
|
|
GXMAP(gkey, XKB_KEY_KP_Subtract, GHOST_kKeyNumpadMinus);
|
|
GXMAP(gkey, XKB_KEY_KP_Multiply, GHOST_kKeyNumpadAsterisk);
|
|
GXMAP(gkey, XKB_KEY_KP_Divide, GHOST_kKeyNumpadSlash);
|
|
|
|
GXMAP(gkey, XKB_KEY_XF86AudioPlay, GHOST_kKeyMediaPlay);
|
|
GXMAP(gkey, XKB_KEY_XF86AudioStop, GHOST_kKeyMediaStop);
|
|
GXMAP(gkey, XKB_KEY_XF86AudioPrev, GHOST_kKeyMediaFirst);
|
|
GXMAP(gkey, XKB_KEY_XF86AudioNext, GHOST_kKeyMediaLast);
|
|
|
|
/* Additional keys for non US layouts. */
|
|
|
|
/* Uses the same physical key as #XKB_KEY_KP_Decimal for QWERTZ layout, see: T102287. */
|
|
GXMAP(gkey, XKB_KEY_KP_Separator, GHOST_kKeyNumpadPeriod);
|
|
|
|
default:
|
|
/* Rely on #xkb_map_gkey_or_scan_code to report when no key can be found. */
|
|
gkey = GHOST_kKeyUnknown;
|
|
}
|
|
#undef GXMAP
|
|
}
|
|
|
|
return gkey;
|
|
}
|
|
|
|
/**
|
|
* Map the keys using the users keyboard layout, if that fails fall back to physical locations.
|
|
* This is needed so users with keyboard layouts that don't expose #GHOST_kKeyAccentGrave
|
|
* (typically the key under escape) in the layout can still use this key in keyboard shortcuts.
|
|
*
|
|
* \param key: The key's scan-code, compatible with values in `linux/input-event-codes.h`.
|
|
*/
|
|
static GHOST_TKey xkb_map_gkey_or_scan_code(const xkb_keysym_t sym, const uint32_t key)
|
|
{
|
|
GHOST_TKey gkey = xkb_map_gkey(sym);
|
|
|
|
if (UNLIKELY(gkey == GHOST_kKeyUnknown)) {
|
|
/* Fall back to physical location for keys that would otherwise do nothing. */
|
|
switch (key) {
|
|
case KEY_GRAVE: {
|
|
gkey = GHOST_kKeyAccentGrave;
|
|
break;
|
|
}
|
|
default: {
|
|
GHOST_PRINT(
|
|
/* Key-code. */
|
|
"unhandled key: " << std::hex << std::showbase << sym << /* Hex. */
|
|
std::dec << " (" << sym << "), " << /* Decimal. */
|
|
/* Scan-code. */
|
|
"scan-code: " << std::hex << std::showbase << key << /* Hex. */
|
|
std::dec << " (" << key << ")" << /* Decimal. */
|
|
std::endl);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return gkey;
|
|
}
|
|
|
|
static int pointer_axis_as_index(const uint32_t axis)
|
|
{
|
|
switch (axis) {
|
|
case WL_POINTER_AXIS_HORIZONTAL_SCROLL: {
|
|
return 0;
|
|
}
|
|
case WL_POINTER_AXIS_VERTICAL_SCROLL: {
|
|
return 1;
|
|
}
|
|
default: {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
static GHOST_TTabletMode tablet_tool_map_type(enum zwp_tablet_tool_v2_type wp_tablet_tool_type)
|
|
{
|
|
switch (wp_tablet_tool_type) {
|
|
case ZWP_TABLET_TOOL_V2_TYPE_ERASER: {
|
|
return GHOST_kTabletModeEraser;
|
|
}
|
|
case ZWP_TABLET_TOOL_V2_TYPE_PEN:
|
|
case ZWP_TABLET_TOOL_V2_TYPE_BRUSH:
|
|
case ZWP_TABLET_TOOL_V2_TYPE_PENCIL:
|
|
case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH:
|
|
case ZWP_TABLET_TOOL_V2_TYPE_FINGER:
|
|
case ZWP_TABLET_TOOL_V2_TYPE_MOUSE:
|
|
case ZWP_TABLET_TOOL_V2_TYPE_LENS: {
|
|
return GHOST_kTabletModeStylus;
|
|
}
|
|
}
|
|
|
|
GHOST_PRINT("unknown tablet tool: " << wp_tablet_tool_type << std::endl);
|
|
return GHOST_kTabletModeStylus;
|
|
}
|
|
|
|
static const int default_cursor_size = 24;
|
|
|
|
static const std::unordered_map<GHOST_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 */
|
|
}
|
|
|
|
static size_t ghost_wl_shm_format_as_size(enum wl_shm_format format)
|
|
{
|
|
switch (format) {
|
|
case WL_SHM_FORMAT_ARGB8888: {
|
|
return 4;
|
|
}
|
|
default: {
|
|
/* Support other formats as needed. */
|
|
GHOST_ASSERT(0, "Unexpected format passed in!");
|
|
return 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a #wl_buffer, ready to have it's data filled in or NULL in case of failure.
|
|
* The caller is responsible for calling `unmap(buffer_data, buffer_size)`.
|
|
*
|
|
* \param r_buffer_data: The buffer to be filled.
|
|
* \param r_buffer_data_size: The size of `r_buffer_data` in bytes.
|
|
*/
|
|
static wl_buffer *ghost_wl_buffer_create_for_image(struct wl_shm *shm,
|
|
const int32_t size_xy[2],
|
|
enum wl_shm_format format,
|
|
void **r_buffer_data,
|
|
size_t *r_buffer_data_size)
|
|
{
|
|
const int fd = memfd_create_sealed("ghost-wl-buffer");
|
|
wl_buffer *buffer = nullptr;
|
|
if (fd >= 0) {
|
|
const int32_t buffer_stride = size_xy[0] * ghost_wl_shm_format_as_size(format);
|
|
const int32_t buffer_size = buffer_stride * size_xy[1];
|
|
if (posix_fallocate(fd, 0, buffer_size) == 0) {
|
|
void *buffer_data = mmap(nullptr, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
if (buffer_data != MAP_FAILED) {
|
|
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, buffer_size);
|
|
buffer = wl_shm_pool_create_buffer(pool, 0, UNPACK2(size_xy), buffer_stride, format);
|
|
wl_shm_pool_destroy(pool);
|
|
if (buffer) {
|
|
*r_buffer_data = buffer_data;
|
|
*r_buffer_data_size = size_t(buffer_size);
|
|
}
|
|
else {
|
|
/* Highly unlikely. */
|
|
munmap(buffer_data, buffer_size);
|
|
}
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Private Keyboard Depressed Key Tracking
|
|
*
|
|
* Don't track physical key-codes because there may be multiple keyboards connected.
|
|
* Instead, count the number of #GHOST_kKey are pressed.
|
|
* This may seem susceptible to bugs with sticky-keys however XKB works this way internally.
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_KEYBOARD_DEPRESSED_STATE = {"ghost.wl.keyboard.depressed"};
|
|
#define LOG (&LOG_WL_KEYBOARD_DEPRESSED_STATE)
|
|
|
|
static void keyboard_depressed_state_reset(GWL_Seat *seat)
|
|
{
|
|
for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) {
|
|
seat->key_depressed.mods[i] = 0;
|
|
}
|
|
}
|
|
|
|
static void keyboard_depressed_state_key_event(GWL_Seat *seat,
|
|
const GHOST_TKey gkey,
|
|
const GHOST_TEventType etype)
|
|
{
|
|
if (GHOST_KEY_MODIFIER_CHECK(gkey)) {
|
|
const int index = GHOST_KEY_MODIFIER_TO_INDEX(gkey);
|
|
int16_t &value = seat->key_depressed.mods[index];
|
|
if (etype == GHOST_kEventKeyUp) {
|
|
value -= 1;
|
|
if (UNLIKELY(value < 0)) {
|
|
CLOG_WARN(LOG, "modifier (%d) has negative keys held (%d)!", index, value);
|
|
value = 0;
|
|
}
|
|
}
|
|
else {
|
|
value += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void keyboard_depressed_state_push_events_from_change(
|
|
GWL_Seat *seat, const GWL_KeyboardDepressedState &key_depressed_prev)
|
|
{
|
|
GHOST_IWindow *win = ghost_wl_surface_user_data(seat->keyboard.wl_surface_window);
|
|
GHOST_SystemWayland *system = seat->system;
|
|
|
|
/* Separate key up and down into separate passes so key down events always come after key up.
|
|
* Do this so users of GHOST can use the last pressed or released modifier to check
|
|
* if the modifier is held instead of counting modifiers pressed as is done here,
|
|
* this isn't perfect but works well enough in practice. */
|
|
for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) {
|
|
for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d < 0; d++) {
|
|
const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i);
|
|
seat->system->pushEvent(
|
|
new GHOST_EventKey(system->getMilliSeconds(), GHOST_kEventKeyUp, win, gkey, false));
|
|
|
|
CLOG_INFO(LOG, 2, "modifier (%d) up", i);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) {
|
|
for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d > 0; d--) {
|
|
const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i);
|
|
seat->system->pushEvent(
|
|
new GHOST_EventKey(system->getMilliSeconds(), GHOST_kEventKeyDown, win, gkey, false));
|
|
CLOG_INFO(LOG, 2, "modifier (%d) down", i);
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef LOG
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (Relative Motion), #zwp_relative_pointer_v1_listener
|
|
*
|
|
* These callbacks are registered for Wayland interfaces and called when
|
|
* an event is received from the compositor.
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_RELATIVE_POINTER = {"ghost.wl.handle.relative_pointer"};
|
|
#define LOG (&LOG_WL_RELATIVE_POINTER)
|
|
|
|
/**
|
|
* The caller is responsible for setting the value of `seat->xy`.
|
|
*/
|
|
static void relative_pointer_handle_relative_motion_impl(GWL_Seat *seat,
|
|
GHOST_WindowWayland *win,
|
|
const wl_fixed_t xy[2])
|
|
{
|
|
const wl_fixed_t scale = win->scale();
|
|
|
|
seat->pointer.xy[0] = xy[0];
|
|
seat->pointer.xy[1] = xy[1];
|
|
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
if (seat->use_pointer_software_confine) {
|
|
GHOST_Rect bounds;
|
|
win->getClientBounds(bounds);
|
|
/* Needed or the cursor is considered outside the window and doesn't restore the location. */
|
|
bounds.m_r -= 1;
|
|
bounds.m_b -= 1;
|
|
|
|
bounds.m_l = wl_fixed_from_int(bounds.m_l) / scale;
|
|
bounds.m_t = wl_fixed_from_int(bounds.m_t) / scale;
|
|
bounds.m_r = wl_fixed_from_int(bounds.m_r) / scale;
|
|
bounds.m_b = wl_fixed_from_int(bounds.m_b) / scale;
|
|
bounds.clampPoint(UNPACK2(seat->pointer.xy));
|
|
}
|
|
#endif
|
|
seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(),
|
|
GHOST_kEventCursorMove,
|
|
win,
|
|
wl_fixed_to_int(scale * seat->pointer.xy[0]),
|
|
wl_fixed_to_int(scale * seat->pointer.xy[1]),
|
|
GHOST_TABLET_DATA_NONE));
|
|
}
|
|
|
|
static void relative_pointer_handle_relative_motion(
|
|
void *data,
|
|
struct zwp_relative_pointer_v1 * /*zwp_relative_pointer_v1*/,
|
|
const uint32_t /*utime_hi*/,
|
|
const uint32_t /*utime_lo*/,
|
|
const wl_fixed_t dx,
|
|
const wl_fixed_t dy,
|
|
const wl_fixed_t /*dx_unaccel*/,
|
|
const wl_fixed_t /*dy_unaccel*/)
|
|
{
|
|
GWL_Seat *seat = static_cast<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(
|
|
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(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);
|
|
|
|
win->setCursorShape(win->getCursorShape());
|
|
|
|
const wl_fixed_t scale = win->scale();
|
|
seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(),
|
|
GHOST_kEventCursorMove,
|
|
win,
|
|
wl_fixed_to_int(scale * seat->pointer.xy[0]),
|
|
wl_fixed_to_int(scale * seat->pointer.xy[1]),
|
|
GHOST_TABLET_DATA_NONE));
|
|
}
|
|
|
|
static void pointer_handle_leave(void *data,
|
|
struct wl_pointer * /*wl_pointer*/,
|
|
const uint32_t /*serial*/,
|
|
struct wl_surface *wl_surface)
|
|
{
|
|
/* First clear the `pointer.wl_surface`, since the window won't exist when closing the window. */
|
|
static_cast<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(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(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(new GHOST_EventWheel(
|
|
seat->system->getMilliSeconds(), win, std::signbit(discrete) ? +1 : -1));
|
|
}
|
|
seat->pointer_scroll.discrete_xy[0] = 0;
|
|
seat->pointer_scroll.discrete_xy[1] = 0;
|
|
}
|
|
|
|
if (seat->pointer_scroll.smooth_xy[0] || seat->pointer_scroll.smooth_xy[1]) {
|
|
if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) {
|
|
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
|
|
const wl_fixed_t scale = win->scale();
|
|
seat->system->pushEvent(new GHOST_EventTrackpad(
|
|
seat->system->getMilliSeconds(),
|
|
win,
|
|
GHOST_kTrackpadEventScroll,
|
|
wl_fixed_to_int(scale * seat->pointer.xy[0]),
|
|
wl_fixed_to_int(scale * seat->pointer.xy[1]),
|
|
/* NOTE: scaling the delta doesn't seem necessary.
|
|
* NOTE: inverting delta gives correct results, see: QTBUG-85767.
|
|
* NOTE: the preference to invert scrolling (in GNOME at least)
|
|
* has already been applied so there is no need to read this preference. */
|
|
-wl_fixed_to_int(seat->pointer_scroll.smooth_xy[0]),
|
|
-wl_fixed_to_int(seat->pointer_scroll.smooth_xy[1]),
|
|
false));
|
|
}
|
|
|
|
seat->pointer_scroll.smooth_xy[0] = 0;
|
|
seat->pointer_scroll.smooth_xy[1] = 0;
|
|
}
|
|
|
|
seat->pointer_scroll.axis_source = WL_POINTER_AXIS_SOURCE_WHEEL;
|
|
}
|
|
static void pointer_handle_axis_source(void *data,
|
|
struct wl_pointer * /*wl_pointer*/,
|
|
uint32_t axis_source)
|
|
{
|
|
CLOG_INFO(LOG, 2, "axis_source (axis_source=%u)", axis_source);
|
|
GWL_Seat *seat = static_cast<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(new GHOST_EventTrackpad(seat->system->getMilliSeconds(),
|
|
win,
|
|
GHOST_kTrackpadEventMagnify,
|
|
event_xy[0],
|
|
event_xy[1],
|
|
scale_as_delta_px,
|
|
0,
|
|
false));
|
|
}
|
|
|
|
if (rotation_as_delta_px) {
|
|
seat->system->pushEvent(new GHOST_EventTrackpad(seat->system->getMilliSeconds(),
|
|
win,
|
|
GHOST_kTrackpadEventRotate,
|
|
event_xy[0],
|
|
event_xy[1],
|
|
rotation_as_delta_px,
|
|
0,
|
|
false));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gesture_pinch_handle_end(void * /*data*/,
|
|
struct zwp_pointer_gesture_pinch_v1 * /*pinch*/,
|
|
uint32_t /*serial*/,
|
|
uint32_t /*time*/,
|
|
int32_t cancelled)
|
|
{
|
|
CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled);
|
|
}
|
|
|
|
static const struct zwp_pointer_gesture_pinch_v1_listener gesture_pinch_listener = {
|
|
gesture_pinch_handle_begin,
|
|
gesture_pinch_handle_update,
|
|
gesture_pinch_handle_end,
|
|
};
|
|
|
|
# undef LOG
|
|
#endif /* ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE */
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (Pointer Gesture: Swipe), #zwp_pointer_gesture_swipe_v1
|
|
*
|
|
* \note In both Gnome-Shell & KDE this gesture isn't emitted at time of writing,
|
|
* instead, high resolution 2D #wl_pointer_listener.axis data is generated which works well.
|
|
* There may be some situations where WAYLAND compositors generate this gesture
|
|
* (swiping with 3+ fingers, for e.g.). So keep this to allow logging & testing gestures.
|
|
* \{ */
|
|
|
|
#ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE
|
|
static CLG_LogRef LOG_WL_POINTER_GESTURE_SWIPE = {"ghost.wl.handle.pointer_gesture.swipe"};
|
|
# define LOG (&LOG_WL_POINTER_GESTURE_SWIPE)
|
|
|
|
static void gesture_swipe_handle_begin(
|
|
void * /*data*/,
|
|
struct zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/,
|
|
uint32_t /*serial*/,
|
|
uint32_t /*time*/,
|
|
struct wl_surface * /*surface*/,
|
|
uint32_t fingers)
|
|
{
|
|
CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers);
|
|
}
|
|
|
|
static void gesture_swipe_handle_update(
|
|
void * /*data*/,
|
|
struct zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/,
|
|
uint32_t /*time*/,
|
|
wl_fixed_t dx,
|
|
wl_fixed_t dy)
|
|
{
|
|
CLOG_INFO(LOG, 2, "update (dx=%.3f, dy=%.3f)", wl_fixed_to_double(dx), wl_fixed_to_double(dy));
|
|
}
|
|
|
|
static void gesture_swipe_handle_end(
|
|
void * /*data*/,
|
|
struct zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/,
|
|
uint32_t /*serial*/,
|
|
uint32_t /*time*/,
|
|
int32_t cancelled)
|
|
{
|
|
CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled);
|
|
}
|
|
|
|
static const struct zwp_pointer_gesture_swipe_v1_listener gesture_swipe_listener = {
|
|
gesture_swipe_handle_begin,
|
|
gesture_swipe_handle_update,
|
|
gesture_swipe_handle_end,
|
|
};
|
|
|
|
# undef LOG
|
|
#endif /* ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE */
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (Touch Seat), #wl_touch_listener
|
|
*
|
|
* NOTE(@campbellbarton): It's not clear if this interface is used by popular compositors.
|
|
* It looks like GNOME/KDE only support `zwp_pointer_gestures_v1_interface`.
|
|
* If this isn't used anywhere, it could be removed.
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_TOUCH = {"ghost.wl.handle.touch"};
|
|
#define LOG (&LOG_WL_TOUCH)
|
|
|
|
static void touch_seat_handle_down(void * /*data*/,
|
|
struct wl_touch * /*wl_touch*/,
|
|
uint32_t /*serial*/,
|
|
uint32_t /*time*/,
|
|
struct wl_surface * /*wl_surface*/,
|
|
int32_t /*id*/,
|
|
wl_fixed_t /*x*/,
|
|
wl_fixed_t /*y*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "down");
|
|
}
|
|
|
|
static void touch_seat_handle_up(void * /*data*/,
|
|
struct wl_touch * /*wl_touch*/,
|
|
uint32_t /*serial*/,
|
|
uint32_t /*time*/,
|
|
int32_t /*id*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "up");
|
|
}
|
|
|
|
static void touch_seat_handle_motion(void * /*data*/,
|
|
struct wl_touch * /*wl_touch*/,
|
|
uint32_t /*time*/,
|
|
int32_t /*id*/,
|
|
wl_fixed_t /*x*/,
|
|
wl_fixed_t /*y*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "motion");
|
|
}
|
|
|
|
static void touch_seat_handle_frame(void * /*data*/, struct wl_touch * /*wl_touch*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "frame");
|
|
}
|
|
|
|
static void touch_seat_handle_cancel(void * /*data*/, struct wl_touch * /*wl_touch*/)
|
|
{
|
|
|
|
CLOG_INFO(LOG, 2, "cancel");
|
|
}
|
|
|
|
static void touch_seat_handle_shape(void * /*data*/,
|
|
struct wl_touch * /*touch*/,
|
|
int32_t /*id*/,
|
|
wl_fixed_t /*major*/,
|
|
wl_fixed_t /*minor*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "shape");
|
|
}
|
|
|
|
static void touch_seat_handle_orientation(void * /*data*/,
|
|
struct wl_touch * /*touch*/,
|
|
int32_t /*id*/,
|
|
wl_fixed_t /*orientation*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "orientation");
|
|
}
|
|
|
|
static const struct wl_touch_listener touch_seat_listener = {
|
|
touch_seat_handle_down,
|
|
touch_seat_handle_up,
|
|
touch_seat_handle_motion,
|
|
touch_seat_handle_frame,
|
|
touch_seat_handle_cancel,
|
|
touch_seat_handle_shape,
|
|
touch_seat_handle_orientation,
|
|
};
|
|
|
|
#undef LOG
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (Tablet Tool), #zwp_tablet_tool_v2_listener
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_TABLET_TOOL = {"ghost.wl.handle.tablet_tool"};
|
|
#define LOG (&LOG_WL_TABLET_TOOL)
|
|
|
|
static void tablet_tool_handle_type(void *data,
|
|
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
|
|
const uint32_t tool_type)
|
|
{
|
|
CLOG_INFO(LOG, 2, "type (type=%u)", tool_type);
|
|
|
|
GWL_TabletTool *tablet_tool = static_cast<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();
|
|
|
|
win->setCursorShape(win->getCursorShape());
|
|
}
|
|
static void tablet_tool_handle_proximity_out(void *data,
|
|
struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
|
|
{
|
|
CLOG_INFO(LOG, 2, "proximity_out");
|
|
GWL_TabletTool *tablet_tool = static_cast<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(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(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(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(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(new GHOST_EventCursor(seat->system->getMilliSeconds(),
|
|
GHOST_kEventCursorMove,
|
|
win,
|
|
wl_fixed_to_int(scale * seat->tablet.xy[0]),
|
|
wl_fixed_to_int(scale * seat->tablet.xy[1]),
|
|
tablet_tool->data));
|
|
if (tablet_tool->proximity == false) {
|
|
win->setCursorShape(win->getCursorShape());
|
|
}
|
|
}
|
|
|
|
if (tablet_tool->proximity == false) {
|
|
seat->tablet.wl_surface_window = nullptr;
|
|
}
|
|
}
|
|
|
|
static const struct zwp_tablet_tool_v2_listener tablet_tool_listner = {
|
|
tablet_tool_handle_type,
|
|
tablet_tool_handle_hardware_serial,
|
|
tablet_tool_handle_hardware_id_wacom,
|
|
tablet_tool_handle_capability,
|
|
tablet_tool_handle_done,
|
|
tablet_tool_handle_removed,
|
|
tablet_tool_handle_proximity_in,
|
|
tablet_tool_handle_proximity_out,
|
|
tablet_tool_handle_down,
|
|
tablet_tool_handle_up,
|
|
tablet_tool_handle_motion,
|
|
tablet_tool_handle_pressure,
|
|
tablet_tool_handle_distance,
|
|
tablet_tool_handle_tilt,
|
|
tablet_tool_handle_rotation,
|
|
tablet_tool_handle_slider,
|
|
tablet_tool_handle_wheel,
|
|
tablet_tool_handle_button,
|
|
tablet_tool_handle_frame,
|
|
};
|
|
|
|
#undef LOG
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (Table Seat), #zwp_tablet_seat_v2_listener
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_TABLET_SEAT = {"ghost.wl.handle.tablet_seat"};
|
|
#define LOG (&LOG_WL_TABLET_SEAT)
|
|
|
|
static void tablet_seat_handle_tablet_added(void * /*data*/,
|
|
struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
|
|
struct zwp_tablet_v2 *id)
|
|
{
|
|
CLOG_INFO(LOG, 2, "tablet_added (id=%p)", id);
|
|
}
|
|
|
|
static void tablet_seat_handle_tool_added(void *data,
|
|
struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
|
|
struct zwp_tablet_tool_v2 *id)
|
|
{
|
|
CLOG_INFO(LOG, 2, "tool_added (id=%p)", id);
|
|
|
|
GWL_Seat *seat = static_cast<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)
|
|
{
|
|
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)
|
|
{
|
|
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: {
|
|
/* The payload will be added again. */
|
|
seat->system->removeTimer(seat->key_repeat.timer);
|
|
seat->key_repeat.timer = nullptr;
|
|
break;
|
|
}
|
|
case CANCEL: {
|
|
delete key_repeat_payload;
|
|
key_repeat_payload = nullptr;
|
|
|
|
seat->system->removeTimer(seat->key_repeat.timer);
|
|
seat->key_repeat.timer = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, key);
|
|
char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'};
|
|
if (etype == GHOST_kEventKeyDown) {
|
|
xkb_state_key_get_utf8(seat->xkb_state, key_code, utf8_buf, sizeof(utf8_buf));
|
|
}
|
|
|
|
seat->data_source_serial = serial;
|
|
|
|
keyboard_depressed_state_key_event(seat, gkey, etype);
|
|
|
|
if (wl_surface *wl_surface_focus = seat->keyboard.wl_surface_window) {
|
|
GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus);
|
|
seat->system->pushEvent(
|
|
new GHOST_EventKey(seat->system->getMilliSeconds(), etype, win, gkey, false, utf8_buf));
|
|
}
|
|
|
|
/* An existing payload means the key repeat timer is reset and will be added again. */
|
|
if (key_repeat_payload == nullptr) {
|
|
/* Start timer for repeating key, if applicable. */
|
|
if ((seat->key_repeat.rate > 0) && (etype == GHOST_kEventKeyDown) &&
|
|
xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb_state), key_code)) {
|
|
key_repeat_payload = new GWL_KeyRepeatPlayload();
|
|
key_repeat_payload->seat = seat;
|
|
key_repeat_payload->key_code = key_code;
|
|
key_repeat_payload->key_data.gkey = gkey;
|
|
}
|
|
}
|
|
|
|
if (key_repeat_payload) {
|
|
auto key_repeat_fn = [](GHOST_ITimerTask *task, uint64_t /*time*/) {
|
|
struct GWL_KeyRepeatPlayload *payload = static_cast<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(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;
|
|
|
|
GHOST_WindowManager *window_manager = output->system->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()) {
|
|
continue;
|
|
}
|
|
win->outputs_changed_update_scale();
|
|
}
|
|
}
|
|
}
|
|
|
|
static const struct wl_output_listener output_listener = {
|
|
output_handle_geometry,
|
|
output_handle_mode,
|
|
output_handle_done,
|
|
output_handle_scale,
|
|
};
|
|
|
|
#undef LOG
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (XDG WM Base), #xdg_wm_base_listener
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_XDG_WM_BASE = {"ghost.wl.handle.xdg_wm_base"};
|
|
#define LOG (&LOG_WL_XDG_WM_BASE)
|
|
|
|
static void shell_handle_ping(void * /*data*/,
|
|
struct xdg_wm_base *xdg_wm_base,
|
|
const uint32_t serial)
|
|
{
|
|
CLOG_INFO(LOG, 2, "ping");
|
|
xdg_wm_base_pong(xdg_wm_base, serial);
|
|
}
|
|
|
|
static const struct xdg_wm_base_listener shell_listener = {
|
|
shell_handle_ping,
|
|
};
|
|
|
|
#undef LOG
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (LibDecor), #libdecor_interface
|
|
* \{ */
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
|
|
static CLG_LogRef LOG_WL_LIBDECOR = {"ghost.wl.handle.libdecor"};
|
|
# define LOG (&LOG_WL_LIBDECOR)
|
|
|
|
static void decor_handle_error(struct libdecor * /*context*/,
|
|
enum libdecor_error error,
|
|
const char *message)
|
|
{
|
|
CLOG_INFO(LOG, 2, "error (id=%d, message=%s)", error, message);
|
|
|
|
(void)(error);
|
|
(void)(message);
|
|
GHOST_PRINT("decoration error (" << error << "): " << message << std::endl);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
static struct libdecor_interface libdecor_interface = {
|
|
decor_handle_error,
|
|
};
|
|
|
|
# undef LOG
|
|
|
|
#endif /* WITH_GHOST_WAYLAND_LIBDECOR. */
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Listener (Registry), #wl_registry_listener
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_REGISTRY = {"ghost.wl.handle.registry"};
|
|
#define LOG (&LOG_WL_REGISTRY)
|
|
|
|
/* #GWL_Display.wl_compositor */
|
|
|
|
static void gwl_registry_compositor_add(GWL_Display *display,
|
|
const GWL_RegisteryAdd_Params *params)
|
|
{
|
|
display->wl_compositor = static_cast<wl_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. */
|
|
GWL_Output *output = static_cast<GWL_Output *>(user_data);
|
|
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 = {
|
|
.name = name,
|
|
.interface_slot = interface_slot,
|
|
.version = version,
|
|
};
|
|
|
|
handler->add_fn(display, ¶ms);
|
|
|
|
added = display->registry_entry != registry_entry_prev;
|
|
}
|
|
else {
|
|
/* Not found. */
|
|
#ifdef USE_GNOME_NEEDS_LIBDECOR_HACK
|
|
if (STRPREFIX(interface, "gtk_shell")) { /* `gtk_shell1` at time of writing. */
|
|
/* Only require `libdecor` when built with X11 support,
|
|
* otherwise there is nothing to fall back on. */
|
|
display->libdecor_required = true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
CLOG_INFO(LOG,
|
|
2,
|
|
"add %s(interface=%s, version=%u, name=%u)",
|
|
(interface_slot != -1) ? (added ? "" : "(found but not added)") : "(skipped), ",
|
|
interface,
|
|
version,
|
|
name);
|
|
|
|
/* Initialization avoids excessive calls by calling update after all have been initialized. */
|
|
if (added) {
|
|
if (display->registry_skip_update_all == false) {
|
|
/* See doc-string for rationale on updating all on add/removal. */
|
|
gwl_registry_entry_update_all(display, interface_slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Announce removal of global object.
|
|
*
|
|
* Notify the client of removed global objects.
|
|
*
|
|
* This event notifies the client that the global identified by
|
|
* name is no longer available. If the client bound to the global
|
|
* using the bind request, the client should now destroy that object.
|
|
*/
|
|
static void global_handle_remove(void *data,
|
|
[[maybe_unused]] struct wl_registry *wl_registry,
|
|
const uint32_t name)
|
|
{
|
|
GWL_Display *display = static_cast<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 Listener (Display), #wl_display_listener
|
|
* \{ */
|
|
|
|
static CLG_LogRef LOG_WL_DISPLAY = {"ghost.wl.handle.display"};
|
|
#define LOG (&LOG_WL_DISPLAY)
|
|
|
|
static void display_handle_error(
|
|
void *data, struct wl_display *wl_display, void *object_id, uint32_t code, const char *message)
|
|
{
|
|
GWL_Display *display = static_cast<GWL_Display *>(data);
|
|
GHOST_ASSERT(display->wl_display == wl_display, "Invalid internal state");
|
|
(void)display;
|
|
|
|
/* NOTE: code is #wl_display_error, there isn't a convenient way to convert to an ID. */
|
|
CLOG_INFO(LOG, 2, "error (code=%u, object_id=%p, message=%s)", code, object_id, message);
|
|
}
|
|
|
|
static void display_handle_delete_id(void *data, struct wl_display *wl_display, uint32_t id)
|
|
{
|
|
GWL_Display *display = static_cast<GWL_Display *>(data);
|
|
GHOST_ASSERT(display->wl_display == wl_display, "Invalid internal state");
|
|
(void)display;
|
|
|
|
CLOG_INFO(LOG, 2, "delete_id (id=%u)", id);
|
|
}
|
|
|
|
static const struct wl_display_listener display_listener = {
|
|
display_handle_error,
|
|
display_handle_delete_id,
|
|
};
|
|
|
|
#undef LOG
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name GHOST Implementation
|
|
*
|
|
* WAYLAND specific implementation of the #GHOST_System interface.
|
|
* \{ */
|
|
|
|
GHOST_SystemWayland::GHOST_SystemWayland(bool background)
|
|
: GHOST_System(), display_(new GWL_Display)
|
|
{
|
|
wl_log_set_handler_client(ghost_wayland_log_handler);
|
|
|
|
display_->system = this;
|
|
/* Connect to the Wayland server. */
|
|
display_->wl_display = wl_display_connect(nullptr);
|
|
if (!display_->wl_display) {
|
|
gwl_display_destroy(display_);
|
|
throw std::runtime_error("Wayland: unable to connect to display!");
|
|
}
|
|
|
|
/* This may be removed later if decorations are required, needed as part of registration. */
|
|
display_->xdg_decor = new GWL_XDG_Decor_System;
|
|
|
|
wl_display_add_listener(display_->wl_display, &display_listener, display_);
|
|
|
|
/* Register interfaces. */
|
|
{
|
|
display_->registry_skip_update_all = true;
|
|
struct wl_registry *registry = wl_display_get_registry(display_->wl_display);
|
|
display_->wl_registry = registry;
|
|
wl_registry_add_listener(registry, ®istry_listener, display_);
|
|
/* First round-trip to receive all registry objects. */
|
|
wl_display_roundtrip(display_->wl_display);
|
|
/* Second round-trip to receive all output events. */
|
|
wl_display_roundtrip(display_->wl_display);
|
|
|
|
/* Account for dependencies between interfaces. */
|
|
gwl_registry_entry_update_all(display_, -1);
|
|
|
|
display_->registry_skip_update_all = false;
|
|
}
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
/* Ignore windowing requirements when running in background mode,
|
|
* as it doesn't make sense to fall back to X11 because of windowing functionality
|
|
* in background mode, also LIBDECOR is crashing in background mode `blender -b -f 1`
|
|
* for e.g. while it could be fixed, requiring the library at all makes no sense . */
|
|
if (background) {
|
|
display_->libdecor_required = false;
|
|
}
|
|
|
|
if (display_->libdecor_required) {
|
|
gwl_xdg_decor_system_destroy(display_, display_->xdg_decor);
|
|
display_->xdg_decor = nullptr;
|
|
|
|
if (!has_libdecor) {
|
|
# ifdef WITH_GHOST_X11
|
|
/* LIBDECOR was the only reason X11 was used, let the user know they need it installed. */
|
|
fprintf(stderr,
|
|
"WAYLAND found but libdecor was not, install libdecor for Wayland support, "
|
|
"falling back to X11\n");
|
|
# endif
|
|
gwl_display_destroy(display_);
|
|
throw std::runtime_error("Wayland: unable to find libdecor!");
|
|
|
|
use_libdecor = true;
|
|
}
|
|
}
|
|
else {
|
|
use_libdecor = false;
|
|
}
|
|
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
if (use_libdecor) {
|
|
display_->libdecor = new GWL_LibDecor_System;
|
|
GWL_LibDecor_System &decor = *display_->libdecor;
|
|
decor.context = libdecor_new(display_->wl_display, &libdecor_interface);
|
|
if (!decor.context) {
|
|
gwl_display_destroy(display_);
|
|
throw std::runtime_error("Wayland: unable to create window decorations!");
|
|
}
|
|
}
|
|
else
|
|
#else
|
|
(void)background;
|
|
#endif
|
|
{
|
|
GWL_XDG_Decor_System &decor = *display_->xdg_decor;
|
|
if (!decor.shell) {
|
|
gwl_display_destroy(display_);
|
|
throw std::runtime_error("Wayland: unable to access xdg_shell!");
|
|
}
|
|
}
|
|
}
|
|
|
|
GHOST_SystemWayland::~GHOST_SystemWayland()
|
|
{
|
|
gwl_display_destroy(display_);
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::init()
|
|
{
|
|
GHOST_TSuccess success = GHOST_System::init();
|
|
|
|
if (success) {
|
|
#ifdef WITH_INPUT_NDOF
|
|
m_ndofManager = new GHOST_NDOFManagerUnix(*this);
|
|
#endif
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
bool GHOST_SystemWayland::processEvents(bool waitForEvent)
|
|
{
|
|
bool any_processed = false;
|
|
|
|
if (getTimerManager()->fireTimers(getMilliSeconds())) {
|
|
any_processed = true;
|
|
}
|
|
|
|
#ifdef WITH_INPUT_NDOF
|
|
if (static_cast<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) {
|
|
if (wl_display_dispatch(display_->wl_display) == -1) {
|
|
ghost_wl_display_report_error(display_->wl_display);
|
|
}
|
|
}
|
|
else {
|
|
if (wl_display_roundtrip(display_->wl_display) == -1) {
|
|
ghost_wl_display_report_error(display_->wl_display);
|
|
}
|
|
}
|
|
|
|
if (getEventManager()->getNumEvents() > 0) {
|
|
any_processed = true;
|
|
}
|
|
|
|
return any_processed;
|
|
}
|
|
|
|
bool GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*action*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
const xkb_mod_mask_t state = xkb_state_serialize_mods(seat->xkb_state, XKB_STATE_MODS_DEPRESSED);
|
|
|
|
bool show_warning = true;
|
|
#ifdef USE_GNOME_KEYBOARD_SUPPRESS_WARNING
|
|
if ((seat->key_depressed_suppress_warning.any_mod_held == true) &&
|
|
(seat->key_depressed_suppress_warning.any_keys_held_on_enter == false)) {
|
|
/* The compositor gave us invalid information, don't show a warning. */
|
|
show_warning = false;
|
|
}
|
|
#endif
|
|
|
|
/* Use local #GWL_KeyboardDepressedState to check which key is pressed.
|
|
* Use XKB as the source of truth, if there is any discrepancy. */
|
|
for (int i = 0; i < MOD_INDEX_NUM; i++) {
|
|
if (UNLIKELY(seat->xkb_keymap_mod_index[i] == XKB_MOD_INVALID)) {
|
|
continue;
|
|
}
|
|
const GWL_ModifierInfo &mod_info = g_modifier_info_table[i];
|
|
const bool val = (state & (1 << seat->xkb_keymap_mod_index[i])) != 0;
|
|
bool val_l = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_l)] > 0;
|
|
bool val_r = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_r)] > 0;
|
|
|
|
/* This shouldn't be needed, but guard against any possibility of modifiers being stuck.
|
|
* Warn so if this happens it can be investigated. */
|
|
if (val) {
|
|
if (UNLIKELY(!(val_l || val_r))) {
|
|
if (show_warning) {
|
|
CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE,
|
|
"modifier (%s) state is inconsistent (GHOST held keys do not match XKB)",
|
|
mod_info.display_name);
|
|
}
|
|
/* Picking the left is arbitrary. */
|
|
val_l = true;
|
|
}
|
|
}
|
|
else {
|
|
if (UNLIKELY(val_l || val_r)) {
|
|
if (show_warning) {
|
|
CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE,
|
|
"modifier (%s) state is inconsistent (GHOST released keys do not match XKB)",
|
|
mod_info.display_name);
|
|
}
|
|
val_l = false;
|
|
val_r = false;
|
|
}
|
|
}
|
|
|
|
keys.set(mod_info.mod_l, val_l);
|
|
keys.set(mod_info.mod_r, val_r);
|
|
}
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat);
|
|
if (!seat_state_pointer) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
buttons = seat_state_pointer->buttons;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
/**
|
|
* Return a mime type which is supported by GHOST and exists in `types`
|
|
* (defined by the data offer).
|
|
*/
|
|
static const char *system_clipboard_text_mime_type(
|
|
const std::unordered_set<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
|
|
{
|
|
char *data = nullptr;
|
|
if (selection) {
|
|
data = system_clipboard_get_primary_selection(display_);
|
|
}
|
|
else {
|
|
data = system_clipboard_get(display_);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
static void system_clipboard_put_primary_selection(GWL_Display *display, const char *buffer)
|
|
{
|
|
if (!display->wp_primary_selection_device_manager) {
|
|
return;
|
|
}
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display);
|
|
if (UNLIKELY(!seat)) {
|
|
return;
|
|
}
|
|
GWL_PrimarySelection *primary = &seat->primary_selection;
|
|
|
|
std::lock_guard lock{primary->data_source_mutex};
|
|
|
|
gwl_primary_selection_discard_source(primary);
|
|
|
|
GWL_PrimarySelection_DataSource *data_source = new GWL_PrimarySelection_DataSource;
|
|
primary->data_source = data_source;
|
|
|
|
/* Copy buffer. */
|
|
gwl_simple_buffer_set_from_string(&data_source->buffer_out, buffer);
|
|
|
|
data_source->wp_source = zwp_primary_selection_device_manager_v1_create_source(
|
|
display->wp_primary_selection_device_manager);
|
|
|
|
zwp_primary_selection_source_v1_add_listener(
|
|
data_source->wp_source, &primary_selection_source_listener, primary);
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_send); i++) {
|
|
zwp_primary_selection_source_v1_offer(data_source->wp_source, ghost_wl_mime_send[i]);
|
|
}
|
|
|
|
if (seat->wp_primary_selection_device) {
|
|
zwp_primary_selection_device_v1_set_selection(
|
|
seat->wp_primary_selection_device, data_source->wp_source, seat->data_source_serial);
|
|
}
|
|
}
|
|
|
|
static void system_clipboard_put(GWL_Display *display, const char *buffer)
|
|
{
|
|
if (!display->wl_data_device_manager) {
|
|
return;
|
|
}
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display);
|
|
if (UNLIKELY(!seat)) {
|
|
return;
|
|
}
|
|
std::lock_guard lock{seat->data_source_mutex};
|
|
|
|
GWL_DataSource *data_source = seat->data_source;
|
|
|
|
/* Copy buffer. */
|
|
gwl_simple_buffer_set_from_string(&data_source->buffer_out, buffer);
|
|
|
|
data_source->wl_source = wl_data_device_manager_create_data_source(
|
|
display->wl_data_device_manager);
|
|
|
|
wl_data_source_add_listener(data_source->wl_source, &data_source_listener, seat);
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_send); i++) {
|
|
wl_data_source_offer(data_source->wl_source, ghost_wl_mime_send[i]);
|
|
}
|
|
|
|
if (seat->wl_data_device) {
|
|
wl_data_device_set_selection(
|
|
seat->wl_data_device, data_source->wl_source, seat->data_source_serial);
|
|
}
|
|
}
|
|
|
|
void GHOST_SystemWayland::putClipboard(const char *buffer, bool selection) const
|
|
{
|
|
if (selection) {
|
|
system_clipboard_put_primary_selection(display_, buffer);
|
|
}
|
|
else {
|
|
system_clipboard_put(display_, buffer);
|
|
}
|
|
}
|
|
|
|
uint8_t GHOST_SystemWayland::getNumDisplays() const
|
|
{
|
|
return display_ ? uint8_t(display_->outputs.size()) : 0;
|
|
}
|
|
|
|
static GHOST_TSuccess getCursorPositionClientRelative_impl(
|
|
const GWL_SeatStatePointer *seat_state_pointer,
|
|
const GHOST_WindowWayland *win,
|
|
int32_t &x,
|
|
int32_t &y)
|
|
{
|
|
const wl_fixed_t scale = win->scale();
|
|
x = wl_fixed_to_int(scale * seat_state_pointer->xy[0]);
|
|
y = wl_fixed_to_int(scale * seat_state_pointer->xy[1]);
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
static GHOST_TSuccess setCursorPositionClientRelative_impl(GWL_Seat *seat,
|
|
GHOST_WindowWayland *win,
|
|
const int32_t x,
|
|
const int32_t y)
|
|
{
|
|
/* NOTE: WAYLAND doesn't support warping the cursor.
|
|
* However when grab is enabled, we already simulate a cursor location
|
|
* so that can be set to a new location. */
|
|
if (!seat->wp_relative_pointer) {
|
|
return GHOST_kFailure;
|
|
}
|
|
const wl_fixed_t scale = win->scale();
|
|
const wl_fixed_t xy_next[2] = {
|
|
wl_fixed_from_int(x) / scale,
|
|
wl_fixed_from_int(y) / scale,
|
|
};
|
|
|
|
/* As the cursor was "warped" generate an event at the new location. */
|
|
relative_pointer_handle_relative_motion_impl(seat, win, xy_next);
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::getCursorPositionClientRelative(const GHOST_IWindow *window,
|
|
int32_t &x,
|
|
int32_t &y) const
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat);
|
|
if (!seat_state_pointer || !seat_state_pointer->wl_surface_window) {
|
|
return GHOST_kFailure;
|
|
}
|
|
const GHOST_WindowWayland *win = static_cast<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)
|
|
{
|
|
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
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat);
|
|
if (!seat_state_pointer) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
if (wl_surface *wl_surface_focus = seat_state_pointer->wl_surface_window) {
|
|
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
|
|
return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y);
|
|
}
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(const int32_t x, const int32_t y)
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
/* Intentionally different from `getCursorPosition` which supports both tablet & pointer.
|
|
* In the case of setting the cursor location, tablets don't support this. */
|
|
if (wl_surface *wl_surface_focus = seat->pointer.wl_surface_window) {
|
|
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
|
|
return setCursorPositionClientRelative_impl(seat, win, x, y);
|
|
}
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
void GHOST_SystemWayland::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const
|
|
{
|
|
if (getNumDisplays() == 0) {
|
|
return;
|
|
}
|
|
/* We assume first output as main. */
|
|
width = uint32_t(display_->outputs[0]->size_native[0]);
|
|
height = uint32_t(display_->outputs[0]->size_native[1]);
|
|
}
|
|
|
|
void GHOST_SystemWayland::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const
|
|
{
|
|
int32_t xy_min[2] = {INT32_MAX, INT32_MAX};
|
|
int32_t xy_max[2] = {INT32_MIN, INT32_MIN};
|
|
|
|
for (const GWL_Output *output : display_->outputs) {
|
|
int32_t xy[2] = {0, 0};
|
|
if (output->has_position_logical) {
|
|
xy[0] = output->position_logical[0];
|
|
xy[1] = output->position_logical[1];
|
|
}
|
|
xy_min[0] = std::min(xy_min[0], xy[0]);
|
|
xy_min[1] = std::min(xy_min[1], xy[1]);
|
|
xy_max[0] = std::max(xy_max[0], xy[0] + output->size_native[0]);
|
|
xy_max[1] = std::max(xy_max[1], xy[1] + output->size_native[1]);
|
|
}
|
|
|
|
width = xy_max[0] - xy_min[0];
|
|
height = xy_max[1] - xy_min[1];
|
|
}
|
|
|
|
static GHOST_Context *createOffscreenContext_impl(GHOST_SystemWayland *system,
|
|
struct wl_display *wl_display,
|
|
wl_egl_window *egl_window)
|
|
{
|
|
GHOST_Context *context;
|
|
|
|
for (int minor = 6; minor >= 0; --minor) {
|
|
context = new GHOST_ContextEGL(system,
|
|
false,
|
|
EGLNativeWindowType(egl_window),
|
|
EGLNativeDisplayType(wl_display),
|
|
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
|
4,
|
|
minor,
|
|
GHOST_OPENGL_EGL_CONTEXT_FLAGS,
|
|
GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
|
|
EGL_OPENGL_API);
|
|
|
|
if (context->initializeDrawingContext()) {
|
|
return context;
|
|
}
|
|
delete context;
|
|
}
|
|
|
|
context = new GHOST_ContextEGL(system,
|
|
false,
|
|
EGLNativeWindowType(egl_window),
|
|
EGLNativeDisplayType(wl_display),
|
|
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
|
3,
|
|
3,
|
|
GHOST_OPENGL_EGL_CONTEXT_FLAGS,
|
|
GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
|
|
EGL_OPENGL_API);
|
|
|
|
if (context->initializeDrawingContext()) {
|
|
return context;
|
|
}
|
|
delete context;
|
|
return nullptr;
|
|
}
|
|
|
|
GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GLSettings /*glSettings*/)
|
|
{
|
|
/* Create new off-screen window. */
|
|
wl_surface *wl_surface = wl_compositor_create_surface(wl_compositor());
|
|
wl_egl_window *egl_window = wl_surface ? wl_egl_window_create(wl_surface, 1, 1) : nullptr;
|
|
|
|
GHOST_Context *context = createOffscreenContext_impl(this, display_->wl_display, egl_window);
|
|
|
|
if (!context) {
|
|
GHOST_PRINT("Cannot create off-screen EGL context" << std::endl);
|
|
if (wl_surface) {
|
|
wl_surface_destroy(wl_surface);
|
|
}
|
|
if (egl_window) {
|
|
wl_egl_window_destroy(egl_window);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
wl_surface_set_user_data(wl_surface, egl_window);
|
|
context->setUserData(wl_surface);
|
|
|
|
return context;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::disposeContext(GHOST_IContext *context)
|
|
{
|
|
struct wl_surface *wl_surface = (struct wl_surface *)((GHOST_Context *)context)->getUserData();
|
|
wl_egl_window *egl_window = (wl_egl_window *)wl_surface_get_user_data(wl_surface);
|
|
wl_egl_window_destroy(egl_window);
|
|
wl_surface_destroy(wl_surface);
|
|
|
|
delete context;
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_IWindow *GHOST_SystemWayland::createWindow(const char *title,
|
|
const int32_t left,
|
|
const int32_t top,
|
|
const uint32_t width,
|
|
const uint32_t height,
|
|
const GHOST_TWindowState state,
|
|
const GHOST_GLSettings glSettings,
|
|
const bool exclusive,
|
|
const bool is_dialog,
|
|
const GHOST_IWindow *parentWindow)
|
|
{
|
|
/* Globally store pointer to window manager. */
|
|
GHOST_WindowWayland *window = new GHOST_WindowWayland(
|
|
this,
|
|
title,
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
state,
|
|
parentWindow,
|
|
glSettings.context_type,
|
|
is_dialog,
|
|
((glSettings.flags & GHOST_glStereoVisual) != 0),
|
|
exclusive);
|
|
|
|
if (window) {
|
|
if (window->getValid()) {
|
|
m_windowManager->addWindow(window);
|
|
m_windowManager->setActiveWindow(window);
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowSize, window));
|
|
}
|
|
else {
|
|
delete window;
|
|
window = nullptr;
|
|
}
|
|
}
|
|
|
|
return window;
|
|
}
|
|
|
|
/**
|
|
* Show the buffer defined by #cursor_buffer_set without changing anything else,
|
|
* so #cursor_buffer_hide can be used to display it again.
|
|
*
|
|
* The caller is responsible for setting `seat->cursor.visible`.
|
|
*/
|
|
static void cursor_buffer_show(const GWL_Seat *seat)
|
|
{
|
|
const GWL_Cursor *cursor = &seat->cursor;
|
|
|
|
if (seat->wl_pointer) {
|
|
const int scale = cursor->is_custom ? cursor->custom_scale : seat->pointer.theme_scale;
|
|
const int32_t hotspot_x = int32_t(cursor->wl_image.hotspot_x) / scale;
|
|
const int32_t hotspot_y = int32_t(cursor->wl_image.hotspot_y) / scale;
|
|
if (seat->wl_pointer) {
|
|
wl_pointer_set_cursor(
|
|
seat->wl_pointer, seat->pointer.serial, cursor->wl_surface_cursor, hotspot_x, hotspot_y);
|
|
}
|
|
}
|
|
|
|
if (!seat->tablet_tools.empty()) {
|
|
const int scale = cursor->is_custom ? cursor->custom_scale : seat->tablet.theme_scale;
|
|
const int32_t hotspot_x = int32_t(cursor->wl_image.hotspot_x) / scale;
|
|
const int32_t hotspot_y = int32_t(cursor->wl_image.hotspot_y) / scale;
|
|
for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->tablet_tools) {
|
|
GWL_TabletTool *tablet_tool = static_cast<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::setCursorShape(const GHOST_TStandardCursor shape)
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
auto cursor_find = ghost_wl_cursors.find(shape);
|
|
const char *cursor_name = (cursor_find == ghost_wl_cursors.end()) ?
|
|
ghost_wl_cursors.at(GHOST_kStandardCursorDefault) :
|
|
(*cursor_find).second;
|
|
|
|
GWL_Cursor *cursor = &seat->cursor;
|
|
|
|
if (!cursor->wl_theme) {
|
|
/* The cursor wl_surface hasn't entered an output yet. Initialize theme with scale 1. */
|
|
cursor->wl_theme = wl_cursor_theme_load(
|
|
cursor->theme_name.c_str(), cursor->theme_size, wl_shm());
|
|
}
|
|
|
|
wl_cursor *wl_cursor = wl_cursor_theme_get_cursor(cursor->wl_theme, cursor_name);
|
|
|
|
if (!wl_cursor) {
|
|
GHOST_PRINT("cursor '" << cursor_name << "' does not exist" << std::endl);
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
struct wl_cursor_image *image = wl_cursor->images[0];
|
|
struct wl_buffer *buffer = wl_cursor_image_get_buffer(image);
|
|
if (!buffer) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
cursor->visible = true;
|
|
cursor->is_custom = false;
|
|
cursor->wl_buffer = buffer;
|
|
cursor->wl_image = *image;
|
|
|
|
cursor_buffer_set(seat, buffer);
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::hasCursorShape(const GHOST_TStandardCursor cursorShape)
|
|
{
|
|
auto cursor_find = ghost_wl_cursors.find(cursorShape);
|
|
if (cursor_find == ghost_wl_cursors.end()) {
|
|
return GHOST_kFailure;
|
|
}
|
|
const char *value = (*cursor_find).second;
|
|
if (*value == '\0') {
|
|
return GHOST_kFailure;
|
|
}
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::setCustomCursorShape(uint8_t *bitmap,
|
|
uint8_t *mask,
|
|
const int sizex,
|
|
const int sizey,
|
|
const int hotX,
|
|
const int hotY,
|
|
const bool /*canInvertColor*/)
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
GWL_Cursor *cursor = &seat->cursor;
|
|
if (cursor->custom_data) {
|
|
munmap(cursor->custom_data, cursor->custom_data_size);
|
|
cursor->custom_data = nullptr;
|
|
cursor->custom_data_size = 0; /* Not needed, but the value is no longer meaningful. */
|
|
}
|
|
|
|
const int32_t size_xy[2] = {sizex, sizey};
|
|
wl_buffer *buffer = ghost_wl_buffer_create_for_image(display_->wl_shm,
|
|
size_xy,
|
|
WL_SHM_FORMAT_ARGB8888,
|
|
&cursor->custom_data,
|
|
&cursor->custom_data_size);
|
|
if (buffer == nullptr) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
wl_buffer_add_listener(buffer, &cursor_buffer_listener, cursor);
|
|
|
|
static constexpr uint32_t black = 0xFF000000;
|
|
static constexpr uint32_t white = 0xFFFFFFFF;
|
|
static constexpr uint32_t transparent = 0x00000000;
|
|
|
|
uint8_t datab = 0, maskb = 0;
|
|
uint32_t *pixel;
|
|
|
|
for (int y = 0; y < sizey; ++y) {
|
|
pixel = &static_cast<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::getCursorBitmap(GHOST_CursorBitmapRef *bitmap)
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
GWL_Cursor *cursor = &seat->cursor;
|
|
if (cursor->custom_data == nullptr) {
|
|
return GHOST_kFailure;
|
|
}
|
|
if (!cursor->is_custom) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
bitmap->data_size[0] = cursor->wl_image.width;
|
|
bitmap->data_size[1] = cursor->wl_image.height;
|
|
|
|
bitmap->hot_spot[0] = cursor->wl_image.hotspot_x;
|
|
bitmap->hot_spot[1] = cursor->wl_image.hotspot_y;
|
|
|
|
bitmap->data = (uint8_t *)static_cast<void *>(cursor->custom_data);
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemWayland::setCursorVisibility(const bool visible)
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
cursor_visible_set(seat, visible, seat->cursor.is_hardware, CURSOR_VISIBLE_ALWAYS_SET);
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
bool GHOST_SystemWayland::supportsCursorWarp()
|
|
{
|
|
/* WAYLAND doesn't support setting the cursor position directly,
|
|
* this is an intentional choice, forcing us to use a software cursor in this case. */
|
|
return false;
|
|
}
|
|
|
|
bool GHOST_SystemWayland::supportsWindowPosition()
|
|
{
|
|
/* WAYLAND doesn't support accessing the window position. */
|
|
return false;
|
|
}
|
|
|
|
bool GHOST_SystemWayland::getCursorGrabUseSoftwareDisplay(const GHOST_TGrabCursorMode mode)
|
|
{
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return false;
|
|
}
|
|
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
const bool use_software_confine = seat->use_pointer_software_confine;
|
|
#else
|
|
const bool use_software_confine = false;
|
|
#endif
|
|
|
|
return cursor_is_software(mode, use_software_confine);
|
|
}
|
|
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
static bool setCursorGrab_use_software_confine(const GHOST_TGrabCursorMode mode,
|
|
wl_surface *wl_surface)
|
|
{
|
|
# ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON
|
|
if (use_gnome_confine_hack == false) {
|
|
return false;
|
|
}
|
|
# endif
|
|
if (mode != GHOST_kGrabNormal) {
|
|
return false;
|
|
}
|
|
const GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface);
|
|
if (!win) {
|
|
return false;
|
|
}
|
|
|
|
# ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON
|
|
if (win->scale() <= 1) {
|
|
return false;
|
|
}
|
|
# endif
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
static GWL_SeatStateGrab seat_grab_state_from_mode(const GHOST_TGrabCursorMode mode,
|
|
const bool use_software_confine)
|
|
{
|
|
/* Initialize all members. */
|
|
GWL_SeatStateGrab grab_state;
|
|
/* Warping happens to require software cursor which also hides. */
|
|
grab_state.use_lock = ELEM(mode, GHOST_kGrabWrap, GHOST_kGrabHide) || use_software_confine;
|
|
grab_state.use_confine = (mode == GHOST_kGrabNormal) && (use_software_confine == false);
|
|
return grab_state;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Public WAYLAND Proxy Ownership API
|
|
* \{ */
|
|
|
|
static const char *ghost_wl_output_tag_id = "GHOST-output";
|
|
static const char *ghost_wl_surface_tag_id = "GHOST-window";
|
|
static const char *ghost_wl_surface_cursor_pointer_tag_id = "GHOST-cursor-pointer";
|
|
static const char *ghost_wl_surface_cursor_tablet_tag_id = "GHOST-cursor-tablet";
|
|
|
|
bool ghost_wl_output_own(const struct wl_output *wl_output)
|
|
{
|
|
return wl_proxy_get_tag((struct wl_proxy *)wl_output) == &ghost_wl_output_tag_id;
|
|
}
|
|
|
|
bool ghost_wl_surface_own(const struct wl_surface *wl_surface)
|
|
{
|
|
return wl_proxy_get_tag((struct wl_proxy *)wl_surface) == &ghost_wl_surface_tag_id;
|
|
}
|
|
|
|
bool ghost_wl_surface_own_cursor_pointer(const struct wl_surface *wl_surface)
|
|
{
|
|
return wl_proxy_get_tag((struct wl_proxy *)wl_surface) ==
|
|
&ghost_wl_surface_cursor_pointer_tag_id;
|
|
}
|
|
|
|
bool ghost_wl_surface_own_cursor_tablet(const struct wl_surface *wl_surface)
|
|
{
|
|
return wl_proxy_get_tag((struct wl_proxy *)wl_surface) == &ghost_wl_surface_cursor_tablet_tag_id;
|
|
}
|
|
|
|
void ghost_wl_output_tag(struct wl_output *wl_output)
|
|
{
|
|
wl_proxy_set_tag((struct wl_proxy *)wl_output, &ghost_wl_output_tag_id);
|
|
}
|
|
|
|
void ghost_wl_surface_tag(struct wl_surface *wl_surface)
|
|
{
|
|
wl_proxy_set_tag((struct wl_proxy *)wl_surface, &ghost_wl_surface_tag_id);
|
|
}
|
|
|
|
void ghost_wl_surface_tag_cursor_pointer(struct wl_surface *wl_surface)
|
|
{
|
|
wl_proxy_set_tag((struct wl_proxy *)wl_surface, &ghost_wl_surface_cursor_pointer_tag_id);
|
|
}
|
|
|
|
void ghost_wl_surface_tag_cursor_tablet(struct wl_surface *wl_surface)
|
|
{
|
|
wl_proxy_set_tag((struct wl_proxy *)wl_surface, &ghost_wl_surface_cursor_tablet_tag_id);
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Public WAYLAND Direct Data Access
|
|
*
|
|
* Expose some members via methods.
|
|
* \{ */
|
|
|
|
wl_display *GHOST_SystemWayland::wl_display()
|
|
{
|
|
return display_->wl_display;
|
|
}
|
|
|
|
wl_compositor *GHOST_SystemWayland::wl_compositor()
|
|
{
|
|
return display_->wl_compositor;
|
|
}
|
|
|
|
struct zwp_primary_selection_device_manager_v1 *GHOST_SystemWayland::wp_primary_selection_manager()
|
|
{
|
|
return display_->wp_primary_selection_device_manager;
|
|
}
|
|
|
|
struct zwp_pointer_gestures_v1 *GHOST_SystemWayland::wp_pointer_gestures()
|
|
{
|
|
return display_->wp_pointer_gestures;
|
|
}
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
|
|
libdecor *GHOST_SystemWayland::libdecor_context()
|
|
{
|
|
return display_->libdecor->context;
|
|
}
|
|
|
|
#endif /* !WITH_GHOST_WAYLAND_LIBDECOR */
|
|
|
|
xdg_wm_base *GHOST_SystemWayland::xdg_decor_shell()
|
|
{
|
|
return display_->xdg_decor->shell;
|
|
}
|
|
|
|
zxdg_decoration_manager_v1 *GHOST_SystemWayland::xdg_decor_manager()
|
|
{
|
|
return display_->xdg_decor->manager;
|
|
}
|
|
|
|
/* End `xdg_decor`. */
|
|
|
|
const std::vector<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.
|
|
* \{ */
|
|
|
|
void GHOST_SystemWayland::seat_active_set(const struct GWL_Seat *seat)
|
|
{
|
|
gwl_display_seat_active_set(display_, seat);
|
|
}
|
|
|
|
void GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface)
|
|
{
|
|
#define SURFACE_CLEAR_PTR(surface_test) \
|
|
if (surface_test == wl_surface) { \
|
|
surface_test = nullptr; \
|
|
} \
|
|
((void)0);
|
|
|
|
/* Only clear window surfaces (not cursors, off-screen surfaces etc). */
|
|
for (GWL_Seat *seat : display_->seats) {
|
|
SURFACE_CLEAR_PTR(seat->pointer.wl_surface_window);
|
|
SURFACE_CLEAR_PTR(seat->tablet.wl_surface_window);
|
|
SURFACE_CLEAR_PTR(seat->keyboard.wl_surface_window);
|
|
SURFACE_CLEAR_PTR(seat->wl_surface_window_focus_dnd);
|
|
}
|
|
#undef SURFACE_CLEAR_PTR
|
|
}
|
|
|
|
bool GHOST_SystemWayland::window_cursor_grab_set(const GHOST_TGrabCursorMode mode,
|
|
const GHOST_TGrabCursorMode mode_current,
|
|
int32_t init_grab_xy[2],
|
|
const GHOST_Rect *wrap_bounds,
|
|
const GHOST_TAxisFlag wrap_axis,
|
|
wl_surface *wl_surface,
|
|
const int scale)
|
|
{
|
|
/* Ignore, if the required protocols are not supported. */
|
|
if (UNLIKELY(!display_->wp_relative_pointer_manager || !display_->wp_pointer_constraints)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
GWL_Seat *seat = gwl_display_seat_active_get(display_);
|
|
if (UNLIKELY(!seat)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
/* No change, success. */
|
|
if (mode == mode_current) {
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
const bool was_software_confine = seat->use_pointer_software_confine;
|
|
const bool use_software_confine = setCursorGrab_use_software_confine(mode, wl_surface);
|
|
#else
|
|
const bool was_software_confine = false;
|
|
const bool use_software_confine = false;
|
|
#endif
|
|
|
|
const struct GWL_SeatStateGrab grab_state_prev = seat_grab_state_from_mode(mode_current,
|
|
was_software_confine);
|
|
const struct GWL_SeatStateGrab grab_state_next = seat_grab_state_from_mode(mode,
|
|
use_software_confine);
|
|
|
|
/* Check for wrap as #supportsCursorWarp isn't supported. */
|
|
const bool use_visible = !(ELEM(mode, GHOST_kGrabHide, GHOST_kGrabWrap) || use_software_confine);
|
|
const bool is_hardware_cursor = !cursor_is_software(mode, use_software_confine);
|
|
|
|
/* Only hide so the cursor is not made visible before it's location is restored.
|
|
* This function is called again at the end of this function which only shows. */
|
|
cursor_visible_set(seat, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_HIDE);
|
|
|
|
/* Switching from one grab mode to another,
|
|
* in this case disable the current locks as it makes logic confusing,
|
|
* postpone changing the cursor to avoid flickering. */
|
|
if (!grab_state_next.use_lock) {
|
|
if (seat->wp_relative_pointer) {
|
|
zwp_relative_pointer_v1_destroy(seat->wp_relative_pointer);
|
|
seat->wp_relative_pointer = nullptr;
|
|
}
|
|
if (seat->wp_locked_pointer) {
|
|
/* Potentially add a motion event so the application has updated X/Y coordinates. */
|
|
int32_t xy_motion[2] = {0, 0};
|
|
bool xy_motion_create_event = false;
|
|
|
|
/* Request location to restore to. */
|
|
if (mode_current == GHOST_kGrabWrap) {
|
|
/* Since this call is initiated by Blender, we can be sure the window wasn't closed
|
|
* by logic outside this function - as the window was needed to make this call. */
|
|
int32_t xy_next[2] = {UNPACK2(seat->pointer.xy)};
|
|
|
|
GHOST_Rect bounds_scale;
|
|
|
|
bounds_scale.m_l = wl_fixed_from_int(wrap_bounds->m_l) / scale;
|
|
bounds_scale.m_t = wl_fixed_from_int(wrap_bounds->m_t) / scale;
|
|
bounds_scale.m_r = wl_fixed_from_int(wrap_bounds->m_r) / scale;
|
|
bounds_scale.m_b = wl_fixed_from_int(wrap_bounds->m_b) / scale;
|
|
|
|
bounds_scale.wrapPoint(UNPACK2(xy_next), 0, wrap_axis);
|
|
|
|
/* Push an event so the new location is registered. */
|
|
if ((xy_next[0] != seat->pointer.xy[0]) || (xy_next[1] != seat->pointer.xy[1])) {
|
|
xy_motion[0] = xy_next[0];
|
|
xy_motion[1] = xy_next[1];
|
|
xy_motion_create_event = true;
|
|
}
|
|
seat->pointer.xy[0] = xy_next[0];
|
|
seat->pointer.xy[1] = xy_next[1];
|
|
|
|
zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp_locked_pointer, UNPACK2(xy_next));
|
|
wl_surface_commit(wl_surface);
|
|
}
|
|
else if (mode_current == GHOST_kGrabHide) {
|
|
if ((init_grab_xy[0] != seat->grab_lock_xy[0]) ||
|
|
(init_grab_xy[1] != seat->grab_lock_xy[1])) {
|
|
const wl_fixed_t xy_next[2] = {
|
|
wl_fixed_from_int(init_grab_xy[0]) / scale,
|
|
wl_fixed_from_int(init_grab_xy[1]) / scale,
|
|
};
|
|
zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp_locked_pointer,
|
|
UNPACK2(xy_next));
|
|
wl_surface_commit(wl_surface);
|
|
|
|
/* NOTE(@campbellbarton): The new cursor position is a hint,
|
|
* it's possible the hint is ignored. It doesn't seem like there is a good way to
|
|
* know if the hint will be used or not, at least not immediately. */
|
|
xy_motion[0] = xy_next[0];
|
|
xy_motion[1] = xy_next[1];
|
|
xy_motion_create_event = true;
|
|
}
|
|
}
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
else if (mode_current == GHOST_kGrabNormal) {
|
|
if (was_software_confine) {
|
|
zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp_locked_pointer,
|
|
UNPACK2(seat->pointer.xy));
|
|
wl_surface_commit(wl_surface);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (xy_motion_create_event) {
|
|
seat->system->pushEvent(new GHOST_EventCursor(seat->system->getMilliSeconds(),
|
|
GHOST_kEventCursorMove,
|
|
ghost_wl_surface_user_data(wl_surface),
|
|
wl_fixed_to_int(scale * xy_motion[0]),
|
|
wl_fixed_to_int(scale * xy_motion[1]),
|
|
GHOST_TABLET_DATA_NONE));
|
|
}
|
|
|
|
zwp_locked_pointer_v1_destroy(seat->wp_locked_pointer);
|
|
seat->wp_locked_pointer = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!grab_state_next.use_confine) {
|
|
if (seat->wp_confined_pointer) {
|
|
zwp_confined_pointer_v1_destroy(seat->wp_confined_pointer);
|
|
seat->wp_confined_pointer = nullptr;
|
|
}
|
|
}
|
|
|
|
if (mode != GHOST_kGrabDisable) {
|
|
if (grab_state_next.use_lock) {
|
|
if (!grab_state_prev.use_lock) {
|
|
/* TODO(@campbellbarton): As WAYLAND does not support warping the pointer it may not be
|
|
* possible to support #GHOST_kGrabWrap by pragmatically settings it's coordinates.
|
|
* An alternative could be to draw the cursor in software (and hide the real cursor),
|
|
* or just accept a locked cursor on WAYLAND. */
|
|
seat->wp_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(
|
|
display_->wp_relative_pointer_manager, seat->wl_pointer);
|
|
zwp_relative_pointer_v1_add_listener(
|
|
seat->wp_relative_pointer, &relative_pointer_listener, seat);
|
|
seat->wp_locked_pointer = zwp_pointer_constraints_v1_lock_pointer(
|
|
display_->wp_pointer_constraints,
|
|
wl_surface,
|
|
seat->wl_pointer,
|
|
nullptr,
|
|
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
|
|
}
|
|
if (mode == GHOST_kGrabHide) {
|
|
/* Set the initial position to detect any changes when un-grabbing,
|
|
* otherwise the unlocked cursor defaults to un-locking in-place. */
|
|
init_grab_xy[0] = wl_fixed_to_int(scale * seat->pointer.xy[0]);
|
|
init_grab_xy[1] = wl_fixed_to_int(scale * seat->pointer.xy[1]);
|
|
seat->grab_lock_xy[0] = init_grab_xy[0];
|
|
seat->grab_lock_xy[1] = init_grab_xy[1];
|
|
}
|
|
}
|
|
else if (grab_state_next.use_confine) {
|
|
if (!grab_state_prev.use_confine) {
|
|
seat->wp_confined_pointer = zwp_pointer_constraints_v1_confine_pointer(
|
|
display_->wp_pointer_constraints,
|
|
wl_surface,
|
|
seat->wl_pointer,
|
|
nullptr,
|
|
ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Only show so the cursor is made visible as the last step. */
|
|
cursor_visible_set(seat, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_SHOW);
|
|
|
|
#ifdef USE_GNOME_CONFINE_HACK
|
|
seat->use_pointer_software_confine = use_software_confine;
|
|
#endif
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
bool GHOST_SystemWayland::use_libdecor_runtime()
|
|
{
|
|
return use_libdecor;
|
|
}
|
|
#endif
|
|
|
|
#ifdef WITH_GHOST_WAYLAND_DYNLOAD
|
|
bool ghost_wl_dynload_libraries_init()
|
|
{
|
|
# ifdef WITH_GHOST_X11
|
|
/* When running in WAYLAND, let the user know when a missing library is the only reason
|
|
* WAYLAND could not be used. Otherwise it's not obvious why X11 is used as a fallback.
|
|
* Otherwise when X11 is used, reporting WAYLAND library warnings is unwelcome noise. */
|
|
bool verbose = getenv("WAYLAND_DISPLAY") != nullptr;
|
|
# else
|
|
bool verbose = true;
|
|
# endif /* !WITH_GHOST_X11 */
|
|
|
|
if (wayland_dynload_client_init(verbose) && /* `libwayland-client`. */
|
|
wayland_dynload_cursor_init(verbose) && /* `libwayland-cursor`. */
|
|
wayland_dynload_egl_init(verbose) /* `libwayland-egl`. */
|
|
) {
|
|
# ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
has_libdecor = wayland_dynload_libdecor_init(verbose); /* `libdecor-0`. */
|
|
# endif
|
|
return true;
|
|
}
|
|
|
|
wayland_dynload_client_exit();
|
|
wayland_dynload_cursor_exit();
|
|
wayland_dynload_egl_exit();
|
|
|
|
return false;
|
|
}
|
|
|
|
void ghost_wl_dynload_libraries_exit()
|
|
{
|
|
wayland_dynload_client_exit();
|
|
wayland_dynload_cursor_exit();
|
|
wayland_dynload_egl_exit();
|
|
# ifdef WITH_GHOST_WAYLAND_LIBDECOR
|
|
wayland_dynload_libdecor_exit();
|
|
# endif
|
|
}
|
|
|
|
#endif /* WITH_GHOST_WAYLAND_DYNLOAD */
|
|
|
|
/** \} */
|