diff options
Diffstat (limited to 'intern/ghost/intern/GHOST_SystemWayland.cpp')
-rw-r--r-- | intern/ghost/intern/GHOST_SystemWayland.cpp | 3544 |
1 files changed, 2784 insertions, 760 deletions
diff --git a/intern/ghost/intern/GHOST_SystemWayland.cpp b/intern/ghost/intern/GHOST_SystemWayland.cpp index 24d3f525c97..d7520f1243f 100644 --- a/intern/ghost/intern/GHOST_SystemWayland.cpp +++ b/intern/ghost/intern/GHOST_SystemWayland.cpp @@ -13,10 +13,19 @@ #include "GHOST_EventWheel.h" #include "GHOST_TimerManager.h" #include "GHOST_WindowManager.h" +#include "GHOST_utildefines.h" #include "GHOST_ContextEGL.h" +#ifdef WITH_GHOST_WAYLAND_DYNLOAD +# include <wayland_dynload_API.h> /* For `ghost_wl_dynload_libraries`. */ +#endif + #include <EGL/egl.h> + +#ifdef WITH_GHOST_WAYLAND_DYNLOAD +# include <wayland_dynload_egl.h> +#endif #include <wayland-egl.h> #include <algorithm> @@ -26,22 +35,63 @@ #include <unordered_map> #include <unordered_set> -#include "GHOST_WaylandCursorSettings.h" -#include <pointer-constraints-client-protocol.h> -#include <relative-pointer-client-protocol.h> +#ifdef WITH_GHOST_WAYLAND_DYNLOAD +# include <wayland_dynload_cursor.h> +#endif #include <wayland-cursor.h> + +#include "GHOST_WaylandCursorSettings.h" + #include <xkbcommon/xkbcommon.h> +/* Generated by `wayland-scanner`. */ +#include <pointer-constraints-unstable-v1-client-protocol.h> +#include <relative-pointer-unstable-v1-client-protocol.h> +#include <tablet-unstable-v2-client-protocol.h> +#include <xdg-output-unstable-v1-client-protocol.h> + #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <cstring> +#include <mutex> + +/* Logging, use `ghost.wl.*` prefix. */ +#include "CLG_log.h" + +static void keyboard_handle_key_repeat_cancel(struct input_t *input); + +static void output_handle_done(void *data, struct wl_output *wl_output); /** + * GNOME (mutter 42.2 had a bug with confine not respecting scale - Hi-DPI), See: T98793. + * Even though this has been fixed, at time of writing it's not yet in a release. + * Workaround the problem by implementing confine with a software cursor. + * While this isn't ideal, it's not adding a lot of overhead as software + * cursors are already used for warping (which WAYLAND doesn't support). + */ +#define USE_GNOME_CONFINE_HACK +/** + * Always use software confine (not just in GNOME). + * Useful for developing with compositors that don't need this workaround. + */ +// #define USE_GNOME_CONFINE_HACK_ALWAYS_ON + +#ifdef USE_GNOME_CONFINE_HACK +static bool use_gnome_confine_hack = false; +#endif + +/* -------------------------------------------------------------------- */ +/** \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 + * only available in more recent kernel versions. + * \{ */ + +/** + * The event codes are used to * to differentiate from which mouse button an event comes from. */ #define BTN_LEFT 0x110 @@ -53,167 +103,394 @@ #define BTN_BACK 0x116 // #define BTN_TASK 0x117 /* UNUSED. */ -struct buffer_t { - void *data; - size_t size; -}; +/** + * Tablet events. + */ +#define BTN_STYLUS 0x14b /* Use as right-mouse. */ +#define BTN_STYLUS2 0x14c /* Use as middle-mouse. */ +/* NOTE(@campbellbarton): Map to an additional button (not sure which hardware uses this). */ +#define BTN_STYLUS3 0x149 + +/** + * Keyboard scan-codes. + */ +#define KEY_GRAVE 41 + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Private Types & Defines + * \{ */ + +/** + * From XKB internals, use for converting a scan-code from WAYLAND to a #xkb_keycode_t. + * Ideally this wouldn't need a local define. + */ +#define EVDEV_OFFSET 8 struct cursor_t { - bool visible; - struct wl_surface *surface = nullptr; - struct wl_buffer *buffer; - struct wl_cursor_image image; - struct buffer_t *file_buffer = nullptr; - struct wl_cursor_theme *theme = nullptr; - int size; + bool visible = false; + /** + * When false, hide the hardware cursor, while the cursor is still considered to be `visible`, + * since the grab-mode determines the state of the software cursor, + * this may change - removing the need for a software cursor and in this case it's important + * the hardware cursor is used. + */ + bool is_hardware = true; + bool is_custom = false; + struct wl_surface *wl_surface = nullptr; + struct wl_buffer *wl_buffer = nullptr; + struct wl_cursor_image wl_image = {0}; + struct wl_cursor_theme *wl_theme = nullptr; + void *custom_data = nullptr; + size_t custom_data_size = 0; + int size = 0; std::string theme_name; - // outputs on which the cursor is visible - std::unordered_set<const output_t *> outputs; - int scale = 1; + + int custom_scale = 1; +}; + +/** + * A single tablet can have multiple tools (pen, eraser, brush... etc). + * WAYLAND exposes tools via #zwp_tablet_tool_v2. + * Since are no API's to access properties of the tool, store the values here. + */ +struct tablet_tool_input_t { + struct input_t *input = nullptr; + struct wl_surface *cursor_surface = nullptr; + /** Used to delay clearing tablet focused surface until the frame is handled. */ + bool proximity = false; + + GHOST_TabletData data = GHOST_TABLET_DATA_NONE; }; struct data_offer_t { std::unordered_set<std::string> types; - uint32_t source_actions; - uint32_t dnd_action; - struct wl_data_offer *id; - std::atomic<bool> in_use; + uint32_t source_actions = 0; + uint32_t dnd_action = 0; + struct wl_data_offer *id = nullptr; + std::atomic<bool> in_use = false; struct { - int x, y; + /** Compatible with #input_t.xy coordinates. */ + wl_fixed_t xy[2] = {0, 0}; } dnd; }; struct data_source_t { - struct wl_data_source *data_source; - /** Last device that was active. */ - uint32_t source_serial; - char *buffer_out; + struct wl_data_source *data_source = nullptr; + char *buffer_out = nullptr; }; +/** + * Data used to implement client-side key-repeat. + * + * \note it's important not to store the target window here + * as it can be closed while the key is repeating, + * instead use the focused keyboard from #intput_t which is cleared when windows are closed. + * Therefor keyboard events must always check the window has not been cleared. + */ struct key_repeat_payload_t { - GHOST_SystemWayland *system; - GHOST_IWindow *window; - GHOST_TKey key; - GHOST_TEventKeyData key_data; + struct input_t *input = nullptr; + + xkb_keycode_t key_code; + + /** + * Don't cache the `utf8_buf` as this changes based on modifiers which may be pressed + * while key repeat is enabled. + */ + struct { + GHOST_TKey gkey; + } key_data; +}; + +/** Internal variables used to track grab-state. */ +struct input_grab_state_t { + bool use_lock = false; + bool use_confine = false; +}; + +/** + * State of the pointing device (tablet or mouse). + */ +struct input_state_pointer_t { + /** + * High precision coordinates. + * + * Mapping to pixels requires the window scale. + * The following example converts these values to screen coordinates. + * \code{.cc} + * const wl_fixed_t scale = win->scale(); + * const int event_xy[2] = { + * wl_fixed_to_int(scale * input_state->xy[0]), + * wl_fixed_to_int(scale * input_state->xy[1]), + * }; + * \endcode + */ + wl_fixed_t xy[2] = {0, 0}; + + /** Outputs on which the cursor is visible. */ + std::unordered_set<const output_t *> outputs; + + int theme_scale = 1; + + /** The serial of the last used pointer or tablet. */ + uint32_t serial = 0; + + /** + * The surface last used with this pointing device + * (events with this pointing device will be sent here). + */ + struct wl_surface *wl_surface = nullptr; + + GHOST_Buttons buttons = GHOST_Buttons(); +}; + +/** + * State of the keyboard. + */ +struct input_state_keyboard_t { + /** The serial of the last used pointer or tablet. */ + uint32_t serial = 0; + + /** + * The surface last used with this pointing device + * (events with this pointing device will be sent here). + */ + struct wl_surface *wl_surface = nullptr; }; struct input_t { - GHOST_SystemWayland *system; + GHOST_SystemWayland *system = nullptr; std::string name; - struct wl_seat *seat; - struct wl_pointer *pointer = nullptr; - struct wl_keyboard *keyboard = nullptr; + struct wl_seat *wl_seat = nullptr; + struct wl_pointer *wl_pointer = nullptr; + struct wl_keyboard *wl_keyboard = nullptr; + struct zwp_tablet_seat_v2 *tablet_seat = nullptr; + + /** All currently active tablet tools (needed for changing the cursor). */ + std::unordered_set<zwp_tablet_tool_v2 *> tablet_tools; + + /** Use to check if the last cursor input was tablet or pointer. */ + uint32_t cursor_source_serial = 0; + + input_state_pointer_t pointer; + + /** Mostly this can be interchanged with `pointer` however it can't be locked/confined. */ + input_state_pointer_t tablet; + + input_state_keyboard_t keyboard; + +#ifdef USE_GNOME_CONFINE_HACK + bool use_pointer_software_confine = false; +#endif + /** The cursor location (in pixel-space) when hidden grab started (#GHOST_kGrabHide). */ + wl_fixed_t grab_lock_xy[2] = {0, 0}; - uint32_t pointer_serial; - int x, y; - GHOST_Buttons buttons; struct cursor_t cursor; - struct zwp_relative_pointer_v1 *relative_pointer; - struct zwp_locked_pointer_v1 *locked_pointer; + struct zwp_relative_pointer_v1 *relative_pointer = nullptr; + struct zwp_locked_pointer_v1 *locked_pointer = nullptr; + struct zwp_confined_pointer_v1 *confined_pointer = nullptr; + + struct xkb_context *xkb_context = nullptr; + + struct xkb_state *xkb_state = nullptr; + /** + * Keep a state with no modifiers active, use for symbol lookups. + */ + struct xkb_state *xkb_state_empty = nullptr; + /** + * Keep a state with number-lock enabled, use to access predictable key-pad symbols. + * If number-lock is not supported by the key-map, this is set to NULL. + */ + struct xkb_state *xkb_state_empty_with_numlock = nullptr; - struct xkb_context *xkb_context; - struct xkb_state *xkb_state; struct { - /* Key repetition in character per second. */ - int32_t rate; - /* Time (milliseconds) after which to start repeating keys. */ - int32_t delay; - /* Timer for key repeats. */ + /** Key repetition in character per second. */ + int32_t rate = 0; + /** Time (milliseconds) after which to start repeating keys. */ + int32_t delay = 0; + /** Timer for key repeats. */ GHOST_ITimerTask *timer = nullptr; } key_repeat; - struct wl_surface *focus_pointer = nullptr; - struct wl_surface *focus_keyboard = nullptr; + struct wl_surface *focus_dnd = nullptr; struct wl_data_device *data_device = nullptr; - struct data_offer_t *data_offer_dnd; /* Drag & Drop. */ - struct data_offer_t *data_offer_copy_paste; /* Copy & Paste. */ + /** Drag & Drop. */ + struct data_offer_t *data_offer_dnd = nullptr; + std::mutex data_offer_dnd_mutex; + + /** Copy & Paste. */ + struct data_offer_t *data_offer_copy_paste = nullptr; + std::mutex data_offer_copy_paste_mutex; - struct data_source_t *data_source; + struct data_source_t *data_source = nullptr; + std::mutex data_source_mutex; + + /** Last device that was active. */ + uint32_t data_source_serial = 0; }; struct display_t { - GHOST_SystemWayland *system; + GHOST_SystemWayland *system = nullptr; - struct wl_display *display; + struct wl_display *display = nullptr; struct wl_compositor *compositor = nullptr; + +#ifdef WITH_GHOST_WAYLAND_LIBDECOR + struct libdecor *decor_context = nullptr; +#else struct xdg_wm_base *xdg_shell = nullptr; struct zxdg_decoration_manager_v1 *xdg_decoration_manager = nullptr; +#endif + + struct zxdg_output_manager_v1 *xdg_output_manager = nullptr; struct wl_shm *shm = nullptr; std::vector<output_t *> outputs; std::vector<input_t *> inputs; - struct { - std::string theme; - int size; - } cursor; + struct wl_data_device_manager *data_device_manager = nullptr; + struct zwp_tablet_manager_v2 *tablet_manager = nullptr; struct zwp_relative_pointer_manager_v1 *relative_pointer_manager = nullptr; struct zwp_pointer_constraints_v1 *pointer_constraints = nullptr; - - std::vector<struct wl_surface *> os_surfaces; - std::vector<struct wl_egl_window *> os_egl_windows; }; +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Private Utility Functions + * \{ */ + static GHOST_WindowManager *window_manager = nullptr; +/** Check this lock before accessing `GHOST_SystemWayland::selection` from a thread. */ +static std::mutex system_selection_mutex; + +/** + * Callback for WAYLAND to run when there is an error. + * + * \note It's useful to set a break-point on this function as some errors are fatal + * (for all intents and purposes) but don't crash the process. + */ +static void ghost_wayland_log_handler(const char *msg, va_list arg) +{ + fprintf(stderr, "GHOST/Wayland: "); + vfprintf(stderr, msg, arg); /* Includes newline. */ + + GHOST_TBacktraceFn backtrace_fn = GHOST_ISystem::getBacktraceFn(); + if (backtrace_fn) { + backtrace_fn(stderr); /* Includes newline. */ + } +} + +static input_state_pointer_t *input_state_pointer_active(input_t *input) +{ + if (input->pointer.serial == input->cursor_source_serial) { + return &input->pointer; + } + if (input->tablet.serial == input->cursor_source_serial) { + return &input->tablet; + } + return nullptr; +} + +static input_state_pointer_t *input_state_pointer_from_cursor_surface(input_t *input, + const wl_surface *wl_surface) +{ + if (ghost_wl_surface_own_cursor_pointer(wl_surface)) { + return &input->pointer; + } + if (ghost_wl_surface_own_cursor_tablet(wl_surface)) { + return &input->tablet; + } + GHOST_ASSERT(0, "Surface found without pointer/tablet tag"); + return nullptr; +} + static void display_destroy(display_t *d) { if (d->data_device_manager) { wl_data_device_manager_destroy(d->data_device_manager); } + if (d->tablet_manager) { + zwp_tablet_manager_v2_destroy(d->tablet_manager); + } + for (output_t *output : d->outputs) { - wl_output_destroy(output->output); + wl_output_destroy(output->wl_output); delete output; } for (input_t *input : d->inputs) { - if (input->data_source) { - free(input->data_source->buffer_out); - if (input->data_source->data_source) { - wl_data_source_destroy(input->data_source->data_source); + + /* First handle members that require locking. + * While highly unlikely, it's possible they are being used while this function runs. */ + { + std::lock_guard lock{input->data_source_mutex}; + if (input->data_source) { + free(input->data_source->buffer_out); + if (input->data_source->data_source) { + wl_data_source_destroy(input->data_source->data_source); + } + delete input->data_source; + } + } + + { + std::lock_guard lock{input->data_offer_dnd_mutex}; + if (input->data_offer_dnd) { + wl_data_offer_destroy(input->data_offer_dnd->id); + delete input->data_offer_dnd; } - delete input->data_source; } - if (input->data_offer_copy_paste) { - wl_data_offer_destroy(input->data_offer_copy_paste->id); - delete input->data_offer_copy_paste; + + { + std::lock_guard lock{input->data_offer_copy_paste_mutex}; + if (input->data_offer_copy_paste) { + wl_data_offer_destroy(input->data_offer_copy_paste->id); + delete input->data_offer_copy_paste; + } } + if (input->data_device) { wl_data_device_release(input->data_device); } - if (input->pointer) { - if (input->cursor.file_buffer) { - munmap(input->cursor.file_buffer->data, input->cursor.file_buffer->size); - delete input->cursor.file_buffer; - } - if (input->cursor.surface) { - wl_surface_destroy(input->cursor.surface); + + if (input->cursor.custom_data) { + munmap(input->cursor.custom_data, input->cursor.custom_data_size); + } + + if (input->wl_pointer) { + if (input->cursor.wl_surface) { + wl_surface_destroy(input->cursor.wl_surface); } - if (input->cursor.theme) { - wl_cursor_theme_destroy(input->cursor.theme); + if (input->cursor.wl_theme) { + wl_cursor_theme_destroy(input->cursor.wl_theme); } - if (input->pointer) { - wl_pointer_destroy(input->pointer); + if (input->wl_pointer) { + wl_pointer_destroy(input->wl_pointer); } } - if (input->keyboard) { + if (input->wl_keyboard) { if (input->key_repeat.timer) { - delete static_cast<key_repeat_payload_t *>(input->key_repeat.timer->getUserData()); - input->system->removeTimer(input->key_repeat.timer); - input->key_repeat.timer = nullptr; + keyboard_handle_key_repeat_cancel(input); } - wl_keyboard_destroy(input->keyboard); - } - if (input->xkb_state) { - xkb_state_unref(input->xkb_state); + wl_keyboard_destroy(input->wl_keyboard); } - if (input->xkb_context) { - xkb_context_unref(input->xkb_context); - } - wl_seat_destroy(input->seat); + + /* Un-referencing checks for NULL case. */ + xkb_state_unref(input->xkb_state); + xkb_state_unref(input->xkb_state_empty); + xkb_state_unref(input->xkb_state_empty_with_numlock); + + xkb_context_unref(input->xkb_context); + + wl_seat_destroy(input->wl_seat); delete input; } @@ -229,18 +506,15 @@ static void display_destroy(display_t *d) zwp_pointer_constraints_v1_destroy(d->pointer_constraints); } - for (wl_egl_window *os_egl_window : d->os_egl_windows) { - wl_egl_window_destroy(os_egl_window); - } - - for (wl_surface *os_surface : d->os_surfaces) { - wl_surface_destroy(os_surface); - } - if (d->compositor) { wl_compositor_destroy(d->compositor); } +#ifdef WITH_GHOST_WAYLAND_LIBDECOR + if (d->decor_context) { + libdecor_unref(d->decor_context); + } +#else if (d->xdg_decoration_manager) { zxdg_decoration_manager_v1_destroy(d->xdg_decoration_manager); } @@ -248,6 +522,7 @@ static void display_destroy(display_t *d) if (d->xdg_shell) { xdg_wm_base_destroy(d->xdg_shell); } +#endif /* !WITH_GHOST_WAYLAND_LIBDECOR */ if (eglGetDisplay) { ::eglTerminate(eglGetDisplay(EGLNativeDisplayType(d->display))); @@ -260,7 +535,7 @@ static void display_destroy(display_t *d) delete d; } -static GHOST_TKey xkb_map_gkey(const xkb_keysym_t &sym) +static GHOST_TKey xkb_map_gkey(const xkb_keysym_t sym) { GHOST_TKey gkey; @@ -351,8 +626,7 @@ static GHOST_TKey xkb_map_gkey(const xkb_keysym_t &sym) GXMAP(gkey, XKB_KEY_XF86AudioPrev, GHOST_kKeyMediaFirst); GXMAP(gkey, XKB_KEY_XF86AudioNext, GHOST_kKeyMediaLast); default: - GHOST_PRINT("unhandled key: " << std::hex << std::showbase << sym << std::dec << " (" - << sym << ")" << std::endl); + /* Rely on #xkb_map_gkey_or_scan_code to report when no key can be found. */ gkey = GHOST_kKeyUnknown; } #undef GXMAP @@ -361,14 +635,70 @@ static GHOST_TKey xkb_map_gkey(const xkb_keysym_t &sym) return gkey; } +/** + * Map the keys using the users keyboard layout, if that fails fall back to physical locations. + * This is needed so users with keyboard layouts that don't expose #GHOST_kKeyAccentGrave + * (typically the key under escape) in the layout can still use this key in keyboard shortcuts. + * + * \param key: The key's scan-code, compatible with values in `linux/input-event-codes.h`. + */ +static GHOST_TKey xkb_map_gkey_or_scan_code(const xkb_keysym_t sym, const uint32_t key) +{ + GHOST_TKey gkey = xkb_map_gkey(sym); + + if (UNLIKELY(gkey == GHOST_kKeyUnknown)) { + /* Fall back to physical location for keys that would otherwise do nothing. */ + switch (key) { + case KEY_GRAVE: { + gkey = GHOST_kKeyAccentGrave; + break; + } + default: { + GHOST_PRINT( + /* Key-code. */ + "unhandled key: " << std::hex << std::showbase << sym << /* Hex. */ + std::dec << " (" << sym << "), " << /* Decimal. */ + /* Scan-code. */ + "scan-code: " << std::hex << std::showbase << key << /* Hex. */ + std::dec << " (" << key << ")" << /* Decimal. */ + std::endl); + break; + } + } + } + + return gkey; +} + +static GHOST_TTabletMode tablet_tool_map_type(enum zwp_tablet_tool_v2_type wl_tablet_tool_type) +{ + switch (wl_tablet_tool_type) { + case ZWP_TABLET_TOOL_V2_TYPE_ERASER: { + return GHOST_kTabletModeEraser; + } + case ZWP_TABLET_TOOL_V2_TYPE_PEN: + case ZWP_TABLET_TOOL_V2_TYPE_BRUSH: + case ZWP_TABLET_TOOL_V2_TYPE_PENCIL: + case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH: + case ZWP_TABLET_TOOL_V2_TYPE_FINGER: + case ZWP_TABLET_TOOL_V2_TYPE_MOUSE: + case ZWP_TABLET_TOOL_V2_TYPE_LENS: { + return GHOST_kTabletModeStylus; + } + } + + GHOST_PRINT("unknown tablet tool: " << wl_tablet_tool_type << std::endl); + return GHOST_kTabletModeStylus; +} + static const int default_cursor_size = 24; -static const std::unordered_map<GHOST_TStandardCursor, std::string> cursors = { +static const std::unordered_map<GHOST_TStandardCursor, const char *> cursors = { {GHOST_kStandardCursorDefault, "left_ptr"}, {GHOST_kStandardCursorRightArrow, "right_ptr"}, {GHOST_kStandardCursorLeftArrow, "left_ptr"}, {GHOST_kStandardCursorInfo, ""}, - {GHOST_kStandardCursorDestroy, ""}, + {GHOST_kStandardCursorDestroy, "pirate"}, {GHOST_kStandardCursorHelp, "question_arrow"}, {GHOST_kStandardCursorWait, "watch"}, {GHOST_kStandardCursorText, "xterm"}, @@ -376,21 +706,21 @@ static const std::unordered_map<GHOST_TStandardCursor, std::string> cursors = { {GHOST_kStandardCursorCrosshairA, ""}, {GHOST_kStandardCursorCrosshairB, ""}, {GHOST_kStandardCursorCrosshairC, ""}, - {GHOST_kStandardCursorPencil, ""}, + {GHOST_kStandardCursorPencil, "pencil"}, {GHOST_kStandardCursorUpArrow, "sb_up_arrow"}, {GHOST_kStandardCursorDownArrow, "sb_down_arrow"}, - {GHOST_kStandardCursorVerticalSplit, ""}, - {GHOST_kStandardCursorHorizontalSplit, ""}, + {GHOST_kStandardCursorVerticalSplit, "split_v"}, + {GHOST_kStandardCursorHorizontalSplit, "split_h"}, {GHOST_kStandardCursorEraser, ""}, {GHOST_kStandardCursorKnife, ""}, - {GHOST_kStandardCursorEyedropper, ""}, - {GHOST_kStandardCursorZoomIn, ""}, - {GHOST_kStandardCursorZoomOut, ""}, + {GHOST_kStandardCursorEyedropper, "color-picker"}, + {GHOST_kStandardCursorZoomIn, "zoom-in"}, + {GHOST_kStandardCursorZoomOut, "zoom-out"}, {GHOST_kStandardCursorMove, "move"}, - {GHOST_kStandardCursorNSEWScroll, ""}, - {GHOST_kStandardCursorNSScroll, ""}, - {GHOST_kStandardCursorEWScroll, ""}, - {GHOST_kStandardCursorStop, ""}, + {GHOST_kStandardCursorNSEWScroll, "size_all"}, /* Not an exact match. */ + {GHOST_kStandardCursorNSScroll, "size_ver"}, /* Not an exact match. */ + {GHOST_kStandardCursorEWScroll, "size_hor"}, /* Not an exact match. */ + {GHOST_kStandardCursorStop, "not-allowed"}, {GHOST_kStandardCursorUpDown, "sb_v_double_arrow"}, {GHOST_kStandardCursorLeftRight, "sb_h_double_arrow"}, {GHOST_kStandardCursorTopSide, "top_side"}, @@ -429,68 +759,209 @@ static const std::vector<std::string> mime_send = { "text/plain", }; +static int memfd_create_sealed(const char *name) +{ +#ifdef HAVE_MEMFD_CREATE + const int fd = memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd >= 0) { + fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL); + } + return fd; +#else /* HAVE_MEMFD_CREATE */ + char *path = getenv("XDG_RUNTIME_DIR"); + if (!path) { + errno = ENOENT; + return -1; + } + char *tmpname; + asprintf(&tmpname, "%s/%s-XXXXXX", path, name); + const int fd = mkostemp(tmpname, O_CLOEXEC); + if (fd >= 0) { + unlink(tmpname); + } + free(tmpname); + return fd; +#endif /* !HAVE_MEMFD_CREATE */ +} + +static size_t ghost_wl_shm_format_as_size(enum wl_shm_format format) +{ + switch (format) { + case WL_SHM_FORMAT_ARGB8888: { + return 4; + } + default: { + /* Support other formats as needed. */ + GHOST_ASSERT(0, "Unexpected format passed in!"); + return 4; + } + } +} + +/** + * Return a #wl_buffer, ready to have it's data filled in or NULL in case of failure. + * The caller is responsible for calling `unmap(buffer_data, buffer_size)`. + * + * \param r_buffer_data: The buffer to be filled. + * \param r_buffer_data_size: The size of `r_buffer_data` in bytes. + */ +static wl_buffer *ghost_wl_buffer_create_for_image(struct wl_shm *shm, + const int32_t size_xy[2], + enum wl_shm_format format, + void **r_buffer_data, + size_t *r_buffer_data_size) +{ + const int fd = memfd_create_sealed("ghost-wl-buffer"); + wl_buffer *buffer = nullptr; + if (fd >= 0) { + const int32_t buffer_stride = size_xy[0] * ghost_wl_shm_format_as_size(format); + const int32_t buffer_size = buffer_stride * size_xy[1]; + if (posix_fallocate(fd, 0, buffer_size) == 0) { + void *buffer_data = mmap(nullptr, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (buffer_data != MAP_FAILED) { + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, buffer_size); + buffer = wl_shm_pool_create_buffer(pool, 0, UNPACK2(size_xy), buffer_stride, format); + wl_shm_pool_destroy(pool); + if (buffer) { + *r_buffer_data = buffer_data; + *r_buffer_data_size = (size_t)buffer_size; + } + else { + /* Highly unlikely. */ + munmap(buffer_data, buffer_size); + } + } + } + close(fd); + } + return buffer; +} + +/** \} */ + /* -------------------------------------------------------------------- */ -/** \name Interface Callbacks +/** \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 void relative_pointer_relative_motion( - void *data, - struct zwp_relative_pointer_v1 * /*zwp_relative_pointer_v1*/, - uint32_t /*utime_hi*/, - uint32_t /*utime_lo*/, - wl_fixed_t dx, - wl_fixed_t dy, - wl_fixed_t /*dx_unaccel*/, - wl_fixed_t /*dy_unaccel*/) +static CLG_LogRef LOG_WL_RELATIVE_POINTER = {"ghost.wl.handle.relative_pointer"}; +#define LOG (&LOG_WL_RELATIVE_POINTER) + +/** + * The caller is responsible for setting the value of `input->xy`. + */ +static void relative_pointer_handle_relative_motion_impl(input_t *input, + GHOST_WindowWayland *win, + const wl_fixed_t xy[2]) { - input_t *input = static_cast<input_t *>(data); + const wl_fixed_t scale = win->scale(); - input->x += wl_fixed_to_int(dx); - input->y += wl_fixed_to_int(dy); + input->pointer.xy[0] = xy[0]; + input->pointer.xy[1] = xy[1]; - GHOST_IWindow *win = static_cast<GHOST_WindowWayland *>( - wl_surface_get_user_data(input->focus_pointer)); +#ifdef USE_GNOME_CONFINE_HACK + if (input->use_pointer_software_confine) { + GHOST_Rect bounds; + win->getClientBounds(bounds); + /* Needed or the cursor is considered outside the window and doesn't restore the location. */ + bounds.m_r -= 1; + bounds.m_b -= 1; + bounds.m_l = wl_fixed_from_int(bounds.m_l) / scale; + bounds.m_t = wl_fixed_from_int(bounds.m_t) / scale; + bounds.m_r = wl_fixed_from_int(bounds.m_r) / scale; + bounds.m_b = wl_fixed_from_int(bounds.m_b) / scale; + bounds.clampPoint(UNPACK2(input->pointer.xy)); + } +#endif input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), GHOST_kEventCursorMove, win, - input->x, - input->y, + wl_fixed_to_int(scale * input->pointer.xy[0]), + wl_fixed_to_int(scale * input->pointer.xy[1]), GHOST_TABLET_DATA_NONE)); } +static void relative_pointer_handle_relative_motion( + void *data, + struct zwp_relative_pointer_v1 * /*zwp_relative_pointer_v1*/, + const uint32_t /*utime_hi*/, + const uint32_t /*utime_lo*/, + const wl_fixed_t dx, + const wl_fixed_t dy, + const wl_fixed_t /*dx_unaccel*/, + const wl_fixed_t /*dy_unaccel*/) +{ + input_t *input = static_cast<input_t *>(data); + if (wl_surface *focus_surface = input->pointer.wl_surface) { + CLOG_INFO(LOG, 2, "relative_motion"); + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + const wl_fixed_t scale = win->scale(); + const wl_fixed_t xy_next[2] = { + input->pointer.xy[0] + (dx / scale), + input->pointer.xy[1] + (dy / scale), + }; + relative_pointer_handle_relative_motion_impl(input, win, xy_next); + } + else { + CLOG_INFO(LOG, 2, "relative_motion (skipped)"); + } +} + static const zwp_relative_pointer_v1_listener relative_pointer_listener = { - relative_pointer_relative_motion, + relative_pointer_handle_relative_motion, }; +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Data Source), #wl_data_source_listener + * \{ */ + +static CLG_LogRef LOG_WL_DATA_SOURCE = {"ghost.wl.handle.data_source"}; +#define LOG (&LOG_WL_DATA_SOURCE) + static void dnd_events(const input_t *const input, const GHOST_TEventType event) { - const uint64_t time = input->system->getMilliSeconds(); - GHOST_IWindow *const window = static_cast<GHOST_WindowWayland *>( - wl_surface_get_user_data(input->focus_pointer)); - for (const std::string &type : mime_preference_order) { - input->system->pushEvent(new GHOST_EventDragnDrop(time, - event, - mime_dnd.at(type), - window, - input->data_offer_dnd->dnd.x, - input->data_offer_dnd->dnd.y, - nullptr)); + /* NOTE: `input->data_offer_dnd_mutex` must already be locked. */ + if (wl_surface *focus_surface = input->focus_dnd) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + const wl_fixed_t scale = win->scale(); + const int event_xy[2] = { + wl_fixed_to_int(scale * input->data_offer_dnd->dnd.xy[0]), + wl_fixed_to_int(scale * input->data_offer_dnd->dnd.xy[1]), + }; + + const uint64_t time = input->system->getMilliSeconds(); + for (const std::string &type : mime_preference_order) { + input->system->pushEvent(new GHOST_EventDragnDrop( + time, event, mime_dnd.at(type), win, UNPACK2(event_xy), nullptr)); + } } } -static std::string read_pipe(data_offer_t *data_offer, const std::string mime_receive) +static std::string read_pipe(data_offer_t *data_offer, + const std::string mime_receive, + std::mutex *mutex) { int pipefd[2]; - if (pipe(pipefd) != 0) { + if (UNLIKELY(pipe(pipefd) != 0)) { return {}; } wl_data_offer_receive(data_offer->id, mime_receive.c_str(), pipefd[1]); close(pipefd[1]); + data_offer->in_use.store(false); + + if (mutex) { + mutex->unlock(); + } + /* WARNING: `data_offer` may be freed from now on. */ + std::string data; ssize_t len; char buffer[4096]; @@ -498,7 +969,6 @@ static std::string read_pipe(data_offer_t *data_offer, const std::string mime_re data.insert(data.end(), buffer, buffer + len); } close(pipefd[0]); - data_offer->in_use.store(false); return data; } @@ -507,29 +977,35 @@ static std::string read_pipe(data_offer_t *data_offer, const std::string mime_re * 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 NULL. + * a target does not accept any of the offered types, type is nullptr. */ -static void data_source_target(void * /*data*/, - struct wl_data_source * /*wl_data_source*/, - const char * /*mime_type*/) +static void data_source_handle_target(void * /*data*/, + struct wl_data_source * /*wl_data_source*/, + const char * /*mime_type*/) { - /* pass */ + CLOG_INFO(LOG, 2, "target"); } -static void data_source_send(void *data, - struct wl_data_source * /*wl_data_source*/, - const char * /*mime_type*/, - int32_t fd) +static void data_source_handle_send(void *data, + struct wl_data_source * /*wl_data_source*/, + const char * /*mime_type*/, + const int32_t fd) { - const char *const buffer = static_cast<char *>(data); - if (write(fd, buffer, strlen(buffer) + 1) < 0) { + input_t *input = static_cast<input_t *>(data); + std::lock_guard lock{input->data_source_mutex}; + + CLOG_INFO(LOG, 2, "send"); + + const char *const buffer = input->data_source->buffer_out; + if (write(fd, buffer, strlen(buffer)) < 0) { GHOST_PRINT("error writing to clipboard: " << std::strerror(errno) << std::endl); } close(fd); } -static void data_source_cancelled(void * /*data*/, struct wl_data_source *wl_data_source) +static void data_source_handle_cancelled(void * /*data*/, struct wl_data_source *wl_data_source) { + CLOG_INFO(LOG, 2, "cancelled"); wl_data_source_destroy(wl_data_source); } @@ -540,10 +1016,10 @@ static void data_source_cancelled(void * /*data*/, struct wl_data_source *wl_dat * 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_dnd_drop_performed(void * /*data*/, - struct wl_data_source * /*wl_data_source*/) +static void data_source_handle_dnd_drop_performed(void * /*data*/, + struct wl_data_source * /*wl_data_source*/) { - /* pass */ + CLOG_INFO(LOG, 2, "dnd_drop_performed"); } /** @@ -553,9 +1029,10 @@ static void data_source_dnd_drop_performed(void * /*data*/, * source, so the client is now free to destroy this data source * and free all associated data. */ -static void data_source_dnd_finished(void * /*data*/, struct wl_data_source * /*wl_data_source*/) +static void data_source_handle_dnd_finished(void * /*data*/, + struct wl_data_source * /*wl_data_source*/) { - /* pass */ + CLOG_INFO(LOG, 2, "dnd_finished"); } /** @@ -565,73 +1042,108 @@ static void data_source_dnd_finished(void * /*data*/, struct wl_data_source * /* * after matching the source/destination side actions. Only one * action (or none) will be offered here. */ -static void data_source_action(void * /*data*/, - struct wl_data_source * /*wl_data_source*/, - uint32_t /*dnd_action*/) +static void data_source_handle_action(void * /*data*/, + struct wl_data_source * /*wl_data_source*/, + const uint32_t dnd_action) { - /* pass */ + CLOG_INFO(LOG, 2, "handle_action (dnd_action=%u)", dnd_action); } static const struct wl_data_source_listener data_source_listener = { - data_source_target, - data_source_send, - data_source_cancelled, - data_source_dnd_drop_performed, - data_source_dnd_finished, - data_source_action, + data_source_handle_target, + data_source_handle_send, + data_source_handle_cancelled, + data_source_handle_dnd_drop_performed, + data_source_handle_dnd_finished, + data_source_handle_action, }; -static void data_offer_offer(void *data, - struct wl_data_offer * /*wl_data_offer*/, - const char *mime_type) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Data Offer), #wl_data_offer_listener + * \{ */ + +static CLG_LogRef LOG_WL_DATA_OFFER = {"ghost.wl.handle.data_offer"}; +#define LOG (&LOG_WL_DATA_OFFER) + +static void data_offer_handle_offer(void *data, + struct wl_data_offer * /*wl_data_offer*/, + const char *mime_type) { + CLOG_INFO(LOG, 2, "offer (mime_type=%s)", mime_type); static_cast<data_offer_t *>(data)->types.insert(mime_type); } -static void data_offer_source_actions(void *data, - struct wl_data_offer * /*wl_data_offer*/, - uint32_t source_actions) +static void data_offer_handle_source_actions(void *data, + struct wl_data_offer * /*wl_data_offer*/, + const uint32_t source_actions) { + CLOG_INFO(LOG, 2, "source_actions (%u)", source_actions); static_cast<data_offer_t *>(data)->source_actions = source_actions; } -static void data_offer_action(void *data, - struct wl_data_offer * /*wl_data_offer*/, - uint32_t dnd_action) +static void data_offer_handle_action(void *data, + struct wl_data_offer * /*wl_data_offer*/, + const uint32_t dnd_action) { + CLOG_INFO(LOG, 2, "actions (%u)", dnd_action); static_cast<data_offer_t *>(data)->dnd_action = dnd_action; } static const struct wl_data_offer_listener data_offer_listener = { - data_offer_offer, - data_offer_source_actions, - data_offer_action, + data_offer_handle_offer, + data_offer_handle_source_actions, + data_offer_handle_action, }; -static void data_device_data_offer(void * /*data*/, - struct wl_data_device * /*wl_data_device*/, - struct wl_data_offer *id) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Data Device), #wl_data_device_listener + * \{ */ + +static CLG_LogRef LOG_WL_DATA_DEVICE = {"ghost.wl.handle.data_device"}; +#define LOG (&LOG_WL_DATA_DEVICE) + +static void data_device_handle_data_offer(void * /*data*/, + struct wl_data_device * /*wl_data_device*/, + struct wl_data_offer *id) { + CLOG_INFO(LOG, 2, "data_offer"); + data_offer_t *data_offer = new data_offer_t; data_offer->id = id; wl_data_offer_add_listener(id, &data_offer_listener, data_offer); } -static void data_device_enter(void *data, - struct wl_data_device * /*wl_data_device*/, - uint32_t serial, - struct wl_surface * /*surface*/, - wl_fixed_t x, - wl_fixed_t y, - struct wl_data_offer *id) +static void data_device_handle_enter(void *data, + struct wl_data_device * /*wl_data_device*/, + const uint32_t serial, + struct wl_surface *surface, + const wl_fixed_t x, + const wl_fixed_t y, + struct wl_data_offer *id) { + if (!ghost_wl_surface_own(surface)) { + CLOG_INFO(LOG, 2, "enter (skipped)"); + return; + } + CLOG_INFO(LOG, 2, "enter"); + input_t *input = static_cast<input_t *>(data); + std::lock_guard lock{input->data_offer_dnd_mutex}; + input->data_offer_dnd = static_cast<data_offer_t *>(wl_data_offer_get_user_data(id)); data_offer_t *data_offer = input->data_offer_dnd; data_offer->in_use.store(true); - data_offer->dnd.x = wl_fixed_to_int(x); - data_offer->dnd.y = wl_fixed_to_int(y); + 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 | @@ -642,14 +1154,19 @@ static void data_device_enter(void *data, wl_data_offer_accept(id, serial, type.c_str()); } + input->focus_dnd = surface; dnd_events(input, GHOST_kEventDraggingEntered); } -static void data_device_leave(void *data, struct wl_data_device * /*wl_data_device*/) +static void data_device_handle_leave(void *data, struct wl_data_device * /*wl_data_device*/) { input_t *input = static_cast<input_t *>(data); + std::lock_guard lock{input->data_offer_dnd_mutex}; + + CLOG_INFO(LOG, 2, "leave"); dnd_events(input, GHOST_kEventDraggingExited); + input->focus_dnd = nullptr; if (input->data_offer_dnd && !input->data_offer_dnd->in_use.load()) { wl_data_offer_destroy(input->data_offer_dnd->id); @@ -658,21 +1175,30 @@ static void data_device_leave(void *data, struct wl_data_device * /*wl_data_devi } } -static void data_device_motion(void *data, - struct wl_data_device * /*wl_data_device*/, - uint32_t /*time*/, - wl_fixed_t x, - wl_fixed_t y) +static void data_device_handle_motion(void *data, + struct wl_data_device * /*wl_data_device*/, + const uint32_t /*time*/, + const wl_fixed_t x, + const wl_fixed_t y) { input_t *input = static_cast<input_t *>(data); - input->data_offer_dnd->dnd.x = wl_fixed_to_int(x); - input->data_offer_dnd->dnd.y = wl_fixed_to_int(y); + std::lock_guard lock{input->data_offer_dnd_mutex}; + + CLOG_INFO(LOG, 2, "motion"); + + input->data_offer_dnd->dnd.xy[0] = x; + input->data_offer_dnd->dnd.xy[1] = y; + dnd_events(input, GHOST_kEventDraggingUpdated); } -static void data_device_drop(void *data, struct wl_data_device * /*wl_data_device*/) +static void data_device_handle_drop(void *data, struct wl_data_device * /*wl_data_device*/) { input_t *input = static_cast<input_t *>(data); + std::lock_guard lock{input->data_offer_dnd_mutex}; + + CLOG_INFO(LOG, 2, "drop"); + data_offer_t *data_offer = input->data_offer_dnd; const std::string mime_receive = *std::find_first_of(mime_preference_order.begin(), @@ -680,13 +1206,13 @@ static void data_device_drop(void *data, struct wl_data_device * /*wl_data_devic data_offer->types.begin(), data_offer->types.end()); - auto read_uris = [](input_t *const input, - data_offer_t *data_offer, - const std::string mime_receive) { - const int x = data_offer->dnd.x; - const int y = data_offer->dnd.y; + auto read_uris_fn = [](input_t *const input, + data_offer_t *data_offer, + wl_surface *surface, + const std::string mime_receive) { + const wl_fixed_t xy[2] = {UNPACK2(data_offer->dnd.xy)}; - const std::string data = read_pipe(data_offer, mime_receive); + const std::string data = read_pipe(data_offer, mime_receive, nullptr); wl_data_offer_finish(data_offer->id); wl_data_offer_destroy(data_offer->id); @@ -700,6 +1226,7 @@ static void data_device_drop(void *data, struct wl_data_device * /*wl_data_devic static constexpr const char *file_proto = "file://"; static constexpr const char *crlf = "\r\n"; + GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); std::vector<std::string> uris; size_t pos = 0; @@ -723,32 +1250,37 @@ static void data_device_drop(void *data, struct wl_data_device * /*wl_data_devic flist->strings[i] = static_cast<uint8_t *>(malloc((uris[i].size() + 1) * sizeof(uint8_t))); memcpy(flist->strings[i], uris[i].data(), uris[i].size() + 1); } - GHOST_IWindow *win = static_cast<GHOST_WindowWayland *>( - wl_surface_get_user_data(input->focus_pointer)); + + const wl_fixed_t scale = win->scale(); system->pushEvent(new GHOST_EventDragnDrop(system->getMilliSeconds(), GHOST_kEventDraggingDropDone, GHOST_kDragnDropTypeFilenames, win, - x, - y, + wl_fixed_to_int(scale * xy[0]), + wl_fixed_to_int(scale * xy[1]), flist)); } - else if (mime_receive == mime_text_plain || mime_receive == mime_text_utf8) { + else if (ELEM(mime_receive, mime_text_plain, mime_text_utf8)) { /* TODO: enable use of internal functions 'txt_insert_buf' and * 'text_update_edited' to behave like dropped text was pasted. */ } wl_display_roundtrip(system->display()); }; - std::thread read_thread(read_uris, input, data_offer, mime_receive); + /* Pass in `input->focus_dnd` instead of accessing it from `input` since the leave callback + * (#data_device_leave) will clear the value once this function starts. */ + std::thread read_thread(read_uris_fn, input, data_offer, input->focus_dnd, mime_receive); read_thread.detach(); } -static void data_device_selection(void *data, - struct wl_data_device * /*wl_data_device*/, - struct wl_data_offer *id) +static void data_device_handle_selection(void *data, + struct wl_data_device * /*wl_data_device*/, + struct wl_data_offer *id) { input_t *input = static_cast<input_t *>(data); + + std::lock_guard lock{input->data_offer_copy_paste_mutex}; + data_offer_t *data_offer = input->data_offer_copy_paste; /* Delete old data offer. */ @@ -759,206 +1291,250 @@ static void data_device_selection(void *data, } if (id == nullptr) { + CLOG_INFO(LOG, 2, "selection: (skipped)"); return; } + CLOG_INFO(LOG, 2, "selection"); /* Get new data offer. */ data_offer = static_cast<data_offer_t *>(wl_data_offer_get_user_data(id)); input->data_offer_copy_paste = data_offer; - std::string mime_receive; - for (const std::string type : {mime_text_utf8, mime_text_plain}) { - if (data_offer->types.count(type)) { - mime_receive = type; - break; + auto read_selection_fn = [](input_t *input) { + GHOST_SystemWayland *const system = input->system; + input->data_offer_copy_paste_mutex.lock(); + + data_offer_t *data_offer = input->data_offer_copy_paste; + std::string mime_receive; + for (const std::string type : {mime_text_utf8, mime_text_plain}) { + if (data_offer->types.count(type)) { + mime_receive = type; + break; + } } - } + const std::string data = read_pipe( + data_offer, mime_receive, &input->data_offer_copy_paste_mutex); - auto read_selection = [](GHOST_SystemWayland *const system, - data_offer_t *data_offer, - const std::string mime_receive) { - const std::string data = read_pipe(data_offer, mime_receive); - system->setSelection(data); + { + std::lock_guard lock{system_selection_mutex}; + system->selection_set(data); + } }; - std::thread read_thread(read_selection, input->system, data_offer, mime_receive); + std::thread read_thread(read_selection_fn, input); read_thread.detach(); } static const struct wl_data_device_listener data_device_listener = { - data_device_data_offer, - data_device_enter, - data_device_leave, - data_device_motion, - data_device_drop, - data_device_selection, + data_device_handle_data_offer, + data_device_handle_enter, + data_device_handle_leave, + data_device_handle_motion, + data_device_handle_drop, + data_device_handle_selection, }; -static void cursor_buffer_release(void *data, struct wl_buffer *wl_buffer) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Buffer), #wl_buffer_listener + * \{ */ + +static CLG_LogRef LOG_WL_CURSOR_BUFFER = {"ghost.wl.handle.cursor_buffer"}; +#define LOG (&LOG_WL_CURSOR_BUFFER) + +static void cursor_buffer_handle_release(void *data, struct wl_buffer *wl_buffer) { - cursor_t *cursor = static_cast<cursor_t *>(data); + CLOG_INFO(LOG, 2, "release"); + cursor_t *cursor = static_cast<cursor_t *>(data); wl_buffer_destroy(wl_buffer); - if (wl_buffer == cursor->buffer) { - /* the mapped buffer was from a custom cursor */ - cursor->buffer = nullptr; + if (wl_buffer == cursor->wl_buffer) { + /* The mapped buffer was from a custom cursor. */ + cursor->wl_buffer = nullptr; } } -const struct wl_buffer_listener cursor_buffer_listener = { - cursor_buffer_release, +static const struct wl_buffer_listener cursor_buffer_listener = { + cursor_buffer_handle_release, }; -static GHOST_IWindow *get_window(struct wl_surface *surface) -{ - if (!surface) { - return nullptr; - } +#undef LOG - for (GHOST_IWindow *win : window_manager->getWindows()) { - if (surface == static_cast<const GHOST_WindowWayland *>(win)->surface()) { - return win; - } - } - return nullptr; -} +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Surface), #wl_surface_listener + * \{ */ + +static CLG_LogRef LOG_WL_CURSOR_SURFACE = {"ghost.wl.handle.cursor_surface"}; +#define LOG (&LOG_WL_CURSOR_SURFACE) -static bool update_cursor_scale(cursor_t &cursor, wl_shm *shm) +static bool update_cursor_scale(cursor_t &cursor, + wl_shm *shm, + input_state_pointer_t *input_state, + wl_surface *cursor_surface) { int scale = 0; - for (const output_t *output : cursor.outputs) { - if (output->scale > scale) + for (const output_t *output : input_state->outputs) { + if (output->scale > scale) { scale = output->scale; + } } - if (scale > 0 && cursor.scale != scale) { - cursor.scale = scale; - wl_surface_set_buffer_scale(cursor.surface, scale); - wl_cursor_theme_destroy(cursor.theme); - cursor.theme = wl_cursor_theme_load(cursor.theme_name.c_str(), scale * cursor.size, shm); + if (scale > 0 && input_state->theme_scale != scale) { + input_state->theme_scale = scale; + if (!cursor.is_custom) { + wl_surface_set_buffer_scale(cursor_surface, scale); + } + wl_cursor_theme_destroy(cursor.wl_theme); + cursor.wl_theme = wl_cursor_theme_load(cursor.theme_name.c_str(), scale * cursor.size, shm); return true; } return false; } -static void cursor_surface_enter(void *data, - struct wl_surface * /*wl_surface*/, - struct wl_output *output) +static void cursor_surface_handle_enter(void *data, + struct wl_surface *wl_surface, + struct wl_output *output) { - input_t *input = static_cast<input_t *>(data); - for (const output_t *reg_output : input->system->outputs()) { - if (reg_output->output == output) { - input->cursor.outputs.insert(reg_output); - } + if (!ghost_wl_output_own(output)) { + CLOG_INFO(LOG, 2, "handle_enter (skipped)"); + return; } - update_cursor_scale(input->cursor, input->system->shm()); + CLOG_INFO(LOG, 2, "handle_enter"); + + input_t *input = static_cast<input_t *>(data); + input_state_pointer_t *input_state = input_state_pointer_from_cursor_surface(input, wl_surface); + const output_t *reg_output = ghost_wl_output_user_data(output); + input_state->outputs.insert(reg_output); + update_cursor_scale(input->cursor, input->system->shm(), input_state, wl_surface); } -static void cursor_surface_leave(void *data, - struct wl_surface * /*wl_surface*/, - struct wl_output *output) +static void cursor_surface_handle_leave(void *data, + struct wl_surface *wl_surface, + struct wl_output *output) { - input_t *input = static_cast<input_t *>(data); - for (const output_t *reg_output : input->system->outputs()) { - if (reg_output->output == output) { - input->cursor.outputs.erase(reg_output); - } + if (!(output && ghost_wl_output_own(output))) { + CLOG_INFO(LOG, 2, "handle_leave (skipped)"); + return; } - update_cursor_scale(input->cursor, input->system->shm()); + CLOG_INFO(LOG, 2, "handle_leave"); + + input_t *input = static_cast<input_t *>(data); + input_state_pointer_t *input_state = input_state_pointer_from_cursor_surface(input, wl_surface); + const output_t *reg_output = ghost_wl_output_user_data(output); + input_state->outputs.erase(reg_output); + update_cursor_scale(input->cursor, input->system->shm(), input_state, wl_surface); } -struct wl_surface_listener cursor_surface_listener = { - cursor_surface_enter, - cursor_surface_leave, +static const struct wl_surface_listener cursor_surface_listener = { + cursor_surface_handle_enter, + cursor_surface_handle_leave, }; -static void pointer_enter(void *data, - struct wl_pointer * /*wl_pointer*/, - uint32_t serial, - struct wl_surface *surface, - wl_fixed_t surface_x, - wl_fixed_t surface_y) -{ - GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(get_window(surface)); +#undef LOG - if (!win) { +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Pointer), #wl_pointer_listener + * \{ */ + +static CLG_LogRef LOG_WL_POINTER = {"ghost.wl.handle.pointer"}; +#define LOG (&LOG_WL_POINTER) + +static void pointer_handle_enter(void *data, + struct wl_pointer * /*wl_pointer*/, + const uint32_t serial, + struct wl_surface *surface, + const wl_fixed_t surface_x, + const wl_fixed_t surface_y) +{ + if (!ghost_wl_surface_own(surface)) { + CLOG_INFO(LOG, 2, "enter (skipped)"); return; } + CLOG_INFO(LOG, 2, "enter"); + + GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); win->activate(); input_t *input = static_cast<input_t *>(data); - input->pointer_serial = serial; - input->x = win->scale() * wl_fixed_to_int(surface_x); - input->y = win->scale() * wl_fixed_to_int(surface_y); - input->focus_pointer = surface; + input->cursor_source_serial = serial; + input->pointer.serial = serial; + input->pointer.xy[0] = surface_x; + input->pointer.xy[1] = surface_y; + input->pointer.wl_surface = surface; win->setCursorShape(win->getCursorShape()); + const wl_fixed_t scale = win->scale(); input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), GHOST_kEventCursorMove, - static_cast<GHOST_WindowWayland *>(win), - input->x, - input->y, + win, + wl_fixed_to_int(scale * input->pointer.xy[0]), + wl_fixed_to_int(scale * input->pointer.xy[1]), GHOST_TABLET_DATA_NONE)); } -static void pointer_leave(void *data, - struct wl_pointer * /*wl_pointer*/, - uint32_t /*serial*/, - struct wl_surface *surface) +static void pointer_handle_leave(void *data, + struct wl_pointer * /*wl_pointer*/, + const uint32_t /*serial*/, + struct wl_surface *surface) { - GHOST_IWindow *win = get_window(surface); - - if (!win) { - return; + /* First clear the `pointer.wl_surface`, since the window won't exist when closing the window. */ + static_cast<input_t *>(data)->pointer.wl_surface = nullptr; + if (surface && ghost_wl_surface_own(surface)) { + CLOG_INFO(LOG, 2, "leave"); + GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); + win->deactivate(); + } + else { + CLOG_INFO(LOG, 2, "leave (skipped)"); } - - static_cast<input_t *>(data)->focus_pointer = nullptr; - static_cast<GHOST_WindowWayland *>(win)->deactivate(); } -static void pointer_motion(void *data, - struct wl_pointer * /*wl_pointer*/, - uint32_t /*time*/, - wl_fixed_t surface_x, - wl_fixed_t surface_y) +static void pointer_handle_motion(void *data, + struct wl_pointer * /*wl_pointer*/, + const uint32_t /*time*/, + const wl_fixed_t surface_x, + const wl_fixed_t surface_y) { input_t *input = static_cast<input_t *>(data); - - GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(get_window(input->focus_pointer)); - - if (!win) { - return; + input->pointer.xy[0] = surface_x; + input->pointer.xy[1] = surface_y; + + if (wl_surface *focus_surface = input->pointer.wl_surface) { + CLOG_INFO(LOG, 2, "motion"); + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + const wl_fixed_t scale = win->scale(); + input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), + GHOST_kEventCursorMove, + win, + wl_fixed_to_int(scale * input->pointer.xy[0]), + wl_fixed_to_int(scale * input->pointer.xy[1]), + GHOST_TABLET_DATA_NONE)); + } + else { + CLOG_INFO(LOG, 2, "motion (skipped)"); } - - input->x = win->scale() * wl_fixed_to_int(surface_x); - input->y = win->scale() * wl_fixed_to_int(surface_y); - - input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), - GHOST_kEventCursorMove, - win, - input->x, - input->y, - GHOST_TABLET_DATA_NONE)); } -static void pointer_button(void *data, - struct wl_pointer * /*wl_pointer*/, - uint32_t serial, - uint32_t /*time*/, - uint32_t button, - uint32_t state) +static void pointer_handle_button(void *data, + struct wl_pointer * /*wl_pointer*/, + const uint32_t serial, + const uint32_t /*time*/, + const uint32_t button, + const uint32_t state) { - input_t *input = static_cast<input_t *>(data); - - GHOST_IWindow *win = get_window(input->focus_pointer); - - if (!win) { - return; - } + CLOG_INFO(LOG, 2, "button (button=%u, state=%u)", button, state); + input_t *input = static_cast<input_t *>(data); GHOST_TEventType etype = GHOST_kEventUnknown; switch (state) { case WL_POINTER_BUTTON_STATE_RELEASED: @@ -969,7 +1545,7 @@ static void pointer_button(void *data, break; } - GHOST_TButtonMask ebutton = GHOST_kButtonMaskLeft; + GHOST_TButton ebutton = GHOST_kButtonMaskLeft; switch (button) { case BTN_LEFT: ebutton = GHOST_kButtonMaskLeft; @@ -994,48 +1570,451 @@ static void pointer_button(void *data, break; } - input->data_source->source_serial = serial; - input->buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED); - input->system->pushEvent(new GHOST_EventButton( - input->system->getMilliSeconds(), etype, win, ebutton, GHOST_TABLET_DATA_NONE)); + input->data_source_serial = serial; + input->pointer.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED); + + if (wl_surface *focus_surface = input->pointer.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent(new GHOST_EventButton( + input->system->getMilliSeconds(), etype, win, ebutton, GHOST_TABLET_DATA_NONE)); + } } -static void pointer_axis(void *data, - struct wl_pointer * /*wl_pointer*/, - uint32_t /*time*/, - uint32_t axis, - wl_fixed_t value) +static void pointer_handle_axis(void *data, + struct wl_pointer * /*wl_pointer*/, + const uint32_t /*time*/, + const uint32_t axis, + const wl_fixed_t value) { + CLOG_INFO(LOG, 2, "axis (axis=%u, value=%d)", axis, value); + input_t *input = static_cast<input_t *>(data); + if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) { + return; + } - GHOST_IWindow *win = get_window(input->focus_pointer); + if (wl_surface *focus_surface = input->pointer.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent(new GHOST_EventWheel( + input->system->getMilliSeconds(), win, std::signbit(value) ? +1 : -1)); + } +} - if (!win) { +static const struct wl_pointer_listener pointer_listener = { + pointer_handle_enter, + pointer_handle_leave, + pointer_handle_motion, + pointer_handle_button, + pointer_handle_axis, +}; + +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Tablet Tool), #zwp_tablet_tool_v2_listener + * \{ */ + +static CLG_LogRef LOG_WL_TABLET_TOOL = {"ghost.wl.handle.tablet_tool"}; +#define LOG (&LOG_WL_TABLET_TOOL) + +static void tablet_tool_handle_type(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t tool_type) +{ + CLOG_INFO(LOG, 2, "type (type=%u)", tool_type); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + + tool_input->data.Active = tablet_tool_map_type((enum zwp_tablet_tool_v2_type)tool_type); +} + +static void tablet_tool_handle_hardware_serial(void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t /*hardware_serial_hi*/, + const uint32_t /*hardware_serial_lo*/) +{ + CLOG_INFO(LOG, 2, "hardware_serial"); +} + +static void tablet_tool_handle_hardware_id_wacom( + void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t /*hardware_id_hi*/, + const uint32_t /*hardware_id_lo*/) +{ + CLOG_INFO(LOG, 2, "hardware_id_wacom"); +} + +static void tablet_tool_handle_capability(void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t capability) +{ + CLOG_INFO(LOG, + 2, + "capability (tilt=%d, distance=%d, rotation=%d, slider=%d, wheel=%d)", + (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_TILT) != 0, + (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_DISTANCE) != 0, + (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_ROTATION) != 0, + (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_SLIDER) != 0, + (capability & ZWP_TABLET_TOOL_V2_CAPABILITY_WHEEL) != 0); +} + +static void tablet_tool_handle_done(void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) +{ + CLOG_INFO(LOG, 2, "done"); +} +static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) +{ + CLOG_INFO(LOG, 2, "removed"); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + + if (tool_input->cursor_surface) { + wl_surface_destroy(tool_input->cursor_surface); + } + input->tablet_tools.erase(zwp_tablet_tool_v2); + + delete tool_input; +} +static void tablet_tool_handle_proximity_in(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t serial, + struct zwp_tablet_v2 * /*tablet*/, + struct wl_surface *surface) +{ + if (!ghost_wl_surface_own(surface)) { + CLOG_INFO(LOG, 2, "proximity_in (skipped)"); return; } + CLOG_INFO(LOG, 2, "proximity_in"); - if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) { + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + tool_input->proximity = true; + + input_t *input = tool_input->input; + input->cursor_source_serial = serial; + input->tablet.wl_surface = surface; + input->tablet.serial = serial; + + input->data_source_serial = serial; + + /* Update #GHOST_TabletData. */ + GHOST_TabletData &td = tool_input->data; + /* Reset, to avoid using stale tilt/pressure. */ + td.Xtilt = 0.0f; + td.Ytilt = 0.0f; + /* In case pressure isn't supported. */ + td.Pressure = 1.0f; + + GHOST_WindowWayland *win = ghost_wl_surface_user_data(input->tablet.wl_surface); + + win->activate(); + + win->setCursorShape(win->getCursorShape()); +} +static void tablet_tool_handle_proximity_out(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) +{ + CLOG_INFO(LOG, 2, "proximity_out"); + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + /* Defer clearing the surface until the frame is handled. + * Without this, the frame can not access the surface. */ + tool_input->proximity = false; +} + +static void tablet_tool_handle_down(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t serial) +{ + CLOG_INFO(LOG, 2, "down"); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + const GHOST_TButton ebutton = GHOST_kButtonMaskLeft; + const GHOST_TEventType etype = GHOST_kEventButtonDown; + + input->data_source_serial = serial; + input->tablet.buttons.set(ebutton, true); + + if (wl_surface *focus_surface = input->tablet.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent(new GHOST_EventButton( + input->system->getMilliSeconds(), etype, win, ebutton, tool_input->data)); + } +} + +static void tablet_tool_handle_up(void *data, struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) +{ + CLOG_INFO(LOG, 2, "up"); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + const GHOST_TButton ebutton = GHOST_kButtonMaskLeft; + const GHOST_TEventType etype = GHOST_kEventButtonUp; + + input->tablet.buttons.set(ebutton, false); + + if (wl_surface *focus_surface = input->tablet.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent(new GHOST_EventButton( + input->system->getMilliSeconds(), etype, win, ebutton, tool_input->data)); + } +} + +static void tablet_tool_handle_motion(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const wl_fixed_t x, + const wl_fixed_t y) +{ + CLOG_INFO(LOG, 2, "motion"); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + + input->tablet.xy[0] = x; + input->tablet.xy[1] = y; + + /* NOTE: #tablet_tool_handle_frame generates the event (with updated pressure, tilt... etc). */ +} + +static void tablet_tool_handle_pressure(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t pressure) +{ + const float pressure_unit = (float)pressure / 65535; + CLOG_INFO(LOG, 2, "pressure (%.4f)", pressure_unit); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + GHOST_TabletData &td = tool_input->data; + td.Pressure = pressure_unit; +} +static void tablet_tool_handle_distance(void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t distance) +{ + CLOG_INFO(LOG, 2, "distance (distance=%u)", distance); +} + +static void tablet_tool_handle_tilt(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const wl_fixed_t tilt_x, + const wl_fixed_t tilt_y) +{ + /* Map degrees to `-1.0..1.0`. */ + const float tilt_unit[2] = { + (float)(wl_fixed_to_double(tilt_x) / 90.0), + (float)(wl_fixed_to_double(tilt_y) / 90.0), + }; + CLOG_INFO(LOG, 2, "tilt (x=%.4f, y=%.4f)", UNPACK2(tilt_unit)); + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + GHOST_TabletData &td = tool_input->data; + td.Xtilt = tilt_unit[0]; + td.Ytilt = tilt_unit[1]; + CLAMP(td.Xtilt, -1.0f, 1.0f); + CLAMP(td.Ytilt, -1.0f, 1.0f); +} + +static void tablet_tool_handle_rotation(void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const wl_fixed_t degrees) +{ + CLOG_INFO(LOG, 2, "rotation (degrees=%.4f)", wl_fixed_to_double(degrees)); +} + +static void tablet_tool_handle_slider(void * /*data*/, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const int32_t position) +{ + CLOG_INFO(LOG, 2, "slider (position=%d)", position); +} +static void tablet_tool_handle_wheel(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const wl_fixed_t /*degrees*/, + const int32_t clicks) +{ + if (clicks == 0) { return; } + CLOG_INFO(LOG, 2, "wheel (clicks=%d)", clicks); - input->system->pushEvent( - new GHOST_EventWheel(input->system->getMilliSeconds(), win, std::signbit(value) ? +1 : -1)); + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + if (wl_surface *focus_surface = input->tablet.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent(new GHOST_EventWheel(input->system->getMilliSeconds(), win, clicks)); + } } +static void tablet_tool_handle_button(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t serial, + const uint32_t button, + const uint32_t state) +{ + CLOG_INFO(LOG, 2, "button (button=%u, state=%u)", button, state); -static const struct wl_pointer_listener pointer_listener = { - pointer_enter, - pointer_leave, - pointer_motion, - pointer_button, - pointer_axis, + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + + GHOST_TEventType etype = GHOST_kEventUnknown; + switch (state) { + case WL_POINTER_BUTTON_STATE_RELEASED: + etype = GHOST_kEventButtonUp; + break; + case WL_POINTER_BUTTON_STATE_PRESSED: + etype = GHOST_kEventButtonDown; + break; + } + + GHOST_TButton ebutton = GHOST_kButtonMaskLeft; + switch (button) { + case BTN_STYLUS: + ebutton = GHOST_kButtonMaskRight; + break; + case BTN_STYLUS2: + ebutton = GHOST_kButtonMaskMiddle; + break; + case BTN_STYLUS3: + ebutton = GHOST_kButtonMaskButton4; + break; + } + + input->data_source_serial = serial; + input->tablet.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED); + + if (wl_surface *focus_surface = input->tablet.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent(new GHOST_EventButton( + input->system->getMilliSeconds(), etype, win, ebutton, tool_input->data)); + } +} +static void tablet_tool_handle_frame(void *data, + struct zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, + const uint32_t /*time*/) +{ + CLOG_INFO(LOG, 2, "frame"); + + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>(data); + input_t *input = tool_input->input; + + /* No need to check the surfaces origin, it's already known to be owned by GHOST. */ + if (wl_surface *focus_surface = input->tablet.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + const wl_fixed_t scale = win->scale(); + input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), + GHOST_kEventCursorMove, + win, + wl_fixed_to_int(scale * input->tablet.xy[0]), + wl_fixed_to_int(scale * input->tablet.xy[1]), + tool_input->data)); + if (tool_input->proximity == false) { + win->setCursorShape(win->getCursorShape()); + } + } + + if (tool_input->proximity == false) { + input->tablet.wl_surface = nullptr; + } +} + +static const struct zwp_tablet_tool_v2_listener tablet_tool_listner = { + tablet_tool_handle_type, + tablet_tool_handle_hardware_serial, + tablet_tool_handle_hardware_id_wacom, + tablet_tool_handle_capability, + tablet_tool_handle_done, + tablet_tool_handle_removed, + tablet_tool_handle_proximity_in, + tablet_tool_handle_proximity_out, + tablet_tool_handle_down, + tablet_tool_handle_up, + tablet_tool_handle_motion, + tablet_tool_handle_pressure, + tablet_tool_handle_distance, + tablet_tool_handle_tilt, + tablet_tool_handle_rotation, + tablet_tool_handle_slider, + tablet_tool_handle_wheel, + tablet_tool_handle_button, + tablet_tool_handle_frame, }; -static void keyboard_keymap( - void *data, struct wl_keyboard * /*wl_keyboard*/, uint32_t format, int32_t fd, uint32_t size) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Table Seat), #zwp_tablet_seat_v2_listener + * \{ */ + +static CLG_LogRef LOG_WL_TABLET_SEAT = {"ghost.wl.handle.tablet_seat"}; +#define LOG (&LOG_WL_TABLET_SEAT) + +static void tablet_seat_handle_tablet_added(void * /*data*/, + struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, + struct zwp_tablet_v2 *id) +{ + CLOG_INFO(LOG, 2, "tablet_added (id=%p)", id); +} + +static void tablet_seat_handle_tool_added(void *data, + struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, + struct zwp_tablet_tool_v2 *id) +{ + CLOG_INFO(LOG, 2, "tool_added (id=%p)", id); + + input_t *input = static_cast<input_t *>(data); + tablet_tool_input_t *tool_input = new tablet_tool_input_t(); + tool_input->input = input; + + /* Every tool has it's own cursor surface. */ + tool_input->cursor_surface = wl_compositor_create_surface(input->system->compositor()); + ghost_wl_surface_tag_cursor_tablet(tool_input->cursor_surface); + + wl_surface_add_listener(tool_input->cursor_surface, &cursor_surface_listener, (void *)input); + + zwp_tablet_tool_v2_add_listener(id, &tablet_tool_listner, tool_input); + + input->tablet_tools.insert(id); +} + +static void tablet_seat_handle_pad_added(void * /*data*/, + struct zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, + struct zwp_tablet_pad_v2 *id) +{ + CLOG_INFO(LOG, 2, "pad_added (id=%p)", id); +} + +static const struct zwp_tablet_seat_v2_listener tablet_seat_listener = { + tablet_seat_handle_tablet_added, + tablet_seat_handle_tool_added, + tablet_seat_handle_pad_added, +}; + +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Keyboard), #wl_keyboard_listener + * \{ */ + +static CLG_LogRef LOG_WL_KEYBOARD = {"ghost.wl.handle.keyboard"}; +#define LOG (&LOG_WL_KEYBOARD) + +static void keyboard_handle_keymap(void *data, + struct wl_keyboard * /*wl_keyboard*/, + const uint32_t format, + const int32_t fd, + const uint32_t size) { input_t *input = static_cast<input_t *>(data); if ((!data) || (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)) { + CLOG_INFO(LOG, 2, "keymap (no data or wrong version)"); close(fd); return; } @@ -1052,11 +2031,32 @@ static void keyboard_keymap( close(fd); if (!keymap) { + CLOG_INFO(LOG, 2, "keymap (not found)"); return; } + CLOG_INFO(LOG, 2, "keymap"); + + /* In practice we can assume `xkb_state_new` always succeeds. */ + xkb_state_unref(input->xkb_state); input->xkb_state = xkb_state_new(keymap); + xkb_state_unref(input->xkb_state_empty); + input->xkb_state_empty = xkb_state_new(keymap); + + xkb_state_unref(input->xkb_state_empty_with_numlock); + input->xkb_state_empty_with_numlock = nullptr; + + { + const xkb_mod_index_t mod2 = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM); + const xkb_mod_index_t num = xkb_keymap_mod_get_index(keymap, "NumLock"); + if (num != XKB_MOD_INVALID && mod2 != XKB_MOD_INVALID) { + input->xkb_state_empty_with_numlock = xkb_state_new(keymap); + xkb_state_update_mask( + input->xkb_state_empty_with_numlock, (1 << mod2), 0, (1 << num), 0, 0, 0); + } + } + xkb_keymap_unref(keymap); } @@ -1066,15 +2066,21 @@ static void keyboard_keymap( * Notification that this seat's keyboard focus is on a certain * surface. */ -static void keyboard_enter(void *data, - struct wl_keyboard * /*wl_keyboard*/, - uint32_t /*serial*/, - struct wl_surface *surface, - struct wl_array * /*keys*/) -{ - if (surface != nullptr) { - static_cast<input_t *>(data)->focus_keyboard = surface; +static void keyboard_handle_enter(void *data, + struct wl_keyboard * /*wl_keyboard*/, + const uint32_t serial, + struct wl_surface *surface, + struct wl_array * /*keys*/) +{ + if (!ghost_wl_surface_own(surface)) { + CLOG_INFO(LOG, 2, "enter (skipped)"); + return; } + CLOG_INFO(LOG, 2, "enter"); + + input_t *input = static_cast<input_t *>(data); + input->keyboard.serial = serial; + input->keyboard.wl_surface = surface; } /** @@ -1083,13 +2089,23 @@ static void keyboard_enter(void *data, * Notification that this seat's keyboard focus is no longer on a * certain surface. */ -static void keyboard_leave(void *data, - struct wl_keyboard * /*wl_keyboard*/, - uint32_t /*serial*/, - struct wl_surface *surface) +static void keyboard_handle_leave(void *data, + struct wl_keyboard * /*wl_keyboard*/, + const uint32_t /*serial*/, + struct wl_surface *surface) { - if (surface != nullptr) { - static_cast<input_t *>(data)->focus_keyboard = nullptr; + if (!(surface && ghost_wl_surface_own(surface))) { + CLOG_INFO(LOG, 2, "leave (skipped)"); + return; + } + CLOG_INFO(LOG, 2, "leave"); + + input_t *input = static_cast<input_t *>(data); + input->keyboard.wl_surface = nullptr; + + /* Losing focus must stop repeating text. */ + if (input->key_repeat.timer) { + keyboard_handle_key_repeat_cancel(input); } } @@ -1097,36 +2113,75 @@ static void keyboard_leave(void *data, * A version of #xkb_state_key_get_one_sym which returns the key without any modifiers pressed. * Needed because #GHOST_TKey uses these values as key-codes. */ -static xkb_keysym_t xkb_state_key_get_one_sym_without_modifiers(struct xkb_state *xkb_state, - xkb_keycode_t key) +static xkb_keysym_t xkb_state_key_get_one_sym_without_modifiers( + struct xkb_state *xkb_state_empty, + struct xkb_state *xkb_state_empty_with_numlock, + const xkb_keycode_t key) { /* Use an empty keyboard state to access key symbol without modifiers. */ - xkb_state_get_keymap(xkb_state); - struct xkb_keymap *keymap = xkb_state_get_keymap(xkb_state); - struct xkb_state *xkb_state_empty = xkb_state_new(keymap); - - /* Enable number-lock. */ - { - const xkb_mod_index_t mod2 = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM); - const xkb_mod_index_t num = xkb_keymap_mod_get_index(keymap, "NumLock"); - if (num != XKB_MOD_INVALID && mod2 != XKB_MOD_INVALID) { - xkb_state_update_mask(xkb_state_empty, (1 << mod2), 0, (1 << num), 0, 0, 0); + xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state_empty, key); + + /* NOTE(@campbellbarton): Only perform the number-locked lookup as a fallback + * when a number-pad key has been pressed. This is important as some key-maps use number lock + * for switching other layers (in particular `de(neo_qwertz)` turns on layer-4), see: T96170. + * Alternative solutions could be to inspect the layout however this could get involved + * and turning on the number-lock is only needed for a limited set of keys. */ + + /* Accounts for key-pad keys typically swapped for numbers when number-lock is enabled: + * `Home Left Up Right Down Prior Page_Up Next Page_Dow End Begin Insert Delete`. */ + if (xkb_state_empty_with_numlock && (sym >= XKB_KEY_KP_Home && sym <= XKB_KEY_KP_Delete)) { + const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(xkb_state_empty_with_numlock, key); + if (sym_test != XKB_KEY_NoSymbol) { + sym = sym_test; } } - const xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state_empty, key); - xkb_state_unref(xkb_state_empty); return sym; } -static void keyboard_key(void *data, - struct wl_keyboard * /*wl_keyboard*/, - uint32_t serial, - uint32_t /*time*/, - uint32_t key, - uint32_t state) +static void keyboard_handle_key_repeat_cancel(input_t *input) +{ + GHOST_ASSERT(input->key_repeat.timer != nullptr, "Caller much check for timer"); + delete static_cast<key_repeat_payload_t *>(input->key_repeat.timer->getUserData()); + input->system->removeTimer(input->key_repeat.timer); + input->key_repeat.timer = nullptr; +} + +/** + * Restart the key-repeat timer. + * \param use_delay: When false, use the interval + * (prevents pause when the setting changes while the key is held). + */ +static void keyboard_handle_key_repeat_reset(input_t *input, const bool use_delay) +{ + GHOST_ASSERT(input->key_repeat.timer != nullptr, "Caller much check for timer"); + GHOST_SystemWayland *system = input->system; + GHOST_ITimerTask *timer = input->key_repeat.timer; + GHOST_TimerProcPtr key_repeat_fn = timer->getTimerProc(); + GHOST_TUserDataPtr payload = input->key_repeat.timer->getUserData(); + input->system->removeTimer(input->key_repeat.timer); + const uint64_t time_step = 1000 / input->key_repeat.rate; + const uint64_t time_start = use_delay ? input->key_repeat.delay : time_step; + input->key_repeat.timer = system->installTimer(time_start, time_step, key_repeat_fn, payload); +} + +static void keyboard_handle_key(void *data, + struct wl_keyboard * /*wl_keyboard*/, + const uint32_t serial, + const uint32_t /*time*/, + const uint32_t key, + const uint32_t state) { input_t *input = static_cast<input_t *>(data); + const xkb_keycode_t key_code = key + EVDEV_OFFSET; + + const xkb_keysym_t sym = xkb_state_key_get_one_sym_without_modifiers( + input->xkb_state_empty, input->xkb_state_empty_with_numlock, key_code); + if (sym == XKB_KEY_NoSymbol) { + CLOG_INFO(LOG, 2, "key (no symbol, skipped)"); + return; + } + CLOG_INFO(LOG, 2, "key"); GHOST_TEventType etype = GHOST_kEventUnknown; switch (state) { @@ -1138,168 +2193,369 @@ static void keyboard_key(void *data, break; } - const xkb_keysym_t sym = xkb_state_key_get_one_sym_without_modifiers(input->xkb_state, key + 8); - - if (sym == XKB_KEY_NoSymbol) { - return; - } - const GHOST_TKey gkey = xkb_map_gkey(sym); + struct key_repeat_payload_t *key_repeat_payload = nullptr; /* Delete previous timer. */ - if (xkb_keymap_key_repeats(xkb_state_get_keymap(input->xkb_state), key + 8) && - input->key_repeat.timer) { - delete static_cast<key_repeat_payload_t *>(input->key_repeat.timer->getUserData()); - input->system->removeTimer(input->key_repeat.timer); - input->key_repeat.timer = nullptr; - } + if (input->key_repeat.timer) { + enum { NOP = 1, RESET, CANCEL } timer_action = NOP; + key_repeat_payload = static_cast<key_repeat_payload_t *>( + input->key_repeat.timer->getUserData()); + + if (input->key_repeat.rate == 0) { + /* Repeat was disabled (unlikely but possible). */ + timer_action = CANCEL; + } + else if (key_code == key_repeat_payload->key_code) { + /* Releasing the key that was held always cancels. */ + timer_action = CANCEL; + } + else if (xkb_keymap_key_repeats(xkb_state_get_keymap(input->xkb_state), key_code)) { + if (etype == GHOST_kEventKeyDown) { + /* Any other key-down always cancels (and may start it's own repeat timer). */ + timer_action = CANCEL; + } + else { + /* Key-up from keys that were not repeating cause the repeat timer to pause. + * + * NOTE(@campbellbarton): This behavior isn't universal, some text input systems will + * stop the repeat entirely. Choose to pause repeat instead as this is what GTK/WIN32 do, + * and it fits better for keyboard input that isn't related to text entry. */ + timer_action = RESET; + } + } - GHOST_TEventKeyData key_data; + switch (timer_action) { + case NOP: { + /* Don't add a new timer, leave the existing timer owning this `key_repeat_payload`. */ + key_repeat_payload = nullptr; + break; + } + case RESET: { + /* The payload will be added again. */ + input->system->removeTimer(input->key_repeat.timer); + input->key_repeat.timer = nullptr; + break; + } + case CANCEL: { + delete key_repeat_payload; + key_repeat_payload = nullptr; - if (etype == GHOST_kEventKeyDown) { - xkb_state_key_get_utf8( - input->xkb_state, key + 8, key_data.utf8_buf, sizeof(GHOST_TEventKeyData::utf8_buf)); - } - else { - key_data.utf8_buf[0] = '\0'; + input->system->removeTimer(input->key_repeat.timer); + input->key_repeat.timer = nullptr; + break; + } + } } - input->data_source->source_serial = serial; + const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, key); + char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'}; + if (etype == GHOST_kEventKeyDown) { + xkb_state_key_get_utf8(input->xkb_state, key_code, utf8_buf, sizeof(utf8_buf)); + } - GHOST_IWindow *win = static_cast<GHOST_WindowWayland *>( - wl_surface_get_user_data(input->focus_keyboard)); - input->system->pushEvent(new GHOST_EventKey( - input->system->getMilliSeconds(), etype, win, gkey, '\0', key_data.utf8_buf, false)); + input->data_source_serial = serial; - /* Start timer for repeating key, if applicable. */ - if (input->key_repeat.rate > 0 && - xkb_keymap_key_repeats(xkb_state_get_keymap(input->xkb_state), key + 8) && - etype == GHOST_kEventKeyDown) { + if (wl_surface *focus_surface = input->keyboard.wl_surface) { + GHOST_IWindow *win = ghost_wl_surface_user_data(focus_surface); + input->system->pushEvent( + new GHOST_EventKey(input->system->getMilliSeconds(), etype, win, gkey, false, utf8_buf)); + } - key_repeat_payload_t *payload = new key_repeat_payload_t({ - .system = input->system, - .window = win, - .key = gkey, - .key_data = key_data, - }); + /* An existing payload means the key repeat timer is reset and will be added again. */ + if (key_repeat_payload == nullptr) { + /* Start timer for repeating key, if applicable. */ + if ((input->key_repeat.rate > 0) && (etype == GHOST_kEventKeyDown) && + xkb_keymap_key_repeats(xkb_state_get_keymap(input->xkb_state), key_code)) { + key_repeat_payload = new key_repeat_payload_t({ + .input = input, + .key_code = key_code, + .key_data = {.gkey = gkey}, + }); + } + } - auto cb = [](GHOST_ITimerTask *task, uint64_t /*time*/) { + if (key_repeat_payload) { + auto key_repeat_fn = [](GHOST_ITimerTask *task, uint64_t /*time*/) { struct key_repeat_payload_t *payload = static_cast<key_repeat_payload_t *>( task->getUserData()); - payload->system->pushEvent(new GHOST_EventKey(payload->system->getMilliSeconds(), - GHOST_kEventKeyDown, - payload->window, - payload->key, - '\0', - payload->key_data.utf8_buf, - true)); + + input_t *input = payload->input; + if (wl_surface *focus_surface = input->keyboard.wl_surface) { + GHOST_IWindow *win = ghost_wl_surface_user_data(focus_surface); + GHOST_SystemWayland *system = input->system; + /* Calculate this value every time in case modifier keys are pressed. */ + char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'}; + xkb_state_key_get_utf8(input->xkb_state, payload->key_code, utf8_buf, sizeof(utf8_buf)); + system->pushEvent(new GHOST_EventKey(system->getMilliSeconds(), + GHOST_kEventKeyDown, + win, + payload->key_data.gkey, + true, + utf8_buf)); + } }; input->key_repeat.timer = input->system->installTimer( - input->key_repeat.delay, 1000 / input->key_repeat.rate, cb, payload); + input->key_repeat.delay, 1000 / input->key_repeat.rate, key_repeat_fn, key_repeat_payload); } } -static void keyboard_modifiers(void *data, - struct wl_keyboard * /*wl_keyboard*/, - uint32_t /*serial*/, - uint32_t mods_depressed, - uint32_t mods_latched, - uint32_t mods_locked, - uint32_t group) +static void keyboard_handle_modifiers(void *data, + struct wl_keyboard * /*wl_keyboard*/, + const uint32_t /*serial*/, + const uint32_t mods_depressed, + const uint32_t mods_latched, + const uint32_t mods_locked, + const uint32_t group) { - xkb_state_update_mask(static_cast<input_t *>(data)->xkb_state, - mods_depressed, - mods_latched, - mods_locked, - 0, - 0, - group); + CLOG_INFO(LOG, 2, "modifiers"); + + input_t *input = static_cast<input_t *>(data); + xkb_state_update_mask(input->xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); + + /* A modifier changed so reset the timer, + * see comment in #keyboard_handle_key regarding this behavior. */ + if (input->key_repeat.timer) { + keyboard_handle_key_repeat_reset(input, true); + } } -static void keyboard_repeat_info(void *data, - struct wl_keyboard * /*wl_keyboard*/, - int32_t rate, - int32_t delay) +static void keyboard_repeat_handle_info(void *data, + struct wl_keyboard * /*wl_keyboard*/, + const int32_t rate, + const int32_t delay) { - input_t *input = static_cast<input_t *>(data); + CLOG_INFO(LOG, 2, "info (rate=%d, delay=%d)", rate, delay); + input_t *input = static_cast<input_t *>(data); input->key_repeat.rate = rate; input->key_repeat.delay = delay; + + /* Unlikely possible this setting changes while repeating. */ + if (input->key_repeat.timer) { + keyboard_handle_key_repeat_reset(input, false); + } } static const struct wl_keyboard_listener keyboard_listener = { - keyboard_keymap, - keyboard_enter, - keyboard_leave, - keyboard_key, - keyboard_modifiers, - keyboard_repeat_info, + keyboard_handle_keymap, + keyboard_handle_enter, + keyboard_handle_leave, + keyboard_handle_key, + keyboard_handle_modifiers, + keyboard_repeat_handle_info, }; -static void seat_capabilities(void *data, struct wl_seat *wl_seat, uint32_t capabilities) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Seat), #wl_seat_listener + * \{ */ + +static CLG_LogRef LOG_WL_SEAT = {"ghost.wl.handle.seat"}; +#define LOG (&LOG_WL_SEAT) + +static void seat_handle_capabilities(void *data, + struct wl_seat *wl_seat, + const uint32_t capabilities) { + CLOG_INFO(LOG, + 2, + "capabilities (pointer=%d, keyboard=%d, touch=%d)", + (capabilities & WL_SEAT_CAPABILITY_POINTER) != 0, + (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0, + (capabilities & WL_SEAT_CAPABILITY_TOUCH) != 0); + input_t *input = static_cast<input_t *>(data); - input->pointer = nullptr; - input->keyboard = nullptr; + input->wl_pointer = nullptr; + input->wl_keyboard = nullptr; if (capabilities & WL_SEAT_CAPABILITY_POINTER) { - input->pointer = wl_seat_get_pointer(wl_seat); - input->cursor.surface = wl_compositor_create_surface(input->system->compositor()); + input->wl_pointer = wl_seat_get_pointer(wl_seat); + input->cursor.wl_surface = wl_compositor_create_surface(input->system->compositor()); input->cursor.visible = true; - input->cursor.buffer = nullptr; - input->cursor.file_buffer = new buffer_t; + input->cursor.wl_buffer = nullptr; if (!get_cursor_settings(input->cursor.theme_name, input->cursor.size)) { input->cursor.theme_name = std::string(); input->cursor.size = default_cursor_size; } - wl_pointer_add_listener(input->pointer, &pointer_listener, data); - wl_surface_add_listener(input->cursor.surface, &cursor_surface_listener, data); + wl_pointer_add_listener(input->wl_pointer, &pointer_listener, data); + + wl_surface_add_listener(input->cursor.wl_surface, &cursor_surface_listener, data); + ghost_wl_surface_tag_cursor_pointer(input->cursor.wl_surface); } if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - input->keyboard = wl_seat_get_keyboard(wl_seat); - wl_keyboard_add_listener(input->keyboard, &keyboard_listener, data); + input->wl_keyboard = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(input->wl_keyboard, &keyboard_listener, data); } } -static void seat_name(void *data, struct wl_seat * /*wl_seat*/, const char *name) +static void seat_handle_name(void *data, struct wl_seat * /*wl_seat*/, const char *name) { + CLOG_INFO(LOG, 2, "name (name=\"%s\")", name); static_cast<input_t *>(data)->name = std::string(name); } static const struct wl_seat_listener seat_listener = { - seat_capabilities, - seat_name, + seat_handle_capabilities, + seat_handle_name, }; -static void output_geometry(void *data, - struct wl_output * /*wl_output*/, - int32_t /*x*/, - int32_t /*y*/, - int32_t physical_width, - int32_t physical_height, - int32_t /*subpixel*/, - const char *make, - const char *model, - int32_t transform) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (XDG Output), #zxdg_output_v1_listener + * \{ */ + +static CLG_LogRef LOG_WL_XDG_OUTPUT = {"ghost.wl.handle.xdg_output"}; +#define LOG (&LOG_WL_XDG_OUTPUT) + +static void xdg_output_handle_logical_position(void *data, + struct zxdg_output_v1 * /*xdg_output*/, + const int32_t x, + const int32_t y) +{ + CLOG_INFO(LOG, 2, "logical_position [%d, %d]", x, y); + + output_t *output = static_cast<output_t *>(data); + output->position_logical[0] = x; + output->position_logical[1] = y; + output->has_position_logical = true; +} + +static void xdg_output_handle_logical_size(void *data, + struct zxdg_output_v1 * /*xdg_output*/, + const int32_t width, + const int32_t height) +{ + CLOG_INFO(LOG, 2, "logical_size [%d, %d]", width, height); + + output_t *output = static_cast<output_t *>(data); + if (output->size_logical[0] != 0 && output->size_logical[1] != 0) { + /* Original comment from SDL. */ + /* FIXME(@flibit): GNOME has a bug where the logical size does not account for + * scale, resulting in bogus viewport sizes. + * + * Until this is fixed, validate that _some_ kind of scaling is being + * done (we can't match exactly because fractional scaling can't be + * detected otherwise), then override if necessary. */ + if ((output->size_logical[0] == width) && (output->scale_fractional == wl_fixed_from_int(1))) { + GHOST_PRINT("xdg_output scale did not match, overriding with wl_output scale"); + +#ifdef USE_GNOME_CONFINE_HACK + /* Use a bug in GNOME to check GNOME is in use. If the bug is fixed this won't cause an issue + * as T98793 has been fixed up-stream too, but not in a release at time of writing. */ + use_gnome_confine_hack = true; +#endif + + return; + } + } + + output->size_logical[0] = width; + output->size_logical[1] = height; + output->has_size_logical = true; +} + +static void xdg_output_handle_done(void *data, struct zxdg_output_v1 * /*xdg_output*/) { + CLOG_INFO(LOG, 2, "done"); + /* NOTE: `xdg-output.done` events are deprecated and only apply below version 3 of the protocol. + * `wl-output.done` event will be emitted in version 3 or higher. */ + output_t *output = static_cast<output_t *>(data); + if (zxdg_output_v1_get_version(output->xdg_output) < 3) { + output_handle_done(data, output->wl_output); + } +} + +static void xdg_output_handle_name(void * /*data*/, + struct zxdg_output_v1 * /*xdg_output*/, + const char *name) +{ + CLOG_INFO(LOG, 2, "name (name=\"%s\")", name); +} + +static void xdg_output_handle_description(void * /*data*/, + struct zxdg_output_v1 * /*xdg_output*/, + const char *description) +{ + CLOG_INFO(LOG, 2, "description (description=\"%s\")", description); +} + +static const struct zxdg_output_v1_listener xdg_output_listener = { + xdg_output_handle_logical_position, + xdg_output_handle_logical_size, + xdg_output_handle_done, + xdg_output_handle_name, + xdg_output_handle_description, +}; + +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Output), #wl_output_listener + * \{ */ + +static CLG_LogRef LOG_WL_OUTPUT = {"ghost.wl.handle.output"}; +#define LOG (&LOG_WL_OUTPUT) + +static void output_handle_geometry(void *data, + struct wl_output * /*wl_output*/, + const int32_t /*x*/, + const int32_t /*y*/, + const int32_t physical_width, + const int32_t physical_height, + const int32_t /*subpixel*/, + const char *make, + const char *model, + const int32_t transform) +{ + CLOG_INFO(LOG, + 2, + "geometry (make=\"%s\", model=\"%s\", transform=%d, size=[%d, %d])", + make, + model, + transform, + physical_width, + physical_height); + output_t *output = static_cast<output_t *>(data); output->transform = transform; output->make = std::string(make); output->model = std::string(model); - output->width_mm = physical_width; - output->height_mm = physical_height; + output->size_mm[0] = physical_width; + output->size_mm[1] = physical_height; } -static void output_mode(void *data, - struct wl_output * /*wl_output*/, - uint32_t /*flags*/, - int32_t width, - int32_t height, - int32_t /*refresh*/) +static void output_handle_mode(void *data, + struct wl_output * /*wl_output*/, + const uint32_t flags, + const int32_t width, + const int32_t height, + const int32_t /*refresh*/) { + if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) { + CLOG_INFO(LOG, 2, "mode (skipped)"); + return; + } + CLOG_INFO(LOG, 2, "mode (size=[%d, %d], flags=%u)", width, height, flags); + output_t *output = static_cast<output_t *>(data); - output->width_pxl = width; - output->height_pxl = height; + output->size_native[0] = width; + output->size_native[1] = height; + + /* Don't rotate this yet, `wl-output` coordinates are transformed in + * handle_done and `xdg-output` coordinates are pre-transformed. */ + if (!output->has_size_logical) { + output->size_logical[0] = width; + output->size_logical[1] = height; + } } /** @@ -1310,42 +2566,143 @@ static void output_mode(void *data, * 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_done(void * /*data*/, struct wl_output * /*wl_output*/) +static void output_handle_done(void *data, struct wl_output * /*wl_output*/) { + CLOG_INFO(LOG, 2, "done"); + + output_t *output = static_cast<output_t *>(data); + int32_t size_native[2]; + if (output->transform & WL_OUTPUT_TRANSFORM_90) { + size_native[0] = output->size_native[1]; + size_native[1] = output->size_native[0]; + } + else { + size_native[0] = output->size_native[0]; + size_native[1] = output->size_native[1]; + } + + /* If `xdg-output` is present, calculate the true scale of the desktop */ + if (output->has_size_logical) { + + /* NOTE: it's not necessary to divide these values by their greatest-common-denominator + * as even a 64k screen resolution doesn't approach overflowing an `int32_t`. */ + + GHOST_ASSERT(size_native[0] && output->size_logical[0], + "Screen size values were not set when they were expected to be."); + + output->scale_fractional = wl_fixed_from_int(size_native[0]) / output->size_logical[0]; + output->has_scale_fractional = true; + } } -static void output_scale(void *data, struct wl_output * /*wl_output*/, int32_t factor) +static void output_handle_scale(void *data, struct wl_output * /*wl_output*/, const int32_t factor) { + CLOG_INFO(LOG, 2, "scale"); static_cast<output_t *>(data)->scale = factor; + + if (window_manager) { + for (GHOST_IWindow *iwin : window_manager->getWindows()) { + GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin); + win->outputs_changed_update_scale(); + /* TODO(@campbellbarton): support refreshing the UI when the DPI changes. + * There are glitches when resizing the monitor which would be nice to solve. */ + } + } } static const struct wl_output_listener output_listener = { - output_geometry, - output_mode, - output_done, - output_scale, + output_handle_geometry, + output_handle_mode, + output_handle_done, + output_handle_scale, }; -static void shell_ping(void * /*data*/, struct xdg_wm_base *xdg_wm_base, uint32_t serial) +#undef LOG + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (XDG WM Base), #xdg_wm_base_listener + * \{ */ + +#ifndef WITH_GHOST_WAYLAND_LIBDECOR + +static CLG_LogRef LOG_WL_XDG_WM_BASE = {"ghost.wl.handle.output"}; +# define LOG (&LOG_WL_XDG_WM_BASE) + +static void shell_handle_ping(void * /*data*/, + struct xdg_wm_base *xdg_wm_base, + const uint32_t serial) { + CLOG_INFO(LOG, 2, "ping"); xdg_wm_base_pong(xdg_wm_base, serial); } static const struct xdg_wm_base_listener shell_listener = { - shell_ping, + shell_handle_ping, +}; + +# undef LOG + +#endif /* !WITH_GHOST_WAYLAND_LIBDECOR. */ + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (LibDecor), #libdecor_interface + * \{ */ + +#ifdef WITH_GHOST_WAYLAND_LIBDECOR + +static CLG_LogRef LOG_WL_LIBDECOR = {"ghost.wl.handle.libdecor"}; +# define LOG (&LOG_WL_LIBDECOR) + +static void decor_handle_error(struct libdecor * /*context*/, + enum libdecor_error error, + const char *message) +{ + CLOG_INFO(LOG, 2, "error (id=%d, message=%s)", error, message); + + (void)(error); + (void)(message); + GHOST_PRINT("decoration error (" << error << "): " << message << std::endl); + exit(EXIT_FAILURE); +} + +static struct libdecor_interface libdecor_interface = { + decor_handle_error, }; -static void global_add(void *data, - struct wl_registry *wl_registry, - uint32_t name, - const char *interface, - uint32_t /*version*/) +# undef LOG + +#endif /* WITH_GHOST_WAYLAND_LIBDECOR. */ + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Listener (Registry), #wl_registry_listener + * \{ */ + +static CLG_LogRef LOG_WL_REGISTRY = {"ghost.wl.handle.registry"}; +#define LOG (&LOG_WL_REGISTRY) + +static void global_handle_add(void *data, + struct wl_registry *wl_registry, + const uint32_t name, + const char *interface, + const uint32_t version) { + /* Log last since it can be noted if the interface was handled or not. */ + bool found = true; + struct display_t *display = static_cast<struct display_t *>(data); if (!strcmp(interface, wl_compositor_interface.name)) { display->compositor = static_cast<wl_compositor *>( wl_registry_bind(wl_registry, name, &wl_compositor_interface, 3)); } +#ifdef WITH_GHOST_WAYLAND_LIBDECOR + /* Pass. */ +#else else if (!strcmp(interface, xdg_wm_base_interface.name)) { display->xdg_shell = static_cast<xdg_wm_base *>( wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, 1)); @@ -1355,30 +2712,41 @@ static void global_add(void *data, display->xdg_decoration_manager = static_cast<zxdg_decoration_manager_v1 *>( wl_registry_bind(wl_registry, name, &zxdg_decoration_manager_v1_interface, 1)); } +#endif /* !WITH_GHOST_WAYLAND_LIBDECOR. */ + else if (!strcmp(interface, zxdg_output_manager_v1_interface.name)) { + display->xdg_output_manager = static_cast<zxdg_output_manager_v1 *>( + wl_registry_bind(wl_registry, name, &zxdg_output_manager_v1_interface, 2)); + for (output_t *output : display->outputs) { + output->xdg_output = zxdg_output_manager_v1_get_xdg_output(display->xdg_output_manager, + output->wl_output); + zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output); + } + } else if (!strcmp(interface, wl_output_interface.name)) { output_t *output = new output_t; - output->scale = 1; - output->output = static_cast<wl_output *>( + output->wl_output = static_cast<wl_output *>( wl_registry_bind(wl_registry, name, &wl_output_interface, 2)); + ghost_wl_output_tag(output->wl_output); + wl_output_set_user_data(output->wl_output, output); + display->outputs.push_back(output); - wl_output_add_listener(output->output, &output_listener, output); + wl_output_add_listener(output->wl_output, &output_listener, output); + + if (display->xdg_output_manager) { + output->xdg_output = zxdg_output_manager_v1_get_xdg_output(display->xdg_output_manager, + output->wl_output); + zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output); + } } else if (!strcmp(interface, wl_seat_interface.name)) { input_t *input = new input_t; input->system = display->system; input->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - input->xkb_state = nullptr; - input->data_offer_dnd = nullptr; - input->data_offer_copy_paste = nullptr; input->data_source = new data_source_t; - input->data_source->data_source = nullptr; - input->data_source->buffer_out = nullptr; - input->relative_pointer = nullptr; - input->locked_pointer = nullptr; - input->seat = static_cast<wl_seat *>( + input->wl_seat = static_cast<wl_seat *>( wl_registry_bind(wl_registry, name, &wl_seat_interface, 4)); display->inputs.push_back(input); - wl_seat_add_listener(input->seat, &seat_listener, input); + wl_seat_add_listener(input->wl_seat, &seat_listener, input); } else if (!strcmp(interface, wl_shm_interface.name)) { display->shm = static_cast<wl_shm *>( @@ -1386,7 +2754,11 @@ static void global_add(void *data, } else if (!strcmp(interface, wl_data_device_manager_interface.name)) { display->data_device_manager = static_cast<wl_data_device_manager *>( - wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, 1)); + wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, 3)); + } + else if (!strcmp(interface, zwp_tablet_manager_v2_interface.name)) { + display->tablet_manager = static_cast<zwp_tablet_manager_v2 *>( + wl_registry_bind(wl_registry, name, &zwp_tablet_manager_v2_interface, 1)); } else if (!strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)) { display->relative_pointer_manager = static_cast<zwp_relative_pointer_manager_v1 *>( @@ -1396,6 +2768,17 @@ static void global_add(void *data, display->pointer_constraints = static_cast<zwp_pointer_constraints_v1 *>( wl_registry_bind(wl_registry, name, &zwp_pointer_constraints_v1_interface, 1)); } + else { + found = false; + } + + CLOG_INFO(LOG, + 2, + "add %s(interface=%s, version=%u, name=%u)", + found ? "" : "(skipped), ", + interface, + version, + name); } /** @@ -1407,25 +2790,32 @@ static void global_add(void *data, * 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_remove(void * /*data*/, struct wl_registry * /*wl_registry*/, uint32_t /*name*/) +static void global_handle_remove(void * /*data*/, + struct wl_registry * /*wl_registry*/, + const uint32_t name) { + CLOG_INFO(LOG, 2, "remove (name=%u)", name); } static const struct wl_registry_listener registry_listener = { - global_add, - global_remove, + global_handle_add, + global_handle_remove, }; +#undef LOG + /** \} */ /* -------------------------------------------------------------------- */ -/** \name Ghost Implementation +/** \name GHOST Implementation * - * Wayland specific implementation of the GHOST_System interface. + * WAYLAND specific implementation of the #GHOST_System interface. * \{ */ GHOST_SystemWayland::GHOST_SystemWayland() : GHOST_System(), d(new display_t) { + wl_log_set_handler_client(ghost_wayland_log_handler); + d->system = this; /* Connect to the Wayland server. */ d->display = wl_display_connect(nullptr); @@ -1443,19 +2833,35 @@ GHOST_SystemWayland::GHOST_SystemWayland() : GHOST_System(), d(new display_t) wl_display_roundtrip(d->display); wl_registry_destroy(registry); +#ifdef WITH_GHOST_WAYLAND_LIBDECOR + d->decor_context = libdecor_new(d->display, &libdecor_interface); + if (!d->decor_context) { + display_destroy(d); + throw std::runtime_error("Wayland: unable to create window decorations!"); + } +#else if (!d->xdg_shell) { display_destroy(d); throw std::runtime_error("Wayland: unable to access xdg_shell!"); } +#endif /* Register data device per seat for IPC between Wayland clients. */ if (d->data_device_manager) { for (input_t *input : d->inputs) { input->data_device = wl_data_device_manager_get_data_device(d->data_device_manager, - input->seat); + input->wl_seat); wl_data_device_add_listener(input->data_device, &data_device_listener, input); } } + + if (d->tablet_manager) { + for (input_t *input : d->inputs) { + input->tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(d->tablet_manager, + input->wl_seat); + zwp_tablet_seat_v2_add_listener(input->tablet_seat, &tablet_seat_listener, input); + } + } } GHOST_SystemWayland::~GHOST_SystemWayland() @@ -1484,42 +2890,52 @@ int GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*actio GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const { - if (!d->inputs.empty()) { - static const xkb_state_component mods_all = xkb_state_component( - XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED | XKB_STATE_MODS_LOCKED | - XKB_STATE_MODS_EFFECTIVE); - - keys.set(GHOST_kModifierKeyLeftShift, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_SHIFT, mods_all) == - 1); - keys.set(GHOST_kModifierKeyRightShift, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_SHIFT, mods_all) == - 1); - keys.set(GHOST_kModifierKeyLeftAlt, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, "LAlt", mods_all) == 1); - keys.set(GHOST_kModifierKeyRightAlt, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, "RAlt", mods_all) == 1); - keys.set(GHOST_kModifierKeyLeftControl, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, "LControl", mods_all) == 1); - keys.set(GHOST_kModifierKeyRightControl, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, "RControl", mods_all) == 1); - keys.set(GHOST_kModifierKeyOS, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, "Super", mods_all) == 1); - keys.set(GHOST_kModifierKeyNumMasks, - xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, "NumLock", mods_all) == 1); - - return GHOST_kSuccess; + if (UNLIKELY(d->inputs.empty())) { + return GHOST_kFailure; } - return GHOST_kFailure; + + static const xkb_state_component mods_all = xkb_state_component( + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED | XKB_STATE_MODS_LOCKED | + XKB_STATE_MODS_EFFECTIVE); + + bool val; + + /* NOTE: XKB doesn't seem to differentiate between left/right modifiers. */ + + val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_SHIFT, mods_all) == 1; + keys.set(GHOST_kModifierKeyLeftShift, val); + keys.set(GHOST_kModifierKeyRightShift, val); + + val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_ALT, mods_all) == 1; + keys.set(GHOST_kModifierKeyLeftAlt, val); + keys.set(GHOST_kModifierKeyRightAlt, val); + + val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_CTRL, mods_all) == 1; + keys.set(GHOST_kModifierKeyLeftControl, val); + keys.set(GHOST_kModifierKeyRightControl, val); + + val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_LOGO, mods_all) == 1; + keys.set(GHOST_kModifierKeyOS, val); + + val = xkb_state_mod_name_is_active(d->inputs[0]->xkb_state, XKB_MOD_NAME_NUM, mods_all) == 1; + keys.set(GHOST_kModifierKeyNum, val); + + return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const { - if (!d->inputs.empty()) { - buttons = d->inputs[0]->buttons; - return GHOST_kSuccess; + if (UNLIKELY(d->inputs.empty())) { + return GHOST_kFailure; } - return GHOST_kFailure; + input_t *input = d->inputs[0]; + input_state_pointer_t *input_state = input_state_pointer_active(input); + if (!input_state) { + return GHOST_kFailure; + } + + buttons = input_state->buttons; + return GHOST_kSuccess; } char *GHOST_SystemWayland::getClipboard(bool /*selection*/) const @@ -1531,28 +2947,33 @@ char *GHOST_SystemWayland::getClipboard(bool /*selection*/) const void GHOST_SystemWayland::putClipboard(const char *buffer, bool /*selection*/) const { - if (!d->data_device_manager || d->inputs.empty()) { + if (UNLIKELY(!d->data_device_manager || d->inputs.empty())) { return; } - data_source_t *data_source = d->inputs[0]->data_source; + input_t *input = d->inputs[0]; + + std::lock_guard lock{input->data_source_mutex}; + + data_source_t *data_source = input->data_source; /* Copy buffer. */ - data_source->buffer_out = static_cast<char *>(malloc(strlen(buffer) + 1)); - std::strcpy(data_source->buffer_out, buffer); + free(data_source->buffer_out); + const size_t buffer_size = strlen(buffer) + 1; + data_source->buffer_out = static_cast<char *>(malloc(buffer_size)); + std::memcpy(data_source->buffer_out, buffer, buffer_size); data_source->data_source = wl_data_device_manager_create_data_source(d->data_device_manager); - wl_data_source_add_listener( - data_source->data_source, &data_source_listener, data_source->buffer_out); + wl_data_source_add_listener(data_source->data_source, &data_source_listener, input); for (const std::string &type : mime_send) { wl_data_source_offer(data_source->data_source, type.c_str()); } - if (!d->inputs.empty() && d->inputs[0]->data_device) { + if (input->data_device) { wl_data_device_set_selection( - d->inputs[0]->data_device, data_source->data_source, data_source->source_serial); + input->data_device, data_source->data_source, input->data_source_serial); } } @@ -1561,53 +2982,145 @@ uint8_t GHOST_SystemWayland::getNumDisplays() const return d ? uint8_t(d->outputs.size()) : 0; } +static GHOST_TSuccess getCursorPositionClientRelative_impl( + const input_state_pointer_t *input_state, + const GHOST_WindowWayland *win, + int32_t &x, + int32_t &y) +{ + const wl_fixed_t scale = win->scale(); + x = wl_fixed_to_int(scale * input_state->xy[0]); + y = wl_fixed_to_int(scale * input_state->xy[1]); + return GHOST_kSuccess; +} + +static GHOST_TSuccess setCursorPositionClientRelative_impl(input_t *input, + GHOST_WindowWayland *win, + const int32_t x, + const int32_t y) +{ + /* NOTE: WAYLAND doesn't support warping the cursor. + * However when grab is enabled, we already simulate a cursor location + * so that can be set to a new location. */ + if (!input->relative_pointer) { + return GHOST_kFailure; + } + const wl_fixed_t scale = win->scale(); + const wl_fixed_t xy_next[2] = { + wl_fixed_from_int(x) / scale, + wl_fixed_from_int(y) / scale, + }; + + /* As the cursor was "warped" generate an event at the new location. */ + relative_pointer_handle_relative_motion_impl(input, win, xy_next); + + return GHOST_kSuccess; +} + +GHOST_TSuccess GHOST_SystemWayland::getCursorPositionClientRelative(const GHOST_IWindow *window, + int32_t &x, + int32_t &y) const +{ + if (UNLIKELY(d->inputs.empty())) { + return GHOST_kFailure; + } + input_t *input = d->inputs[0]; + input_state_pointer_t *input_state = input_state_pointer_active(input); + if (!input_state || !input_state->wl_surface) { + return GHOST_kFailure; + } + const GHOST_WindowWayland *win = static_cast<const GHOST_WindowWayland *>(window); + return getCursorPositionClientRelative_impl(input_state, win, x, y); +} + +GHOST_TSuccess GHOST_SystemWayland::setCursorPositionClientRelative(GHOST_IWindow *window, + const int32_t x, + const int32_t y) +{ + if (UNLIKELY(d->inputs.empty())) { + return GHOST_kFailure; + } + input_t *input = d->inputs[0]; + GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(window); + return setCursorPositionClientRelative_impl(input, win, x, y); +} + GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const { - if (!d->inputs.empty() && (d->inputs[0]->focus_pointer != nullptr)) { - x = d->inputs[0]->x; - y = d->inputs[0]->y; - return GHOST_kSuccess; + if (UNLIKELY(d->inputs.empty())) { + return GHOST_kFailure; } - else { + input_t *input = d->inputs[0]; + input_state_pointer_t *input_state = input_state_pointer_active(input); + if (!input_state) { return GHOST_kFailure; } + + if (wl_surface *focus_surface = input_state->wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + return getCursorPositionClientRelative_impl(input_state, win, x, y); + } + return GHOST_kFailure; } -GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(int32_t /*x*/, int32_t /*y*/) +GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(const int32_t x, const int32_t y) { + if (UNLIKELY(d->inputs.empty())) { + return GHOST_kFailure; + } + input_t *input = d->inputs[0]; + + /* Intentionally different from `getCursorPosition` which supports both tablet & pointer. + * In the case of setting the cursor location, tablets don't support this. */ + if (wl_surface *focus_surface = input->pointer.wl_surface) { + GHOST_WindowWayland *win = ghost_wl_surface_user_data(focus_surface); + return setCursorPositionClientRelative_impl(input, win, x, y); + } return GHOST_kFailure; } void GHOST_SystemWayland::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const { - if (getNumDisplays() > 0) { - /* We assume first output as main. */ - width = uint32_t(d->outputs[0]->width_pxl) / d->outputs[0]->scale; - height = uint32_t(d->outputs[0]->height_pxl) / d->outputs[0]->scale; + if (getNumDisplays() == 0) { + return; } + /* We assume first output as main. */ + width = uint32_t(d->outputs[0]->size_native[0]); + height = uint32_t(d->outputs[0]->size_native[1]); } void GHOST_SystemWayland::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const { - getMainDisplayDimensions(width, height); -} + int32_t xy_min[2] = {INT32_MAX, INT32_MAX}; + int32_t xy_max[2] = {INT32_MIN, INT32_MIN}; -GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GLSettings /*glSettings*/) -{ - /* Create new off-screen window. */ - wl_surface *os_surface = wl_compositor_create_surface(compositor()); - wl_egl_window *os_egl_window = wl_egl_window_create(os_surface, int(1), int(1)); + for (const output_t *output : d->outputs) { + int32_t xy[2] = {0, 0}; + if (output->has_position_logical) { + xy[0] = output->position_logical[0]; + xy[1] = output->position_logical[1]; + } + xy_min[0] = std::min(xy_min[0], xy[0]); + xy_min[1] = std::min(xy_min[1], xy[1]); + xy_max[0] = std::max(xy_max[0], xy[0] + output->size_native[0]); + xy_max[1] = std::max(xy_max[1], xy[1] + output->size_native[1]); + } - d->os_surfaces.push_back(os_surface); - d->os_egl_windows.push_back(os_egl_window); + width = xy_max[0] - xy_min[0]; + height = xy_max[1] - xy_min[1]; +} +static GHOST_Context *createOffscreenContext_impl(GHOST_SystemWayland *system, + struct wl_display *wl_display, + wl_egl_window *egl_window) +{ GHOST_Context *context; for (int minor = 6; minor >= 0; --minor) { - context = new GHOST_ContextEGL(this, + context = new GHOST_ContextEGL(system, false, - EGLNativeWindowType(os_egl_window), - EGLNativeDisplayType(d->display), + EGLNativeWindowType(egl_window), + EGLNativeDisplayType(wl_display), EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, 4, minor, @@ -1615,16 +3128,16 @@ GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GLSettings /*g GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY, EGL_OPENGL_API); - if (context->initializeDrawingContext()) + if (context->initializeDrawingContext()) { return context; - else - delete context; + } + delete context; } - context = new GHOST_ContextEGL(this, + context = new GHOST_ContextEGL(system, false, - EGLNativeWindowType(os_egl_window), - EGLNativeDisplayType(d->display), + EGLNativeWindowType(egl_window), + EGLNativeDisplayType(wl_display), EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, 3, 3, @@ -1635,34 +3148,60 @@ GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GLSettings /*g if (context->initializeDrawingContext()) { return context; } - else { - delete context; + delete context; + return nullptr; +} + +GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GLSettings /*glSettings*/) +{ + /* Create new off-screen window. */ + wl_surface *wl_surface = wl_compositor_create_surface(compositor()); + wl_egl_window *egl_window = wl_surface ? wl_egl_window_create(wl_surface, 1, 1) : nullptr; + + GHOST_Context *context = createOffscreenContext_impl(this, d->display, egl_window); + + if (!context) { + GHOST_PRINT("Cannot create off-screen EGL context" << std::endl); + if (wl_surface) { + wl_surface_destroy(wl_surface); + } + if (egl_window) { + wl_egl_window_destroy(egl_window); + } + return nullptr; } - GHOST_PRINT("Cannot create off-screen EGL context" << std::endl); + wl_surface_set_user_data(wl_surface, egl_window); + context->setUserData(wl_surface); - return nullptr; + return context; } GHOST_TSuccess GHOST_SystemWayland::disposeContext(GHOST_IContext *context) { + struct wl_surface *wl_surface = (struct wl_surface *)((GHOST_Context *)context)->getUserData(); + wl_egl_window *egl_window = (wl_egl_window *)wl_surface_get_user_data(wl_surface); + wl_egl_window_destroy(egl_window); + wl_surface_destroy(wl_surface); + delete context; + return GHOST_kSuccess; } GHOST_IWindow *GHOST_SystemWayland::createWindow(const char *title, - int32_t left, - int32_t top, - uint32_t width, - uint32_t height, - GHOST_TWindowState state, - GHOST_TDrawingContextType type, - GHOST_GLSettings glSettings, + const int32_t left, + const int32_t top, + const uint32_t width, + const uint32_t height, + const GHOST_TWindowState state, + const GHOST_TDrawingContextType type, + const GHOST_GLSettings glSettings, const bool exclusive, const bool is_dialog, const GHOST_IWindow *parentWindow) { - /* globally store pointer to window manager */ + /* Globally store pointer to window manager. */ if (!window_manager) { window_manager = getWindowManager(); } @@ -1696,76 +3235,210 @@ GHOST_IWindow *GHOST_SystemWayland::createWindow(const char *title, return window; } -wl_display *GHOST_SystemWayland::display() -{ - return d->display; -} - -wl_compositor *GHOST_SystemWayland::compositor() +/** + * Show the buffer defined by #cursor_buffer_set without changing anything else, + * so #cursor_buffer_hide can be used to display it again. + * + * The caller is responsible for setting `input->cursor.visible`. + */ +static void cursor_buffer_show(const input_t *input) { - return d->compositor; -} + const cursor_t *c = &input->cursor; -xdg_wm_base *GHOST_SystemWayland::shell() -{ - return d->xdg_shell; -} + if (input->wl_pointer) { + const int scale = c->is_custom ? c->custom_scale : input->pointer.theme_scale; + const int32_t hotspot_x = int32_t(c->wl_image.hotspot_x) / scale; + const int32_t hotspot_y = int32_t(c->wl_image.hotspot_y) / scale; + if (input->wl_pointer) { + wl_pointer_set_cursor( + input->wl_pointer, input->pointer.serial, c->wl_surface, hotspot_x, hotspot_y); + } + } -zxdg_decoration_manager_v1 *GHOST_SystemWayland::decoration_manager() -{ - return d->xdg_decoration_manager; + if (!input->tablet_tools.empty()) { + const int scale = c->is_custom ? c->custom_scale : input->tablet.theme_scale; + const int32_t hotspot_x = int32_t(c->wl_image.hotspot_x) / scale; + const int32_t hotspot_y = int32_t(c->wl_image.hotspot_y) / scale; + for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) { + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>( + zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); + zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, + input->tablet.serial, + tool_input->cursor_surface, + hotspot_x, + hotspot_y); + } + } } -const std::vector<output_t *> &GHOST_SystemWayland::outputs() const +/** + * Hide the buffer defined by #cursor_buffer_set without changing anything else, + * so #cursor_buffer_show can be used to display it again. + * + * The caller is responsible for setting `input->cursor.visible`. + */ +static void cursor_buffer_hide(const input_t *input) { - return d->outputs; + wl_pointer_set_cursor(input->wl_pointer, input->pointer.serial, nullptr, 0, 0); + for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) { + zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, input->tablet.serial, nullptr, 0, 0); + } } -wl_shm *GHOST_SystemWayland::shm() const +/** + * Needed to ensure the cursor size is always a multiple of scale. + */ +static int cursor_buffer_compatible_scale_from_image(const struct wl_cursor_image *wl_image, + int scale) { - return d->shm; + const int32_t image_size_x = int32_t(wl_image->width); + const int32_t image_size_y = int32_t(wl_image->height); + while (scale > 1) { + if ((image_size_x % scale) == 0 && (image_size_y % scale) == 0) { + break; + } + scale -= 1; + } + return scale; +} + +static void cursor_buffer_set_surface_impl(const input_t *input, + wl_buffer *buffer, + struct wl_surface *wl_surface, + const int scale) +{ + const wl_cursor_image *wl_image = &input->cursor.wl_image; + const int32_t image_size_x = int32_t(wl_image->width); + const int32_t image_size_y = int32_t(wl_image->height); + GHOST_ASSERT((image_size_x % scale) == 0 && (image_size_y % scale) == 0, + "The size must be a multiple of the scale!"); + + wl_surface_set_buffer_scale(wl_surface, scale); + wl_surface_attach(wl_surface, buffer, 0, 0); + wl_surface_damage(wl_surface, 0, 0, image_size_x, image_size_y); + wl_surface_commit(wl_surface); +} + +static void cursor_buffer_set(const input_t *input, wl_buffer *buffer) +{ + const cursor_t *c = &input->cursor; + const wl_cursor_image *wl_image = &input->cursor.wl_image; + const bool visible = (c->visible && c->is_hardware); + + /* This is a requirement of WAYLAND, when this isn't the case, + * it causes Blender's window to close intermittently. */ + if (input->wl_pointer) { + const int scale = cursor_buffer_compatible_scale_from_image( + wl_image, c->is_custom ? c->custom_scale : input->pointer.theme_scale); + const int32_t hotspot_x = int32_t(wl_image->hotspot_x) / scale; + const int32_t hotspot_y = int32_t(wl_image->hotspot_y) / scale; + cursor_buffer_set_surface_impl(input, buffer, c->wl_surface, scale); + wl_pointer_set_cursor(input->wl_pointer, + input->pointer.serial, + visible ? c->wl_surface : nullptr, + hotspot_x, + hotspot_y); + } + + /* Set the cursor for all tablet tools as well. */ + if (!input->tablet_tools.empty()) { + const int scale = cursor_buffer_compatible_scale_from_image( + wl_image, c->is_custom ? c->custom_scale : input->tablet.theme_scale); + const int32_t hotspot_x = int32_t(wl_image->hotspot_x) / scale; + const int32_t hotspot_y = int32_t(wl_image->hotspot_y) / scale; + for (struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : input->tablet_tools) { + tablet_tool_input_t *tool_input = static_cast<tablet_tool_input_t *>( + zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); + cursor_buffer_set_surface_impl(input, buffer, tool_input->cursor_surface, scale); + zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, + input->tablet.serial, + visible ? tool_input->cursor_surface : nullptr, + hotspot_x, + hotspot_y); + } + } } -void GHOST_SystemWayland::setSelection(const std::string &selection) -{ - this->selection = selection; -} +enum eCursorSetMode { + CURSOR_VISIBLE_ALWAYS_SET = 1, + CURSOR_VISIBLE_ONLY_HIDE, + CURSOR_VISIBLE_ONLY_SHOW, +}; -static void set_cursor_buffer(input_t *input, wl_buffer *buffer) +static void cursor_visible_set(input_t *input, + const bool visible, + const bool is_hardware, + const enum eCursorSetMode set_mode) { - cursor_t *c = &input->cursor; - - c->visible = (buffer != nullptr); + cursor_t *cursor = &input->cursor; + const bool was_visible = cursor->is_hardware && cursor->visible; + const bool use_visible = is_hardware && visible; - wl_surface_attach(c->surface, buffer, 0, 0); + 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; + } + } - wl_surface_damage(c->surface, 0, 0, int32_t(c->image.width), int32_t(c->image.height)); - wl_pointer_set_cursor(input->pointer, - input->pointer_serial, - c->visible ? c->surface : nullptr, - int32_t(c->image.hotspot_x) / c->scale, - int32_t(c->image.hotspot_y) / c->scale); + if (use_visible) { + if (!was_visible) { + cursor_buffer_show(input); + } + } + else { + if (was_visible) { + cursor_buffer_hide(input); + } + } + cursor->visible = visible; + cursor->is_hardware = is_hardware; +} - wl_surface_commit(c->surface); +static bool cursor_is_software(const GHOST_TGrabCursorMode mode, const bool use_software_confine) +{ + if (mode == GHOST_kGrabWrap) { + return true; + } +#ifdef USE_GNOME_CONFINE_HACK + if (mode == GHOST_kGrabNormal) { + if (use_software_confine) { + return true; + } + } +#else + (void)use_software_confine; +#endif + return false; } -GHOST_TSuccess GHOST_SystemWayland::setCursorShape(GHOST_TStandardCursor shape) +GHOST_TSuccess GHOST_SystemWayland::setCursorShape(const GHOST_TStandardCursor shape) { - if (d->inputs.empty()) { + if (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } - const std::string cursor_name = cursors.count(shape) ? cursors.at(shape) : - cursors.at(GHOST_kStandardCursorDefault); + auto cursor_find = cursors.find(shape); + const char *cursor_name = (cursor_find == cursors.end()) ? + cursors.at(GHOST_kStandardCursorDefault) : + (*cursor_find).second; input_t *input = d->inputs[0]; cursor_t *c = &input->cursor; - if (!c->theme) { + if (!c->wl_theme) { /* The cursor surface hasn't entered an output yet. Initialize theme with scale 1. */ - c->theme = wl_cursor_theme_load(c->theme_name.c_str(), c->size, d->inputs[0]->system->shm()); + c->wl_theme = wl_cursor_theme_load( + c->theme_name.c_str(), c->size, d->inputs[0]->system->shm()); } - wl_cursor *cursor = wl_cursor_theme_get_cursor(c->theme, cursor_name.c_str()); + wl_cursor *cursor = wl_cursor_theme_get_cursor(c->wl_theme, cursor_name); if (!cursor) { GHOST_PRINT("cursor '" << cursor_name << "' does not exist" << std::endl); @@ -1778,81 +3451,56 @@ GHOST_TSuccess GHOST_SystemWayland::setCursorShape(GHOST_TStandardCursor shape) return GHOST_kFailure; } - c->buffer = buffer; - c->image = *image; + c->visible = true; + c->is_custom = false; + c->wl_buffer = buffer; + c->wl_image = *image; - set_cursor_buffer(input, buffer); + cursor_buffer_set(input, buffer); return GHOST_kSuccess; } -GHOST_TSuccess GHOST_SystemWayland::hasCursorShape(GHOST_TStandardCursor cursorShape) +GHOST_TSuccess GHOST_SystemWayland::hasCursorShape(const GHOST_TStandardCursor cursorShape) { - return GHOST_TSuccess(cursors.count(cursorShape) && !cursors.at(cursorShape).empty()); + auto cursor_find = cursors.find(cursorShape); + if (cursor_find == cursors.end()) { + return GHOST_kFailure; + } + const char *value = (*cursor_find).second; + if (*value == '\0') { + return GHOST_kFailure; + } + return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::setCustomCursorShape(uint8_t *bitmap, uint8_t *mask, - int sizex, - int sizey, - int hotX, - int hotY, - bool /*canInvertColor*/) + const int sizex, + const int sizey, + const int hotX, + const int hotY, + const bool /*canInvertColor*/) { - if (d->inputs.empty()) { + if (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } cursor_t *cursor = &d->inputs[0]->cursor; - static const int32_t stride = sizex * 4; /* ARGB */ - cursor->file_buffer->size = size_t(stride * sizey); - -#ifdef HAVE_MEMFD_CREATE - const int fd = memfd_create("blender-cursor-custom", MFD_CLOEXEC | MFD_ALLOW_SEALING); - if (fd >= 0) { - fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL); - } -#else - char *path = getenv("XDG_RUNTIME_DIR"); - if (!path) { - errno = ENOENT; - return GHOST_kFailure; + if (cursor->custom_data) { + munmap(cursor->custom_data, cursor->custom_data_size); + cursor->custom_data = nullptr; + cursor->custom_data_size = 0; /* Not needed, but the value is no longer meaningful. */ } - char *tmpname; - asprintf(&tmpname, "%s/%s", path, "blender-XXXXXX"); - const int fd = mkostemp(tmpname, O_CLOEXEC); - if (fd >= 0) { - unlink(tmpname); - } - free(tmpname); -#endif - - if (fd < 0) { + const int32_t size_xy[2] = {sizex, sizey}; + wl_buffer *buffer = ghost_wl_buffer_create_for_image( + d->shm, size_xy, WL_SHM_FORMAT_ARGB8888, &cursor->custom_data, &cursor->custom_data_size); + if (buffer == nullptr) { return GHOST_kFailure; } - if (posix_fallocate(fd, 0, int32_t(cursor->file_buffer->size)) != 0) { - return GHOST_kFailure; - } - - cursor->file_buffer->data = mmap( - nullptr, cursor->file_buffer->size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - - if (cursor->file_buffer->data == MAP_FAILED) { - close(fd); - return GHOST_kFailure; - } - - struct wl_shm_pool *pool = wl_shm_create_pool(d->shm, fd, int32_t(cursor->file_buffer->size)); - - wl_buffer *buffer = wl_shm_pool_create_buffer( - pool, 0, sizex, sizey, stride, WL_SHM_FORMAT_ARGB8888); - - wl_shm_pool_destroy(pool); - close(fd); - wl_buffer_add_listener(buffer, &cursor_buffer_listener, cursor); static constexpr uint32_t black = 0xFF000000; @@ -1863,7 +3511,7 @@ GHOST_TSuccess GHOST_SystemWayland::setCustomCursorShape(uint8_t *bitmap, uint32_t *pixel; for (int y = 0; y < sizey; ++y) { - pixel = &static_cast<uint32_t *>(cursor->file_buffer->data)[y * sizex]; + pixel = &static_cast<uint32_t *>(cursor->custom_data)[y * sizex]; for (int x = 0; x < sizex; ++x) { if ((x % 8) == 0) { datab = *bitmap++; @@ -1885,90 +3533,466 @@ GHOST_TSuccess GHOST_SystemWayland::setCustomCursorShape(uint8_t *bitmap, } } - cursor->buffer = buffer; - cursor->image.width = uint32_t(sizex); - cursor->image.height = uint32_t(sizey); - cursor->image.hotspot_x = uint32_t(hotX); - cursor->image.hotspot_y = uint32_t(hotY); + cursor->visible = true; + cursor->is_custom = true; + cursor->custom_scale = 1; /* TODO: support Hi-DPI custom cursors. */ + cursor->wl_buffer = buffer; + cursor->wl_image.width = uint32_t(sizex); + cursor->wl_image.height = uint32_t(sizey); + cursor->wl_image.hotspot_x = uint32_t(hotX); + cursor->wl_image.hotspot_y = uint32_t(hotY); - set_cursor_buffer(d->inputs[0], buffer); + cursor_buffer_set(d->inputs[0], buffer); return GHOST_kSuccess; } -GHOST_TSuccess GHOST_SystemWayland::setCursorVisibility(bool visible) +GHOST_TSuccess GHOST_SystemWayland::getCursorBitmap(GHOST_CursorBitmapRef *bitmap) { - if (d->inputs.empty()) { + cursor_t *cursor = &d->inputs[0]->cursor; + if (cursor->custom_data == nullptr) { + return GHOST_kFailure; + } + if (!cursor->is_custom) { + return GHOST_kFailure; + } + + bitmap->data_size[0] = cursor->wl_image.width; + bitmap->data_size[1] = cursor->wl_image.height; + + bitmap->hot_spot[0] = cursor->wl_image.hotspot_x; + bitmap->hot_spot[1] = cursor->wl_image.hotspot_y; + + bitmap->data = (uint8_t *)static_cast<void *>(cursor->custom_data); + + return GHOST_kSuccess; +} + +GHOST_TSuccess GHOST_SystemWayland::setCursorVisibility(const bool visible) +{ + if (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } input_t *input = d->inputs[0]; + cursor_visible_set(input, visible, input->cursor.is_hardware, CURSOR_VISIBLE_ALWAYS_SET); + return GHOST_kSuccess; +} - cursor_t *cursor = &input->cursor; - if (visible) { - if (!cursor->visible) { - set_cursor_buffer(input, cursor->buffer); - } +bool GHOST_SystemWayland::supportsCursorWarp() +{ + /* WAYLAND doesn't support setting the cursor position directly, + * this is an intentional choice, forcing us to use a software cursor in this case. */ + return false; +} + +bool GHOST_SystemWayland::supportsWindowPosition() +{ + /* WAYLAND doesn't support accessing the window position. */ + return false; +} + +bool GHOST_SystemWayland::getCursorGrabUseSoftwareDisplay(const GHOST_TGrabCursorMode mode) +{ + if (UNLIKELY(d->inputs.empty())) { + return false; } - else { - if (cursor->visible) { - set_cursor_buffer(input, nullptr); - } + +#ifdef USE_GNOME_CONFINE_HACK + input_t *input = d->inputs[0]; + const bool use_software_confine = input->use_pointer_software_confine; +#else + const bool use_software_confine = false; +#endif + + return cursor_is_software(mode, use_software_confine); +} + +#ifdef USE_GNOME_CONFINE_HACK +static bool setCursorGrab_use_software_confine(const GHOST_TGrabCursorMode mode, + wl_surface *surface) +{ +# ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON + if (use_gnome_confine_hack == false) { + return false; + } +# endif + if (mode != GHOST_kGrabNormal) { + return false; + } + const GHOST_WindowWayland *win = ghost_wl_surface_user_data(surface); + if (!win) { + return false; } - return GHOST_kSuccess; +# ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON + if (win->scale() <= 1) { + return false; + } +# endif + return true; +} +#endif + +static input_grab_state_t input_grab_state_from_mode(const GHOST_TGrabCursorMode mode, + const bool use_software_confine) +{ + /* Initialize all members. */ + const struct input_grab_state_t grab_state = { + /* Warping happens to require software cursor which also hides. */ + .use_lock = ELEM(mode, GHOST_kGrabWrap, GHOST_kGrabHide) || use_software_confine, + .use_confine = (mode == GHOST_kGrabNormal) && (use_software_confine == false), + }; + return grab_state; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Public WAYLAND Proxy Ownership API + * \{ */ + +static const char *ghost_wl_output_tag_id = "GHOST-output"; +static const char *ghost_wl_surface_tag_id = "GHOST-window"; +static const char *ghost_wl_surface_cursor_pointer_tag_id = "GHOST-cursor-pointer"; +static const char *ghost_wl_surface_cursor_tablet_tag_id = "GHOST-cursor-tablet"; + +bool ghost_wl_output_own(const struct wl_output *output) +{ + return wl_proxy_get_tag((struct wl_proxy *)output) == &ghost_wl_output_tag_id; +} + +bool ghost_wl_surface_own(const struct wl_surface *surface) +{ + return wl_proxy_get_tag((struct wl_proxy *)surface) == &ghost_wl_surface_tag_id; +} + +bool ghost_wl_surface_own_cursor_pointer(const struct wl_surface *surface) +{ + return wl_proxy_get_tag((struct wl_proxy *)surface) == &ghost_wl_surface_cursor_pointer_tag_id; +} + +bool ghost_wl_surface_own_cursor_tablet(const struct wl_surface *surface) +{ + return wl_proxy_get_tag((struct wl_proxy *)surface) == &ghost_wl_surface_cursor_tablet_tag_id; +} + +void ghost_wl_output_tag(struct wl_output *output) +{ + wl_proxy_set_tag((struct wl_proxy *)output, &ghost_wl_output_tag_id); +} + +void ghost_wl_surface_tag(struct wl_surface *surface) +{ + wl_proxy_set_tag((struct wl_proxy *)surface, &ghost_wl_surface_tag_id); +} + +void ghost_wl_surface_tag_cursor_pointer(struct wl_surface *surface) +{ + wl_proxy_set_tag((struct wl_proxy *)surface, &ghost_wl_surface_cursor_pointer_tag_id); +} + +void ghost_wl_surface_tag_cursor_tablet(struct wl_surface *surface) +{ + wl_proxy_set_tag((struct wl_proxy *)surface, &ghost_wl_surface_cursor_tablet_tag_id); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Public WAYLAND Direct Data Access + * + * Expose some members via methods. + * \{ */ + +wl_display *GHOST_SystemWayland::display() +{ + return d->display; +} + +wl_compositor *GHOST_SystemWayland::compositor() +{ + return d->compositor; } -GHOST_TSuccess GHOST_SystemWayland::setCursorGrab(const GHOST_TGrabCursorMode mode, - const GHOST_TGrabCursorMode mode_current, +#ifdef WITH_GHOST_WAYLAND_LIBDECOR + +libdecor *GHOST_SystemWayland::decor_context() +{ + return d->decor_context; +} + +#else /* WITH_GHOST_WAYLAND_LIBDECOR */ + +xdg_wm_base *GHOST_SystemWayland::xdg_shell() +{ + return d->xdg_shell; +} - wl_surface *surface) +zxdg_decoration_manager_v1 *GHOST_SystemWayland::xdg_decoration_manager() { - /* ignore, if the required protocols are not supported */ - if (!d->relative_pointer_manager || !d->pointer_constraints) { + return d->xdg_decoration_manager; +} + +#endif /* !WITH_GHOST_WAYLAND_LIBDECOR */ + +const std::vector<output_t *> &GHOST_SystemWayland::outputs() const +{ + return d->outputs; +} + +wl_shm *GHOST_SystemWayland::shm() const +{ + return d->shm; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Public WAYLAND Query Access + * \{ */ + +struct output_t *ghost_wl_output_user_data(struct wl_output *wl_output) +{ + GHOST_ASSERT(wl_output, "output must not be NULL"); + GHOST_ASSERT(ghost_wl_output_own(wl_output), "output is not owned by GHOST"); + output_t *output = static_cast<output_t *>(wl_output_get_user_data(wl_output)); + return output; +} + +GHOST_WindowWayland *ghost_wl_surface_user_data(struct wl_surface *surface) +{ + GHOST_ASSERT(surface, "surface must not be NULL"); + GHOST_ASSERT(ghost_wl_surface_own(surface), "surface is not owned by GHOST"); + GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(wl_surface_get_user_data(surface)); + return win; +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Public WAYLAND Utility Functions + * + * Functionality only used for the WAYLAND implementation. + * \{ */ + +void GHOST_SystemWayland::selection_set(const std::string &selection) +{ + this->selection = selection; +} + +void GHOST_SystemWayland::window_surface_unref(const wl_surface *surface) +{ +#define SURFACE_CLEAR_PTR(surface_test) \ + if (surface_test == surface) { \ + surface_test = nullptr; \ + } \ + ((void)0); + + /* Only clear window surfaces (not cursors, off-screen surfaces etc). */ + for (input_t *input : d->inputs) { + SURFACE_CLEAR_PTR(input->pointer.wl_surface); + SURFACE_CLEAR_PTR(input->tablet.wl_surface); + SURFACE_CLEAR_PTR(input->keyboard.wl_surface); + SURFACE_CLEAR_PTR(input->focus_dnd); + } +#undef SURFACE_CLEAR_PTR +} + +bool GHOST_SystemWayland::window_cursor_grab_set(const GHOST_TGrabCursorMode mode, + const GHOST_TGrabCursorMode mode_current, + int32_t init_grab_xy[2], + const GHOST_Rect *wrap_bounds, + const GHOST_TAxisFlag wrap_axis, + wl_surface *surface, + const int scale) +{ + /* Ignore, if the required protocols are not supported. */ + if (UNLIKELY(!d->relative_pointer_manager || !d->pointer_constraints)) { return GHOST_kFailure; } - if (d->inputs.empty()) { + if (UNLIKELY(d->inputs.empty())) { return GHOST_kFailure; } + /* No change, success. */ + if (mode == mode_current) { + return GHOST_kSuccess; + } input_t *input = d->inputs[0]; - if (mode != GHOST_kGrabDisable) { - /* TODO(@campbellbarton): Support #GHOST_kGrabWrap, - * where the cursor moves but is constrained to a region. */ - input->relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer( - d->relative_pointer_manager, input->pointer); - zwp_relative_pointer_v1_add_listener( - input->relative_pointer, &relative_pointer_listener, input); - input->locked_pointer = zwp_pointer_constraints_v1_lock_pointer( - d->pointer_constraints, - surface, - input->pointer, - nullptr, - ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); - - if (mode == GHOST_kGrabHide) { - setCursorVisibility(false); - } - } - else { +#ifdef USE_GNOME_CONFINE_HACK + const bool was_software_confine = input->use_pointer_software_confine; + const bool use_software_confine = setCursorGrab_use_software_confine(mode, surface); +#else + const bool was_software_confine = false; + const bool use_software_confine = false; +#endif + + const struct input_grab_state_t grab_state_prev = input_grab_state_from_mode( + mode_current, was_software_confine); + const struct input_grab_state_t grab_state_next = input_grab_state_from_mode( + mode, use_software_confine); + + /* Check for wrap as #supportsCursorWarp isn't supported. */ + const bool use_visible = !(ELEM(mode, GHOST_kGrabHide, GHOST_kGrabWrap) || use_software_confine); + const bool is_hardware_cursor = !cursor_is_software(mode, use_software_confine); + + /* Only hide so the cursor is not made visible before it's location is restored. + * This function is called again at the end of this function which only shows. */ + cursor_visible_set(input, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_HIDE); + + /* Switching from one grab mode to another, + * in this case disable the current locks as it makes logic confusing, + * postpone changing the cursor to avoid flickering. */ + if (!grab_state_next.use_lock) { if (input->relative_pointer) { zwp_relative_pointer_v1_destroy(input->relative_pointer); input->relative_pointer = nullptr; } if (input->locked_pointer) { + /* Request location to restore to. */ + if (mode_current == GHOST_kGrabWrap) { + /* Since this call is initiated by Blender, we can be sure the window wasn't closed + * by logic outside this function - as the window was needed to make this call. */ + int32_t xy_new[2] = {UNPACK2(input->pointer.xy)}; + + GHOST_Rect bounds_scale; + + bounds_scale.m_l = wl_fixed_from_int(wrap_bounds->m_l) / scale; + bounds_scale.m_t = wl_fixed_from_int(wrap_bounds->m_t) / scale; + bounds_scale.m_r = wl_fixed_from_int(wrap_bounds->m_r) / scale; + bounds_scale.m_b = wl_fixed_from_int(wrap_bounds->m_b) / scale; + + bounds_scale.wrapPoint(UNPACK2(xy_new), 0, wrap_axis); + + /* Push an event so the new location is registered. */ + if ((xy_new[0] != input->pointer.xy[0]) || (xy_new[1] != input->pointer.xy[1])) { + input->system->pushEvent(new GHOST_EventCursor(input->system->getMilliSeconds(), + GHOST_kEventCursorMove, + ghost_wl_surface_user_data(surface), + wl_fixed_to_int(scale * xy_new[0]), + wl_fixed_to_int(scale * xy_new[1]), + GHOST_TABLET_DATA_NONE)); + } + input->pointer.xy[0] = xy_new[0]; + input->pointer.xy[1] = xy_new[1]; + + zwp_locked_pointer_v1_set_cursor_position_hint(input->locked_pointer, UNPACK2(xy_new)); + wl_surface_commit(surface); + } + else if (mode_current == GHOST_kGrabHide) { + if ((init_grab_xy[0] != input->grab_lock_xy[0]) || + (init_grab_xy[1] != input->grab_lock_xy[1])) { + const wl_fixed_t xy_next[2] = { + wl_fixed_from_int(init_grab_xy[0]) / scale, + wl_fixed_from_int(init_grab_xy[1]) / scale, + }; + zwp_locked_pointer_v1_set_cursor_position_hint(input->locked_pointer, UNPACK2(xy_next)); + wl_surface_commit(surface); + } + } +#ifdef USE_GNOME_CONFINE_HACK + else if (mode_current == GHOST_kGrabNormal) { + if (was_software_confine) { + zwp_locked_pointer_v1_set_cursor_position_hint(input->locked_pointer, + UNPACK2(input->pointer.xy)); + wl_surface_commit(surface); + } + } +#endif + zwp_locked_pointer_v1_destroy(input->locked_pointer); input->locked_pointer = nullptr; } + } + + if (!grab_state_next.use_confine) { + if (input->confined_pointer) { + zwp_confined_pointer_v1_destroy(input->confined_pointer); + input->confined_pointer = nullptr; + } + } - if (mode_current == GHOST_kGrabHide) { - setCursorVisibility(false); + if (mode != GHOST_kGrabDisable) { + if (grab_state_next.use_lock) { + if (!grab_state_prev.use_lock) { + /* TODO(@campbellbarton): As WAYLAND does not support warping the pointer it may not be + * possible to support #GHOST_kGrabWrap by pragmatically settings it's coordinates. + * An alternative could be to draw the cursor in software (and hide the real cursor), + * or just accept a locked cursor on WAYLAND. */ + input->relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer( + d->relative_pointer_manager, input->wl_pointer); + zwp_relative_pointer_v1_add_listener( + input->relative_pointer, &relative_pointer_listener, input); + input->locked_pointer = zwp_pointer_constraints_v1_lock_pointer( + d->pointer_constraints, + surface, + input->wl_pointer, + nullptr, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + } + if (mode == GHOST_kGrabHide) { + /* Set the initial position to detect any changes when un-grabbing, + * otherwise the unlocked cursor defaults to un-locking in-place. */ + init_grab_xy[0] = wl_fixed_to_int(scale * input->pointer.xy[0]); + init_grab_xy[1] = wl_fixed_to_int(scale * input->pointer.xy[1]); + input->grab_lock_xy[0] = init_grab_xy[0]; + input->grab_lock_xy[1] = init_grab_xy[1]; + } + } + else if (grab_state_next.use_confine) { + if (!grab_state_prev.use_confine) { + input->confined_pointer = zwp_pointer_constraints_v1_confine_pointer( + d->pointer_constraints, + surface, + input->wl_pointer, + nullptr, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + } } } + /* Only show so the cursor is made visible as the last step. */ + cursor_visible_set(input, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_SHOW); + +#ifdef USE_GNOME_CONFINE_HACK + input->use_pointer_software_confine = use_software_confine; +#endif + return GHOST_kSuccess; } +#ifdef WITH_GHOST_WAYLAND_DYNLOAD +bool ghost_wl_dynload_libraries() +{ + /* Only report when `libwayland-client` is not found when building without X11, + * which will be used as a fallback. */ +# ifdef WITH_GHOST_X11 + bool verbose = false; +# else + bool verbose = true; +# endif + + if (wayland_dynload_client_init(verbose) && /* `libwayland-client`. */ + wayland_dynload_cursor_init(verbose) && /* `libwayland-cursor`. */ + wayland_dynload_egl_init(verbose) && /* `libwayland-egl`. */ +# ifdef WITH_GHOST_WAYLAND_LIBDECOR + wayland_dynload_libdecor_init(verbose) && /* `libdecor-0`. */ +# endif + true) { + return true; + } +# ifdef WITH_GHOST_WAYLAND_LIBDECOR + wayland_dynload_libdecor_exit(); +# endif + wayland_dynload_client_exit(); + wayland_dynload_cursor_exit(); + wayland_dynload_egl_exit(); + + return false; +} +#endif /* WITH_GHOST_WAYLAND_DYNLOAD */ + /** \} */ |