Files
test/intern/ghost/intern/GHOST_SystemWayland.cc
John Kiril Swenson 96f5bb9f05 VSE: Slip toolbar tool
This PR adds a slip tool in the toolbar with its own custom icon for
applying slips using the mouse. This is useful for e.g. tablets where a
keyboard is not handy and a button would be best to activate the
operator.

There is also a custom cursor that appears when hovering over valid,
slippable strips (and a "stop" icon when the strip cannot be slipped).

Alt may be used in order to ignore slipping connected strips when using
the tool, similar to selection logic. The slip tool only performs its
sole function of slipping and as such does not change the selection
state.

In the future, we can also add "slide" functionality to the same tool,
giving it multiple functions.

Pull Request: https://projects.blender.org/blender/blender/pulls/143513
2025-08-25 20:36:01 +02:00

9909 lines
338 KiB
C++

/* SPDX-FileCopyrightText: 2020-2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup GHOST
*/
#include "GHOST_SystemWayland.hh"
#include "GHOST_Context.hh"
#include "GHOST_Event.hh"
#include "GHOST_EventButton.hh"
#include "GHOST_EventCursor.hh"
#include "GHOST_EventDragnDrop.hh"
#include "GHOST_EventKey.hh"
#include "GHOST_EventTrackpad.hh"
#include "GHOST_EventWheel.hh"
#include "GHOST_PathUtils.hh"
#include "GHOST_TimerManager.hh"
#include "GHOST_WaylandUtils.hh"
#include "GHOST_WindowManager.hh"
#include "GHOST_utildefines.hh"
#ifdef WITH_OPENGL_BACKEND
# include "GHOST_ContextEGL.hh"
#endif
#ifdef WITH_VULKAN_BACKEND
# include "GHOST_ContextVK.hh"
#endif
#ifdef WITH_INPUT_NDOF
# include "GHOST_NDOFManagerUnix.hh"
#endif
#ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_API.h> /* For `ghost_wl_dynload_libraries`. */
#endif
#ifdef WITH_OPENGL_BACKEND
# ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_egl.h>
# endif
# include <wayland-egl.h>
#endif /* WITH_OPENGL_BACKEND */
#include <algorithm>
#include <atomic>
#include <optional>
#include <thread>
#include <unordered_set>
#ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_cursor.h>
#endif
#include <wayland-cursor.h>
#include <xkbcommon/xkbcommon-compose.h>
#include <xkbcommon/xkbcommon.h>
/* Generated by `wayland-scanner`. */
#include <cursor-shape-v1-client-protocol.h>
#include <fractional-scale-v1-client-protocol.h>
#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-v2-client-protocol.h>
#include <viewporter-client-protocol.h>
#include <xdg-activation-v1-client-protocol.h>
#include <xdg-output-unstable-v1-client-protocol.h>
#ifdef WITH_INPUT_IME
# include <text-input-unstable-v3-client-protocol.h>
#endif
/* Decorations `xdg_decor`. */
#include <xdg-decoration-unstable-v1-client-protocol.h>
#include <xdg-shell-client-protocol.h>
/* End `xdg_decor`. */
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstdlib> /* For `exit`. */
#include <cstring>
#include <mutex>
#include <pthread.h> /* For setting the thread priority. */
#ifdef HAVE_POLL
# include <poll.h>
#endif
/* Logging, use `ghost.wl.*` prefix. */
#include "CLG_log.h"
#ifdef USE_EVENT_BACKGROUND_THREAD
# include "GHOST_TimerTask.hh"
#endif
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
static bool use_libdecor = true;
# ifdef WITH_GHOST_WAYLAND_DYNLOAD
static bool has_libdecor = false;
# else
static bool has_libdecor = true;
# endif
#endif
static signed char has_wl_trackpad_physical_direction = -1;
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
/* -------------------------------------------------------------------- */
/** \name Forward Declarations
* \{ */
static void keyboard_handle_key_repeat_cancel(GWL_Seat *seat);
static void output_handle_done(void *data, 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 GWL_RegistryHandler *gwl_registry_handler_from_interface_slot(int interface_slot);
static bool xkb_compose_state_feed_and_get_utf8(
xkb_compose_state *compose_state,
xkb_state *state,
const xkb_keycode_t key,
char r_utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)]);
#ifdef USE_EVENT_BACKGROUND_THREAD
static void gwl_display_event_thread_destroy(GWL_Display *display);
static void ghost_wl_display_lock_without_input(wl_display *wl_display, std::mutex *server_mutex);
/** Default size for pending event vector. */
constexpr size_t events_pending_default_size = 4096 / sizeof(void *);
#endif /* USE_EVENT_BACKGROUND_THREAD */
/** In nearly all cases use `pushEvent_maybe_pending`
* at least when called from WAYLAND callbacks. */
#define pushEvent DONT_USE
/** \} */
/* -------------------------------------------------------------------- */
/** \name Workaround Compositor Specific Bugs
* \{ */
/**
* GNOME (mutter 42.2 had a bug with confine not respecting scale - Hi-DPI), See: #98793.
* 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
/**
* 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: #102048.
* See: https://bugs.kde.org/show_bug.cgi?id=461001
*/
#define USE_KDE_TABLET_HIDDEN_CURSOR_HACK
/** \} */
/* -------------------------------------------------------------------- */
/** \name Local Defines
*
* Control local functionality, compositors specific workarounds.
* \{ */
/**
* A version clamping macro that optionally prints when the version is outdated.
* This is useful when investigating when newer versions of an interface might be supported.
*
* This addresses the following:
* - Filling in callbacks which wont be called because they aren't part of the older interface.
* - Not taking advantage of newer interfaces which would be beneficial.
* - When interface versions need to be bumped to support new features,
* avoid large version bumps that could change behavior in unexpected ways
* due to versions changes between each version that wont have been accounted for.
*
* This should only be enabled during development, never enabled for regular releases.
*/
// #define USE_VERBOSE_OLD_IFACE_PRINT
#ifdef USE_VERBOSE_OLD_IFACE_PRINT
# define _VERBOSE_OLD_IFACE_PRINT(params_version, version_max) \
((params_version > version_max) ? \
fprintf(stderr, \
"%s: version_max=%u, is smaller than run-time version=%u\n", \
__func__, \
version_max, \
params_version) : \
0)
#else
# define _VERBOSE_OLD_IFACE_PRINT(params_version, version_max) \
((void)(params_version), (version_max))
#endif
#define GWL_IFACE_VERSION_CLAMP(params_version, version_min, version_max) \
((void)_VERBOSE_OLD_IFACE_PRINT(params_version, version_max), \
std::clamp(params_version, version_min, version_max))
/**
* Fix short-cut part of keyboard reading code not properly handling some keys, see: #102194.
* \note This is similar to X11 workaround by the same name, see: #47228.
*/
#define USE_NON_LATIN_KB_WORKAROUND
#define WL_NAME_UNSET uint32_t(-1)
/**
* Initializer for GHOST integer coordinates from `wl_fixed_t`,
* taking window scale into account.
*/
#define WL_FIXED_TO_INT_FOR_WINDOW_V2(win, xy) \
wl_fixed_to_int((win)->wl_fixed_to_window((xy)[0])), \
wl_fixed_to_int((win)->wl_fixed_to_window((xy)[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.
*/
enum {
BTN_LEFT = 0x110,
BTN_RIGHT = 0x111,
BTN_MIDDLE = 0x112,
BTN_SIDE = 0x113,
BTN_EXTRA = 0x114,
BTN_FORWARD = 0x115,
BTN_BACK = 0x116
};
// #define BTN_TASK 0x117 /* UNUSED. */
#define BTN_RANGE_MIN BTN_LEFT
#define BTN_RANGE_MAX BTN_BACK
/**
* 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.
*/
enum {
/** Use as middle-mouse. */
BTN_STYLUS = 0x14b,
/** Use as right-mouse. */
BTN_STYLUS2 = 0x14c,
/** NOTE(@ideasman42): Map to an additional button (not sure which hardware uses this). */
BTN_STYLUS3 = 0x149,
};
/**
* Keyboard scan-codes.
*
* From `linux/input-event-codes.h`.
*/
enum {
KEY_GRAVE = 41,
/**
* Sometimes called OEM 102, used for German `GrLess` key.
* For the common case this key will be mapped using #XKB_KEY_less.
* Use a scan-code to prevent the key being unknown.
*/
KEY_102ND = 86,
#ifdef USE_NON_LATIN_KB_WORKAROUND
KEY_1 = 2,
KEY_2 = 3,
KEY_3 = 4,
KEY_4 = 5,
KEY_5 = 6,
KEY_6 = 7,
KEY_7 = 8,
KEY_8 = 9,
KEY_9 = 10,
KEY_0 = 11,
#endif
};
/* Only defined in XKB 1.8x and newer, it seems XKB doesn't provide a version define. */
#ifndef XKB_VMOD_NAME_HYPER
# define XKB_VMOD_NAME_HYPER "Hyper"
#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,
MOD_INDEX_HYPER = 4,
};
#define MOD_INDEX_NUM (MOD_INDEX_HYPER + 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,
},
/*MOD_INDEX_HYPER*/
{
/*display_name*/ "Hyper",
/*xkb_id*/ XKB_VMOD_NAME_HYPER,
/*key_l*/ GHOST_kKeyLeftHyper,
/*key_r*/ GHOST_kKeyRightHyper,
/*mod_l*/ GHOST_kModifierKeyLeftHyper,
/*mod_r*/ GHOST_kModifierKeyRightHyper,
},
};
/** \} */
/* -------------------------------------------------------------------- */
/** \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
/**
* Wayland cursor shape protocol types.
* Not used if the compositor doesn't support the cursor shape protocol.
*/
struct GWL_CursorShape {
/**
* The enum_id is currently only used to keep track of the last cursor shape used.
* This is so we can restore it after hiding the cursor.
*/
wp_cursor_shape_device_v1_shape enum_id = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
wp_cursor_shape_device_v1 *device = nullptr;
};
struct GWL_Cursor {
/** Wayland core types. */
struct {
/* Everything below in this wl struct is used for custom cursor shapes. */
wl_surface *surface_cursor = nullptr;
wl_buffer *buffer = nullptr;
wl_cursor_image image = {0};
} wl;
GWL_CursorShape shape;
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;
void *custom_data = nullptr;
/** The size of `custom_data` in bytes. */
size_t custom_data_size = 0;
/** The current displayed size, use to check if the cursor need to be re-generated. */
int custom_cursor_scale = 0;
/**
* The size of the cursor in logical pixels.
* This must be scaled by the maximum output scale when calculating the physical pixels.
* See #update_cursor_scale.
*/
int theme_size = 0;
/**
* Prefer dark theme cursors where possible.
*/
bool use_dark_theme = false;
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_TabletTool Type
* \{ */
/**
* Collect tablet event data before the frame callback runs.
*/
enum class GWL_TabletTool_EventTypes {
Motion = 0,
Pressure,
Tilt,
Wheel,
/* NOTE: Keep buttons last (simplifies switch statement). */
/* Left mouse button. */
Stylus0_Down,
Stylus0_Up,
/* Middle mouse button. */
Stylus1_Down,
Stylus1_Up,
/* Right mouse button. */
Stylus2_Down,
Stylus2_Up,
/* Mouse button number 4. */
Stylus3_Down,
Stylus3_Up,
#define GWL_TabletTool_FrameTypes_NUM (int(GWL_TabletTool_EventTypes::Stylus3_Up) + 1)
};
static const GHOST_TButton gwl_tablet_tool_ebutton[] = {
GHOST_kButtonMaskLeft, /* `Stylus0_*`. */
GHOST_kButtonMaskMiddle, /* `Stylus1_*`. */
GHOST_kButtonMaskRight, /* `Stylus2_*`. */
GHOST_kButtonMaskButton4, /* `Stylus3_*`. */
};
/**
* 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 {
/** Wayland native types. */
struct {
/**
* Tablets have a separate cursor to the 'pointer',
* this surface is used for cursor drawing.
*/
wl_surface *surface_cursor = nullptr;
} wl;
GWL_CursorShape shape;
GWL_Seat *seat = nullptr;
/** The serial, set on `proximity_in`, cleared on `proximity_out`. */
uint32_t serial = 0;
/** Used to delay clearing tablet focused wl_surface until the frame is handled. */
bool proximity = false;
GHOST_TabletData data = GHOST_TABLET_DATA_NONE;
/** Motion. */
int32_t xy[2] = {0};
bool has_xy = false;
/**
* Collect data before the #zwp_tablet_tool_v2_listener::frame runs.
*/
struct {
GWL_TabletTool_EventTypes frame_types[GWL_TabletTool_FrameTypes_NUM] = {
GWL_TabletTool_EventTypes::Motion, /* Dummy, never used. */
};
int frame_types_num = 0;
int frame_types_mask = 0;
struct {
int32_t clicks = 0;
} wheel;
} frame_pending;
};
static void gwl_tablet_tool_frame_event_add(GWL_TabletTool *tablet_tool,
const GWL_TabletTool_EventTypes ty)
{
const int ty_mask = 1 << int(ty);
/* Motion callback may run multiple times. */
if (tablet_tool->frame_pending.frame_types_mask & ty_mask) {
return;
}
tablet_tool->frame_pending.frame_types_mask |= ty_mask;
int i = tablet_tool->frame_pending.frame_types_num++;
tablet_tool->frame_pending.frame_types[i] = ty;
}
static void gwl_tablet_tool_frame_event_reset(GWL_TabletTool *tablet_tool)
{
tablet_tool->frame_pending.frame_types_num = 0;
tablet_tool->frame_pending.frame_types_mask = 0;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_DataOffer Type
* \{ */
/**
* Data storage used for clipboard paste & drag-and-drop.
*/
struct GWL_DataOffer {
/** Wayland native types. */
struct {
wl_data_offer *id = nullptr;
} wl;
std::unordered_set<std::string> types;
struct {
/**
* 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 {
/** Wayland core types. */
struct {
wl_data_source *source = nullptr;
} wl;
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 {
GWL_Seat *seat = nullptr;
xkb_keycode_t key_code;
/** Time this timer started. */
uint64_t time_ms_init;
/**
* 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 {
/** Wayland core types. */
struct {
/**
* The wl_surface last used with this pointing device
* (events with this pointing device will be sent here).
*/
wl_surface *surface_window = nullptr;
} wl;
/**
* 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 buffer_scale = 1;
/**
* The serial of the last used pointer or tablet.
*
* \note For tablet cursors, use: #GWL_TabletTool::serial instead.
*/
uint32_t serial = 0;
GHOST_Buttons buttons = GHOST_Buttons();
};
enum class GWL_Pointer_EventTypes {
Motion = 0,
/**
* Wheel & smooth scroll.
*
* \note The logic for scrolling is quite involved.
* This event is more of a catch-all that scrolling needs to be computed.
*/
Scroll,
/* NOTE: Keep buttons last (simplifies switch statement). */
/* #BTN_LEFT mouse button. */
Button0_Down,
Button0_Up,
/* #BTN_RIGHT mouse button. */
Button1_Down,
Button1_Up,
/* #BTN_MIDDLE mouse button. */
Button2_Down,
Button2_Up,
/* #BTN_SIDE mouse button. */
Button3_Down,
Button3_Up,
/* #BTN_EXTRA mouse button. */
Button4_Down,
Button4_Up,
/* #BTN_FORWARD mouse button. */
Button5_Down,
Button5_Up,
/* #BTN_BACK mouse button. */
Button6_Down,
Button6_Up,
#define GWL_SeatStatePointer_EventTypes_NUM (int(GWL_Pointer_EventTypes::Button6_Up) + 1)
};
static const GHOST_TButton gwl_pointer_events_ebutton[] = {
GHOST_kButtonMaskLeft, /* `Button0_*` / #BTN_LEFT. */
GHOST_kButtonMaskRight, /* `Button1_*` / #BTN_RIGHT. */
GHOST_kButtonMaskMiddle, /* `Button2_*` / #BTN_MIDDLE. */
GHOST_kButtonMaskButton4, /* `Button3_*` / #BTN_SIDE. */
GHOST_kButtonMaskButton5, /* `Button4_*` / #BTN_EXTRA. */
GHOST_kButtonMaskButton6, /* `Button5_*` / #BTN_FORWARD. */
GHOST_kButtonMaskButton7, /* `Button6_*` / #BTN_BACK. */
};
static_assert(ARRAY_SIZE(gwl_pointer_events_ebutton) ==
GHOST_kButtonNum - (int(GHOST_kButtonMaskNone) + 1),
"Buttons missing");
struct GWL_SeatStatePointer_Events {
/**
* Collect data before the #zwp_tablet_tool_v2_listener::frame runs.
*/
struct {
GWL_Pointer_EventTypes frame_types[GWL_TabletTool_FrameTypes_NUM] = {
GWL_Pointer_EventTypes::Motion, /* Dummy, never used. */
};
uint64_t frame_event_ms[GWL_TabletTool_FrameTypes_NUM] = {0};
int frame_types_num = 0;
int frame_types_mask = 0;
} frame_pending;
};
static void gwl_pointer_handle_frame_event_add(GWL_SeatStatePointer_Events *pointer_events,
const GWL_Pointer_EventTypes ty,
const uint64_t event_ms)
{
/* It's a quirk of WAYLAND that most scroll events don't have a time-stamp.
* Scroll events use their own time-stamp (see #GWL_SeatStatePointerScroll::event_ms usage).
* Ensure the API is used as intended. */
if (ty == GWL_Pointer_EventTypes::Scroll) {
GHOST_ASSERT(event_ms == 0, "Scroll events must not have a time-stamp");
}
else {
GHOST_ASSERT(event_ms != 0, "Non-scroll events must have a time-stamp");
}
const int ty_mask = 1 << int(ty);
/* Motion callback may run multiple times. */
if (pointer_events->frame_pending.frame_types_mask & ty_mask) {
return;
}
pointer_events->frame_pending.frame_types_mask |= ty_mask;
int i = pointer_events->frame_pending.frame_types_num++;
pointer_events->frame_pending.frame_types[i] = ty;
pointer_events->frame_pending.frame_event_ms[i] = event_ms;
}
static void gwl_pointer_handle_frame_event_reset(GWL_SeatStatePointer_Events *pointer_events)
{
pointer_events->frame_pending.frame_types_num = 0;
pointer_events->frame_pending.frame_types_mask = 0;
}
/**
* Support for converting smooth-scrolling as discrete steps.
*/
struct GWL_SeatStatePointerScroll_SmoothAsDiscrete {
wl_fixed_t smooth_xy_accum[2] = {0, 0};
};
/** Number of smooth steps for a discrete step (matches X11 for touch-pads). */
static constexpr int smooth_as_discrete_steps = 2400;
/**
* 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};
/** Discrete scrolling, v8 of the seat API (handled & reset with pointer "frame" callback). */
int32_t discrete120_xy[2] = {0, 0};
/** Accumulated value from `discrete120_xy`, not reset between "frame" callbacks. */
int32_t discrete120_xy_accum[2] = {0, 0};
/** True when the axis is inverted (also known is "natural" scrolling). */
bool inverted_xy[2] = {false, false};
/** The source of scroll event. */
enum wl_pointer_axis_source axis_source = WL_POINTER_AXIS_SOURCE_WHEEL;
/**
* While the time should always be available, not all callbacks set the time
* so account for it not being set.
*/
bool has_event_ms = false;
/** Event time-stamp. */
uint64_t event_ms = 0;
GWL_SeatStatePointerScroll_SmoothAsDiscrete smooth_as_discrete;
};
/**
* 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 {
/** Wayland core types. */
struct {
/**
* The wl_surface last used with this pointing device
* (events with this pointing device will be sent here).
*/
wl_surface *surface_window = nullptr;
} wl;
/** The serial of the last used pointer or tablet. */
uint32_t serial = 0;
};
/**
* 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 {
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 {
xdg_wm_base *shell = nullptr;
uint32_t shell_name = WL_NAME_UNSET;
zxdg_decoration_manager_v1 *manager = nullptr;
uint32_t manager_name = WL_NAME_UNSET;
};
static void gwl_xdg_decor_system_destroy(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 {
/** Wayland native types. */
struct {
zwp_primary_selection_offer_v1 *id = nullptr;
} wp;
std::unordered_set<std::string> types;
};
struct GWL_PrimarySelection_DataSource {
/** Wayland native types. */
struct {
zwp_primary_selection_source_v1 *source = nullptr;
} wp;
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->wp.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;
}
#ifdef WITH_INPUT_IME
struct GWL_SeatIME {
/**
* The surface associated with this text input method.
*
* \note Even when null, IME callbacks run and events are generated to ensure
* the IME state remains consistent & allow for the compositor to assign the surface
* at any point in time which is then defines `window` IME events are associated with.
*/
wl_surface *surface_window = nullptr;
GHOST_TEventImeData event_ime_data = {
/** Storage for #GHOST_TEventImeData::result (the result of the `commit_string` callback). */
/*result*/ "",
/** Storage for #GHOST_TEventImeData::composite (the result of the `preedit_string`
callback). */
/*composite*/ "",
/*cursor_position*/ -1,
/*target_start*/ -1,
/*target_end*/ -1,
};
/** When true, the client has indicated that IME input should be activated on text entry. */
bool is_enabled = false;
/**
* When true, some pre-edit text has been entered
* (an IME popup may be showing however this isn't known).
*/
bool has_preedit = false;
/** #zwp_text_input_v3_listener::commit_string was called with a null text argument. */
bool result_is_null = false;
/** #zwp_text_input_v3_listener::preedit_string was called with a null text argument. */
bool composite_is_null = false;
/** #zwp_text_input_v3_listener::preedit_string was called. */
bool has_preedit_string_callback = false;
/** #zwp_text_input_v3_listener::commit_string was called. */
bool has_commit_string_callback = false;
/** Bounds (use for comparison). */
struct {
int x = -1;
int y = -1;
int w = -1;
int h = -1;
} rect;
};
#endif /* WITH_INPUT_IME */
struct GWL_Seat {
/** Wayland core types. */
struct {
wl_seat *seat = nullptr;
wl_pointer *pointer = nullptr;
wl_touch *touch = nullptr;
wl_keyboard *keyboard = nullptr;
wl_surface *surface_window_focus_dnd = nullptr;
wl_data_device *data_device = nullptr;
} wl;
/** Wayland native types. */
struct {
zwp_tablet_seat_v2 *tablet_seat = nullptr;
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
zwp_pointer_gesture_hold_v1 *pointer_gesture_hold = nullptr;
#endif
#ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE
zwp_pointer_gesture_pinch_v1 *pointer_gesture_pinch = nullptr;
#endif
#ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE
zwp_pointer_gesture_swipe_v1 *pointer_gesture_swipe = nullptr;
#endif
zwp_relative_pointer_v1 *relative_pointer = nullptr;
zwp_locked_pointer_v1 *locked_pointer = nullptr;
zwp_confined_pointer_v1 *confined_pointer = nullptr;
zwp_primary_selection_device_v1 *primary_selection_device = nullptr;
/** All currently active tablet tools (needed for changing the cursor). */
std::unordered_set<zwp_tablet_tool_v2 *> tablet_tools;
#ifdef WITH_INPUT_IME
zwp_text_input_v3 *text_input = nullptr;
#endif
} wp;
/** XKB native types. */
struct {
xkb_context *context = nullptr;
/** The compose key table (check for null before use). */
xkb_compose_table *compose_table = nullptr;
/** The compose state is expected to use the keyboard `state` (check for null before use). */
xkb_compose_state *compose_state = nullptr;
xkb_state *state = nullptr;
/**
* Keep a state with no modifiers active, use for symbol lookups.
*/
xkb_state *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 nullptr.
*/
xkb_state *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 nullptr.
*/
xkb_state *state_empty_with_numlock = nullptr;
/**
* The active layout, where a single #xkb_keymap may have multiple layouts.
*/
xkb_layout_index_t layout_active = 0;
} xkb;
#ifdef WITH_INPUT_IME
GWL_SeatIME ime;
#endif
GHOST_SystemWayland *system = nullptr;
std::string name;
/** Use to check if the last cursor input was tablet or pointer. */
uint32_t cursor_source_serial = 0;
GWL_SeatStatePointer pointer;
GWL_SeatStatePointer_Events pointer_events;
GWL_SeatStatePointerScroll pointer_scroll;
GWL_SeatStatePointerGesture_Pinch pointer_gesture_pinch;
bool use_pointer_scroll_smooth_as_discrete = false;
/**
* Mostly this can be interchanged with `pointer` key differences are:
* - It can't be locked/confined.
*/
GWL_SeatStatePointer tablet;
/**
* Mostly this can be interchanged with `pointer` key differences are:
* - It can't be locked/confined.
* - The #GWL_SeatStatePointer::outputs isn't set,
* nor anything relating to the cursor buffer scale.
*/
GWL_SeatStatePointer touch;
GWL_SeatStateKeyboard keyboard;
/**
* This structure accumulates touch input that will be applied on the next *frame* event.
*
* Currently only track one active contact point on the screen
* and map it to pointer motion and left-clicks.
* Multi-touch, pinching & swiping are not yet supported.
*/
struct {
bool is_touching = false;
uint32_t down_id = 0;
bool down_pending = false;
uint64_t down_event_time_ms = 0;
bool up_pending = false;
uint64_t up_event_time_ms = 0;
bool motion_pending = false;
uint64_t motion_event_time_ms = 0;
} touch_state;
#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};
GWL_Cursor cursor;
#ifdef USE_NON_LATIN_KB_WORKAROUND
bool xkb_use_non_latin_workaround = false;
#endif
/** Keys held matching `xkb_state`. */
GWL_KeyboardDepressedState key_depressed;
/**
* 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] = {XKB_MOD_INVALID};
/* Cache result for other modifiers which aren't stored in `xkb_keymap_mod_index`
* since their depressed state isn't tracked. */
/** Cache result of `xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM)`. */
xkb_mod_index_t xkb_keymap_mod_index_mod2 = XKB_MOD_INVALID;
/** Cache result of `xkb_keymap_mod_get_index(keymap, "NumLock")`. */
xkb_mod_index_t xkb_keymap_mod_index_numlock = XKB_MOD_INVALID;
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.
*
* \note For as long as #USE_EVENT_BACKGROUND_THREAD is defined, any access to this
* (including null checks, must lock `timer_mutex` first.
*/
GHOST_ITimerTask *timer = nullptr;
} key_repeat;
/** Drag & Drop. */
GWL_DataOffer *data_offer_dnd = nullptr;
std::mutex data_offer_dnd_mutex;
/** Copy & Paste. */
GWL_DataOffer *data_offer_copy_paste = nullptr;
std::mutex data_offer_copy_paste_mutex;
/**
* Cache the result of #GHOST_SystemWayland::hasClipboardImage as checking the file
* header every time will be expensive, especially if this happens on redraw.
* Reset whenever the data offer changes.
*/
std::optional<GHOST_TSuccess> data_offer_copy_paste_has_image = std::nullopt;
GWL_DataSource *data_source = nullptr;
std::mutex data_source_mutex;
GWL_PrimarySelection primary_selection;
/**
* Last input device that was active (pointer/tablet/keyboard).
*
* \note Multi-touch gestures don't set this value,
* if there is some use-case where this is needed - assignments can be added.
*/
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;
}
if (seat->touch.serial == seat->cursor_source_serial) {
return &seat->touch;
}
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;
}
/**
* Account for changes to #GWL_Seat::xkb::layout_active by running #xkb_state_update_mask
* on static states which are used for lookups.
*
* This is also used when initializing the states.
*/
static void gwl_seat_key_layout_active_state_update_mask(GWL_Seat *seat)
{
const xkb_layout_index_t group = seat->xkb.layout_active;
const xkb_mod_index_t mod_shift = seat->xkb_keymap_mod_index[MOD_INDEX_SHIFT];
const xkb_mod_index_t mod_mod2 = seat->xkb_keymap_mod_index_mod2;
const xkb_mod_index_t mod_numlock = seat->xkb_keymap_mod_index_numlock;
/* Handle `state_empty`. */
xkb_state_update_mask(seat->xkb.state_empty, 0, 0, 0, 0, 0, group);
/* Handle `state_empty_with_shift`. */
GHOST_ASSERT((mod_shift == XKB_MOD_INVALID) == (seat->xkb.state_empty_with_shift == nullptr),
"Invalid state for SHIFT");
if (seat->xkb.state_empty_with_shift) {
xkb_state_update_mask(seat->xkb.state_empty_with_shift, 1 << mod_shift, 0, 0, 0, 0, group);
}
/* Handle `state_empty_with_shift`. */
GHOST_ASSERT((mod_mod2 == XKB_MOD_INVALID || mod_numlock == XKB_MOD_INVALID) ==
(seat->xkb.state_empty_with_numlock == nullptr),
"Invalid state for NUMLOCK");
if (seat->xkb.state_empty_with_numlock) {
xkb_state_update_mask(
seat->xkb.state_empty_with_numlock, 1 << mod_mod2, 0, 1 << mod_numlock, 0, 0, group);
}
}
/** Callback that runs from GHOST's timer. */
static void gwl_seat_key_repeat_timer_fn(GHOST_ITimerTask *task, uint64_t time_ms)
{
GWL_KeyRepeatPlayload *payload = static_cast<GWL_KeyRepeatPlayload *>(task->getUserData());
GWL_Seat *seat = payload->seat;
wl_surface *wl_surface_focus = seat->keyboard.wl.surface_window;
if (UNLIKELY(wl_surface_focus == nullptr)) {
return;
}
GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus);
GHOST_SystemWayland *system = seat->system;
const uint64_t event_ms = payload->time_ms_init + time_ms;
/* Calculate this value every time in case modifier keys are pressed. */
char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'};
if (seat->xkb.compose_state &&
xkb_compose_state_feed_and_get_utf8(
seat->xkb.compose_state, seat->xkb.state, payload->key_code, utf8_buf))
{
/* `utf8_buf` has been filled by a compose action. */
}
else {
xkb_state_key_get_utf8(seat->xkb.state, payload->key_code, utf8_buf, sizeof(utf8_buf));
}
system->pushEvent_maybe_pending(new GHOST_EventKey(
event_ms, GHOST_kEventKeyDown, win, payload->key_data.gkey, true, utf8_buf));
}
/**
* \note Caller must lock `timer_mutex`.
*
* \note A `seat->key_repeat.rate` of zero indicates that client-side key repeat is disabled,
* the compositor may generate repeat events.
* The caller must ensure this function isn't called in that case.
*/
static void gwl_seat_key_repeat_timer_add(GWL_Seat *seat,
GHOST_TimerProcPtr key_repeat_fn,
GHOST_TUserDataPtr payload,
const bool use_delay)
{
/* Caller is expected to ensure this. */
GHOST_ASSERT(seat->key_repeat.rate > 0, "invalid rate");
GHOST_SystemWayland *system = seat->system;
const uint64_t time_now = system->getMilliSeconds();
const uint64_t time_step = 1000 / seat->key_repeat.rate;
const uint64_t time_start = use_delay ? seat->key_repeat.delay : time_step;
static_cast<GWL_KeyRepeatPlayload *>(payload)->time_ms_init = time_now;
#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_TimerTask *timer = new GHOST_TimerTask(
time_now + time_start, time_step, key_repeat_fn, payload);
seat->key_repeat.timer = timer;
system->ghost_timer_manager()->addTimer(timer);
#else
seat->key_repeat.timer = system->installTimer(time_start, time_step, key_repeat_fn, payload);
#endif
}
/**
* \note The caller must lock `timer_mutex`.
*/
static void gwl_seat_key_repeat_timer_remove(GWL_Seat *seat)
{
GHOST_SystemWayland *system = seat->system;
#ifdef USE_EVENT_BACKGROUND_THREAD
system->ghost_timer_manager()->removeTimer(
static_cast<GHOST_TimerTask *>(seat->key_repeat.timer));
#else
system->removeTimer(seat->key_repeat.timer);
#endif
seat->key_repeat.timer = nullptr;
}
#ifdef WITH_INPUT_IME
static void gwl_seat_ime_full_reset(GWL_Seat *seat)
{
const GWL_SeatIME ime_default{};
/* Preserve the following members since they represent the state of the connection to Wayland.
* or which callbacks have run (which shouldn't be reset). */
wl_surface *surface_window = seat->ime.surface_window;
const bool is_enabled = seat->ime.is_enabled;
const bool has_preedit_string_callback = seat->ime.has_preedit_string_callback;
const bool has_commit_string_callback = seat->ime.has_commit_string_callback;
seat->ime = ime_default;
seat->ime.surface_window = surface_window;
seat->ime.is_enabled = is_enabled;
seat->ime.has_preedit_string_callback = has_preedit_string_callback;
seat->ime.has_commit_string_callback = has_commit_string_callback;
}
static void gwl_seat_ime_result_reset(GWL_Seat *seat)
{
GHOST_TEventImeData &event_ime_data = seat->ime.event_ime_data;
event_ime_data.result.clear();
seat->ime.result_is_null = false;
}
static void gwl_seat_ime_preedit_reset(GWL_Seat *seat)
{
GHOST_TEventImeData &event_ime_data = seat->ime.event_ime_data;
event_ime_data.composite.clear();
seat->ime.composite_is_null = false;
event_ime_data.cursor_position = -1;
event_ime_data.target_start = -1;
event_ime_data.target_end = -1;
}
static void gwl_seat_ime_rect_reset(GWL_Seat *seat)
{
seat->ime.rect.x = -1;
seat->ime.rect.y = -1;
seat->ime.rect.w = -1;
seat->ime.rect.h = -1;
}
#endif /* WITH_INPUT_IME */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Display Type (#wl_display & #wl_compositor wrapper)
* \{ */
struct GWL_RegistryEntry;
/**
* Variables for converting input timestamps,
* see: #GHOST_SystemWayland::milliseconds_from_input_time.
*/
struct GWL_DisplayTimeStamp {
/**
* When true, the GHOST & WAYLAND time-stamps are compatible,
* in most cases this means they will be equal however for systems with long up-times
* it may equal `uint32(GHOST_SystemWayland::getMilliSeconds())`,
* the `offsets` member is set to account for this case.
*/
bool exact_match = false;
uint32_t last = 0;
uint64_t offset = 0;
};
struct GWL_Display {
/** Wayland core types. */
struct {
wl_registry *registry = nullptr;
wl_display *display = nullptr;
wl_compositor *compositor = nullptr;
wl_shm *shm = nullptr;
/* Managers. */
wl_data_device_manager *data_device_manager = nullptr;
} wl;
/** Wayland native types. */
struct {
/* Managers. */
zwp_tablet_manager_v2 *tablet_manager = nullptr;
zwp_relative_pointer_manager_v1 *relative_pointer_manager = nullptr;
zwp_primary_selection_device_manager_v1 *primary_selection_device_manager = nullptr;
wp_fractional_scale_manager_v1 *fractional_scale_manager = nullptr;
wp_viewporter *viewporter = nullptr;
zwp_pointer_constraints_v1 *pointer_constraints = nullptr;
zwp_pointer_gestures_v1 *pointer_gestures = nullptr;
#ifdef WITH_INPUT_IME
zwp_text_input_manager_v3 *text_input_manager = nullptr;
#endif
wp_cursor_shape_manager_v1 *cursor_shape_manager = nullptr;
} wp;
/** Wayland XDG types. */
struct {
/* Managers. */
zxdg_output_manager_v1 *output_manager = nullptr;
xdg_activation_v1 *activation_manager = nullptr;
} xdg;
GWL_DisplayTimeStamp input_timestamp;
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. */
GWL_RegistryEntry *registry_entry = nullptr;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
GWL_LibDecor_System *libdecor = nullptr;
#endif
GWL_XDG_Decor_System *xdg_decor = nullptr;
/**
* NOTE(@ideasman42): Handling of outputs must account for this vector to be empty.
* While this seems like something which might not ever happen, it can occur when
* unplugging monitors (presumably from dodgy cables/connections too).
*/
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(@ideasman42): This could be extended and developed further extended to support
* an active seat per window (for example), basic support is sufficient for now as currently
* isn't a widely used feature.
*/
int seats_active_index = 0;
/**
* When true, running without any windows.
* Wayland is only used to access the GPU.
*
* \note In general logic should not diverge too much in background mode,
* so as to avoid maintaining multiple code-paths however some logic can be skipped
* such as libraries for showing window decorations and threaded event handling.
*/
bool background = false;
/* Threaded event handling. */
#ifdef USE_EVENT_BACKGROUND_THREAD
/**
* Run a thread that consumes events in the background.
* Use `pthread` because `std::thread` leaks memory.
*
* Not set when `background == true`.
*/
pthread_t events_pthread = 0;
/**
* Use to exit the event reading loop.
*
* Not set when `background == true`.
*/
bool events_pthread_is_active = false;
/**
* Events added from the event reading thread.
* Added into the main event queue when on #GHOST_SystemWayland::processEvents.
*/
std::vector<const GHOST_IEvent *> events_pending;
/** Guard against multiple threads accessing `events_pending` at once. */
std::mutex events_pending_mutex;
/**
* A separate timer queue, needed so the WAYLAND thread can lock access.
* Using the system's #GHOST_Sysem::getTimerManager is not thread safe because
* access to the timer outside of WAYLAND specific logic will not lock.
*
* Needed because #GHOST_System::dispatchEvents fires timers
* outside of WAYLAND (without locking the `timer_mutex`).
*/
GHOST_TimerManager *ghost_timer_manager = nullptr;
#endif /* USE_EVENT_BACKGROUND_THREAD */
};
/**
* Free the #GWL_Display and its related members.
*
* \note This may run on a partially initialized struct,
* so it can't be assumed all members are set.
*/
static void gwl_display_destroy(GWL_Display *display)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
if (!display->background) {
if (display->events_pthread) {
ghost_wl_display_lock_without_input(display->wl.display, display->system->server_mutex);
display->events_pthread_is_active = false;
}
}
#endif
/* For typical WAYLAND use this will always be set.
* However when WAYLAND isn't running, this will early-exit and be null. */
if (display->wl.registry) {
wl_registry_destroy(display->wl.registry);
display->wl.registry = nullptr;
}
/* Unregister items in reverse order. */
gwl_registry_entry_remove_all(display);
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
if (display->libdecor) {
gwl_libdecor_system_destroy(display->libdecor);
display->libdecor = nullptr;
}
}
else
#endif
{
if (display->xdg_decor) {
gwl_xdg_decor_system_destroy(display, display->xdg_decor);
display->xdg_decor = nullptr;
}
}
#ifdef WITH_OPENGL_BACKEND
if (display->wl.display) {
if (eglGetDisplay) {
::eglTerminate(eglGetDisplay(EGLNativeDisplayType(display->wl.display)));
}
}
#endif
#ifdef USE_EVENT_BACKGROUND_THREAD
if (!display->background) {
if (display->events_pthread) {
gwl_display_event_thread_destroy(display);
display->system->server_mutex->unlock();
}
}
/* Important to remove after the seats which may have key repeat timers active. */
if (display->ghost_timer_manager) {
delete display->ghost_timer_manager;
display->ghost_timer_manager = nullptr;
}
/* Pending events may be left unhandled. */
for (const GHOST_IEvent *event : display->events_pending) {
delete event;
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
if (display->wl.display) {
wl_display_disconnect(display->wl.display);
}
delete display;
}
static int gwl_display_seat_index(GWL_Display *display, const GWL_Seat *seat)
{
std::vector<GWL_Seat *>::iterator iter = std::find(
display->seats.begin(), display->seats.end(), seat);
const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) :
-1;
GHOST_ASSERT(index != -1, "invalid internal state");
return index;
}
/**
* Callers must null check the return value unless it's known there is a seat.
*
* \note Running Blender in an instance of the Weston compositor
* called with `--backend=headless` causes there to be no seats.
* CMake's `WITH_UI_TESTS` does this.
*/
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 example 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 itself), needed as the values aren't set on startup. */
const char *const *interface_p = nullptr;
/** Add the interface. */
GWL_RegistryHandler_AddFn add_fn = nullptr;
/** Optional update the interface (when other interfaces have been added/removed). */
GWL_RegistryHandler_UpdateFn update_fn = nullptr;
/** Remove the interface. */
GWL_RegistryEntry_RemoveFn remove_fn = nullptr;
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_RegistryEntry
* \{ */
/**
* Registered global objects can be removed by the compositor,
* these entries are a registry of objects and callbacks to properly remove them.
* These are also used to remove all registered objects before exiting.
*/
struct GWL_RegistryEntry {
GWL_RegistryEntry *next = nullptr;
/**
* Optional pointer passed to `remove_fn`, typically the container in #GWL_Display
* in cases multiple instances of the same interface are supported.
*/
void *user_data = nullptr;
/**
* A unique identifier used as a handle by `wl_registry_listener.global_remove`.
*/
uint32_t name = WL_NAME_UNSET;
/**
* Version passed by the add callback.
*/
uint32_t version;
/**
* The index in `gwl_registry_handlers`,
* useful for accessing the interface name (for logging for example).
*/
int interface_slot = 0;
};
static void gwl_registry_entry_add(GWL_Display *display,
const GWL_RegisteryAdd_Params &params,
void *user_data)
{
GWL_RegistryEntry *reg = new GWL_RegistryEntry;
reg->interface_slot = params.interface_slot;
reg->name = params.name;
reg->version = params.version;
reg->user_data = user_data;
reg->next = display->registry_entry;
display->registry_entry = reg;
}
static bool gwl_registry_entry_remove_by_name(GWL_Display *display,
uint32_t name,
int *r_interface_slot)
{
GWL_RegistryEntry *reg = display->registry_entry;
GWL_RegistryEntry **reg_link_p = &display->registry_entry;
bool found = false;
if (r_interface_slot) {
*r_interface_slot = -1;
}
while (reg) {
if (reg->name == name) {
GWL_RegistryEntry *reg_next = reg->next;
const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
reg->interface_slot);
handler->remove_fn(display, reg->user_data, false);
if (r_interface_slot) {
*r_interface_slot = reg->interface_slot;
}
delete reg;
*reg_link_p = reg_next;
found = true;
break;
}
reg_link_p = &reg->next;
reg = reg->next;
}
return found;
}
static bool gwl_registry_entry_remove_by_interface_slot(GWL_Display *display,
const int interface_slot,
const bool on_exit)
{
GWL_RegistryEntry *reg = display->registry_entry;
GWL_RegistryEntry **reg_link_p = &display->registry_entry;
bool found = false;
while (reg) {
if (reg->interface_slot == interface_slot) {
GWL_RegistryEntry *reg_next = reg->next;
const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
interface_slot);
handler->remove_fn(display, reg->user_data, on_exit);
delete reg;
*reg_link_p = reg_next;
reg = reg_next;
found = true;
continue;
}
reg_link_p = &reg->next;
reg = reg->next;
}
return found;
}
/**
* Remove all global objects (on exit).
*/
static void gwl_registry_entry_remove_all(GWL_Display *display)
{
const bool on_exit = true;
/* NOTE(@ideasman42): 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 example).
* Pass as -1 to update all slots.
*
* NOTE(@ideasman42): 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 example So while it's possible to store dependency links to avoid unnecessary
* looping over data - it ends up being a non issue.
*/
static void gwl_registry_entry_update_all(GWL_Display *display, const int interface_slot_exclude)
{
GHOST_ASSERT(interface_slot_exclude == -1 || (uint(interface_slot_exclude) <
uint(gwl_registry_handler_interface_slot_max())),
"Invalid exclude slot");
for (GWL_RegistryEntry *reg = display->registry_entry; reg; reg = reg->next) {
if (reg->interface_slot == interface_slot_exclude) {
continue;
}
const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
reg->interface_slot);
if (handler->update_fn == nullptr) {
continue;
}
GWL_RegisteryUpdate_Params params{};
params.name = reg->name;
params.interface_slot = reg->interface_slot;
params.version = reg->version;
params.user_data = reg->user_data;
handler->update_fn(display, params);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Private Utility Functions
* \{ */
static uint32_t round_up_uint(const uint32_t x, const uint32_t multiple)
{
return ((x + multiple - 1) / multiple) * multiple;
}
static uint32_t rgba_straight_to_premul(uint32_t rgba_uint)
{
uint8_t *rgba = reinterpret_cast<uint8_t *>(&rgba_uint);
const uint32_t alpha = uint32_t(rgba[3]);
rgba[0] = uint8_t(((alpha * rgba[0]) + (0xff / 2)) / 0xff);
rgba[1] = uint8_t(((alpha * rgba[1]) + (0xff / 2)) / 0xff);
rgba[2] = uint8_t(((alpha * rgba[2]) + (0xff / 2)) / 0xff);
return rgba_uint;
}
static uint32_t rgba_straight_to_premul_inverted(uint32_t rgba_uint)
{
uint8_t *rgba = reinterpret_cast<uint8_t *>(&rgba_uint);
const uint32_t alpha = uint32_t(rgba[3]);
rgba[0] = uint8_t(((alpha * (0xff - rgba[0])) + (0xff / 2)) / 0xff);
rgba[1] = uint8_t(((alpha * (0xff - rgba[1])) + (0xff / 2)) / 0xff);
rgba[2] = uint8_t(((alpha * (0xff - rgba[2])) + (0xff / 2)) / 0xff);
return rgba_uint;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
static const char *strchr_or_end(const char *str, const char ch)
{
const char *p = str;
while (!ELEM(*p, ch, '\0')) {
p++;
}
return p;
}
static bool string_elem_split_by_delim(const char *haystack, const char delim, const char *needle)
{
/* Local copy of #BLI_string_elem_split_by_delim (would be a bad level call). */
/* May be zero, returns true when an empty span exists. */
const size_t needle_len = strlen(needle);
const char *p = haystack, *p_next;
while (true) {
p_next = strchr_or_end(p, delim);
if ((size_t(p_next - p) == needle_len) && (memcmp(p, needle, needle_len) == 0)) {
return true;
}
if (*p_next == '\0') {
break;
}
p = p_next + 1;
}
return false;
}
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
static uint64_t sub_abs_u64(const uint64_t a, const uint64_t b)
{
return a > b ? a - b : b - a;
}
/**
* Return milliseconds from a microsecond uint32 pair (used by some wayland functions).
*/
static uint64_t ghost_wl_ms_from_utime_pair(uint32_t utime_hi, uint32_t utime_lo)
{
return ((uint64_t(utime_hi) << 32) | uint64_t(utime_lo)) / 1000;
}
/**
* Access the LOCALE (with a fallback).
*/
static const char *ghost_wl_locale_from_env_with_default()
{
const char *locale = getenv("LC_ALL");
if (!locale || !*locale) {
locale = getenv("LC_CTYPE");
if (!locale || !*locale) {
locale = getenv("LANG");
if (!locale || !*locale) {
locale = "C";
}
}
}
return locale;
}
static void ghost_wl_display_report_error_from_code(wl_display *display, const int ecode)
{
GHOST_ASSERT(ecode, "Error not set!");
if (ELEM(ecode, EPIPE, ECONNRESET)) {
fprintf(stderr, "The Wayland connection broke. Did the Wayland compositor die?\n");
return;
}
if (ecode == EPROTO) {
const wl_interface *interface = nullptr;
const int ecode_proto = wl_display_get_protocol_error(display, &interface, nullptr);
fprintf(stderr,
"The Wayland connection experienced a protocol error %d in interface: %s\n",
ecode_proto,
interface ? interface->name : "<nil>");
const char *env_debug = "WAYLAND_DEBUG";
if (getenv(env_debug) == nullptr) {
fprintf(stderr, "Run with the environment variable \"%s=1\" for details.\n", env_debug);
}
return;
}
fprintf(stderr, "The Wayland connection experienced a fatal error: %s\n", strerror(ecode));
}
static void ghost_wl_display_report_error(wl_display *display)
{
int ecode = wl_display_get_error(display);
GHOST_ASSERT(ecode, "Error not set!");
ghost_wl_display_report_error_from_code(display, ecode);
/* NOTE(@ideasman42): The application is running,
* however an error closes all windows and most importantly:
* shuts down the GPU context (loosing all GPU state - shaders, bind codes etc),
* so recovering from this effectively involves restarting.
*
* Keeping the GPU state alive doesn't seem to be supported as windows EGL context must use the
* same connection as the used for all other WAYLAND interactions (see #wl_display_connect).
* So in practice re-connecting to the display server isn't an option.
*
* Exit since leaving the process open will simply flood the output and do nothing.
* Although as the process is in a valid state, auto-save for example is possible, see: #100855.
*/
::exit(-1);
}
bool ghost_wl_display_report_error_if_set(wl_display *display)
{
const int ecode = wl_display_get_error(display);
if (ecode == 0) {
return false;
}
ghost_wl_display_report_error_from_code(display, ecode);
return true;
}
#ifdef __GNUC__
static void ghost_wayland_log_handler(const char *msg, va_list arg)
__attribute__((format(printf, 1, 0)));
static void ghost_wayland_log_handler_background(const char *msg, va_list arg)
__attribute__((format(printf, 1, 0)));
#endif
/**
* 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. */
}
}
/** A wrapper for #ghost_wayland_log_handler to be used when running in the background. */
static void ghost_wayland_log_handler_background(const char *msg, va_list arg)
{
/* This is fine in background mode, we will try to fall back to headless GPU context.
* Happens when render farm process runs without user login session. */
if (strstr(msg, "error: XDG_RUNTIME_DIR not set in the environment") ||
strstr(msg, "error: XDG_RUNTIME_DIR is invalid or not set in the environment"))
{
return;
}
ghost_wayland_log_handler(msg, arg);
}
#if defined(WITH_GHOST_X11) && defined(WITH_GHOST_WAYLAND_LIBDECOR)
/**
* Check if the system is running X11.
* This is not intended to be a fool-proof check (the `DISPLAY` is not validated for example).
* Just check `DISPLAY` is set and not-empty.
*/
static bool ghost_wayland_is_x11_available()
{
const char *x11_display = getenv("DISPLAY");
if (x11_display && x11_display[0]) {
return true;
}
return false;
}
#endif /* WITH_GHOST_X11 && WITH_GHOST_WAYLAND_LIBDECOR */
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_Hyper_L, GHOST_kKeyLeftHyper);
GXMAP(gkey, XKB_KEY_Hyper_R, GHOST_kKeyRightHyper);
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: #102287. */
GXMAP(gkey, XKB_KEY_KP_Separator, GHOST_kKeyNumpadPeriod);
GXMAP(gkey, XKB_KEY_less, GHOST_kKeyGrLess);
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;
}
case KEY_102ND: {
gkey = GHOST_kKeyGrLess;
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 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_list = "text/uri-list";
static const char *ghost_wl_mime_preference_order[] = {
ghost_wl_mime_text_uri_list,
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",
};
/**
* Return a list of URI ranges (without the `file://` prefix),
* the result should be decoded via #GHOST_URL_decode_alloc before passed to the file-system.
*/
static std::vector<std::string_view> gwl_clipboard_uri_ranges(const char *data_buf,
size_t data_buf_len)
{
std::vector<std::string_view> uris;
const char file_proto[] = "file://";
/* NOTE: some applications CRLF (`\r\n`) GTK3 for example & others don't `pcmanfm-qt`.
* So support both, once `\n` is found, strip the preceding `\r` if found. */
const char lf = '\n';
const std::string_view data = std::string_view(data_buf, data_buf_len);
size_t pos = 0;
while (pos != std::string::npos) {
pos = data.find(file_proto, pos);
if (pos == std::string::npos) {
break;
}
const size_t start = pos + sizeof(file_proto) - 1;
pos = data.find(lf, pos);
size_t end = pos;
if (UNLIKELY(end == std::string::npos)) {
/* Note that most well behaved file managers will add a trailing newline,
* Gnome's web browser (44.3) doesn't, so support reading up until the last byte. */
end = data.size();
}
/* Account for 'CRLF' case. */
if (data[end - 1] == '\r') {
end -= 1;
}
std::string_view data_substr = data.substr(start, end - start);
uris.push_back(data_substr);
}
return uris;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
static void pthread_set_min_priority(pthread_t handle)
{
int policy;
sched_param sch_params;
if (pthread_getschedparam(handle, &policy, &sch_params) == 0) {
sch_params.sched_priority = sched_get_priority_min(policy);
pthread_setschedparam(handle, policy, &sch_params);
}
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
static int memfd_create_sealed(const char *name)
{
#ifdef HAVE_MEMFD_CREATE
const int fd = memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (fd >= 0) {
fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL);
}
return fd;
#else /* HAVE_MEMFD_CREATE */
char *path = getenv("XDG_RUNTIME_DIR");
if (!path) {
errno = ENOENT;
return -1;
}
char *tmpname;
asprintf(&tmpname, "%s/%s-XXXXXX", path, name);
const int fd = mkostemp(tmpname, O_CLOEXEC);
if (fd >= 0) {
unlink(tmpname);
}
free(tmpname);
return fd;
#endif /* !HAVE_MEMFD_CREATE */
}
enum {
GWL_IOR_READ = 1 << 0,
GWL_IOR_WRITE = 1 << 1,
GWL_IOR_NO_RETRY = 1 << 2,
};
static int file_descriptor_is_io_ready(int fd, const int flags, const int timeout_ms)
{
int result;
GHOST_ASSERT(flags & (GWL_IOR_READ | GWL_IOR_WRITE), "X");
/* NOTE: We don't bother to account for elapsed time if we get #EINTR. */
do {
#ifdef HAVE_POLL
pollfd info;
info.fd = fd;
info.events = 0;
if (flags & GWL_IOR_READ) {
info.events |= POLLIN | POLLPRI;
}
if (flags & GWL_IOR_WRITE) {
info.events |= POLLOUT;
}
result = poll(&info, 1, timeout_ms);
#else
fd_set rfdset, *rfdp = nullptr;
fd_set wfdset, *wfdp = nullptr;
struct timeval tv, *tvp = nullptr;
/* If this assert triggers we'll corrupt memory here */
GHOST_ASSERT(fd >= 0 && fd < FD_SETSIZE, "X");
if (flags & GWL_IOR_READ) {
FD_ZERO(&rfdset);
FD_SET(fd, &rfdset);
rfdp = &rfdset;
}
if (flags & GWL_IOR_WRITE) {
FD_ZERO(&wfdset);
FD_SET(fd, &wfdset);
wfdp = &wfdset;
}
if (timeout_ms >= 0) {
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
tvp = &tv;
}
result = select(fd + 1, rfdp, wfdp, nullptr, tvp);
#endif /* !HAVE_POLL */
} while (result < 0 && errno == EINTR && !(flags & GWL_IOR_NO_RETRY));
return result;
}
static int ghost_wl_display_event_pump(wl_display *wl_display)
{
/* Based on SDL's `Wayland_PumpEvents`. */
int err;
/* NOTE: Without this, interactions with window borders via LIBDECOR doesn't function. */
wl_display_flush(wl_display);
if (wl_display_prepare_read(wl_display) == 0) {
/* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */
if (file_descriptor_is_io_ready(
wl_display_get_fd(wl_display), GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0)
{
err = wl_display_read_events(wl_display);
}
else {
wl_display_cancel_read(wl_display);
err = 0;
}
}
else {
err = wl_display_dispatch_pending(wl_display);
}
return err;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
static void ghost_wl_display_lock_without_input(wl_display *wl_display, std::mutex *server_mutex)
{
const int fd = wl_display_get_fd(wl_display);
int state;
do {
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
/* Re-check `state` with a lock held, needed to avoid holding the lock. */
if (state == 0) {
server_mutex->lock();
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
if (state == 0) {
break;
}
}
} while (state == 0);
}
static int ghost_wl_display_event_pump_from_thread(wl_display *wl_display,
const int fd,
std::mutex *server_mutex)
{
/* Based on SDL's `Wayland_PumpEvents`. */
server_mutex->lock();
int err = 0;
if (wl_display_prepare_read(wl_display) == 0) {
bool wait_on_fd = false;
/* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */
if (file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0) {
err = wl_display_read_events(wl_display);
}
else {
wl_display_cancel_read(wl_display);
/* Without this, the thread will loop continuously, 100% CPU. */
wait_on_fd = true;
}
server_mutex->unlock();
if (wait_on_fd) {
/* Important this runs after unlocking. */
file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX);
}
}
else {
server_mutex->unlock();
/* Wait for input (unlocked, so as not to block other threads). */
int state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX);
/* Re-check `state` with a lock held, needed to avoid holding the lock. */
if (state > 0) {
server_mutex->lock();
state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
if (state > 0) {
err = wl_display_dispatch_pending(wl_display);
}
server_mutex->unlock();
}
}
return err;
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
static size_t ghost_wl_shm_format_as_size(enum wl_shm_format format)
{
switch (format) {
case WL_SHM_FORMAT_ARGB8888: {
return 4;
}
default: {
/* Support other formats as needed. */
GHOST_ASSERT(0, "Unexpected format passed in!");
return 4;
}
}
}
/**
* Return a #wl_buffer, ready to have it's data filled in or nullptr 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(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) {
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;
}
/**
* A version of `read` which will read `nbytes` or as many bytes as possible,
* useful as the LIBC version may `read` less than requested.
*/
static ssize_t read_exhaustive(const int fd, void *data, size_t nbytes)
{
ssize_t nbytes_read = read(fd, data, nbytes);
if (nbytes_read > 0) {
while (nbytes_read < nbytes) {
const ssize_t nbytes_extra = read(
fd, static_cast<char *>(data) + nbytes_read, nbytes - nbytes_read);
if (nbytes_extra > 0) {
nbytes_read += nbytes_extra;
}
else {
if (UNLIKELY(nbytes_extra < 0)) {
nbytes_read = nbytes_extra; /* Error. */
}
break;
}
}
}
return nbytes_read;
}
/**
* Read from `fd` into a buffer which is returned.
* Use for files where seeking to determine the final size isn't supported (pipes for example).
*
* \return the buffer or null on failure.
* On failure `errno` will be set.
*/
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 *)];
};
bool ok = true;
size_t len = 0;
ByteChunk *chunk_first = static_cast<ByteChunk *>(malloc(sizeof(ByteChunk)));
{
ByteChunk **chunk_link_p = &chunk_first;
ByteChunk *chunk = chunk_first;
while (true) {
if (UNLIKELY(chunk == nullptr)) {
errno = ENOMEM;
ok = false;
break;
}
chunk->next = nullptr;
/* Using `read` causes issues with GNOME, see: #106040). */
const ssize_t len_chunk = read_exhaustive(fd, chunk->data, sizeof(ByteChunk::data));
if (len_chunk <= 0) {
if (UNLIKELY(len_chunk < 0)) {
ok = false;
}
if (chunk == chunk_first) {
chunk_first = nullptr;
}
free(chunk);
break;
}
*chunk_link_p = chunk;
chunk_link_p = &chunk->next;
len += len_chunk;
if (len_chunk != sizeof(ByteChunk::data)) {
break;
}
chunk = static_cast<ByteChunk *>(malloc(sizeof(ByteChunk)));
}
}
char *buf = nullptr;
if (ok) {
buf = static_cast<char *>(malloc(len + (nil_terminate ? 1 : 0)));
if (UNLIKELY(buf == nullptr)) {
errno = ENOMEM;
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;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Private Cursor API
* \{ */
static void cursor_buffer_set_surface_impl(const wl_cursor_image *wl_image,
wl_buffer *buffer,
wl_surface *wl_surface,
const int scale)
{
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!");
(void)image_size_x;
(void)image_size_y;
wl_surface_set_buffer_scale(wl_surface, scale);
wl_surface_attach(wl_surface, buffer, 0, 0);
if (wl_surface_get_version(wl_surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
wl_surface_damage_buffer(wl_surface,
0,
0,
std::numeric_limits<int32_t>::max(),
std::numeric_limits<int32_t>::max());
}
else {
/* Effectively deprecated according to documentation. */
wl_surface_damage(wl_surface,
0,
0,
std::numeric_limits<int32_t>::max(),
std::numeric_limits<int32_t>::max());
}
wl_surface_commit(wl_surface);
}
static std::optional<wp_cursor_shape_device_v1_shape> gwl_seat_cursor_find_wl_shape_from_ghost(
const GHOST_TStandardCursor shape)
{
/* Cases that return `std::nullopt` mean the cursor is not available. */
/* Unused WAYLAND cursors:
*
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS.
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CELL.
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP.
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE.
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE.
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK.
* #WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE.
*/
switch (shape) {
case GHOST_kStandardCursorDefault:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
case GHOST_kStandardCursorRightArrow:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE;
case GHOST_kStandardCursorLeftArrow:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE;
case GHOST_kStandardCursorInfo:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CONTEXT_MENU;
case GHOST_kStandardCursorDestroy:
return std::nullopt;
case GHOST_kStandardCursorHelp:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP;
case GHOST_kStandardCursorWait:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT;
case GHOST_kStandardCursorText:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT;
case GHOST_kStandardCursorCrosshair:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
case GHOST_kStandardCursorCrosshairA:
return std::nullopt;
case GHOST_kStandardCursorCrosshairB:
return std::nullopt;
case GHOST_kStandardCursorCrosshairC:
return std::nullopt;
case GHOST_kStandardCursorPencil:
return std::nullopt;
case GHOST_kStandardCursorUpArrow:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE;
case GHOST_kStandardCursorDownArrow:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE;
case GHOST_kStandardCursorVerticalSplit:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE;
case GHOST_kStandardCursorHorizontalSplit:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE;
case GHOST_kStandardCursorEraser:
return std::nullopt;
case GHOST_kStandardCursorKnife:
return std::nullopt;
case GHOST_kStandardCursorEyedropper:
return std::nullopt;
case GHOST_kStandardCursorZoomIn:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_IN;
case GHOST_kStandardCursorZoomOut:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_OUT;
case GHOST_kStandardCursorMove:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE;
case GHOST_kStandardCursorHandOpen:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB;
case GHOST_kStandardCursorHandClosed:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING;
case GHOST_kStandardCursorHandPoint:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER;
case GHOST_kStandardCursorNSEWScroll:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL;
case GHOST_kStandardCursorNSScroll:
return std::nullopt;
case GHOST_kStandardCursorEWScroll:
return std::nullopt;
case GHOST_kStandardCursorStop:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED;
case GHOST_kStandardCursorUpDown:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE;
case GHOST_kStandardCursorLeftRight:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE;
case GHOST_kStandardCursorTopSide:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE;
case GHOST_kStandardCursorBottomSide:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE;
case GHOST_kStandardCursorLeftSide:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE;
case GHOST_kStandardCursorRightSide:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE;
case GHOST_kStandardCursorTopLeftCorner:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE;
case GHOST_kStandardCursorTopRightCorner:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE;
case GHOST_kStandardCursorBottomRightCorner:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE;
case GHOST_kStandardCursorBottomLeftCorner:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE;
case GHOST_kStandardCursorCopy:
return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COPY;
case GHOST_kStandardCursorLeftHandle:
return std::nullopt;
case GHOST_kStandardCursorRightHandle:
return std::nullopt;
case GHOST_kStandardCursorBothHandles:
return std::nullopt;
case GHOST_kStandardCursorBlade:
return std::nullopt;
case GHOST_kStandardCursorSlip:
return std::nullopt;
case GHOST_kStandardCursorCustom:
return std::nullopt;
}
GHOST_ASSERT(0, "Unhandled GHOST cursor shape!");
/* Shape not found. */
return std::nullopt;
}
/**
* Currently account for all cursors as the buffer is shared.
*
* Note that a per pointing device buffer would be better,
* this is more of a low priority to-do.
*/
static int gwl_seat_cursor_buffer_scale_calc(const GWL_Seat *seat)
{
int scale = 1;
if (seat->wl.pointer) {
scale = std::max(scale, seat->pointer.buffer_scale);
}
if (seat->wp.tablet_seat) {
scale = std::max(scale, seat->tablet.buffer_scale);
}
return scale;
}
/**
* Show the buffer defined by #gwl_seat_cursor_buffer_set without changing anything else,
* so #gwl_seat_cursor_buffer_hide can be used to display it again.
*
* The caller is responsible for setting `seat->cursor.visible`.
*/
static void gwl_seat_cursor_buffer_show(GWL_Seat *seat)
{
const GWL_Cursor *cursor = &seat->cursor;
if (seat->wl.pointer) {
if (seat->cursor.shape.device) {
wp_cursor_shape_device_v1_set_shape(
seat->cursor.shape.device, seat->pointer.serial, seat->cursor.shape.enum_id);
}
else {
/* TODO: use `seat->pointer.buffer_scale`, give each device it's own buffer. */
const int scale = gwl_seat_cursor_buffer_scale_calc(seat);
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;
wl_pointer_set_cursor(
seat->wl.pointer, seat->pointer.serial, cursor->wl.surface_cursor, hotspot_x, hotspot_y);
}
}
if (!seat->wp.tablet_tools.empty()) {
/* TODO: use `seat->tablet.buffer_scale`, give each device it's own buffer. */
const int scale = gwl_seat_cursor_buffer_scale_calc(seat);
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 (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) {
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(
zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
if (tablet_tool->shape.device) {
wp_cursor_shape_device_v1_set_shape(
tablet_tool->shape.device, tablet_tool->serial, tablet_tool->shape.enum_id);
}
else {
zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2,
tablet_tool->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 #gwl_seat_cursor_buffer_set without changing anything else,
* so #gwl_seat_cursor_buffer_show can be used to display it again.
*
* The caller is responsible for setting `seat->cursor.visible`.
*/
static void gwl_seat_cursor_buffer_hide(GWL_Seat *seat)
{
wl_pointer_set_cursor(seat->wl.pointer, seat->pointer.serial, nullptr, 0, 0);
for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.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, tablet_tool->serial, nullptr, 0, 0);
}
}
static void gwl_seat_cursor_buffer_set(const GWL_Seat *seat,
const wl_cursor_image *wl_image,
wl_buffer *buffer)
{
const GWL_Cursor *cursor = &seat->cursor;
const bool visible = (cursor->visible && cursor->is_hardware);
const int scale = gwl_seat_cursor_buffer_scale_calc(seat);
/* 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 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(wl_image, 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->wp.tablet_tools.empty()) {
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 (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.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(wl_image, buffer, tablet_tool->wl.surface_cursor, scale);
zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2,
tablet_tool->serial,
visible ? tablet_tool->wl.surface_cursor : nullptr,
hotspot_x,
hotspot_y);
}
}
}
static void gwl_seat_cursor_buffer_set_current(GWL_Seat *seat)
{
const GWL_Cursor *cursor = &seat->cursor;
gwl_seat_cursor_buffer_set(seat, &cursor->wl.image, cursor->wl.buffer);
}
enum eCursorSetMode {
CURSOR_VISIBLE_ALWAYS_SET = 1,
CURSOR_VISIBLE_ONLY_HIDE,
CURSOR_VISIBLE_ONLY_SHOW,
};
static void gwl_seat_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) {
gwl_seat_cursor_buffer_show(seat);
}
}
else {
if (was_visible) {
gwl_seat_cursor_buffer_hide(seat);
}
}
cursor->visible = visible;
cursor->is_hardware = is_hardware;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \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,
GHOST_IWindow *win,
const uint64_t event_ms,
const GWL_KeyboardDepressedState &key_depressed_prev)
{
/* Separate key up and down into separate passes so key down events always come after key up.
* Do this so users of GHOST can use the last pressed or released modifier to check
* if the modifier is held instead of counting modifiers pressed as is done here,
* this isn't perfect but works well enough in practice. */
for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) {
for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d < 0; d++) {
const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i);
seat->system->pushEvent_maybe_pending(
new GHOST_EventKey(event_ms, GHOST_kEventKeyUp, win, gkey, false));
CLOG_DEBUG(LOG, "modifier (%d) up", i);
}
}
for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) {
for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d > 0; d--) {
const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i);
seat->system->pushEvent_maybe_pending(
new GHOST_EventKey(event_ms, GHOST_kEventKeyDown, win, gkey, false));
CLOG_DEBUG(LOG, "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 uint64_t event_ms)
{
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.r_ -= 1;
bounds.b_ -= 1;
bounds.l_ = win->wl_fixed_from_window(wl_fixed_from_int(bounds.l_));
bounds.t_ = win->wl_fixed_from_window(wl_fixed_from_int(bounds.t_));
bounds.r_ = win->wl_fixed_from_window(wl_fixed_from_int(bounds.r_));
bounds.b_ = win->wl_fixed_from_window(wl_fixed_from_int(bounds.b_));
bounds.clampPoint(UNPACK2(seat->pointer.xy));
}
#endif
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)};
seat->system->pushEvent_maybe_pending(new GHOST_EventCursor(
event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), GHOST_TABLET_DATA_NONE));
}
static void relative_pointer_handle_relative_motion(
void *data,
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);
const uint64_t time = ghost_wl_ms_from_utime_pair(utime_hi, utime_lo);
const uint64_t event_ms = seat->system->ms_from_input_time(time);
if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) {
CLOG_DEBUG(LOG, "relative_motion");
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
const wl_fixed_t xy_next[2] = {
seat->pointer.xy[0] + win->wl_fixed_from_window(dx),
seat->pointer.xy[1] + win->wl_fixed_from_window(dy),
};
relative_pointer_handle_relative_motion_impl(seat, win, xy_next, event_ms);
}
else {
CLOG_DEBUG(LOG, "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,
const uint64_t event_ms)
{
/* 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 int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->data_offer_dnd->dnd.xy)};
for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order_type); i++) {
const GHOST_TDragnDropTypes type = ghost_wl_mime_preference_order_type[i];
seat->system->pushEvent_maybe_pending(
new GHOST_EventDragnDrop(event_ms, event, type, win, UNPACK2(event_xy), nullptr));
}
}
}
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->wl.id, mime_receive, pipefd[1]);
close(pipefd[1]);
}
else {
CLOG_WARN(LOG, "error creating pipe: %s", std::strerror(errno));
}
if (mutex) {
mutex->unlock();
}
/* NOTE: Regarding `data_offer`:
* - In the case of the clipboard - unlocking the `mutex` means, it may be feed from now on.
* - In the case of drag & drop - the caller owns & no locking is needed
* (locking is performed while transferring the ownership).
*/
char *buf = nullptr;
if (pipefd_ok) {
buf = read_file_as_buffer(pipefd[0], nil_terminate, r_len);
if (buf == nullptr) {
CLOG_WARN(LOG, "unable to pipe into buffer: %s", std::strerror(errno));
}
close(pipefd[0]);
}
else {
*r_len = 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->wp.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);
if (buf == nullptr) {
CLOG_WARN(LOG, "unable to pipe into buffer: %s", std::strerror(errno));
}
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*/,
wl_data_source * /*wl_data_source*/,
const char * /*mime_type*/)
{
CLOG_DEBUG(LOG, "target");
}
static void data_source_handle_send(void *data,
wl_data_source * /*wl_data_source*/,
const char * /*mime_type*/,
const int32_t fd)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
CLOG_DEBUG(LOG, "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, wl_data_source *wl_data_source)
{
CLOG_DEBUG(LOG, "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*/,
wl_data_source * /*wl_data_source*/)
{
CLOG_DEBUG(LOG, "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*/, wl_data_source * /*wl_data_source*/)
{
CLOG_DEBUG(LOG, "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*/,
wl_data_source * /*wl_data_source*/,
const uint32_t dnd_action)
{
CLOG_DEBUG(LOG, "handle_action (dnd_action=%u)", dnd_action);
}
static const wl_data_source_listener data_source_listener = {
/*target*/ data_source_handle_target,
/*send*/ data_source_handle_send,
/*cancelled*/ data_source_handle_cancelled,
/*dnd_drop_performed*/ data_source_handle_dnd_drop_performed,
/*dnd_finished*/ data_source_handle_dnd_finished,
/*action*/ 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,
wl_data_offer * /*wl_data_offer*/,
const char *mime_type)
{
/* NOTE: locking isn't needed as the #GWL_DataOffer wont have been assigned to the #GWL_Seat. */
CLOG_DEBUG(LOG, "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,
wl_data_offer * /*wl_data_offer*/,
const uint32_t source_actions)
{
/* NOTE: locking isn't needed as the #GWL_DataOffer wont have been assigned to the #GWL_Seat. */
CLOG_DEBUG(LOG, "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,
wl_data_offer * /*wl_data_offer*/,
const uint32_t dnd_action)
{
/* NOTE: locking isn't needed as the #GWL_DataOffer wont have been assigned to the #GWL_Seat. */
CLOG_DEBUG(LOG, "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 wl_data_offer_listener data_offer_listener = {
/*offer*/ data_offer_handle_offer,
/*source_actions*/ data_offer_handle_source_actions,
/*action*/ 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*/,
wl_data_device * /*wl_data_device*/,
wl_data_offer *id)
{
CLOG_DEBUG(LOG, "data_offer");
/* The ownership of data-offer isn't so obvious:
* At this point it's not known if this will be used for drag & drop or selection.
*
* The API docs state that the following callbacks run immediately after this callback:
* - #wl_data_device_listener::enter (for drag & drop).
* - #wl_data_device_listener::selection (for copy & paste).
*
* In the case of GHOST, this means they will be assigned to either:
* - #GWL_Seat::data_offer_dnd
* - #GWL_Seat::data_offer_copy_paste
*/
GWL_DataOffer *data_offer = new GWL_DataOffer;
data_offer->wl.id = id;
wl_data_offer_add_listener(id, &data_offer_listener, data_offer);
}
static void data_device_handle_enter(void *data,
wl_data_device * /*wl_data_device*/,
const uint32_t serial,
wl_surface *wl_surface,
const wl_fixed_t x,
const wl_fixed_t y,
wl_data_offer *id)
{
/* Always clear the current data-offer no matter what else happens. */
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
const uint64_t event_ms = seat->system->getMilliSeconds();
std::lock_guard lock{seat->data_offer_dnd_mutex};
if (seat->data_offer_dnd) {
wl_data_offer_destroy(seat->data_offer_dnd->wl.id);
delete seat->data_offer_dnd;
seat->data_offer_dnd = nullptr;
}
/* Clearing complete. */
/* Handle the new offer. */
GWL_DataOffer *data_offer = static_cast<GWL_DataOffer *>(wl_data_offer_get_user_data(id));
if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
CLOG_DEBUG(LOG, "enter (skipped)");
wl_data_offer_destroy(data_offer->wl.id);
delete data_offer;
return;
}
CLOG_DEBUG(LOG, "enter");
/* Transfer ownership of the `data_offer`. */
seat->data_offer_dnd = data_offer;
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, event_ms);
}
static void data_device_handle_leave(void *data, wl_data_device * /*wl_data_device*/)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
const uint64_t event_ms = seat->system->getMilliSeconds();
std::lock_guard lock{seat->data_offer_dnd_mutex};
/* The user may have only dragged over the window decorations. */
if (seat->data_offer_dnd == nullptr) {
return;
}
CLOG_DEBUG(LOG, "leave");
dnd_events(seat, GHOST_kEventDraggingExited, event_ms);
seat->wl.surface_window_focus_dnd = nullptr;
if (seat->data_offer_dnd) {
wl_data_offer_destroy(seat->data_offer_dnd->wl.id);
delete seat->data_offer_dnd;
seat->data_offer_dnd = nullptr;
}
}
static void data_device_handle_motion(void *data,
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);
const uint64_t event_ms = seat->system->ms_from_input_time(time);
std::lock_guard lock{seat->data_offer_dnd_mutex};
/* The user may have only dragged over the window decorations. */
if (seat->data_offer_dnd == nullptr) {
return;
}
CLOG_DEBUG(LOG, "motion");
seat->data_offer_dnd->dnd.xy[0] = x;
seat->data_offer_dnd->dnd.xy[1] = y;
dnd_events(seat, GHOST_kEventDraggingUpdated, event_ms);
}
static void data_device_handle_drop(void *data, wl_data_device * /*wl_data_device*/)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
std::lock_guard lock{seat->data_offer_dnd_mutex};
/* No need to check this for null (as other callbacks do).
* because the data-offer has not been accepted (actions set... etc). */
GWL_DataOffer *data_offer = seat->data_offer_dnd;
/* Take ownership of `data_offer` to prevent a double-free, see: #128766.
* The thread this function spawns is responsible for freeing it. */
seat->data_offer_dnd = nullptr;
/* 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 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_DEBUG(LOG, "drop mime_recieve=%s", mime_receive);
auto read_drop_data_fn = [](GWL_Seat *const seat,
GWL_DataOffer *data_offer,
wl_surface *wl_surface_window,
const char *mime_receive) {
const uint64_t event_ms = seat->system->getMilliSeconds();
const wl_fixed_t xy[2] = {UNPACK2(data_offer->dnd.xy)};
const bool nil_terminate = (mime_receive != ghost_wl_mime_text_uri_list);
size_t data_buf_len = 0;
const char *data_buf = read_buffer_from_data_offer(
data_offer, mime_receive, nullptr, nil_terminate, &data_buf_len);
CLOG_DEBUG(LOG, "read_drop_data mime_receive=%s, data_len=%zu", mime_receive, data_buf_len);
wl_data_offer_finish(data_offer->wl.id);
wl_data_offer_destroy(data_offer->wl.id);
delete data_offer;
data_offer = nullptr;
/* Don't generate a drop event if the data could not be read,
* an error will have been logged. */
if (data_buf != nullptr) {
GHOST_TDragnDropTypes ghost_dnd_type = GHOST_kDragnDropTypeUnknown;
void *ghost_dnd_data = nullptr;
/* Failure to receive drop data. */
if (mime_receive == ghost_wl_mime_text_uri_list) {
std::vector<std::string_view> uris = gwl_clipboard_uri_ranges(data_buf, data_buf_len);
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] = reinterpret_cast<uint8_t *>(
GHOST_URL_decode_alloc(uris[i].data(), uris[i].size()));
}
CLOG_DEBUG(LOG, "read_drop_data file_count=%d", flist->count);
ghost_dnd_type = GHOST_kDragnDropTypeFilenames;
ghost_dnd_data = flist;
}
else if (ELEM(mime_receive, ghost_wl_mime_text_plain, ghost_wl_mime_text_utf8)) {
ghost_dnd_type = GHOST_kDragnDropTypeString;
ghost_dnd_data = (void *)data_buf; /* Move ownership to the event. */
data_buf = nullptr;
}
if (ghost_dnd_type != GHOST_kDragnDropTypeUnknown) {
GHOST_SystemWayland *const system = seat->system;
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_window);
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, xy)};
system->pushEvent_maybe_pending(new GHOST_EventDragnDrop(event_ms,
GHOST_kEventDraggingDropDone,
ghost_dnd_type,
win,
UNPACK2(event_xy),
ghost_dnd_data));
wl_display_roundtrip(system->wl_display_get());
}
else {
CLOG_DEBUG(LOG, "read_drop_data, unhandled!");
}
free(const_cast<char *>(data_buf));
}
};
/* 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_drop_data_fn, seat, data_offer, seat->wl.surface_window_focus_dnd, mime_receive);
read_thread.detach();
}
static void data_device_handle_selection(void *data,
wl_data_device * /*wl_data_device*/,
wl_data_offer *id)
{
/* Always clear the current data-offer no matter what else happens. */
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
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->wl.id);
delete seat->data_offer_copy_paste;
seat->data_offer_copy_paste = nullptr;
seat->data_offer_copy_paste_has_image = std::nullopt;
}
/* Clearing complete. */
/* Handle the new offer. */
if (id == nullptr) {
CLOG_DEBUG(LOG, "selection: (skipped)");
return;
}
CLOG_DEBUG(LOG, "selection");
GWL_DataOffer *data_offer = static_cast<GWL_DataOffer *>(wl_data_offer_get_user_data(id));
/* Transfer ownership of the `data_offer`. */
seat->data_offer_copy_paste = data_offer;
seat->data_offer_copy_paste_has_image = std::nullopt;
}
static const wl_data_device_listener data_device_listener = {
/*data_offer*/ data_device_handle_data_offer,
/*enter*/ data_device_handle_enter,
/*leave*/ data_device_handle_leave,
/*motion*/ data_device_handle_motion,
/*drop*/ data_device_handle_drop,
/*selection*/ 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, wl_buffer *wl_buffer)
{
CLOG_DEBUG(LOG, "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 wl_buffer_listener cursor_buffer_listener = {
/*release*/ 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_Seat *seat,
GWL_Cursor &cursor,
GWL_SeatStatePointer *seat_state_pointer)
{
/* NOTE: currently there is no special handling for fractional scaling.
* This could be supported however the default behavior of generating a cursor
* rounded up to the nearest integer scaling works fairly well. */
int scale = 0;
for (const GWL_Output *output : seat_state_pointer->outputs) {
scale = std::max(scale, output->scale);
}
if (scale > 0 && seat_state_pointer->buffer_scale != scale) {
seat_state_pointer->buffer_scale = scale;
}
scale = gwl_seat_cursor_buffer_scale_calc(seat);
if (scale > 0 && cursor.custom_cursor_scale != scale) {
cursor.custom_cursor_scale = scale;
if (cursor.is_custom) {
wl_surface *wl_surface_focus = seat_state_pointer->wl.surface_window;
if (wl_surface_focus) {
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
if (win) {
win->cursor_shape_refresh();
}
}
}
return true;
}
return false;
}
static void cursor_surface_handle_enter(void *data, wl_surface *wl_surface, wl_output *wl_output)
{
if (!ghost_wl_output_own(wl_output)) {
CLOG_DEBUG(LOG, "handle_enter (skipped)");
return;
}
CLOG_DEBUG(LOG, "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, seat->cursor, seat_state_pointer);
}
static void cursor_surface_handle_leave(void *data, wl_surface *wl_surface, wl_output *wl_output)
{
if (!(wl_output && ghost_wl_output_own(wl_output))) {
CLOG_DEBUG(LOG, "handle_leave (skipped)");
return;
}
CLOG_DEBUG(LOG, "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, seat->cursor, seat_state_pointer);
}
static void cursor_surface_handle_preferred_buffer_scale(void * /*data*/,
wl_surface * /*wl_surface*/,
const int32_t factor)
{
/* Only available in interface version 6. */
CLOG_DEBUG(LOG, "handle_preferred_buffer_scale (factor=%d)", factor);
}
#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) && \
defined(WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION)
static void cursor_surface_handle_preferred_buffer_transform(void * /*data*/,
wl_surface * /*wl_surface*/,
uint32_t transform)
{
/* Only available in interface version 6. */
CLOG_DEBUG(LOG, "handle_preferred_buffer_transform (transform=%u)", transform);
}
#endif /* WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION && \
* WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION */
static const wl_surface_listener cursor_surface_listener = {
/*enter*/ cursor_surface_handle_enter,
/*leave*/ cursor_surface_handle_leave,
#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) && \
defined(WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION)
/*preferred_buffer_scale*/ cursor_surface_handle_preferred_buffer_scale,
/*preferred_buffer_transform*/ cursor_surface_handle_preferred_buffer_transform,
#endif
};
#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,
wl_pointer * /*wl_pointer*/,
const uint32_t serial,
wl_surface *wl_surface,
const wl_fixed_t surface_x,
const wl_fixed_t surface_y)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
const uint64_t event_ms = seat->system->getMilliSeconds();
/* Null when just destroyed. */
if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
CLOG_DEBUG(LOG, "enter (skipped)");
return;
}
CLOG_DEBUG(LOG, "enter");
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface);
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 = GWL_SeatStatePointerScroll{};
seat->pointer.wl.surface_window = wl_surface;
seat->system->seat_active_set(seat);
win->cursor_shape_refresh();
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)};
seat->system->pushEvent_maybe_pending(new GHOST_EventCursor(
event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), GHOST_TABLET_DATA_NONE));
}
static void pointer_handle_leave(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t /*serial*/,
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 (!ghost_wl_surface_own_with_null_check(wl_surface)) {
CLOG_DEBUG(LOG, "leave (skipped)");
return;
}
CLOG_DEBUG(LOG, "leave");
}
static void pointer_handle_motion(void *data,
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);
const uint64_t event_ms = seat->system->ms_from_input_time(time);
seat->pointer.xy[0] = surface_x;
seat->pointer.xy[1] = surface_y;
CLOG_DEBUG(LOG, "motion");
gwl_pointer_handle_frame_event_add(
&seat->pointer_events, GWL_Pointer_EventTypes::Motion, event_ms);
}
static void pointer_handle_button(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t serial,
const uint32_t time,
const uint32_t button,
const uint32_t state)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
CLOG_DEBUG(LOG, "button (button=%u, state=%u)", button, state);
/* Always set the serial, even if the button event is not sent. */
seat->data_source_serial = serial;
int button_release;
switch (state) {
case WL_POINTER_BUTTON_STATE_RELEASED: {
button_release = 1;
break;
}
case WL_POINTER_BUTTON_STATE_PRESSED: {
button_release = 0;
break;
}
default: {
return;
}
}
const uint32_t button_index = button - BTN_RANGE_MIN;
if (button_index >= (BTN_RANGE_MAX - BTN_RANGE_MIN)) {
return;
}
const GWL_Pointer_EventTypes ty = GWL_Pointer_EventTypes(
int(GWL_Pointer_EventTypes::Button0_Down) + ((button_index * 2) + button_release));
const uint64_t event_ms = seat->system->ms_from_input_time(time);
gwl_pointer_handle_frame_event_add(&seat->pointer_events, ty, event_ms);
}
static void pointer_handle_axis(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t time,
const uint32_t axis,
const wl_fixed_t value)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->pointer_scroll.event_ms = seat->system->ms_from_input_time(time);
seat->pointer_scroll.has_event_ms = true;
/* 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_DEBUG(LOG, "axis (axis=%u, value=%d)", axis, value);
const int index = pointer_axis_as_index(axis);
if (UNLIKELY(index == -1)) {
return;
}
seat->pointer_scroll.smooth_xy[index] = value;
gwl_pointer_handle_frame_event_add(&seat->pointer_events, GWL_Pointer_EventTypes::Scroll, 0);
}
static void pointer_handle_frame(void *data, wl_pointer * /*wl_pointer*/)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
CLOG_DEBUG(LOG, "frame");
if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) {
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
for (int ty_index = 0; ty_index < seat->pointer_events.frame_pending.frame_types_num;
ty_index++)
{
const GWL_Pointer_EventTypes ty = seat->pointer_events.frame_pending.frame_types[ty_index];
const uint64_t event_ms = seat->pointer_events.frame_pending.frame_event_ms[ty_index];
switch (ty) {
/* Use motion for pressure and tilt as there are no explicit event types for these. */
case GWL_Pointer_EventTypes::Motion: {
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)};
seat->system->pushEvent_maybe_pending(new GHOST_EventCursor(
event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), GHOST_TABLET_DATA_NONE));
break;
}
case GWL_Pointer_EventTypes::Scroll: {
GWL_SeatStatePointerScroll &ps = seat->pointer_scroll;
/* The scroll data is "interpreted" before generating the events,
* this is needed because data is accumulated
* with support for handling smooth scroll as discrete events (for example). */
/* Most scroll events have no time, use `ps.event_ms` instead.
* This is assigned for the scroll events that do set a time.
* In practice the values tends to be set but fall back to the current time. */
GHOST_ASSERT(event_ms == 0, "Scroll events are not expected to have a time!");
/* Handle value120 to discrete steps first. */
if (ps.discrete120_xy[0] || ps.discrete120_xy[1]) {
for (int i = 0; i < 2; i++) {
ps.discrete120_xy_accum[i] += ps.discrete120_xy[i];
ps.discrete120_xy[i] = 0;
/* The values will have been normalized so 120 represents a single click-step. */
ps.discrete_xy[i] = ps.discrete120_xy_accum[i] / 120;
ps.discrete120_xy_accum[i] -= ps.discrete_xy[i] * 120;
}
}
/* Multiple wheel events may have been generated and it's not known which.
* The logic here handles prioritizing how they should be handled. */
if (ps.axis_source == WL_POINTER_AXIS_SOURCE_WHEEL) {
/* We never want mouse wheel events to be treated as smooth scrolling as this
* causes mouse wheel scroll to orbit the view, see #120587.
* Although it could be supported if the event system would forward
* the source of the scroll action (a wheel or touch device). */
ps.smooth_xy[0] = 0;
ps.smooth_xy[1] = 0;
}
else if (ps.axis_source == WL_POINTER_AXIS_SOURCE_FINGER) {
if (seat->use_pointer_scroll_smooth_as_discrete) {
GWL_SeatStatePointerScroll_SmoothAsDiscrete &smooth_as_discrete =
ps.smooth_as_discrete;
/* If discrete steps have been sent, use them as-is. */
if ((ps.discrete_xy[0] == 0) && (ps.discrete_xy[1] == 0)) {
/* Convert smooth to discrete. */
for (int i = 0; i < 2; i++) {
smooth_as_discrete.smooth_xy_accum[i] += ps.smooth_xy[i];
if (std::abs(smooth_as_discrete.smooth_xy_accum[i]) >= smooth_as_discrete_steps)
{
ps.discrete_xy[i] = smooth_as_discrete.smooth_xy_accum[i] /
smooth_as_discrete_steps;
smooth_as_discrete.smooth_xy_accum[i] -= ps.discrete_xy[i] *
smooth_as_discrete_steps;
}
}
}
ps.smooth_xy[0] = 0;
ps.smooth_xy[1] = 0;
}
}
/* 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 (ps.axis_source == WL_POINTER_AXIS_SOURCE_WHEEL) {
if (ps.discrete_xy[0]) {
ps.smooth_xy[0] = 0;
}
if (ps.discrete_xy[1]) {
ps.smooth_xy[1] = 0;
}
}
else {
if (ps.smooth_xy[0]) {
ps.discrete_xy[0] = 0;
}
if (ps.smooth_xy[1]) {
ps.discrete_xy[1] = 0;
}
}
/* Done evaluating scroll input, generate the events. */
if (ps.discrete_xy[0] || ps.discrete_xy[1]) {
if (ps.discrete_xy[0]) {
seat->system->pushEvent_maybe_pending(new GHOST_EventWheel(
ps.has_event_ms ? ps.event_ms : seat->system->getMilliSeconds(),
win,
GHOST_kEventWheelAxisHorizontal,
ps.discrete_xy[0]));
}
if (ps.discrete_xy[1]) {
seat->system->pushEvent_maybe_pending(new GHOST_EventWheel(
ps.has_event_ms ? ps.event_ms : seat->system->getMilliSeconds(),
win,
GHOST_kEventWheelAxisVertical,
-ps.discrete_xy[1]));
}
ps.discrete_xy[0] = 0;
ps.discrete_xy[1] = 0;
}
if (ps.smooth_xy[0] || ps.smooth_xy[1]) {
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)};
seat->system->pushEvent_maybe_pending(new GHOST_EventTrackpad(
ps.has_event_ms ? ps.event_ms : seat->system->getMilliSeconds(),
win,
GHOST_kTrackpadEventScroll,
UNPACK2(event_xy),
/* 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(ps.smooth_xy[0]),
-wl_fixed_to_int(ps.smooth_xy[1]),
/* NOTE: GHOST does not support per-axis inversion.
* Assume inversion is used or not. */
ps.inverted_xy[0] || ps.inverted_xy[1]));
ps.smooth_xy[0] = 0;
ps.smooth_xy[1] = 0;
}
ps.axis_source = WL_POINTER_AXIS_SOURCE_WHEEL;
ps.inverted_xy[0] = false;
ps.inverted_xy[1] = false;
ps.event_ms = 0;
ps.has_event_ms = false;
break;
}
#ifdef NDEBUG
default:
#else /* Warn when any events aren't handled (in debug builds). */
case GWL_Pointer_EventTypes::Button0_Down:
case GWL_Pointer_EventTypes::Button0_Up:
case GWL_Pointer_EventTypes::Button1_Down:
case GWL_Pointer_EventTypes::Button1_Up:
case GWL_Pointer_EventTypes::Button2_Down:
case GWL_Pointer_EventTypes::Button2_Up:
case GWL_Pointer_EventTypes::Button3_Down:
case GWL_Pointer_EventTypes::Button3_Up:
case GWL_Pointer_EventTypes::Button4_Down:
case GWL_Pointer_EventTypes::Button4_Up:
case GWL_Pointer_EventTypes::Button5_Down:
case GWL_Pointer_EventTypes::Button5_Up:
case GWL_Pointer_EventTypes::Button6_Down:
case GWL_Pointer_EventTypes::Button6_Up:
#endif
{
const int button_enum_offset = int(ty) - int(GWL_Pointer_EventTypes::Button0_Down);
const int button_index = button_enum_offset / 2;
const bool button_down = (button_index * 2) == button_enum_offset;
const GHOST_TButton ebutton = gwl_pointer_events_ebutton[button_index];
const GHOST_TEventType etype = button_down ? GHOST_kEventButtonDown :
GHOST_kEventButtonUp;
seat->pointer.buttons.set(ebutton, button_down);
seat->system->pushEvent_maybe_pending(
new GHOST_EventButton(event_ms, etype, win, ebutton, GHOST_TABLET_DATA_NONE));
break;
}
}
}
}
gwl_pointer_handle_frame_event_reset(&seat->pointer_events);
}
static void pointer_handle_axis_source(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t axis_source)
{
CLOG_DEBUG(LOG, "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,
wl_pointer * /*wl_pointer*/,
const uint32_t time,
const uint32_t axis)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->pointer_scroll.event_ms = seat->system->ms_from_input_time(time);
seat->pointer_scroll.has_event_ms = true;
if (seat->use_pointer_scroll_smooth_as_discrete) {
/* Reset the scroll steps when the touch event ends.
* Done so the user doesn't accidentally bump smooth scroll input a small number of steps
* causing an unexpected discrete step caused by a very small amount of smooth-scrolling. */
GWL_SeatStatePointerScroll_SmoothAsDiscrete &smooth_as_discrete =
seat->pointer_scroll.smooth_as_discrete;
smooth_as_discrete.smooth_xy_accum[0] = 0;
smooth_as_discrete.smooth_xy_accum[1] = 0;
}
CLOG_DEBUG(LOG, "axis_stop (axis=%u)", axis);
}
static void pointer_handle_axis_discrete(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t axis,
const 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_DEBUG(LOG, "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;
gwl_pointer_handle_frame_event_add(&seat->pointer_events, GWL_Pointer_EventTypes::Scroll, 0);
}
static void pointer_handle_axis_value120(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t axis,
const int32_t value120)
{
/* Only available in interface version 8. */
CLOG_DEBUG(LOG, "axis_value120 (axis=%u, value120=%d)", axis, value120);
const int index = pointer_axis_as_index(axis);
if (UNLIKELY(index == -1)) {
return;
}
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->pointer_scroll.discrete120_xy[index] = value120;
gwl_pointer_handle_frame_event_add(&seat->pointer_events, GWL_Pointer_EventTypes::Scroll, 0);
}
#ifdef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM /* Requires WAYLAND 1.22 or newer. */
static void pointer_handle_axis_relative_direction(void *data,
wl_pointer * /*wl_pointer*/,
const uint32_t axis,
const uint32_t direction)
{
/* Only available in interface version 9. */
CLOG_DEBUG(LOG, "axis_relative_direction (axis=%u, direction=%u)", axis, direction);
const int index = pointer_axis_as_index(axis);
if (UNLIKELY(index == -1)) {
return;
}
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->pointer_scroll.inverted_xy[index] = (direction ==
WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED);
}
#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */
static const wl_pointer_listener pointer_listener = {
/*enter*/ pointer_handle_enter,
/*leave*/ pointer_handle_leave,
/*motion*/ pointer_handle_motion,
/*button*/ pointer_handle_button,
/*axis*/ pointer_handle_axis,
/*frame*/ pointer_handle_frame,
/*axis_source*/ pointer_handle_axis_source,
/*axis_stop*/ pointer_handle_axis_stop,
/*axis_discrete*/ pointer_handle_axis_discrete,
/*axis_value120*/ pointer_handle_axis_value120,
#ifdef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM
/*axis_relative_direction*/ pointer_handle_axis_relative_direction,
#endif
};
#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*/,
zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/,
const uint32_t /*serial*/,
const uint32_t /*time*/,
wl_surface * /*surface*/,
const uint32_t fingers)
{
CLOG_DEBUG(LOG, "begin (fingers=%u)", fingers);
}
static void gesture_hold_handle_end(void * /*data*/,
zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/,
const uint32_t /*serial*/,
const uint32_t /*time*/,
const int32_t cancelled)
{
CLOG_DEBUG(LOG, "end (cancelled=%i)", cancelled);
}
static const zwp_pointer_gesture_hold_v1_listener gesture_hold_listener = {
/*begin*/ gesture_hold_handle_begin,
/*end*/ 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,
zwp_pointer_gesture_pinch_v1 * /*pinch*/,
const uint32_t /*serial*/,
const uint32_t time,
wl_surface * /*surface*/,
const uint32_t fingers)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
(void)seat->system->ms_from_input_time(time); /* Only update internal time. */
CLOG_DEBUG(LOG, "begin (fingers=%u)", fingers);
/* Reset defaults. */
seat->pointer_gesture_pinch = GWL_SeatStatePointerGesture_Pinch{};
const 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(@ideasman42): 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;
/* 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;
if (win) {
/* NOTE(@ideasman42): Blender's use of trackpad 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 though the window scale is correct, it doesn't account for the 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 GWL_WindowScaleParams &scale_params = win->scale_params_get();
seat->pointer_gesture_pinch.scale.factor = gwl_window_scale_int_to(
scale_params, seat->pointer_gesture_pinch.scale.factor);
seat->pointer_gesture_pinch.rotation.factor = gwl_window_scale_int_to(
scale_params, seat->pointer_gesture_pinch.rotation.factor);
}
}
static void gesture_pinch_handle_update(void *data,
zwp_pointer_gesture_pinch_v1 * /*pinch*/,
const uint32_t time,
const wl_fixed_t dx,
const wl_fixed_t dy,
const wl_fixed_t scale,
const wl_fixed_t rotation)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
const uint64_t event_ms = seat->system->ms_from_input_time(time);
CLOG_DEBUG(LOG,
"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));
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 int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)};
if (scale_as_delta_px) {
seat->system->pushEvent_maybe_pending(new GHOST_EventTrackpad(event_ms,
win,
GHOST_kTrackpadEventMagnify,
event_xy[0],
event_xy[1],
scale_as_delta_px,
0,
false));
}
if (rotation_as_delta_px) {
seat->system->pushEvent_maybe_pending(new GHOST_EventTrackpad(event_ms,
win,
GHOST_kTrackpadEventRotate,
event_xy[0],
event_xy[1],
rotation_as_delta_px,
0,
false));
}
}
}
static void gesture_pinch_handle_end(void *data,
zwp_pointer_gesture_pinch_v1 * /*pinch*/,
const uint32_t /*serial*/,
const uint32_t time,
const int32_t cancelled)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
(void)seat->system->ms_from_input_time(time); /* Only update internal time. */
CLOG_DEBUG(LOG, "end (cancelled=%i)", cancelled);
}
static const zwp_pointer_gesture_pinch_v1_listener gesture_pinch_listener = {
/*begin*/ gesture_pinch_handle_begin,
/*update*/ gesture_pinch_handle_update,
/*end*/ 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 example). 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*/,
zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/,
uint32_t /*serial*/,
uint32_t /*time*/,
wl_surface * /*surface*/,
uint32_t fingers)
{
CLOG_DEBUG(LOG, "begin (fingers=%u)", fingers);
}
static void gesture_swipe_handle_update(
void * /*data*/,
zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/,
uint32_t /*time*/,
wl_fixed_t dx,
wl_fixed_t dy)
{
CLOG_DEBUG(LOG, "update (dx=%.3f, dy=%.3f)", wl_fixed_to_double(dx), wl_fixed_to_double(dy));
}
static void gesture_swipe_handle_end(
void * /*data*/,
zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/,
uint32_t /*serial*/,
uint32_t /*time*/,
int32_t cancelled)
{
CLOG_DEBUG(LOG, "end (cancelled=%i)", cancelled);
}
static const zwp_pointer_gesture_swipe_v1_listener gesture_swipe_listener = {
/*begin*/ gesture_swipe_handle_begin,
/*update*/ gesture_swipe_handle_update,
/*end*/ gesture_swipe_handle_end,
};
# undef LOG
#endif /* ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Touch Seat), #wl_touch_listener
*
* NOTE(@ideasman42): 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.
*
* NOTE(@felipe-choi): While X11 used to provide a virtual "touchscreen mouse" device,
* wayland provides dedicated touch events instead, so they go through
* this set of handler callbacks.
* \{ */
static CLG_LogRef LOG_WL_TOUCH = {"ghost.wl.handle.touch"};
#define LOG (&LOG_WL_TOUCH)
static void touch_seat_handle_down(void *data,
wl_touch * /*touch*/,
const uint32_t serial,
const uint32_t time,
wl_surface *surface,
const int32_t id,
const wl_fixed_t x,
const wl_fixed_t y)
{
/* Touching down is equivalent to moving a pointer and holding its left mouse button. */
CLOG_DEBUG(LOG, "down");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
/* Null when just destroyed. */
if (!ghost_wl_surface_own_with_null_check(surface)) {
CLOG_DEBUG(LOG, "down (skipped on empty surface)");
return;
}
/* Only track one point at a time.
* At some point *full* touch supported could be. */
if (seat->touch_state.is_touching) {
return;
}
const uint64_t event_ms = seat->system->ms_from_input_time(time);
/* Set generic pointer state. */
seat->touch.xy[0] = x;
seat->touch.xy[1] = y;
seat->touch.serial = serial;
seat->touch.wl.surface_window = surface;
/* Set the active pointer. */
seat->cursor_source_serial = serial;
/* Set touch-tracking state. */
seat->touch_state.is_touching = true;
seat->touch_state.down_id = id;
seat->touch_state.motion_pending = true;
seat->touch_state.motion_event_time_ms = event_ms;
seat->touch_state.down_pending = true;
seat->touch_state.down_event_time_ms = event_ms;
/* Signal the window manager to update the cursor shape
* into whatever shape it considers correct for the touchscreen's pointer. */
GHOST_WindowWayland *win = ghost_wl_surface_user_data(seat->touch.wl.surface_window);
win->cursor_shape_refresh();
}
static void touch_seat_handle_up(void *data,
wl_touch * /*touch*/,
const uint32_t /*serial*/,
const uint32_t time,
const int32_t id)
{
CLOG_DEBUG(LOG, "up");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
/* Only track one contact point at a time. */
if (seat->touch_state.down_id != id) {
return;
}
const uint64_t event_ms = seat->system->ms_from_input_time(time);
seat->touch_state.is_touching = false;
seat->touch_state.up_pending = true;
seat->touch_state.up_event_time_ms = event_ms;
}
static void touch_seat_handle_motion(void *data,
wl_touch * /*touch*/,
const uint32_t time,
const int32_t id,
const wl_fixed_t x,
const wl_fixed_t y)
{
CLOG_DEBUG(LOG, "motion");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
/* Only track one contact point at a time. */
if (seat->touch_state.down_id != id) {
return;
}
if (seat->touch_state.is_touching == false) {
return;
}
const uint64_t event_ms = seat->system->ms_from_input_time(time);
seat->touch.xy[0] = x;
seat->touch.xy[1] = y;
seat->touch_state.motion_event_time_ms = event_ms;
seat->touch_state.motion_pending = true;
}
static void touch_seat_handle_frame(void *data, wl_touch * /*touch*/)
{
CLOG_DEBUG(LOG, "frame");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
if (wl_surface *wl_surface_focus = seat->touch.wl.surface_window) {
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
GHOST_Event *touch_events[3];
int touch_events_num = 0;
/* For a finger move, generate a cursor move. */
if (seat->touch_state.motion_pending == true) {
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->touch.xy)};
touch_events[touch_events_num++] = new GHOST_EventCursor(
seat->touch_state.motion_event_time_ms,
GHOST_kEventCursorMove,
win,
UNPACK2(event_xy),
GHOST_TABLET_DATA_NONE);
seat->touch_state.motion_pending = false;
seat->touch_state.motion_event_time_ms = 0;
}
/* For a finger press, generate left-mouse button press. */
if (seat->touch_state.down_pending == true) {
seat->touch.buttons.set(GHOST_kButtonMaskLeft, true);
touch_events[touch_events_num++] = new GHOST_EventButton(
seat->touch_state.down_event_time_ms,
GHOST_kEventButtonDown,
win,
GHOST_kButtonMaskLeft,
GHOST_TABLET_DATA_NONE);
seat->touch_state.down_pending = false;
seat->touch_state.down_event_time_ms = 0;
}
/* For a finger release, generate a left-mouse button release. */
if (seat->touch_state.up_pending == true) {
seat->touch.buttons.set(GHOST_kButtonMaskLeft, false);
touch_events[touch_events_num++] = new GHOST_EventButton(seat->touch_state.up_event_time_ms,
GHOST_kEventButtonUp,
win,
GHOST_kButtonMaskLeft,
GHOST_TABLET_DATA_NONE);
seat->touch_state.up_pending = false;
seat->touch_state.up_event_time_ms = 0;
seat->touch.wl.surface_window = nullptr;
}
GHOST_ASSERT(touch_events_num <= sizeof(touch_events) / sizeof(touch_events[0]),
"Buffer overflow");
/* Ensure events are ordered in time. */
if (UNLIKELY(touch_events_num > 1)) {
std::sort(touch_events,
touch_events + touch_events_num,
[](const GHOST_Event *event_a, const GHOST_Event *event_b) -> bool {
return event_a->getTime() < event_b->getTime();
});
}
for (int i = 0; i < touch_events_num; i++) {
seat->system->pushEvent_maybe_pending(touch_events[i]);
}
}
}
static void touch_seat_handle_cancel(void * /*data*/, wl_touch * /*wl_touch*/)
{
CLOG_DEBUG(LOG, "cancel");
}
static void touch_seat_handle_shape(void * /*data*/,
wl_touch * /*touch*/,
const int32_t /*id*/,
const wl_fixed_t /*major*/,
const wl_fixed_t /*minor*/)
{
CLOG_DEBUG(LOG, "shape");
}
static void touch_seat_handle_orientation(void * /*data*/,
wl_touch * /*touch*/,
const int32_t /*id*/,
const wl_fixed_t /*orientation*/)
{
CLOG_DEBUG(LOG, "orientation");
}
static const wl_touch_listener touch_seat_listener = {
/*down*/ touch_seat_handle_down,
/*up*/ touch_seat_handle_up,
/*motion*/ touch_seat_handle_motion,
/*frame*/ touch_seat_handle_frame,
/*cancel*/ touch_seat_handle_cancel,
/*shape*/ touch_seat_handle_shape,
/*orientation*/ 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,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t tool_type)
{
CLOG_DEBUG(LOG, "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*/,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*hardware_serial_hi*/,
const uint32_t /*hardware_serial_lo*/)
{
CLOG_DEBUG(LOG, "hardware_serial");
}
static void tablet_tool_handle_hardware_id_wacom(void * /*data*/,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t /*hardware_id_hi*/,
const uint32_t /*hardware_id_lo*/)
{
CLOG_DEBUG(LOG, "hardware_id_wacom");
}
static void tablet_tool_handle_capability(void * /*data*/,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t capability)
{
CLOG_DEBUG(LOG,
"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*/, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
{
CLOG_DEBUG(LOG, "done");
}
static void tablet_tool_handle_removed(void *data, zwp_tablet_tool_v2 *zwp_tablet_tool_v2)
{
CLOG_DEBUG(LOG, "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);
}
if (tablet_tool->shape.device) {
wp_cursor_shape_device_v1_destroy(tablet_tool->shape.device);
}
seat->wp.tablet_tools.erase(zwp_tablet_tool_v2);
delete tablet_tool;
}
static void tablet_tool_handle_proximity_in(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t serial,
zwp_tablet_v2 * /*tablet*/,
wl_surface *wl_surface)
{
if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
CLOG_DEBUG(LOG, "proximity_in (skipped)");
return;
}
CLOG_DEBUG(LOG, "proximity_in");
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
tablet_tool->proximity = true;
tablet_tool->serial = serial;
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);
GHOST_WindowWayland *win = ghost_wl_surface_user_data(seat->tablet.wl.surface_window);
win->cursor_shape_refresh();
/* 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;
}
static void tablet_tool_handle_proximity_out(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
{
CLOG_DEBUG(LOG, "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;
tablet_tool->serial = 0;
}
static void tablet_tool_handle_down(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t serial)
{
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
GWL_Seat *seat = tablet_tool->seat;
CLOG_DEBUG(LOG, "down");
seat->data_source_serial = serial;
gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Stylus0_Down);
}
static void tablet_tool_handle_up(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/)
{
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
CLOG_DEBUG(LOG, "up");
gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Stylus0_Up);
}
static void tablet_tool_handle_motion(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t x,
const wl_fixed_t y)
{
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
CLOG_DEBUG(LOG, "motion");
tablet_tool->xy[0] = x;
tablet_tool->xy[1] = y;
tablet_tool->has_xy = true;
gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Motion);
}
static void tablet_tool_handle_pressure(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t pressure)
{
const float pressure_unit = float(pressure) / 65535;
CLOG_DEBUG(LOG, "pressure (%.4f)", pressure_unit);
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
GHOST_TabletData &td = tablet_tool->data;
td.Pressure = pressure_unit;
gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Pressure);
}
static void tablet_tool_handle_distance(void * /*data*/,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t distance)
{
CLOG_DEBUG(LOG, "distance (distance=%u)", distance);
}
static void tablet_tool_handle_tilt(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t tilt_x,
const wl_fixed_t tilt_y)
{
/* Map X tilt to `-1.0 (left)..1.0 (right)`.
* Map Y tilt to `-1.0 (away from user)..1.0 (toward user)`. */
const float tilt_unit[2] = {
float(wl_fixed_to_double(tilt_x) / 90.0f),
float(wl_fixed_to_double(tilt_y) / 90.0f),
};
CLOG_DEBUG(LOG, "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 = std::clamp(tilt_unit[0], -1.0f, 1.0f);
td.Ytilt = std::clamp(tilt_unit[1], -1.0f, 1.0f);
gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Tilt);
}
static void tablet_tool_handle_rotation(void * /*data*/,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t degrees)
{
CLOG_DEBUG(LOG, "rotation (degrees=%.4f)", wl_fixed_to_double(degrees));
}
static void tablet_tool_handle_slider(void * /*data*/,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const int32_t position)
{
CLOG_DEBUG(LOG, "slider (position=%d)", position);
}
static void tablet_tool_handle_wheel(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const wl_fixed_t /*degrees*/,
const int32_t clicks)
{
if (clicks == 0) {
return;
}
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
CLOG_DEBUG(LOG, "wheel (clicks=%d)", clicks);
tablet_tool->frame_pending.wheel.clicks = clicks;
gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Wheel);
}
static void tablet_tool_handle_button(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t serial,
const uint32_t button,
const uint32_t state)
{
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
GWL_Seat *seat = tablet_tool->seat;
CLOG_DEBUG(LOG, "button (button=%u, state=%u)", button, state);
bool is_press = false;
switch (state) {
case WL_POINTER_BUTTON_STATE_RELEASED: {
is_press = false;
break;
}
case WL_POINTER_BUTTON_STATE_PRESSED: {
is_press = true;
break;
}
}
seat->data_source_serial = serial;
GWL_TabletTool_EventTypes ty = GWL_TabletTool_EventTypes::Motion;
switch (button) {
case BTN_STYLUS: {
ty = is_press ? GWL_TabletTool_EventTypes::Stylus1_Down :
GWL_TabletTool_EventTypes::Stylus1_Up;
break;
}
case BTN_STYLUS2: {
ty = is_press ? GWL_TabletTool_EventTypes::Stylus2_Down :
GWL_TabletTool_EventTypes::Stylus2_Up;
break;
}
case BTN_STYLUS3: {
ty = is_press ? GWL_TabletTool_EventTypes::Stylus3_Down :
GWL_TabletTool_EventTypes::Stylus3_Up;
break;
}
}
if (ty != GWL_TabletTool_EventTypes::Motion) {
gwl_tablet_tool_frame_event_add(tablet_tool, ty);
}
}
static void tablet_tool_handle_frame(void *data,
zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
const uint32_t time)
{
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
GWL_Seat *seat = tablet_tool->seat;
const uint64_t event_ms = seat->system->ms_from_input_time(time);
CLOG_DEBUG(LOG, "frame");
/* 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);
bool has_motion = false;
for (int ty_index = 0; ty_index < tablet_tool->frame_pending.frame_types_num; ty_index++) {
const GWL_TabletTool_EventTypes ty = tablet_tool->frame_pending.frame_types[ty_index];
switch (ty) {
/* Use motion for pressure and tilt as there are no explicit event types for these. */
case GWL_TabletTool_EventTypes::Motion:
case GWL_TabletTool_EventTypes::Pressure:
case GWL_TabletTool_EventTypes::Tilt: {
/* Only one motion event per frame. */
if (has_motion) {
break;
}
/* Can happen when there is pressure/tilt without motion. */
if (tablet_tool->has_xy == false) {
break;
}
seat->tablet.xy[0] = tablet_tool->xy[0];
seat->tablet.xy[1] = tablet_tool->xy[1];
const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, tablet_tool->xy)};
seat->system->pushEvent_maybe_pending(new GHOST_EventCursor(
event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), tablet_tool->data));
has_motion = true;
break;
}
#ifdef NDEBUG
default:
#else /* Warn when any events aren't handled (in debug builds). */
case GWL_TabletTool_EventTypes::Stylus0_Down:
case GWL_TabletTool_EventTypes::Stylus0_Up:
case GWL_TabletTool_EventTypes::Stylus1_Down:
case GWL_TabletTool_EventTypes::Stylus1_Up:
case GWL_TabletTool_EventTypes::Stylus2_Down:
case GWL_TabletTool_EventTypes::Stylus2_Up:
case GWL_TabletTool_EventTypes::Stylus3_Down:
case GWL_TabletTool_EventTypes::Stylus3_Up:
#endif
{
const int button_enum_offset = int(ty) - int(GWL_TabletTool_EventTypes::Stylus0_Down);
const int button_index = button_enum_offset / 2;
const bool button_down = (button_index * 2) == button_enum_offset;
const GHOST_TButton ebutton = gwl_tablet_tool_ebutton[button_index];
const GHOST_TEventType etype = button_down ? GHOST_kEventButtonDown :
GHOST_kEventButtonUp;
seat->tablet.buttons.set(ebutton, button_down);
seat->system->pushEvent_maybe_pending(
new GHOST_EventButton(event_ms, etype, win, ebutton, tablet_tool->data));
break;
}
case GWL_TabletTool_EventTypes::Wheel: {
seat->system->pushEvent_maybe_pending(
new GHOST_EventWheel(event_ms,
win,
GHOST_kEventWheelAxisVertical,
-tablet_tool->frame_pending.wheel.clicks));
break;
}
}
}
if (tablet_tool->proximity == false) {
win->cursor_shape_refresh();
}
}
if (tablet_tool->proximity == false) {
seat->tablet.wl.surface_window = nullptr;
}
gwl_tablet_tool_frame_event_reset(tablet_tool);
}
static const zwp_tablet_tool_v2_listener tablet_tool_listner = {
/*type*/ tablet_tool_handle_type,
/*hardware_serial*/ tablet_tool_handle_hardware_serial,
/*hardware_id_wacom*/ tablet_tool_handle_hardware_id_wacom,
/*capability*/ tablet_tool_handle_capability,
/*done*/ tablet_tool_handle_done,
/*removed*/ tablet_tool_handle_removed,
/*proximity_in*/ tablet_tool_handle_proximity_in,
/*proximity_out*/ tablet_tool_handle_proximity_out,
/*down*/ tablet_tool_handle_down,
/*up*/ tablet_tool_handle_up,
/*motion*/ tablet_tool_handle_motion,
/*pressure*/ tablet_tool_handle_pressure,
/*distance*/ tablet_tool_handle_distance,
/*tilt*/ tablet_tool_handle_tilt,
/*rotation*/ tablet_tool_handle_rotation,
/*slider*/ tablet_tool_handle_slider,
/*wheel*/ tablet_tool_handle_wheel,
/*button*/ tablet_tool_handle_button,
/*frame*/ 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*/,
zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
zwp_tablet_v2 *id)
{
CLOG_DEBUG(LOG, "tablet_added (id=%p)", id);
}
static void tablet_seat_handle_tool_added(void *data,
zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
zwp_tablet_tool_v2 *id)
{
CLOG_DEBUG(LOG, "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 its own cursor wl_surface. */
tablet_tool->wl.surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor_get());
ghost_wl_surface_tag_cursor_tablet(tablet_tool->wl.surface_cursor);
wl_surface_add_listener(
tablet_tool->wl.surface_cursor, &cursor_surface_listener, static_cast<void *>(seat));
zwp_tablet_tool_v2_add_listener(id, &tablet_tool_listner, tablet_tool);
seat->wp.tablet_tools.insert(id);
}
static void tablet_seat_handle_pad_added(void * /*data*/,
zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
zwp_tablet_pad_v2 *id)
{
CLOG_DEBUG(LOG, "pad_added (id=%p)", id);
}
static const zwp_tablet_seat_v2_listener tablet_seat_listener = {
/*tablet_added*/ tablet_seat_handle_tablet_added,
/*tool_added*/ tablet_seat_handle_tool_added,
/*pad_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,
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_DEBUG(LOG, "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);
CLOG_DEBUG(LOG, "keymap mmap failed: %s", std::strerror(errno));
return;
}
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_DEBUG(LOG, "keymap (not found)");
return;
}
CLOG_DEBUG(LOG, "keymap");
/* Reset in case there was a previous non-zero active layout for the last key-map.
* Note that this is set later by `wl_keyboard_listener::modifiers`, it's possible that handling
* the first modifier will run #xkb_state_update_mask again (if the active layout is non-zero)
* however as this is only done when the layout changed, it's harmless.
* With a single layout - in practice the active layout will be zero. */
seat->xkb.layout_active = 0;
if (seat->xkb.compose_state) {
xkb_compose_state_reset(seat->xkb.compose_state);
}
else if (seat->xkb.compose_table) {
seat->xkb.compose_state = xkb_compose_state_new(seat->xkb.compose_table,
XKB_COMPOSE_STATE_NO_FLAGS);
}
/* 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);
}
seat->xkb_keymap_mod_index_mod2 = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM);
seat->xkb_keymap_mod_index_numlock = xkb_keymap_mod_get_index(keymap, "NumLock");
xkb_state_unref(seat->xkb.state_empty_with_shift);
seat->xkb.state_empty_with_shift = nullptr;
if (seat->xkb_keymap_mod_index[MOD_INDEX_SHIFT] != XKB_MOD_INVALID) {
seat->xkb.state_empty_with_shift = xkb_state_new(keymap);
}
xkb_state_unref(seat->xkb.state_empty_with_numlock);
seat->xkb.state_empty_with_numlock = nullptr;
if ((seat->xkb_keymap_mod_index_mod2 != XKB_MOD_INVALID) &&
(seat->xkb_keymap_mod_index_numlock != XKB_MOD_INVALID))
{
seat->xkb.state_empty_with_numlock = xkb_state_new(keymap);
}
gwl_seat_key_layout_active_state_update_mask(seat);
#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,
wl_keyboard * /*wl_keyboard*/,
const uint32_t serial,
wl_surface *wl_surface,
wl_array *keys)
{
/* Null when just destroyed. */
if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
CLOG_DEBUG(LOG, "enter (skipped)");
return;
}
CLOG_DEBUG(LOG, "enter");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface);
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);
/* Keep track of the last held repeating key, start the repeat timer if one exists. */
struct {
uint32_t key = std::numeric_limits<uint32_t>::max();
xkb_keysym_t sym = 0;
} repeat;
uint32_t *key;
WL_ARRAY_FOR_EACH (key, keys) {
const xkb_keycode_t key_code = *key + EVDEV_OFFSET;
CLOG_DEBUG(LOG, "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);
}
if (xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb.state), key_code)) {
repeat.key = *key;
repeat.sym = sym;
}
}
/* Caller has no time-stamp, set from system. */
const uint64_t event_ms = seat->system->getMilliSeconds();
keyboard_depressed_state_push_events_from_change(seat, win, event_ms, key_depressed_prev);
if ((repeat.key != std::numeric_limits<uint32_t>::max()) && (seat->key_repeat.rate > 0)) {
/* Since the key has been held, immediately send a press event.
* This also ensures the key will be registered as pressed, see #117896. */
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
/* Should have been cleared on leave, set here just in case. */
if (UNLIKELY(seat->key_repeat.timer)) {
keyboard_handle_key_repeat_cancel(seat);
}
const xkb_keycode_t key_code = repeat.key + EVDEV_OFFSET;
const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(repeat.sym, repeat.key);
GWL_KeyRepeatPlayload *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;
gwl_seat_key_repeat_timer_add(seat, gwl_seat_key_repeat_timer_fn, key_repeat_payload, false);
/* Ensure there is a press event on enter so this is known to be held before any mouse
* button events which may use a key-binding that depends on this key being held. */
gwl_seat_key_repeat_timer_fn(seat->key_repeat.timer, 0);
}
}
/**
* Leave event.
*
* Notification that this seat's keyboard focus is no longer on a certain wl_surface.
*/
static void keyboard_handle_leave(void *data,
wl_keyboard * /*wl_keyboard*/,
const uint32_t /*serial*/,
wl_surface *wl_surface)
{
if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
CLOG_DEBUG(LOG, "leave (skipped)");
return;
}
CLOG_DEBUG(LOG, "leave");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->keyboard.wl.surface_window = nullptr;
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
/* Losing focus must stop repeating text. */
if (seat->key_repeat.timer) {
keyboard_handle_key_repeat_cancel(seat);
}
}
}
/**
* 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(
xkb_state *xkb_state_empty,
xkb_state *xkb_state_empty_with_numlock,
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(@ideasman42): 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: #96170.
* 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 bool xkb_compose_state_feed_and_get_utf8(
xkb_compose_state *compose_state,
xkb_state *state,
const xkb_keycode_t key,
char r_utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)])
{
const xkb_keysym_t sym = xkb_state_key_get_one_sym(state, key);
const xkb_compose_feed_result result = xkb_compose_state_feed(compose_state, sym);
bool handled = false;
if (result == XKB_COMPOSE_FEED_ACCEPTED) {
switch (xkb_compose_state_get_status(compose_state)) {
case XKB_COMPOSE_NOTHING: {
break;
}
case XKB_COMPOSE_COMPOSING: {
r_utf8_buf[0] = '\0';
handled = true;
break;
}
case XKB_COMPOSE_COMPOSED: {
char utf8_buf_compose[sizeof(GHOST_TEventKeyData::utf8_buf) + 1] = {'\0'};
const int utf8_buf_compose_len = xkb_compose_state_get_utf8(
compose_state, utf8_buf_compose, sizeof(utf8_buf_compose));
if (utf8_buf_compose_len > 0) {
if (utf8_buf_compose_len > sizeof(GHOST_TEventKeyData::utf8_buf)) {
/* TODO(@ideasman42): keyboard events in GHOST only support a single character.
*
* - In the case XKB compose enters multiple code-points only the first will be used.
*
* - Besides supporting multiple characters per key input,
* one possible solution would be to generate an IME event.
*
* - In practice I'm not sure how common these are.
* So far no bugs have been reported about this.
*/
CLOG_WARN(LOG, "key (compose_size=%d) exceeds the maximum size", utf8_buf_compose_len);
}
memcpy(r_utf8_buf, utf8_buf_compose, sizeof(GHOST_TEventKeyData::utf8_buf));
handled = true;
}
break;
}
case XKB_COMPOSE_CANCELLED: {
/* NOTE(@ideasman42): QT & GTK ignore these events as well as not inputting any text
* so `<Compose><Backspace>` for example causes a cancel and *not* back-space.
* This isn't supported under GHOST at the moment.
* The key-event could also be ignored but this means tracking held state of
* keys wont work properly, so don't do any input and pass in the key-symbol. */
r_utf8_buf[0] = '\0';
handled = true;
break;
}
}
}
return handled;
}
/**
* \note Caller must lock `timer_mutex`.
*/
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());
gwl_seat_key_repeat_timer_remove(seat);
}
/**
* Restart the key-repeat timer.
* \param use_delay: When false, use the interval
* (prevents pause when the setting changes while the key is held).
*
* \note Caller must lock `timer_mutex`.
*/
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_TimerProcPtr key_repeat_fn = seat->key_repeat.timer->getTimerProc();
GHOST_TUserDataPtr payload = seat->key_repeat.timer->getUserData();
gwl_seat_key_repeat_timer_remove(seat);
gwl_seat_key_repeat_timer_add(seat, key_repeat_fn, payload, use_delay);
}
static void keyboard_handle_key(void *data,
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 uint64_t event_ms = seat->system->ms_from_input_time(time);
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_DEBUG(LOG, "key (code=%d, state=%u, no symbol, skipped)", int(key_code), state);
return;
}
CLOG_DEBUG(LOG, "key (code=%d, state=%u)", int(key_code), state);
GHOST_TEventType etype = GHOST_kEventUnknown;
bool is_repeat = false;
switch (state) {
case WL_KEYBOARD_KEY_STATE_RELEASED: {
etype = GHOST_kEventKeyUp;
break;
}
#ifdef WL_KEYBOARD_KEY_STATE_REPEATED_SINCE_VERSION
case WL_KEYBOARD_KEY_STATE_REPEATED: {
/* Server side key repeat. */
is_repeat = true;
[[fallthrough]];
}
#endif
case WL_KEYBOARD_KEY_STATE_PRESSED: {
etype = GHOST_kEventKeyDown;
break;
}
}
#ifdef USE_EVENT_BACKGROUND_THREAD
/* Any access to `seat->key_repeat.timer` must lock. */
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
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 its own repeat timer). */
timer_action = CANCEL;
}
else {
/* Key-up from keys that were not repeating cause the repeat timer to pause.
*
* NOTE(@ideasman42): 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. */
gwl_seat_key_repeat_timer_remove(seat);
break;
}
case CANCEL: {
delete key_repeat_payload;
key_repeat_payload = nullptr;
gwl_seat_key_repeat_timer_remove(seat);
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) {
/* Handle key-compose (dead-keys). */
if (seat->xkb.compose_state &&
xkb_compose_state_feed_and_get_utf8(
seat->xkb.compose_state, seat->xkb.state, key_code, utf8_buf))
{
/* `utf8_buf` has been filled by a compose action. */
}
else {
xkb_state_key_get_utf8(seat->xkb.state, key_code, utf8_buf, sizeof(utf8_buf));
}
}
seat->data_source_serial = serial;
keyboard_depressed_state_key_event(seat, gkey, etype);
if (wl_surface *wl_surface_focus = seat->keyboard.wl.surface_window) {
GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus);
seat->system->pushEvent_maybe_pending(
new GHOST_EventKey(event_ms, etype, win, gkey, is_repeat, 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) {
gwl_seat_key_repeat_timer_add(seat, gwl_seat_key_repeat_timer_fn, key_repeat_payload, true);
}
}
static void keyboard_handle_modifiers(void *data,
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_DEBUG(LOG,
"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);
/* Account for the active layout changing within the same key-map,
* needed so modifiers are detected from the expected layout, see: #115160. */
if (group != seat->xkb.layout_active) {
seat->xkb.layout_active = group;
gwl_seat_key_layout_active_state_update_mask(seat);
}
/* A modifier changed so reset the timer,
* see comment in #keyboard_handle_key regarding this behavior. */
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
if (seat->key_repeat.timer) {
keyboard_handle_key_repeat_reset(seat, true);
}
}
seat->data_source_serial = serial;
}
static void keyboard_handle_repeat_info(void *data,
wl_keyboard * /*wl_keyboard*/,
const int32_t rate,
const int32_t delay)
{
CLOG_DEBUG(LOG, "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;
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
/* Unlikely possible this setting changes while repeating. */
if (seat->key_repeat.timer) {
if (rate > 0) {
keyboard_handle_key_repeat_reset(seat, false);
}
else {
/* A zero rate disables. */
keyboard_handle_key_repeat_cancel(seat);
}
}
}
}
static const wl_keyboard_listener keyboard_listener = {
/*keymap*/ keyboard_handle_keymap,
/*enter*/ keyboard_handle_enter,
/*leave*/ keyboard_handle_leave,
/*key*/ keyboard_handle_key,
/*modifiers*/ keyboard_handle_modifiers,
/*repeat_info*/ keyboard_handle_repeat_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,
zwp_primary_selection_offer_v1 *id,
const char *type)
{
/* NOTE: locking isn't needed as the #GWL_DataOffer wont have been assigned to the #GWL_Seat. */
GWL_PrimarySelection_DataOffer *data_offer = static_cast<GWL_PrimarySelection_DataOffer *>(data);
if (data_offer->wp.id != id) {
CLOG_DEBUG(LOG, "offer: %p: offer for unknown selection %p of %s (skipped)", data, id, type);
return;
}
data_offer->types.insert(std::string(type));
}
static const zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = {
/*offer*/ 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*/,
zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/,
zwp_primary_selection_offer_v1 *id)
{
CLOG_DEBUG(LOG, "data_offer");
GWL_PrimarySelection_DataOffer *data_offer = new GWL_PrimarySelection_DataOffer;
data_offer->wp.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,
zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/,
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_DEBUG(LOG, "selection: (skipped)");
return;
}
CLOG_DEBUG(LOG, "selection");
/* Transfer ownership of the `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 zwp_primary_selection_device_v1_listener primary_selection_device_listener = {
/*data_offer*/ primary_selection_device_handle_data_offer,
/*selection*/ 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,
zwp_primary_selection_source_v1 * /*source*/,
const char * /*mime_type*/,
const int32_t fd)
{
CLOG_DEBUG(LOG, "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, zwp_primary_selection_source_v1 *source)
{
CLOG_DEBUG(LOG, "cancelled");
GWL_PrimarySelection *primary = static_cast<GWL_PrimarySelection *>(data);
if (source == primary->data_source->wp.source) {
gwl_primary_selection_discard_source(primary);
}
}
static const zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
/*send*/ primary_selection_source_send,
/*cancelled*/ primary_selection_source_cancelled,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Text Input), #zwp_text_input_manager_v3
* \{ */
#ifdef WITH_INPUT_IME
class GHOST_EventIME : public GHOST_Event {
protected:
GHOST_TEventImeData event_ime_data;
public:
/**
* Constructor.
* \param msec: The time this event was generated.
* \param type: The type of key event.
* \param key: The key code of the key.
*/
GHOST_EventIME(uint64_t msec,
GHOST_TEventType type,
GHOST_IWindow *window,
const GHOST_TEventImeData *customdata)
: GHOST_Event(msec, type, window)
{
/* Make sure that we keep a copy of the IME input. Otherwise it might get lost
* because we overwrite it before it can be read in Blender. (See #137346). */
this->event_ime_data = *customdata;
this->data_ = &this->event_ime_data;
}
};
static CLG_LogRef LOG_WL_TEXT_INPUT = {"ghost.wl.handle.text_input"};
# define LOG (&LOG_WL_TEXT_INPUT)
static void text_input_handle_enter(void *data,
zwp_text_input_v3 * /*zwp_text_input_v3*/,
wl_surface *surface)
{
/* Can be null when closing a window, see: #141777. */
if (!ghost_wl_surface_own_with_null_check(surface)) {
return;
}
CLOG_DEBUG(LOG, "enter");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->ime.surface_window = surface;
/* If text input is enabled, should call `enable` after receive `enter` event.
* This support switch input method during input, otherwise input method will not work. */
if (seat->ime.is_enabled) {
zwp_text_input_v3_enable(seat->wp.text_input);
zwp_text_input_v3_commit(seat->wp.text_input);
}
}
static void text_input_handle_leave(void *data,
zwp_text_input_v3 * /*zwp_text_input_v3*/,
wl_surface *surface)
{
/* Can be null when closing a window. */
if (!ghost_wl_surface_own_with_null_check(surface)) {
return;
}
CLOG_DEBUG(LOG, "leave");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
if (seat->ime.surface_window == surface) {
seat->ime.surface_window = nullptr;
}
/* Always call `disable` after receive `leave` event. */
zwp_text_input_v3_disable(seat->wp.text_input);
zwp_text_input_v3_commit(seat->wp.text_input);
}
static void text_input_handle_preedit_string(void *data,
zwp_text_input_v3 * /*zwp_text_input_v3*/,
const char *text,
const int32_t cursor_begin,
const int32_t cursor_end)
{
CLOG_DEBUG(LOG,
"preedit_string (text=\"%s\", cursor_begin=%d, cursor_end=%d)",
text ? text : "<null>",
cursor_begin,
cursor_end);
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
if (seat->ime.has_preedit == false) {
/* Starting IME input. */
gwl_seat_ime_full_reset(seat);
}
seat->ime.composite_is_null = (text == nullptr);
if (!seat->ime.composite_is_null) {
seat->ime.event_ime_data.composite = text;
seat->ime.event_ime_data.cursor_position = cursor_begin;
seat->ime.event_ime_data.target_start = cursor_begin;
seat->ime.event_ime_data.target_end = cursor_end;
}
seat->ime.has_preedit_string_callback = true;
}
static void text_input_handle_commit_string(void *data,
zwp_text_input_v3 * /*zwp_text_input_v3*/,
const char *text)
{
CLOG_DEBUG(LOG, "commit_string (text=\"%s\")", text ? text : "<null>");
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
seat->ime.result_is_null = (text == nullptr);
seat->ime.event_ime_data.result = text ? text : "";
seat->ime.event_ime_data.cursor_position = seat->ime.event_ime_data.result.size();
seat->ime.has_commit_string_callback = true;
}
static void text_input_handle_delete_surrounding_text(void * /*data*/,
zwp_text_input_v3 * /*zwp_text_input_v3*/,
const uint32_t before_length,
const uint32_t after_length)
{
CLOG_DEBUG(LOG,
"delete_surrounding_text (before_length=%u, after_length=%u)",
before_length,
after_length);
/* NOTE: Currently unused, do we care about this event?
* SDL ignores this event. */
}
static void text_input_handle_done(void *data,
zwp_text_input_v3 * /*zwp_text_input_v3*/,
const uint32_t /*serial*/)
{
GWL_Seat *seat = static_cast<GWL_Seat *>(data);
GHOST_SystemWayland *system = seat->system;
const uint64_t event_ms = seat->system->getMilliSeconds();
CLOG_DEBUG(LOG, "done");
GHOST_WindowWayland *win = seat->ime.surface_window ?
ghost_wl_surface_user_data(seat->ime.surface_window) :
nullptr;
if (seat->ime.has_commit_string_callback) {
if (seat->ime.has_preedit) {
const bool is_end = seat->ime.composite_is_null;
if (is_end) {
seat->ime.has_preedit = false;
/* `commit_string` (end). */
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data));
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data));
}
else {
/* `commit_string` (continues). */
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data));
}
}
else {
/* `commit_string` ran with no active IME popup, start & end to insert text. */
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeCompositionStart, win, &seat->ime.event_ime_data));
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data));
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data));
}
if (seat->ime.has_preedit == false) {
gwl_seat_ime_preedit_reset(seat);
}
}
else if (seat->ime.has_preedit_string_callback) {
const bool is_end = seat->ime.composite_is_null;
if (is_end) {
/* `preedit_string` (end). */
seat->ime.has_preedit = false;
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data));
}
else {
const bool is_start = seat->ime.has_preedit == false;
/* `preedit_string` (start or continue). */
seat->ime.has_preedit = true;
system->pushEvent_maybe_pending(new GHOST_EventIME(
event_ms,
is_start ? GHOST_kEventImeCompositionStart : GHOST_kEventImeComposition,
win,
&seat->ime.event_ime_data));
}
}
seat->ime.has_preedit_string_callback = false;
seat->ime.has_commit_string_callback = false;
}
static zwp_text_input_v3_listener text_input_listener = {
/*enter*/ text_input_handle_enter,
/*leave*/ text_input_handle_leave,
/*preedit_string*/ text_input_handle_preedit_string,
/*commit_string*/ text_input_handle_commit_string,
/*delete_surrounding_text*/ text_input_handle_delete_surrounding_text,
/*done*/ text_input_handle_done,
};
# undef LOG
#endif /* WITH_INPUT_IME. */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Seat), #wl_seat_listener
* \{ */
static CLG_LogRef LOG_WL_SEAT = {"ghost.wl.handle.seat"};
#define LOG (&LOG_WL_SEAT)
static bool gwl_seat_capability_pointer_multitouch_check(const GWL_Seat *seat, const bool fallback)
{
const zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get();
if (pointer_gestures == nullptr) {
return fallback;
}
bool found = false;
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
if (seat->wp.pointer_gesture_hold) {
return true;
}
found = true;
#endif
#ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE
if (seat->wp.pointer_gesture_pinch) {
return true;
}
found = true;
#endif
#ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE
if (seat->wp.pointer_gesture_swipe) {
return true;
}
found = true;
#endif
if (seat->use_pointer_scroll_smooth_as_discrete == false) {
return true;
}
if (found == false) {
return fallback;
}
return false;
}
static void gwl_seat_capability_pointer_multitouch_enable(GWL_Seat *seat)
{
/* Smooth to discrete handling. */
seat->use_pointer_scroll_smooth_as_discrete = false;
seat->pointer_scroll.smooth_as_discrete = GWL_SeatStatePointerScroll_SmoothAsDiscrete{};
zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get();
if (pointer_gestures == nullptr) {
return;
}
const uint pointer_gestures_version = zwp_pointer_gestures_v1_get_version(pointer_gestures);
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
if (pointer_gestures_version >= ZWP_POINTER_GESTURES_V1_GET_HOLD_GESTURE_SINCE_VERSION)
{ /* Hold gesture. */
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. */
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. */
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_multitouch_disable(GWL_Seat *seat)
{
/* Smooth to discrete handling. */
seat->use_pointer_scroll_smooth_as_discrete = true;
seat->pointer_scroll.smooth_as_discrete = GWL_SeatStatePointerScroll_SmoothAsDiscrete{};
seat->pointer_scroll.smooth_xy[0] = 0;
seat->pointer_scroll.smooth_xy[1] = 0;
const zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get();
if (pointer_gestures == nullptr) {
return;
}
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
{ /* Hold gesture. */
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. */
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. */
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
}
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_get());
seat->cursor.visible = true;
seat->cursor.wl.buffer = nullptr;
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);
gwl_seat_capability_pointer_multitouch_enable(seat);
{
/* Use environment variables, falling back to defaults.
* These environment variables are used by enough WAYLAND applications
* that it makes sense to check them (see `Xcursor` man page). */
const char *env;
env = getenv("XCURSOR_SIZE");
seat->cursor.theme_size = default_cursor_size;
if (env && (*env != '\0')) {
char *env_end = nullptr;
/* While clamping is not needed on the WAYLAND side,
* GHOST's internal logic may get confused by negative values, so ensure it's at least 1. */
const long value = strtol(env, &env_end, 10);
if ((*env_end == '\0') && (value > 0)) {
seat->cursor.theme_size = int(value);
}
}
/* TODO: detect this from the system.
* We *could* have weak support based on checking for known themes. */
seat->cursor.use_dark_theme = true;
}
}
static void gwl_seat_capability_pointer_disable(GWL_Seat *seat)
{
if (!seat->wl.pointer) {
return;
}
if (seat->cursor.shape.device) {
wp_cursor_shape_device_v1_destroy(seat->cursor.shape.device);
seat->cursor.shape.device = nullptr;
}
gwl_seat_capability_pointer_multitouch_disable(seat);
if (seat->cursor.wl.surface_cursor) {
wl_surface_destroy(seat->cursor.wl.surface_cursor);
seat->cursor.wl.surface_cursor = 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;
}
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
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]] wl_seat *wl_seat,
const uint32_t capabilities)
{
CLOG_DEBUG(LOG,
"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, wl_seat * /*wl_seat*/, const char *name)
{
CLOG_DEBUG(LOG, "name (name=\"%s\")", name);
static_cast<GWL_Seat *>(data)->name = std::string(name);
}
static const wl_seat_listener seat_listener = {
/*capabilities*/ seat_handle_capabilities,
/*name*/ 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,
zxdg_output_v1 * /*xdg_output*/,
const int32_t x,
const int32_t y)
{
CLOG_DEBUG(LOG, "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,
zxdg_output_v1 * /*xdg_output*/,
const int32_t width,
const int32_t height)
{
CLOG_DEBUG(LOG, "logical_size [%d, %d]", width, height);
GWL_Output *output = static_cast<GWL_Output *>(data);
if (output->size_native[0] != 0 && output->size_native[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. */
int width_native = output->size_native[(output->transform & WL_OUTPUT_TRANSFORM_90) ? 1 : 0];
if ((width_native == width) && (output->scale_fractional == (1 * FRACTIONAL_DENOMINATOR))) {
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 #98793 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, zxdg_output_v1 * /*xdg_output*/)
{
CLOG_DEBUG(LOG, "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*/,
zxdg_output_v1 * /*xdg_output*/,
const char *name)
{
CLOG_DEBUG(LOG, "name (name=\"%s\")", name);
}
static void xdg_output_handle_description(void * /*data*/,
zxdg_output_v1 * /*xdg_output*/,
const char *description)
{
CLOG_DEBUG(LOG, "description (description=\"%s\")", description);
}
static const zxdg_output_v1_listener xdg_output_listener = {
/*logical_position*/ xdg_output_handle_logical_position,
/*logical_size*/ xdg_output_handle_logical_size,
/*done*/ xdg_output_handle_done,
/*name*/ xdg_output_handle_name,
/*description*/ 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,
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_DEBUG(LOG,
"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,
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_DEBUG(LOG, "mode (skipped)");
return;
}
CLOG_DEBUG(LOG, "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;
}
/**
* 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, wl_output * /*wl_output*/)
{
CLOG_DEBUG(LOG, "done");
GWL_Output *output = static_cast<GWL_Output *>(data);
int32_t size_native[2] = {UNPACK2(output->size_native)};
if (output->transform & WL_OUTPUT_TRANSFORM_90) {
std::swap(size_native[0], 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 = (size_native[0] * FRACTIONAL_DENOMINATOR) / output->size_logical[0];
output->has_scale_fractional = true;
}
}
static void output_handle_scale(void *data, wl_output * /*wl_output*/, const int32_t factor)
{
CLOG_DEBUG(LOG, "scale");
GWL_Output *output = static_cast<GWL_Output *>(data);
output->scale = factor;
output->system->output_scale_update(output);
}
static void output_handle_name(void * /*data*/, wl_output * /*wl_output*/, const char *name)
{
/* Only available in interface version 4. */
CLOG_DEBUG(LOG, "name (%s)", name);
}
static void output_handle_description(void * /*data*/,
wl_output * /*wl_output*/,
const char *description)
{
/* Only available in interface version 4. */
CLOG_DEBUG(LOG, "description (%s)", description);
}
static const wl_output_listener output_listener = {
/*geometry*/ output_handle_geometry,
/*mode*/ output_handle_mode,
/*done*/ output_handle_done,
/*scale*/ output_handle_scale,
/*name*/ output_handle_name,
/*description*/ output_handle_description,
};
#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*/, xdg_wm_base *xdg_wm_base, const uint32_t serial)
{
CLOG_DEBUG(LOG, "ping");
xdg_wm_base_pong(xdg_wm_base, serial);
}
static const xdg_wm_base_listener shell_listener = {
/*ping*/ 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(libdecor * /*context*/,
enum libdecor_error error,
const char *message)
{
CLOG_DEBUG(LOG, "error (id=%d, message=%s)", error, message);
(void)(error);
(void)(message);
GHOST_PRINT("decoration error (" << error << "): " << message << std::endl);
exit(EXIT_FAILURE);
}
static 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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 3u, 6u);
display->wl.compositor = static_cast<wl_compositor *>(
wl_registry_bind(display->wl.registry, params.name, &wl_compositor_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_compositor_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 6u);
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, version));
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;
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
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, version));
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;
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 2u, 3u);
display->xdg.output_manager = static_cast<zxdg_output_manager_v1 *>(wl_registry_bind(
display->wl.registry, params.name, &zxdg_output_manager_v1_interface, version));
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*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 2u, 4u);
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, version));
ghost_wl_output_tag(output->wl.output);
wl_output_set_user_data(output->wl.output, output);
display->outputs.push_back(output);
wl_output_add_listener(output->wl.output, &output_listener, output);
gwl_registry_entry_add(display, params, static_cast<void *>(output));
}
static void gwl_registry_wl_output_update(GWL_Display *display,
const GWL_RegisteryUpdate_Params &params)
{
GWL_Output *output = static_cast<GWL_Output *>(params.user_data);
if (display->xdg.output_manager) {
if (output->xdg.output == nullptr) {
output->xdg.output = zxdg_output_manager_v1_get_xdg_output(display->xdg.output_manager,
output->wl.output);
zxdg_output_v1_add_listener(output->xdg.output, &xdg_output_listener, output);
}
}
else {
output->xdg.output = nullptr;
}
}
static void gwl_registry_wl_output_remove(GWL_Display *display,
void *user_data,
const bool on_exit)
{
/* While windows & cursors hold references to outputs, there is no need to manually remove
* these references as the compositor will remove references via #wl_surface_listener.leave.
*
* WARNING: this is not the case for WLROOTS based compositors which have a (bug?)
* where surface leave events don't run. So `system->output_leave(..)` is needed
* until the issue is resolved in WLROOTS. */
GWL_Output *output = static_cast<GWL_Output *>(user_data);
if (!on_exit) {
/* Needed for WLROOTS, does nothing if surface leave callbacks have already run. */
if (output->system->output_unref(output->wl.output)) {
CLOG_WARN(LOG,
"mis-behaving compositor failed to call \"surface_listener.leave\" "
"window scale may be invalid!");
}
}
if (output->xdg.output) {
zxdg_output_v1_destroy(output->xdg.output);
}
wl_output_destroy(output->wl.output);
std::vector<GWL_Output *>::iterator iter = std::find(
display->outputs.begin(), display->outputs.end(), output);
const int index = (iter != display->outputs.cend()) ?
std::distance(display->outputs.begin(), iter) :
-1;
GHOST_ASSERT(index != -1, "invalid internal state");
/* NOTE: always erase even when `on_exit` because `output->xdg_output` is cleared later. */
display->outputs.erase(display->outputs.begin() + index);
delete output;
}
/* #GWL_Display.seats */
static void gwl_registry_wl_seat_add(GWL_Display *display, const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 5u, 9u);
GWL_Seat *seat = new GWL_Seat;
seat->system = display->system;
seat->xkb.context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
/* May be null (skip dead-key support in this case). */
seat->xkb.compose_table = xkb_compose_table_new_from_locale(
seat->xkb.context, ghost_wl_locale_from_env_with_default(), XKB_COMPOSE_COMPILE_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, version));
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));
has_wl_trackpad_physical_direction = version >= 9;
}
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;
}
#ifdef WITH_INPUT_IME
if (display->wp.text_input_manager) {
if (seat->wp.text_input == nullptr) {
seat->wp.text_input = zwp_text_input_manager_v3_get_text_input(
display->wp.text_input_manager, seat->wl.seat);
zwp_text_input_v3_set_user_data(seat->wp.text_input, seat);
zwp_text_input_v3_add_listener(seat->wp.text_input, &text_input_listener, seat);
}
}
else {
seat->wp.text_input = nullptr;
}
#endif /* WITH_INPUT_IME */
}
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->wl.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->wl.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);
}
#ifdef WITH_INPUT_IME
if (seat->wp.text_input) {
zwp_text_input_v3_destroy(seat->wp.text_input);
}
#endif
if (seat->wl.data_device) {
wl_data_device_release(seat->wl.data_device);
}
if (seat->wp.tablet_seat) {
zwp_tablet_seat_v2_destroy(seat->wp.tablet_seat);
}
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 nullptr 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_compose_state_unref(seat->xkb.compose_state);
xkb_compose_table_unref(seat->xkb.compose_table);
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wl.shm = static_cast<wl_shm *>(
wl_registry_bind(display->wl.registry, params.name, &wl_shm_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wl_shm_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 3u, 3u);
display->wl.data_device_manager = static_cast<wl_data_device_manager *>(wl_registry_bind(
display->wl.registry, params.name, &wl_data_device_manager_interface, version));
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*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wp.tablet_manager = static_cast<zwp_tablet_manager_v2 *>(wl_registry_bind(
display->wl.registry, params.name, &zwp_tablet_manager_v2_interface, version));
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*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
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, version));
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*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wp.pointer_constraints = static_cast<zwp_pointer_constraints_v1 *>(wl_registry_bind(
display->wl.registry, params.name, &zwp_pointer_constraints_v1_interface, version));
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*/)
{
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)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 3u, 3u);
display->wp.pointer_gestures = static_cast<zwp_pointer_gestures_v1 *>(
wl_registry_bind(display->wl.registry,
params.name,
&zwp_pointer_gestures_v1_interface,
std::min(params.version, version)));
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*/)
{
zwp_pointer_gestures_v1 **value_p = &display->wp.pointer_gestures;
zwp_pointer_gestures_v1_destroy(*value_p);
*value_p = nullptr;
}
/* #GWL_Display.xdg_activation */
static void gwl_registry_xdg_activation_add(GWL_Display *display,
const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->xdg.activation_manager = static_cast<xdg_activation_v1 *>(
wl_registry_bind(display->wl.registry, params.name, &xdg_activation_v1_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_xdg_activation_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
xdg_activation_v1 **value_p = &display->xdg.activation_manager;
xdg_activation_v1_destroy(*value_p);
*value_p = nullptr;
}
/* #GWL_Display.wp_fractional_scale_manger */
static void gwl_registry_wp_fractional_scale_manager_add(GWL_Display *display,
const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wp.fractional_scale_manager = static_cast<wp_fractional_scale_manager_v1 *>(
wl_registry_bind(
display->wl.registry, params.name, &wp_fractional_scale_manager_v1_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_fractional_scale_manager_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
wp_fractional_scale_manager_v1 **value_p = &display->wp.fractional_scale_manager;
wp_fractional_scale_manager_v1_destroy(*value_p);
*value_p = nullptr;
}
/* #GWL_Display.wl_viewport */
static void gwl_registry_wp_viewporter_add(GWL_Display *display,
const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wp.viewporter = static_cast<wp_viewporter *>(
wl_registry_bind(display->wl.registry, params.name, &wp_viewporter_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_viewporter_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
wp_viewporter **value_p = &display->wp.viewporter;
wp_viewporter_destroy(*value_p);
*value_p = nullptr;
}
/* #GWL_Display.wp_primary_selection_device_manager */
static void gwl_registry_wp_primary_selection_device_manager_add(
GWL_Display *display, const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
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,
version));
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*/)
{
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;
}
#ifdef WITH_INPUT_IME
/* #GWL_Display.wp_text_input_manager */
static void gwl_registry_wp_text_input_manager_add(GWL_Display *display,
const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wp.text_input_manager = static_cast<zwp_text_input_manager_v3 *>(wl_registry_bind(
display->wl.registry, params.name, &zwp_text_input_manager_v3_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_text_input_manager_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
zwp_text_input_manager_v3 **value_p = &display->wp.text_input_manager;
zwp_text_input_manager_v3_destroy(*value_p);
*value_p = nullptr;
}
#endif /* WITH_INPUT_IME */
/* #GWL_Display.wp_cursor_shape_manager */
static void gwl_registry_wp_cursor_shape_manager_add(GWL_Display *display,
const GWL_RegisteryAdd_Params &params)
{
const uint version = GWL_IFACE_VERSION_CLAMP(params.version, 1u, 1u);
display->wp.cursor_shape_manager = static_cast<wp_cursor_shape_manager_v1 *>(wl_registry_bind(
display->wl.registry, params.name, &wp_cursor_shape_manager_v1_interface, version));
gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_cursor_shape_manager_remove(GWL_Display *display,
void * /*user_data*/,
const bool /*on_exit*/)
{
wp_cursor_shape_manager_v1 **value_p = &display->wp.cursor_shape_manager;
wp_cursor_shape_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. */
{
/*interface_p*/ &wl_compositor_interface.name,
/*add_fn*/ gwl_registry_compositor_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_compositor_remove,
},
{
/*interface_p*/ &wl_shm_interface.name,
/*add_fn*/ gwl_registry_wl_shm_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wl_shm_remove,
},
{
/*interface_p*/ &xdg_wm_base_interface.name,
/*add_fn*/ gwl_registry_xdg_wm_base_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_xdg_wm_base_remove,
},
/* Managers. */
{
/*interface_p*/ &zxdg_decoration_manager_v1_interface.name,
/*add_fn*/ gwl_registry_xdg_decoration_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_xdg_decoration_manager_remove,
},
{
/*interface_p*/ &zxdg_output_manager_v1_interface.name,
/*add_fn*/ gwl_registry_xdg_output_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_xdg_output_manager_remove,
},
{
/*interface_p*/ &wl_data_device_manager_interface.name,
/*add_fn*/ gwl_registry_wl_data_device_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wl_data_device_manager_remove,
},
{
/*interface_p*/ &zwp_primary_selection_device_manager_v1_interface.name,
/*add_fn*/ gwl_registry_wp_primary_selection_device_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_primary_selection_device_manager_remove,
},
{
/*interface_p*/ &zwp_tablet_manager_v2_interface.name,
/*add_fn*/ gwl_registry_wp_tablet_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_tablet_manager_remove,
},
{
/*interface_p*/ &zwp_relative_pointer_manager_v1_interface.name,
/*add_fn*/ gwl_registry_wp_relative_pointer_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_relative_pointer_manager_remove,
},
#ifdef WITH_INPUT_IME
{
/*interface_p*/ &zwp_text_input_manager_v3_interface.name,
/*add_fn*/ gwl_registry_wp_text_input_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_text_input_manager_remove,
},
#endif
{
/*interface_p*/ &wp_cursor_shape_manager_v1_interface.name,
/*add_fn*/ gwl_registry_wp_cursor_shape_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_cursor_shape_manager_remove,
},
/* Higher level interfaces. */
{
/*interface_p*/ &zwp_pointer_constraints_v1_interface.name,
/*add_fn*/ gwl_registry_wp_pointer_constraints_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_pointer_constraints_remove,
},
{
/*interface_p*/ &zwp_pointer_gestures_v1_interface.name,
/*add_fn*/ gwl_registry_wp_pointer_gestures_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_pointer_gestures_remove,
},
{
/*interface_p*/ &xdg_activation_v1_interface.name,
/*add_fn*/ gwl_registry_xdg_activation_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_xdg_activation_remove,
},
{
/*interface_p*/ &wp_fractional_scale_manager_v1_interface.name,
/*add_fn*/ gwl_registry_wp_fractional_scale_manager_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_fractional_scale_manager_remove,
},
{
/*interface_p*/ &wp_viewporter_interface.name,
/*add_fn*/ gwl_registry_wp_viewporter_add,
/*update_fn*/ nullptr,
/*remove_fn*/ gwl_registry_wp_viewporter_remove,
},
/* Display outputs. */
{
/*interface_p*/ &wl_output_interface.name,
/*add_fn*/ gwl_registry_wl_output_add,
/*update_fn*/ gwl_registry_wl_output_update,
/*remove_fn*/ 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. */
{
/*interface_p*/ &wl_seat_interface.name,
/*add_fn*/ gwl_registry_wl_seat_add,
/*update_fn*/ gwl_registry_wl_seat_update,
/*remove_fn*/ gwl_registry_wl_seat_remove,
},
{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]] wl_registry *wl_registry,
const uint32_t name,
const char *interface,
const uint32_t version)
{
/* Log last since it's useful to know if the interface was handled or not. */
GWL_Display *display = static_cast<GWL_Display *>(data);
GHOST_ASSERT(display->wl.registry == wl_registry, "Registry argument must match!");
const int interface_slot = gwl_registry_handler_interface_slot_from_string(interface);
bool added = false;
if (interface_slot != -1) {
const GWL_RegistryHandler *handler = &gwl_registry_handlers[interface_slot];
const GWL_RegistryEntry *registry_entry_prev = display->registry_entry;
/* The interface name that is ensured not to be freed. */
GWL_RegisteryAdd_Params params{};
params.name = name;
params.interface_slot = interface_slot;
params.version = version;
handler->add_fn(display, params);
added = display->registry_entry != registry_entry_prev;
}
CLOG_DEBUG(LOG,
"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]] 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_DEBUG(LOG,
"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 wl_registry_listener registry_listener = {
/*global*/ global_handle_add,
/*global_remove*/ global_handle_remove,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Event Thread
* \{ */
#ifdef USE_EVENT_BACKGROUND_THREAD
static void *gwl_display_event_thread_fn(void *display_voidp)
{
GWL_Display *display = static_cast<GWL_Display *>(display_voidp);
GHOST_ASSERT(!display->background, "Foreground only");
const int fd = wl_display_get_fd(display->wl.display);
while (display->events_pthread_is_active) {
/* Wait for an event, this thread is dedicated to event handling. */
if (ghost_wl_display_event_pump_from_thread(
display->wl.display, fd, display->system->server_mutex) == -1)
{
break;
}
}
/* Wait until the main thread cancels this thread, otherwise this thread may exit
* before cancel is called, causing a crash on exit. */
while (true) {
pause();
}
return nullptr;
}
/* Event reading thread. */
static void gwl_display_event_thread_create(GWL_Display *display)
{
GHOST_ASSERT(!display->background, "Foreground only");
GHOST_ASSERT(display->events_pthread == 0, "Only call once");
display->events_pending.reserve(events_pending_default_size);
display->events_pthread_is_active = true;
pthread_create(&display->events_pthread, nullptr, gwl_display_event_thread_fn, display);
/* Application logic should take priority, this only ensures events don't accumulate when busy
* which typically takes a while (5+ seconds of frantic mouse motion for example). */
pthread_set_min_priority(display->events_pthread);
pthread_detach(display->events_pthread);
}
static void gwl_display_event_thread_destroy(GWL_Display *display)
{
GHOST_ASSERT(!display->background, "Foreground only");
pthread_cancel(display->events_pthread);
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
/** \} */
/* -------------------------------------------------------------------- */
/** \name GHOST Implementation
*
* WAYLAND specific implementation of the #GHOST_System interface.
* \{ */
GHOST_SystemWayland::GHOST_SystemWayland(const bool background)
: GHOST_System(),
#ifdef USE_EVENT_BACKGROUND_THREAD
server_mutex(new std::mutex),
timer_mutex(new std::mutex),
main_thread_id(std::this_thread::get_id()),
#endif
display_(new GWL_Display)
{
wl_log_set_handler_client(background ? ghost_wayland_log_handler_background :
ghost_wayland_log_handler);
display_->system = this;
display_->background = background;
/* Connect to the Wayland server. */
display_->wl.display = wl_display_connect(nullptr);
if (!display_->wl.display) {
display_destroy_and_free_all();
throw std::runtime_error("unable to connect to display!");
}
/* This may be removed later if decorations are required, needed as part of registration. */
display_->xdg_decor = new GWL_XDG_Decor_System;
/* Register interfaces. */
{
display_->registry_skip_update_all = true;
wl_registry *registry = wl_display_get_registry(display_->wl.display);
display_->wl.registry = registry;
wl_registry_add_listener(registry, &registry_listener, display_);
/* First round-trip to receive all registry objects. */
wl_display_roundtrip(display_->wl.display);
/* Second round-trip to receive all output events. */
wl_display_roundtrip(display_->wl.display);
/* Account for dependencies between interfaces. */
gwl_registry_entry_update_all(display_, -1);
display_->registry_skip_update_all = false;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
bool libdecor_required = false;
{
const char *xdg_current_desktop = [] {
/* Account for VSCode overriding this value (TSK!), see: #133921. */
const char *key = "ORIGINAL_XDG_CURRENT_DESKTOP";
const char *value = getenv(key);
return value ? value : getenv(key + 9);
}();
if (xdg_current_desktop) {
/* See the free-desktop specifications for details on `XDG_CURRENT_DESKTOP`.
* https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
*/
if (string_elem_split_by_delim(xdg_current_desktop, ':', "GNOME")) {
libdecor_required = true;
}
}
}
if (libdecor_required) {
/* 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 example while it could be fixed, requiring the library at all makes no sense. */
if (background) {
libdecor_required = false;
}
# ifdef WITH_GHOST_X11
else if (!has_libdecor && !ghost_wayland_is_x11_available()) {
/* Only require LIBDECOR when X11 is available, otherwise there is nothing to fall back to.
* It's better to open without window decorations than failing entirely. */
libdecor_required = false;
}
# endif /* WITH_GHOST_X11 */
}
if (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
display_destroy_and_free_all();
throw std::runtime_error("unable to find libdecor!");
}
}
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) {
display_destroy_and_free_all();
throw std::runtime_error("unable to create window decorations!");
}
}
else
#endif
{
const GWL_XDG_Decor_System &decor = *display_->xdg_decor;
if (!decor.shell) {
display_destroy_and_free_all();
throw std::runtime_error("unable to access xdg_shell!");
}
}
/* Without this, the output fractional size from `display->xdg.output_manager` isn't known,
* while this isn't essential, the first window creation uses this for setting the size.
* Supporting both XDG initialized/uninitialized outputs is possible it complicates logic.
* see: #113328 for an example of size on startup issues. */
wl_display_roundtrip(display_->wl.display);
#ifdef USE_EVENT_BACKGROUND_THREAD
/* There is no need for an event handling thread in background mode
* because there no polling for user input. */
if (background) {
GHOST_ASSERT(display_->events_pthread_is_active == false, "Expected to be false");
}
else {
gwl_display_event_thread_create(display_);
}
/* Could be null in background mode, however there are enough
* references to the timer-manager that it's safer to create it. */
display_->ghost_timer_manager = new GHOST_TimerManager();
#endif
}
void GHOST_SystemWayland::display_destroy_and_free_all()
{
gwl_display_destroy(display_);
#ifdef USE_EVENT_BACKGROUND_THREAD
delete server_mutex;
delete timer_mutex;
#endif
}
GHOST_SystemWayland::~GHOST_SystemWayland()
{
display_destroy_and_free_all();
}
GHOST_TSuccess GHOST_SystemWayland::init()
{
GHOST_TSuccess success = GHOST_System::init();
if (success) {
#ifdef WITH_INPUT_NDOF
ndof_manager_ = new GHOST_NDOFManagerUnix(*this);
#endif
return GHOST_kSuccess;
}
return GHOST_kFailure;
}
#undef pushEvent
bool GHOST_SystemWayland::processEvents(bool waitForEvent)
{
bool any_processed = false;
#ifdef USE_EVENT_BACKGROUND_THREAD
if (UNLIKELY(has_pending_actions_for_window.exchange(false))) {
std::lock_guard lock_server_guard{*server_mutex};
for (GHOST_IWindow *iwin : getWindowManager()->getWindows()) {
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
win->pending_actions_handle();
}
}
if (!display_->background) {
std::lock_guard lock{display_->events_pending_mutex};
for (const GHOST_IEvent *event : display_->events_pending) {
/* Perform actions that aren't handled in a thread. */
switch (event->getType()) {
case GHOST_kEventWindowActivate: {
getWindowManager()->setActiveWindow(event->getWindow());
break;
}
case GHOST_kEventWindowDeactivate: {
getWindowManager()->setWindowInactive(event->getWindow());
break;
}
default: {
break;
}
}
pushEvent(event);
}
display_->events_pending.clear();
if (UNLIKELY(display_->events_pending.capacity() > events_pending_default_size)) {
/* Avoid over allocation in the case of occasional delay between processing events
* causing many events to be collected and making this into a large array. */
display_->events_pending.shrink_to_fit();
display_->events_pending.reserve(events_pending_default_size);
}
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
{
const uint64_t now = getMilliSeconds();
#ifdef USE_EVENT_BACKGROUND_THREAD
{
std::lock_guard lock_timer_guard{*display_->system->timer_mutex};
if (ghost_timer_manager()->fireTimers(now)) {
any_processed = true;
}
}
#endif
if (getTimerManager()->fireTimers(now)) {
any_processed = true;
}
}
#ifdef WITH_INPUT_NDOF
if (static_cast<GHOST_NDOFManagerUnix *>(ndof_manager_)->processEvents()) {
/* As NDOF bypasses WAYLAND event handling,
* never wait for an event when an NDOF event was found. */
waitForEvent = false;
any_processed = true;
}
#endif /* WITH_INPUT_NDOF */
if (waitForEvent) {
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (wl_display_dispatch(display_->wl.display) == -1) {
ghost_wl_display_report_error(display_->wl.display);
}
}
else {
#ifdef USE_EVENT_BACKGROUND_THREAD
/* NOTE: this works, but as the events are being read in a thread,
* this could be removed and event handling still works.. */
if (server_mutex->try_lock()) {
if (ghost_wl_display_event_pump(display_->wl.display) == -1) {
ghost_wl_display_report_error(display_->wl.display);
}
server_mutex->unlock();
}
#else
if (ghost_wl_display_event_pump(display_->wl.display) == -1) {
ghost_wl_display_report_error(display_->wl.display);
}
#endif /* !USE_EVENT_BACKGROUND_THREAD */
}
if (getEventManager()->getNumEvents() > 0) {
any_processed = true;
}
return any_processed;
}
bool GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*action*/)
{
return false;
}
GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
/* Only read the underlying `seat->xkb_state` when there is an active window.
* Without this, the following situation occurs:
*
* - A window is activated (before the #wl_keyboard_listener::enter has run).
* - The modifiers from `seat->xkb_state` don't match `seat->key_depressed`.
* - Dummy values are written into `seat->key_depressed` to account for the discrepancy
* (as `seat->xkb_state` is the source of truth), however the number of held modifiers
* is not longer valid (because it's not known from dummy values).
* - #wl_keyboard_listener::enter runs, however the events generated from the state change
* may not match the physically held keys because the dummy values are not accurate.
*
* As this is an edge-case caused by the order of callbacks that run on window activation,
* don't attempt to *fix* the values in `seat->key_depressed` before the keyboard enter
* handler runs. This means the result of `getModifierKeys` may be momentarily incorrect
* however it's corrected once #wl_keyboard_listener::enter runs.
*/
const bool is_keyboard_active = seat->keyboard.wl.surface_window != nullptr;
const xkb_mod_mask_t state = is_keyboard_active ?
xkb_state_serialize_mods(seat->xkb.state,
XKB_STATE_MODS_DEPRESSED) :
0;
/* 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];
/* NOTE(@ideasman42): it's important to write the XKB state back to #GWL_KeyboardDepressedState
* otherwise changes to modifiers in the future wont generate events.
* This can cause modifiers to be stuck when switching between windows in GNOME because
* window activation is handled before the keyboard enter callback runs, see: #107314.
* Now resolved upstream, keep this for GNOME 45 and older releases & misbehaving compositors
* as the workaround doesn't have significant down-sides. */
int16_t &depressed_l = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_l)];
int16_t &depressed_r = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_r)];
bool val_l = depressed_l > 0;
bool val_r = depressed_r > 0;
if (is_keyboard_active) {
const bool val = (state & (1 << seat->xkb_keymap_mod_index[i])) != 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))) {
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;
depressed_l = 1;
}
}
else {
if (UNLIKELY(val_l || val_r)) {
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;
depressed_l = 0;
depressed_r = 0;
}
}
}
keys.set(mod_info.mod_l, val_l);
keys.set(mod_info.mod_r, val_r);
}
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
const 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,
const bool nil_terminate,
const char *mime_receive_override,
size_t *r_data_len)
{
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 = mime_receive_override ?
mime_receive_override :
system_clipboard_text_mime_type(data_offer->types);
GHOST_ASSERT((mime_receive_override == nullptr) ||
data_offer->types.count(mime_receive_override) != 0,
"Mime type override not found in data offer, caller must check");
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;
size_t data_len = 0;
std::atomic<bool> done = false;
} thread_result;
auto read_clipboard_fn = [](GWL_PrimarySelection_DataOffer *data_offer,
const bool nil_terminate,
const char *mime_receive,
std::mutex *mutex,
ThreadResult *thread_result) {
thread_result->data = read_buffer_from_primary_selection_offer(
data_offer, mime_receive, mutex, nil_terminate, &thread_result->data_len);
thread_result->done = true;
};
std::thread read_thread(
read_clipboard_fn, data_offer, nil_terminate, mime_receive, &mutex, &thread_result);
read_thread.detach();
while (!thread_result.done) {
wl_display_roundtrip(display->wl.display);
}
data = thread_result.data;
*r_data_len = thread_result.data_len;
/* 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,
const bool nil_terminate,
const char *mime_receive_override,
size_t *r_data_len)
{
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 = mime_receive_override ?
mime_receive_override :
system_clipboard_text_mime_type(data_offer->types);
GHOST_ASSERT((mime_receive_override == nullptr) ||
data_offer->types.count(mime_receive_override) != 0,
"Mime type override not found in data offer, caller must check");
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;
size_t data_len = 0;
std::atomic<bool> done = false;
} thread_result;
auto read_clipboard_fn = [](GWL_DataOffer *data_offer,
const bool nil_terminate,
const char *mime_receive,
std::mutex *mutex,
ThreadResult *thread_result) {
thread_result->data = read_buffer_from_data_offer(
data_offer, mime_receive, mutex, nil_terminate, &thread_result->data_len);
thread_result->done = true;
};
std::thread read_thread(
read_clipboard_fn, data_offer, nil_terminate, mime_receive, &mutex, &thread_result);
read_thread.detach();
while (!thread_result.done) {
wl_display_roundtrip(display->wl.display);
}
data = thread_result.data;
*r_data_len = thread_result.data_len;
/* Reading the data offer unlocks the mutex. */
mutex_locked = false;
}
}
if (mutex_locked) {
mutex.unlock();
}
return data;
}
char *GHOST_SystemWayland::getClipboard(const bool selection) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
const bool nil_terminate = true;
char *data = nullptr;
size_t data_len = 0;
if (selection) {
data = system_clipboard_get_primary_selection(display_, nil_terminate, nullptr, &data_len);
}
else {
data = system_clipboard_get(display_, nil_terminate, nullptr, &data_len);
}
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, const bool selection) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (selection) {
system_clipboard_put_primary_selection(display_, buffer);
}
else {
system_clipboard_put(display_, buffer);
}
}
static constexpr const char *ghost_wl_mime_img_png = "image/png";
GHOST_TSuccess GHOST_SystemWayland::hasClipboardImage() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
if (seat->data_offer_copy_paste_has_image.has_value()) {
return *seat->data_offer_copy_paste_has_image;
}
GHOST_TSuccess result = GHOST_kFailure;
GWL_DataOffer *data_offer = seat->data_offer_copy_paste;
if (data_offer) {
if (data_offer->types.count(ghost_wl_mime_img_png)) {
result = GHOST_kSuccess;
}
else if (data_offer->types.count(ghost_wl_mime_text_uri_list)) {
const bool nil_terminate = true;
size_t data_buf_len = 0;
char *data = system_clipboard_get(
display_, nil_terminate, ghost_wl_mime_text_uri_list, &data_buf_len);
if (data) {
std::vector<std::string_view> uris = gwl_clipboard_uri_ranges(data, data_buf_len);
if (!uris.empty()) {
const std::string_view &uri = uris.front();
char *filepath = GHOST_URL_decode_alloc(uri.data(), uri.size());
if (IMB_test_image(filepath)) {
result = GHOST_kSuccess;
}
free(filepath);
}
free(data);
}
}
}
seat->data_offer_copy_paste_has_image = result;
return result;
}
uint *GHOST_SystemWayland::getClipboardImage(int *r_width, int *r_height) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return nullptr;
}
uint *rgba = nullptr;
GWL_DataOffer *data_offer = seat->data_offer_copy_paste;
if (data_offer) {
ImBuf *ibuf = nullptr;
/* Check if the source offers a supported mime type.
* This check could be skipped, because the paste option is not supposed to be enabled
* otherwise. */
if (data_offer->types.count(ghost_wl_mime_img_png)) {
size_t data_len = 0;
char *data = system_clipboard_get(display_, false, ghost_wl_mime_img_png, &data_len);
if (data) {
/* Generate the image buffer with the received data. */
ibuf = IMB_load_image_from_memory(
(const uint8_t *)data, data_len, IB_byte_data, "<clipboard>");
free(data);
}
}
else if (data_offer->types.count(ghost_wl_mime_text_uri_list)) {
const bool nil_terminate = true;
size_t data_len = 0;
char *data = system_clipboard_get(
display_, nil_terminate, ghost_wl_mime_text_uri_list, &data_len);
if (data) {
std::vector<std::string_view> uris = gwl_clipboard_uri_ranges(data, data_len);
if (!uris.empty()) {
const std::string_view &uri = uris.front();
char *filepath = GHOST_URL_decode_alloc(uri.data(), uri.size());
ibuf = IMB_load_image_from_filepath(filepath, IB_byte_data);
free(filepath);
}
free(data);
}
}
if (ibuf) {
*r_width = ibuf->x;
*r_height = ibuf->y;
const size_t byte_count = size_t(ibuf->x) * size_t(ibuf->y) * 4;
rgba = (uint *)malloc(byte_count);
std::memcpy(rgba, ibuf->byte_buffer.data, byte_count);
IMB_freeImBuf(ibuf);
}
}
return rgba;
}
GHOST_TSuccess GHOST_SystemWayland::putClipboardImage(uint *rgba, int width, int height) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
/* Create a #wl_data_source object. */
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
std::lock_guard lock(seat->data_source_mutex);
GWL_DataSource *data_source = seat->data_source;
/* Load buffer into an #ImBuf and convert to PNG. */
ImBuf *ibuf = IMB_allocFromBuffer(reinterpret_cast<uint8_t *>(rgba), nullptr, width, height, 32);
ibuf->ftype = IMB_FTYPE_PNG;
ibuf->foptions.quality = 15;
if (!IMB_save_image(ibuf, "<memory>", IB_byte_data | IB_mem)) {
IMB_freeImBuf(ibuf);
return GHOST_kFailure;
}
/* Copy #ImBuf encoded_buffer to data source. */
GWL_SimpleBuffer *imgbuffer = &data_source->buffer_out;
gwl_simple_buffer_free_data(imgbuffer);
imgbuffer->data_size = ibuf->encoded_buffer_size;
char *data = static_cast<char *>(malloc(imgbuffer->data_size));
std::memcpy(data, ibuf->encoded_buffer.data, ibuf->encoded_buffer_size);
imgbuffer->data = data;
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);
/* Advertise the mime types supported. */
wl_data_source_offer(data_source->wl.source, ghost_wl_mime_img_png);
if (seat->wl.data_device) {
wl_data_device_set_selection(
seat->wl.data_device, data_source->wl.source, seat->data_source_serial);
}
IMB_freeImBuf(ibuf);
return GHOST_kSuccess;
}
uint8_t GHOST_SystemWayland::getNumDisplays() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
return display_ ? uint8_t(display_->outputs.size()) : 0;
}
uint64_t GHOST_SystemWayland::getMilliSeconds() const
{
/* Match the timing method used by LIBINPUT, so the result is closer to WAYLAND's time-stamps. */
timespec ts = {0, 0};
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t(ts.tv_sec) * 1000) + uint64_t(ts.tv_nsec / 1000000);
}
static GHOST_TSuccess getCursorPositionClientRelative_impl(
const GWL_SeatStatePointer *seat_state_pointer,
const GHOST_WindowWayland *win,
int32_t &x,
int32_t &y)
{
if (win->getCursorGrabModeIsWarp()) {
/* As the cursor is restored at the warped location,
* apply warping when requesting the cursor location. */
GHOST_Rect wrap_bounds{};
if (win->getCursorGrabBounds(wrap_bounds) == GHOST_kFailure) {
win->getClientBounds(wrap_bounds);
}
wl_fixed_t xy_wrap[2] = {
seat_state_pointer->xy[0],
seat_state_pointer->xy[1],
};
GHOST_Rect wrap_bounds_scale;
wrap_bounds_scale.l_ = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.l_));
wrap_bounds_scale.t_ = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.t_));
wrap_bounds_scale.r_ = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.r_));
wrap_bounds_scale.b_ = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.b_));
wrap_bounds_scale.wrapPoint(UNPACK2(xy_wrap), 0, win->getCursorGrabAxis());
x = wl_fixed_to_int(win->wl_fixed_to_window(xy_wrap[0]));
y = wl_fixed_to_int(win->wl_fixed_to_window(xy_wrap[1]));
}
else {
x = wl_fixed_to_int(win->wl_fixed_to_window(seat_state_pointer->xy[0]));
y = wl_fixed_to_int(win->wl_fixed_to_window(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 xy_next[2]{
win->wl_fixed_from_window(wl_fixed_from_int(x)),
win->wl_fixed_from_window(wl_fixed_from_int(y)),
};
/* As the cursor was "warped" generate an event at the new location. */
const uint64_t event_ms = seat->system->getMilliSeconds();
relative_pointer_handle_relative_motion_impl(seat, win, xy_next, event_ms);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::getCursorPositionClientRelative(const GHOST_IWindow *window,
int32_t &x,
int32_t &y) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
const GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat);
if (!seat_state_pointer || !seat_state_pointer->wl.surface_window) {
return GHOST_kFailure;
}
const GHOST_WindowWayland *win = static_cast<const GHOST_WindowWayland *>(window);
return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y);
}
GHOST_TSuccess GHOST_SystemWayland::setCursorPositionClientRelative(GHOST_IWindow *window,
const int32_t x,
const int32_t y)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(window);
return setCursorPositionClientRelative_impl(seat, win, x, y);
}
GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
const 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) {
const GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y);
}
return GHOST_kFailure;
}
GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(const int32_t x, const int32_t y)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
/* Intentionally different from `getCursorPosition` which supports both tablet & pointer.
* In the case of setting the cursor location, tablets don't support this. */
if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) {
GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus);
return setCursorPositionClientRelative_impl(seat, win, x, y);
}
return GHOST_kFailure;
}
uint32_t GHOST_SystemWayland::getCursorPreferredLogicalSize() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GWL_Seat *seat = gwl_display_seat_active_get(display_);
return seat->cursor.theme_size;
}
void GHOST_SystemWayland::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (!display_->outputs.empty()) {
/* We assume first output as main. */
const GWL_Output *output = display_->outputs[0];
int32_t size_native[2] = {UNPACK2(output->size_native)};
if (output->transform & WL_OUTPUT_TRANSFORM_90) {
std::swap(size_native[0], size_native[1]);
}
width = uint32_t(size_native[0]);
height = uint32_t(size_native[1]);
}
}
void GHOST_SystemWayland::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
if (!display_->outputs.empty()) {
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};
int32_t size_native[2] = {UNPACK2(output->size_native)};
if (output->has_position_logical) {
xy[0] = output->position_logical[0];
xy[1] = output->position_logical[1];
}
if (output->transform & WL_OUTPUT_TRANSFORM_90) {
std::swap(size_native[0], size_native[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] + size_native[0]);
xy_max[1] = std::max(xy_max[1], xy[1] + size_native[1]);
}
width = xy_max[0] - xy_min[0];
height = xy_max[1] - xy_min[1];
}
}
GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GPUSettings gpu_settings)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
const GHOST_ContextParams context_params_offscreen =
GHOST_CONTEXT_PARAMS_FROM_GPU_SETTINGS_OFFSCREEN(gpu_settings);
switch (gpu_settings.context_type) {
#ifdef WITH_VULKAN_BACKEND
case GHOST_kDrawingContextTypeVulkan: {
/* Create new off-screen surface only for vulkan. */
wl_surface *wl_surface = wl_compositor_create_surface(wl_compositor_get());
GHOST_Context *context = new GHOST_ContextVK(context_params_offscreen,
GHOST_kVulkanPlatformWayland,
0,
nullptr,
wl_surface,
display_->wl.display,
nullptr,
1,
2,
gpu_settings.preferred_device);
if (context->initializeDrawingContext()) {
context->setUserData(wl_surface);
return context;
}
delete context;
if (wl_surface) {
wl_surface_destroy(wl_surface);
}
return nullptr;
}
#endif /* WITH_VULKAN_BACKEND */
#ifdef WITH_OPENGL_BACKEND
case GHOST_kDrawingContextTypeOpenGL: {
/* Create new off-screen window. */
wl_surface *wl_surface = wl_compositor_create_surface(wl_compositor_get());
wl_egl_window *egl_window = wl_surface ? wl_egl_window_create(wl_surface, 1, 1) : nullptr;
for (int minor = 6; minor >= 3; --minor) {
/* Caller must lock `system->server_mutex`. */
GHOST_Context *context = new GHOST_ContextEGL(
this,
context_params_offscreen,
EGLNativeWindowType(egl_window),
EGLNativeDisplayType(display_->wl.display),
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
4,
minor,
GHOST_OPENGL_EGL_CONTEXT_FLAGS |
(context_params_offscreen.is_debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0),
GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
EGL_OPENGL_API);
if (context->initializeDrawingContext()) {
wl_surface_set_user_data(wl_surface, egl_window);
context->setUserData(wl_surface);
return context;
}
delete 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;
}
#endif /* WITH_OPENGL_BACKEND */
default: {
/* Unsupported backend. */
return nullptr;
}
}
}
GHOST_TSuccess GHOST_SystemWayland::disposeContext(GHOST_IContext *context)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*server_mutex};
#endif
GHOST_TDrawingContextType type = GHOST_kDrawingContextTypeNone;
#ifdef WITH_OPENGL_BACKEND
if (dynamic_cast<GHOST_ContextEGL *>(context)) {
type = GHOST_kDrawingContextTypeOpenGL;
}
#endif /* WITH_OPENGL_BACKEND */
#ifdef WITH_VULKAN_BACKEND
if (dynamic_cast<GHOST_ContextVK *>(context)) {
type = GHOST_kDrawingContextTypeVulkan;
}
#endif /* WITH_VULKAN_BACKEND */
wl_surface *wl_surface = static_cast<struct wl_surface *>(
(static_cast<GHOST_Context *>(context))->getUserData());
/* Delete the context before the window so the context is able to release
* native resources (such as the #EGLSurface) before WAYLAND frees them. */
delete context;
#ifdef WITH_OPENGL_BACKEND
if (type == GHOST_kDrawingContextTypeOpenGL) {
wl_egl_window *egl_window = static_cast<wl_egl_window *>(wl_surface_get_user_data(wl_surface));
if (egl_window != nullptr) {
wl_egl_window_destroy(egl_window);
}
}
#endif /* WITH_OPENGL_BACKEND */
wl_surface_destroy(wl_surface);
(void)type; /* Maybe unused. */
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_GPUSettings gpu_settings,
const bool exclusive,
const bool is_dialog,
const GHOST_IWindow *parent_window)
{
const GHOST_ContextParams context_params = GHOST_CONTEXT_PARAMS_FROM_GPU_SETTINGS(gpu_settings);
/* Globally store pointer to window manager. */
GHOST_WindowWayland *window = new GHOST_WindowWayland(this,
title,
left,
top,
width,
height,
state,
parent_window,
gpu_settings.context_type,
is_dialog,
context_params,
exclusive,
gpu_settings.preferred_device);
if (window) {
if (window->getValid()) {
window_manager_->addWindow(window);
window_manager_->setActiveWindow(window);
const uint64_t event_ms = getMilliSeconds();
pushEvent(new GHOST_Event(event_ms, GHOST_kEventWindowSize, window));
}
else {
delete window;
window = nullptr;
}
}
return window;
}
static bool cursor_is_software(const GHOST_TGrabCursorMode mode, const bool use_software_confine)
{
if (mode == GHOST_kGrabWrap) {
return true;
}
#ifdef USE_GNOME_CONFINE_HACK
if (mode == GHOST_kGrabNormal) {
if (use_software_confine) {
return true;
}
}
#else
(void)use_software_confine;
#endif
return false;
}
GHOST_TSuccess GHOST_SystemWayland::cursor_shape_set(const GHOST_TStandardCursor shape)
{
/* Caller must lock `server_mutex`. */
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
if (!display_->wp.cursor_shape_manager) {
return GHOST_kFailure;
}
const std::optional<wp_cursor_shape_device_v1_shape> wl_shape =
gwl_seat_cursor_find_wl_shape_from_ghost(shape);
if (wl_shape == std::nullopt) {
return GHOST_kFailure;
}
if (seat->wl.pointer) {
/* Set cursor for the pointer device. */
if (seat->cursor.shape.device == nullptr) {
seat->cursor.shape.device = wp_cursor_shape_manager_v1_get_pointer(
display_->wp.cursor_shape_manager, seat->wl.pointer);
}
if (seat->cursor.shape.device) {
if (seat->cursor.is_hardware) {
wp_cursor_shape_device_v1_set_shape(
seat->cursor.shape.device, seat->pointer.serial, *wl_shape);
}
}
/* Set this to make sure we remember which shape we set when unhiding cursors. */
seat->cursor.shape.enum_id = *wl_shape;
GWL_Cursor *cursor = &seat->cursor;
cursor->visible = true;
cursor->is_custom = false;
}
/* Set cursor for all tablet tool devices. */
for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) {
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(
zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
if (tablet_tool->shape.device == nullptr) {
tablet_tool->shape.device = wp_cursor_shape_manager_v1_get_tablet_tool_v2(
display_->wp.cursor_shape_manager, zwp_tablet_tool_v2);
}
if (tablet_tool->shape.device) {
if (seat->cursor.is_hardware) {
wp_cursor_shape_device_v1_set_shape(
tablet_tool->shape.device, tablet_tool->serial, *wl_shape);
}
}
/* Set this to make sure we remember which shape we set when unhiding cursors. */
tablet_tool->shape.enum_id = *wl_shape;
}
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::cursor_shape_check(const GHOST_TStandardCursor cursor_shape)
{
/* No need to lock `server_mutex`. */
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
if (!display_->wp.cursor_shape_manager) {
return GHOST_kFailure;
}
const std::optional<wp_cursor_shape_device_v1_shape> wl_shape =
gwl_seat_cursor_find_wl_shape_from_ghost(cursor_shape);
if (wl_shape == std::nullopt) {
return GHOST_kFailure;
}
return GHOST_kSuccess;
}
static wl_buffer *ghost_wl_buffer_from_cursor_generator(const GHOST_CursorGenerator &cg,
wl_shm *shm,
void **buffer_data_p,
size_t *buffer_data_size_p,
const int cursor_size,
const int cursor_size_max,
const int use_dark_theme,
const int scale,
int r_bitmap_size[2],
int r_hot_spot[2])
{
if (*buffer_data_p) {
munmap(*buffer_data_p, *buffer_data_size_p);
*buffer_data_p = nullptr;
*buffer_data_size_p = 0; /* Not needed, but the value is no longer meaningful. */
}
int bitmap_size_src[2];
int hot_spot[2];
bool can_invert_color = false;
uint8_t *bitmap_src = cg.generate_fn(
&cg,
cursor_size,
cursor_size_max,
[](size_t size) -> uint8_t * { return new uint8_t[size]; },
bitmap_size_src,
hot_spot,
&can_invert_color);
if (bitmap_src == nullptr) {
return nullptr;
}
const bool invert_color = can_invert_color && use_dark_theme;
/* There is no need to adjust the hot-spot when resizing. */
int bitmap_size_dst[2] = {
int(round_up_uint(bitmap_size_src[0], scale)),
int(round_up_uint(bitmap_size_src[1], scale)),
};
wl_buffer *buffer = ghost_wl_buffer_create_for_image(
shm, bitmap_size_dst, WL_SHM_FORMAT_ARGB8888, buffer_data_p, buffer_data_size_p);
if (buffer != nullptr) {
const bool is_trivial_copy = (bitmap_size_src[0] == bitmap_size_dst[0]) &&
(bitmap_size_src[1] == bitmap_size_dst[1]);
/* NOTE: the copy could be skipped in trivial cases.
* Since it's such a small amount of data it hardly seems worth it. */
if (is_trivial_copy) {
/* RGBA color. */
const uint32_t *px_src = reinterpret_cast<const uint32_t *>(bitmap_src);
uint32_t *px_dst = static_cast<uint32_t *>(*buffer_data_p);
if (invert_color) {
for (int y = 0; y < bitmap_size_src[1]; y++) {
for (int x = 0; x < bitmap_size_src[0]; x++) {
*px_dst++ = rgba_straight_to_premul_inverted(*px_src++);
}
}
}
else {
for (int y = 0; y < bitmap_size_src[1]; y++) {
for (int x = 0; x < bitmap_size_src[0]; x++) {
*px_dst++ = rgba_straight_to_premul(*px_src++);
}
}
}
}
else {
/* RGBA color, copy into an expanded buffer. */
const uint32_t *px_src = reinterpret_cast<const uint32_t *>(bitmap_src);
uint32_t *px_dst = static_cast<uint32_t *>(*buffer_data_p);
if (invert_color) {
for (int y = 0; y < bitmap_size_dst[1]; y++) {
for (int x = 0; x < bitmap_size_dst[0]; x++) {
*px_dst++ = (x >= bitmap_size_src[0] || y >= bitmap_size_src[1]) ?
0x0 :
rgba_straight_to_premul_inverted(*px_src++);
}
}
}
else {
for (int y = 0; y < bitmap_size_dst[1]; y++) {
for (int x = 0; x < bitmap_size_dst[0]; x++) {
*px_dst++ = (x >= bitmap_size_src[0] || y >= bitmap_size_src[1]) ?
0x0 :
rgba_straight_to_premul(*px_src++);
}
}
}
}
r_bitmap_size[0] = bitmap_size_dst[0];
r_bitmap_size[1] = bitmap_size_dst[1];
r_hot_spot[0] = hot_spot[0];
r_hot_spot[1] = hot_spot[1];
}
delete[] bitmap_src;
return buffer;
}
GHOST_TSuccess GHOST_SystemWayland::cursor_shape_custom_set(const GHOST_CursorGenerator &cg)
{
/* Caller needs to lock `server_mutex`. */
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
/* If we were using a wayland cursor shape, be sure to free it up before we try to use any
* custom shapes. */
if (seat->cursor.shape.device) {
wp_cursor_shape_device_v1_destroy(seat->cursor.shape.device);
seat->cursor.shape.device = nullptr;
}
/* If we were using a wayland cursor shape, be sure to free it for the tablet tools
* before we try to use any custom shapes. */
for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) {
GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(
zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
if (tablet_tool->shape.device) {
wp_cursor_shape_device_v1_destroy(tablet_tool->shape.device);
tablet_tool->shape.device = nullptr;
}
}
/* Generate the buffer. */
GWL_Cursor &cursor = seat->cursor;
int bitmap_size[2] = {0, 0};
int hot_spot[2] = {0, 0};
const int scale = gwl_seat_cursor_buffer_scale_calc(seat);
const int cursor_size = seat->cursor.theme_size * scale;
/* NOTE: Regarding the maximum cursor size.
* - 256 seems to be a hardware limit.
* - Twice the cursor size to allow "text" cursor to display wider than a typical cursor
* without being *very* large - as that looks strange.
*/
const int cursor_size_max = std::min(256, cursor_size * 2);
wl_buffer *buffer = ghost_wl_buffer_from_cursor_generator(cg,
display_->wl.shm,
&cursor.custom_data,
&cursor.custom_data_size,
cursor_size,
cursor_size_max,
cursor.use_dark_theme,
scale,
bitmap_size,
hot_spot);
if (UNLIKELY(buffer == nullptr)) {
return GHOST_kFailure;
}
wl_buffer_add_listener(buffer, &cursor_buffer_listener, &cursor);
cursor.visible = true;
cursor.is_custom = true;
cursor.wl.buffer = buffer;
cursor.wl.image.width = uint32_t(bitmap_size[0]);
cursor.wl.image.height = uint32_t(bitmap_size[1]);
cursor.wl.image.hotspot_x = uint32_t(hot_spot[0]);
cursor.wl.image.hotspot_y = uint32_t(hot_spot[1]);
gwl_seat_cursor_buffer_set_current(seat);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::cursor_bitmap_get(GHOST_CursorBitmapRef *bitmap)
{
/* Caller must lock `server_mutex`. */
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
GWL_Cursor *cursor = &seat->cursor;
if (cursor->custom_data == nullptr) {
return GHOST_kFailure;
}
if (!cursor->is_custom) {
return GHOST_kFailure;
}
bitmap->data_size[0] = cursor->wl.image.width;
bitmap->data_size[1] = cursor->wl.image.height;
bitmap->hot_spot[0] = cursor->wl.image.hotspot_x;
bitmap->hot_spot[1] = cursor->wl.image.hotspot_y;
bitmap->data = static_cast<const uint8_t *>(cursor->custom_data);
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_SystemWayland::cursor_visibility_set(const bool visible)
{
/* Caller must lock `server_mutex`. */
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return GHOST_kFailure;
}
gwl_seat_cursor_visible_set(seat, visible, seat->cursor.is_hardware, CURSOR_VISIBLE_ALWAYS_SET);
return GHOST_kSuccess;
}
GHOST_TCapabilityFlag GHOST_SystemWayland::getCapabilities() const
{
/* It's possible there are no seats, ignore the value in this case. */
GHOST_ASSERT(((gwl_display_seat_active_get(display_) == nullptr) ||
(has_wl_trackpad_physical_direction != -1)),
"The trackpad direction was expected to be initialized");
return GHOST_TCapabilityFlag(
GHOST_CAPABILITY_FLAG_ALL &
/* NOTE: order the following flags as they they're declared in the source. */
~(
/* WAYLAND doesn't support accessing the window position. */
GHOST_kCapabilityWindowPosition |
/* WAYLAND doesn't support setting the cursor position directly,
* this is an intentional choice, forcing us to use a software cursor in this case. */
GHOST_kCapabilityCursorWarp |
/* Some drivers don't support front-buffer reading, see: #98462 & #106264.
*
* NOTE(@ideasman42): the EGL flag `EGL_BUFFER_PRESERVED` is intended request support for
* front-buffer reading however in my tests requesting the flag didn't work with AMD,
* and it's not even requirement - so we can't rely on this feature being supported.
*
* Instead of assuming this is not supported, the graphics card driver could be inspected
* (enable for NVIDIA for example), but the advantage in supporting this is minimal.
* In practice it means an off-screen buffer is used to redraw the window for the
* screen-shot and eye-dropper sampling logic, both operations where the overhead
* is negligible. */
GHOST_kCapabilityGPUReadFrontBuffer |
/* This WAYLAND back-end has not yet implemented desktop color sample. */
GHOST_kCapabilityDesktopSample |
/* This flag will eventually be removed when support
* for the old track-pad protocol is dropped. */
((has_wl_trackpad_physical_direction == 1) ?
0 :
GHOST_kCapabilityTrackpadPhysicalDirection) |
/* This WAYLAND back-end doesn't have support for window decoration styles.
* In all likelihood, this back-end will eventually need to support client-side
* decorations, see #113795. */
GHOST_kCapabilityWindowDecorationStyles));
}
bool GHOST_SystemWayland::cursor_grab_use_software_display_get(const GHOST_TGrabCursorMode mode)
{
/* Caller must lock `server_mutex`. */
const 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_get() <= 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;
}
void GHOST_SystemWayland::setMultitouchGestures(const bool use)
{
if (multitouch_gestures_ == use) {
return;
}
multitouch_gestures_ = use;
#ifdef USE_EVENT_BACKGROUND_THREAD
/* Ensure this listeners aren't removed while events are generated. */
std::lock_guard lock_server_guard{*server_mutex};
#endif
for (GWL_Seat *seat : display_->seats) {
if (use == gwl_seat_capability_pointer_multitouch_check(seat, use)) {
continue;
}
if (use) {
gwl_seat_capability_pointer_multitouch_enable(seat);
}
else {
gwl_seat_capability_pointer_multitouch_disable(seat);
}
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \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 wl_output *wl_output)
{
const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_output);
return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) == &ghost_wl_output_tag_id;
}
bool ghost_wl_surface_own(const wl_surface *wl_surface)
{
const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_surface);
return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) == &ghost_wl_surface_tag_id;
}
bool ghost_wl_surface_own_with_null_check(const wl_surface *wl_surface)
{
return wl_surface && ghost_wl_surface_own(wl_surface);
}
bool ghost_wl_surface_own_cursor_pointer(const wl_surface *wl_surface)
{
const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_surface);
return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) ==
&ghost_wl_surface_cursor_pointer_tag_id;
}
bool ghost_wl_surface_own_cursor_tablet(const wl_surface *wl_surface)
{
const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_surface);
return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) == &ghost_wl_surface_cursor_tablet_tag_id;
}
void ghost_wl_output_tag(wl_output *wl_output)
{
wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_output);
wl_proxy_set_tag(proxy, &ghost_wl_output_tag_id);
}
void ghost_wl_surface_tag(wl_surface *wl_surface)
{
wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_surface);
wl_proxy_set_tag(proxy, &ghost_wl_surface_tag_id);
}
void ghost_wl_surface_tag_cursor_pointer(wl_surface *wl_surface)
{
wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_surface);
wl_proxy_set_tag(proxy, &ghost_wl_surface_cursor_pointer_tag_id);
}
void ghost_wl_surface_tag_cursor_tablet(wl_surface *wl_surface)
{
wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_surface);
wl_proxy_set_tag(proxy, &ghost_wl_surface_cursor_tablet_tag_id);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Direct Data Access
*
* Expose some members via methods.
* \{ */
wl_display *GHOST_SystemWayland::wl_display_get()
{
return display_->wl.display;
}
wl_compositor *GHOST_SystemWayland::wl_compositor_get()
{
return display_->wl.compositor;
}
zwp_primary_selection_device_manager_v1 *GHOST_SystemWayland::wp_primary_selection_manager_get()
{
return display_->wp.primary_selection_device_manager;
}
xdg_activation_v1 *GHOST_SystemWayland::xdg_activation_manager_get()
{
return display_->xdg.activation_manager;
}
wp_fractional_scale_manager_v1 *GHOST_SystemWayland::wp_fractional_scale_manager_get()
{
return display_->wp.fractional_scale_manager;
}
wp_viewporter *GHOST_SystemWayland::wp_viewporter_get()
{
return display_->wp.viewporter;
}
zwp_pointer_gestures_v1 *GHOST_SystemWayland::wp_pointer_gestures_get()
{
return display_->wp.pointer_gestures;
}
/* This value is expected to match the base name of the `.desktop` file. see #101805.
*
* NOTE: the XDG desktop-entry-spec defines that this should follow the "reverse DNS" convention.
* For example `org.blender.Blender` - however the `.desktop` file distributed with Blender is
* simply called `blender.desktop`, so the it's important to follow that name.
* Other distributions such as SNAP & FLATPAK may need to change this value #101779.
* Currently there isn't a way to configure this, we may want to support that. */
static const char *ghost_wl_app_id = (
#ifdef WITH_GHOST_WAYLAND_APP_ID
STRINGIFY(WITH_GHOST_WAYLAND_APP_ID)
#else
"blender"
#endif
);
const char *GHOST_SystemWayland::xdg_app_id_get()
{
return ghost_wl_app_id;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
libdecor *GHOST_SystemWayland::libdecor_context_get()
{
return display_->libdecor->context;
}
#endif /* !WITH_GHOST_WAYLAND_LIBDECOR */
xdg_wm_base *GHOST_SystemWayland::xdg_decor_shell_get()
{
return display_->xdg_decor->shell;
}
zxdg_decoration_manager_v1 *GHOST_SystemWayland::xdg_decor_manager_get()
{
return display_->xdg_decor->manager;
}
/* End `xdg_decor`. */
const std::vector<GWL_Output *> &GHOST_SystemWayland::outputs_get() const
{
return display_->outputs;
}
wl_shm *GHOST_SystemWayland::wl_shm_get() const
{
return display_->wl.shm;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_TimerManager *GHOST_SystemWayland::ghost_timer_manager()
{
return display_->ghost_timer_manager;
}
#endif
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Text Input (IME) Functions
*
* Functionality only used for the WAYLAND implementation.
* \{ */
#ifdef WITH_INPUT_IME
void GHOST_SystemWayland::ime_begin(const GHOST_WindowWayland *win,
const int32_t x,
const int32_t y,
const int32_t w,
const int32_t h,
const bool completed) const
{
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return;
}
if (seat->wp.text_input == nullptr) {
return;
}
/* Prevent a feedback loop because the commits from this function cause
* #zwp_text_input_v3_listener::preedit_string to run again which sends an event,
* refreshing the position, running this function again. */
gwl_seat_ime_result_reset(seat);
/* Don't re-enable if we're already enabled. */
if (seat->ime.is_enabled && completed) {
return;
}
bool force_rect_update = false;
if (seat->ime.is_enabled == false) {
seat->ime.has_preedit = false;
seat->ime.is_enabled = true;
zwp_text_input_v3_enable(seat->wp.text_input);
zwp_text_input_v3_commit(seat->wp.text_input);
/* Now that it's enabled, set the input properties. */
zwp_text_input_v3_set_content_type(seat->wp.text_input,
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
gwl_seat_ime_rect_reset(seat);
force_rect_update = true;
}
if ((force_rect_update == false) && /* Was just created, always update. */
(seat->ime.rect.x == x) && /* X. */
(seat->ime.rect.y == y) && /* Y. */
(seat->ime.rect.w == w) && /* W. */
(seat->ime.rect.h == h)) /* H. */
{
/* Only re-update the rectangle as needed. */
}
else {
const int rect_x = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(x)));
const int rect_y = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(y)));
const int rect_w = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(w))) + 1;
const int rect_h = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(h))) + 1;
zwp_text_input_v3_set_cursor_rectangle(seat->wp.text_input, rect_x, rect_y, rect_w, rect_h);
zwp_text_input_v3_commit(seat->wp.text_input);
seat->ime.rect.x = x;
seat->ime.rect.y = y;
seat->ime.rect.w = w;
seat->ime.rect.h = h;
}
}
void GHOST_SystemWayland::ime_end(const GHOST_WindowWayland * /*window*/) const
{
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return;
}
seat->ime.is_enabled = false;
gwl_seat_ime_rect_reset(seat);
if (seat->wp.text_input == nullptr) {
return;
}
zwp_text_input_v3_disable(seat->wp.text_input);
zwp_text_input_v3_commit(seat->wp.text_input);
}
#endif /* WITH_INPUT_IME */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Query Access
* \{ */
GWL_Output *ghost_wl_output_user_data(wl_output *wl_output)
{
GHOST_ASSERT(wl_output, "output must not be nullptr");
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(wl_surface *wl_surface)
{
GHOST_ASSERT(wl_surface, "wl_surface must not be nullptr");
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.
* \{ */
uint64_t GHOST_SystemWayland::ms_from_input_time(const uint32_t timestamp_as_uint)
{
/* NOTE(@ideasman42): Return a time compatible with `getMilliSeconds()`,
* this is needed as WAYLAND time-stamps don't have a well defined beginning
* use `timestamp_as_uint` to calculate an offset which is applied to future events.
* This is updated because time may have passed between generating the time-stamp and `now`.
* The method here is used by SDL. */
uint64_t timestamp = uint64_t(timestamp_as_uint);
GWL_DisplayTimeStamp &input_timestamp = display_->input_timestamp;
if (UNLIKELY(timestamp_as_uint < input_timestamp.last)) {
/* NOTE(@ideasman42): Sometimes event times are out of order,
* while this should _never_ happen, it occasionally does:
* - When resizing the window then clicking on the window with GNOME+LIBDECOR.
* - With accepting IME text with GNOME-v45.2 the timestamp is in seconds, see:
* https://gitlab.gnome.org/GNOME/mutter/-/issues/3214
* Accept events must occur within ~25 days, out-of-order time-stamps above this time-frame
* will be treated as a wrapped integer. */
if (input_timestamp.last - timestamp_as_uint > std::numeric_limits<uint32_t>::max() / 2) {
/* Finally check to avoid invalid rollover,
* ensure the rolled over time is closer to "now" than it is currently. */
const uint64_t offset_test = input_timestamp.offset +
uint64_t(std::numeric_limits<uint32_t>::max()) + 1;
const uint64_t now = getMilliSeconds();
if (sub_abs_u64(now, timestamp + offset_test) <
sub_abs_u64(now, timestamp + input_timestamp.offset))
{
/* 32-bit timer rollover, bump the offset. */
input_timestamp.offset = offset_test;
}
}
}
input_timestamp.last = timestamp_as_uint;
if (input_timestamp.exact_match) {
timestamp += input_timestamp.offset;
}
else {
const uint64_t now = getMilliSeconds();
const uint32_t now_as_uint32 = uint32_t(now);
if (now_as_uint32 == timestamp_as_uint) {
input_timestamp.exact_match = true;
/* For systems with up times exceeding 47 days
* it's possible we need to begin with an offset. */
input_timestamp.offset = now - uint64_t(now_as_uint32);
timestamp = now;
}
else {
if (!input_timestamp.offset) {
input_timestamp.offset = (now - timestamp);
}
timestamp += input_timestamp.offset;
if (timestamp > now) {
input_timestamp.offset -= (timestamp - now);
timestamp = now;
}
}
}
return timestamp;
}
GHOST_TSuccess GHOST_SystemWayland::pushEvent_maybe_pending(const GHOST_IEvent *event)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_ASSERT(!display_->background, "Foreground only");
if (main_thread_id != std::this_thread::get_id()) {
std::lock_guard lock{display_->events_pending_mutex};
display_->events_pending.push_back(event);
return GHOST_kSuccess;
}
#endif
return pushEvent(event);
}
void GHOST_SystemWayland::seat_active_set(const GWL_Seat *seat)
{
gwl_display_seat_active_set(display_, seat);
}
wl_seat *GHOST_SystemWayland::wl_seat_active_get_with_input_serial(uint32_t &serial)
{
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return nullptr;
}
serial = seat->data_source_serial;
return seat->wl.seat;
}
bool GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface)
{
bool changed = false;
#define SURFACE_CLEAR_PTR(surface_test) \
if (surface_test == wl_surface) { \
surface_test = nullptr; \
changed = true; \
} \
((void)0);
/* Only clear window surfaces (not cursors, off-screen surfaces etc). */
for (GWL_Seat *seat : display_->seats) {
SURFACE_CLEAR_PTR(seat->pointer.wl.surface_window);
SURFACE_CLEAR_PTR(seat->tablet.wl.surface_window);
SURFACE_CLEAR_PTR(seat->keyboard.wl.surface_window);
SURFACE_CLEAR_PTR(seat->wl.surface_window_focus_dnd);
#ifdef WITH_INPUT_IME
SURFACE_CLEAR_PTR(seat->ime.surface_window);
#endif
}
#undef SURFACE_CLEAR_PTR
return changed;
}
bool GHOST_SystemWayland::output_unref(wl_output *wl_output)
{
bool changed = false;
if (!ghost_wl_output_own(wl_output)) {
return changed;
}
/* NOTE: keep in sync with `output_scale_update`. */
GWL_Output *output = ghost_wl_output_user_data(wl_output);
const GHOST_WindowManager *window_manager = getWindowManager();
if (window_manager) {
for (GHOST_IWindow *iwin : window_manager->getWindows()) {
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
if (win->outputs_leave(output)) {
win->outputs_changed_update_scale_tag();
changed = true;
}
}
}
for (GWL_Seat *seat : display_->seats) {
if (seat->pointer.outputs.erase(output)) {
changed = true;
}
if (seat->tablet.outputs.erase(output)) {
changed = true;
}
}
return changed;
}
void GHOST_SystemWayland::output_scale_update(GWL_Output *output)
{
/* NOTE: keep in sync with `output_unref`. */
const GHOST_WindowManager *window_manager = getWindowManager();
if (window_manager) {
for (GHOST_IWindow *iwin : window_manager->getWindows()) {
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
const std::vector<GWL_Output *> &outputs = win->outputs_get();
if (!(std::find(outputs.begin(), outputs.end(), output) == outputs.cend())) {
win->outputs_changed_update_scale_tag();
}
}
}
for (GWL_Seat *seat : display_->seats) {
if (seat->pointer.outputs.count(output)) {
update_cursor_scale(seat, seat->cursor, &seat->pointer);
}
if (seat->tablet.outputs.count(output)) {
update_cursor_scale(seat, seat->cursor, &seat->tablet);
}
}
}
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 GWL_WindowScaleParams &scale_params)
{
/* Caller must lock `server_mutex`. */
/* Ignore, if the required protocols are not supported. */
if (UNLIKELY(!display_->wp.relative_pointer_manager || !display_->wp.pointer_constraints)) {
return false;
}
GWL_Seat *seat = gwl_display_seat_active_get(display_);
if (UNLIKELY(!seat)) {
return false;
}
/* No change, success. */
if (mode == mode_current) {
return true;
}
#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 GWL_SeatStateGrab grab_state_prev = seat_grab_state_from_mode(mode_current,
was_software_confine);
const GWL_SeatStateGrab grab_state_next = seat_grab_state_from_mode(mode, use_software_confine);
/* Check for wrap as #GHOST_kCapabilityCursorWarp 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. */
gwl_seat_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. */
wl_fixed_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. */
wl_fixed_t xy_next[2] = {UNPACK2(seat->pointer.xy)};
GHOST_Rect bounds_scale;
bounds_scale.l_ = gwl_window_scale_wl_fixed_from(scale_params,
wl_fixed_from_int(wrap_bounds->l_));
bounds_scale.t_ = gwl_window_scale_wl_fixed_from(scale_params,
wl_fixed_from_int(wrap_bounds->t_));
bounds_scale.r_ = gwl_window_scale_wl_fixed_from(scale_params,
wl_fixed_from_int(wrap_bounds->r_));
bounds_scale.b_ = gwl_window_scale_wl_fixed_from(scale_params,
wl_fixed_from_int(wrap_bounds->b_));
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) {
const wl_fixed_t xy_next[2] = {
gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(init_grab_xy[0])),
gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(init_grab_xy[1])),
};
if ((init_grab_xy[0] != seat->grab_lock_xy[0]) ||
(init_grab_xy[1] != seat->grab_lock_xy[1]))
{
zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp.locked_pointer,
UNPACK2(xy_next));
wl_surface_commit(wl_surface);
/* NOTE(@ideasman42): 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;
}
else if (grab_state_prev.use_lock) {
/* NOTE(@ideasman42): From WAYLAND's perspective the cursor did not move.
* The application will have received "hidden" events to warped locations.
* So generate event without setting the cursor position hint. */
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) {
/* Caller has no time-stamp. */
const uint64_t event_ms = getMilliSeconds();
seat->system->pushEvent_maybe_pending(new GHOST_EventCursor(
event_ms,
GHOST_kEventCursorMove,
ghost_wl_surface_user_data(wl_surface),
wl_fixed_to_int(gwl_window_scale_wl_fixed_to(scale_params, xy_motion[0])),
wl_fixed_to_int(gwl_window_scale_wl_fixed_to(scale_params, 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) {
/* As WAYLAND does not support setting the cursor coordinates programmatically,
* #GHOST_kGrabWrap cannot be supported by positioning the cursor directly.
* Instead the cursor is locked in place, using a software cursor that is warped.
* Then WAYLAND's #zwp_locked_pointer_v1_set_cursor_position_hint is used to restore
* the cursor to the warped location. */
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(
gwl_window_scale_wl_fixed_to(scale_params, seat->pointer.xy[0]));
init_grab_xy[1] = wl_fixed_to_int(
gwl_window_scale_wl_fixed_to(scale_params, 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. */
gwl_seat_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 true;
}
#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`. */
# ifdef WITH_OPENGL_BACKEND
wayland_dynload_egl_init(verbose) /* `libwayland-egl`. */
# else
true
# endif
)
{
# 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();
# ifdef WITH_OPENGL_BACKEND
wayland_dynload_egl_exit();
# endif
return false;
}
void ghost_wl_dynload_libraries_exit()
{
wayland_dynload_client_exit();
wayland_dynload_cursor_exit();
# ifdef WITH_OPENGL_BACKEND
wayland_dynload_egl_exit();
# endif
# ifdef WITH_GHOST_WAYLAND_LIBDECOR
wayland_dynload_libdecor_exit();
# endif
}
#endif /* WITH_GHOST_WAYLAND_DYNLOAD */
/** \} */