/* SPDX-FileCopyrightText: 2020-2023 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup GHOST */ #include "GHOST_SystemWayland.hh" #include "GHOST_Context.hh" #include "GHOST_Event.hh" #include "GHOST_EventButton.hh" #include "GHOST_EventCursor.hh" #include "GHOST_EventDragnDrop.hh" #include "GHOST_EventKey.hh" #include "GHOST_EventTrackpad.hh" #include "GHOST_EventWheel.hh" #include "GHOST_PathUtils.hh" #include "GHOST_TimerManager.hh" #include "GHOST_WaylandUtils.hh" #include "GHOST_WindowManager.hh" #include "GHOST_utildefines.hh" #ifdef WITH_OPENGL_BACKEND # include "GHOST_ContextEGL.hh" #endif #ifdef WITH_VULKAN_BACKEND # include "GHOST_ContextVK.hh" #endif #ifdef WITH_INPUT_NDOF # include "GHOST_NDOFManagerUnix.hh" #endif #ifdef WITH_GHOST_WAYLAND_DYNLOAD # include /* For `ghost_wl_dynload_libraries`. */ #endif #ifdef WITH_OPENGL_BACKEND # ifdef WITH_GHOST_WAYLAND_DYNLOAD # include # endif # include #endif /* WITH_OPENGL_BACKEND */ #include #include #include #include #include #include #ifdef WITH_GHOST_WAYLAND_DYNLOAD # include #endif #include #include #include /* Generated by `wayland-scanner`. */ #include #include #include #include #include #include #include #include #include #ifdef WITH_INPUT_IME # include #endif /* Decorations `xdg_decor`. */ #include #include /* End `xdg_decor`. */ #include #include #include #include /* For `exit`. */ #include #include #include /* For setting the thread priority. */ #ifdef HAVE_POLL # include #endif /* Logging, use `ghost.wl.*` prefix. */ #include "CLG_log.h" #ifdef USE_EVENT_BACKGROUND_THREAD # include "GHOST_TimerTask.hh" #endif #ifdef WITH_GHOST_WAYLAND_LIBDECOR static bool use_libdecor = true; # ifdef WITH_GHOST_WAYLAND_DYNLOAD static bool has_libdecor = false; # else static bool has_libdecor = true; # endif #endif /* -------------------------------------------------------------------- */ /** \name Forward Declarations * \{ */ static void keyboard_handle_key_repeat_cancel(GWL_Seat *seat); static void output_handle_done(void *data, wl_output *wl_output); static void gwl_seat_capability_pointer_disable(GWL_Seat *seat); static void gwl_seat_capability_keyboard_disable(GWL_Seat *seat); static void gwl_seat_capability_touch_disable(GWL_Seat *seat); static void gwl_seat_cursor_anim_begin(GWL_Seat *seat); static void gwl_seat_cursor_anim_begin_if_needed(GWL_Seat *seat); static void gwl_seat_cursor_anim_end(GWL_Seat *seat); static void gwl_seat_cursor_anim_reset(GWL_Seat *seat); static bool gwl_registry_entry_remove_by_name(GWL_Display *display, uint32_t name, int *r_interface_slot); static void gwl_registry_entry_remove_all(GWL_Display *display); struct GWL_RegistryHandler; static int gwl_registry_handler_interface_slot_max(); static int gwl_registry_handler_interface_slot_from_string(const char *interface); static const GWL_RegistryHandler *gwl_registry_handler_from_interface_slot(int interface_slot); #ifdef USE_EVENT_BACKGROUND_THREAD static void gwl_display_event_thread_destroy(GWL_Display *display); static void ghost_wl_display_lock_without_input(wl_display *wl_display, std::mutex *server_mutex); /** Default size for pending event vector. */ constexpr size_t events_pending_default_size = 4096 / sizeof(void *); #endif /* USE_EVENT_BACKGROUND_THREAD */ /** In nearly all cases use `pushEvent_maybe_pending` * at least when called from WAYLAND callbacks. */ #define pushEvent DONT_USE /** \} */ /* -------------------------------------------------------------------- */ /** \name Workaround Compositor Specific Bugs * \{ */ /** * GNOME (mutter 42.2 had a bug with confine not respecting scale - Hi-DPI), See: #98793. * Even though this has been fixed, at time of writing it's not yet in a release. * Workaround the problem by implementing confine with a software cursor. * While this isn't ideal, it's not adding a lot of overhead as software * cursors are already used for warping (which WAYLAND doesn't support). */ #define USE_GNOME_CONFINE_HACK /** * Always use software confine (not just in GNOME). * Useful for developing with compositors that don't need this workaround. */ // #define USE_GNOME_CONFINE_HACK_ALWAYS_ON #ifdef USE_GNOME_CONFINE_HACK static bool use_gnome_confine_hack = false; #endif /** * KDE (plasma 5.26.1) has a bug where the cursor surface needs to be committed * (via `wl_surface_commit`) when it was hidden and is being set to visible again, see: #102048. * See: https://bugs.kde.org/show_bug.cgi?id=461001 */ #define USE_KDE_TABLET_HIDDEN_CURSOR_HACK /** * When GNOME is found, require `libdecor`. * This is a hack because it seems there is no way to check if the compositor supports * server side decorations when initializing WAYLAND. */ #ifdef WITH_GHOST_WAYLAND_LIBDECOR # define USE_GNOME_NEEDS_LIBDECOR_HACK #endif /** \} */ /* -------------------------------------------------------------------- */ /** \name Local Defines * * Control local functionality, compositors specific workarounds. * \{ */ /** * Fix short-cut part of keyboard reading code not properly handling some keys, see: #102194. * \note This is similar to X11 workaround by the same name, see: #47228. */ #define USE_NON_LATIN_KB_WORKAROUND #define WL_NAME_UNSET uint32_t(-1) /** * Initializer for GHOST integer coordinates from `wl_fixed_t`, * taking window scale into account. */ #define WL_FIXED_TO_INT_FOR_WINDOW_V2(win, xy) \ wl_fixed_to_int((win)->wl_fixed_to_window((xy)[0])), \ wl_fixed_to_int((win)->wl_fixed_to_window((xy)[1])), /** \} */ /* -------------------------------------------------------------------- */ /** \name Inline Event Codes * * Selected input event code defines from `linux/input-event-codes.h` * We include some of the button input event codes here, since the header is * only available in more recent kernel versions. * \{ */ /** * The event codes are used to differentiate from which mouse button an event comes from. */ enum { BTN_LEFT = 0x110, BTN_RIGHT = 0x111, BTN_MIDDLE = 0x112, BTN_SIDE = 0x113, BTN_EXTRA = 0x114, BTN_FORWARD = 0x115, BTN_BACK = 0x116 }; // #define BTN_TASK 0x117 /* UNUSED. */ /** * Tablet events. * * \note Gnome/GTK swap middle/right, where the same application in X11 will swap the middle/right * mouse button when running under WAYLAND. KDE doesn't do this, and according to artists * at the Blender studio, having the button closest to the nib be MMB is preferable, * so use this as a default. If needs be - swapping these could be a preference. */ enum { /** Use as middle-mouse. */ BTN_STYLUS = 0x14b, /** Use as right-mouse. */ BTN_STYLUS2 = 0x14c, /** NOTE(@ideasman42): Map to an additional button (not sure which hardware uses this). */ BTN_STYLUS3 = 0x149, }; /** * Keyboard scan-codes. */ enum { KEY_GRAVE = 41, #ifdef USE_NON_LATIN_KB_WORKAROUND KEY_1 = 2, KEY_2 = 3, KEY_3 = 4, KEY_4 = 5, KEY_5 = 6, KEY_6 = 7, KEY_7 = 8, KEY_8 = 9, KEY_9 = 10, KEY_0 = 11, #endif }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Modifier Table * * Convenient access to modifier key values, allow looping over modifier keys. * \{ */ enum { MOD_INDEX_SHIFT = 0, MOD_INDEX_ALT = 1, MOD_INDEX_CTRL = 2, MOD_INDEX_OS = 3, }; #define MOD_INDEX_NUM (MOD_INDEX_OS + 1) struct GWL_ModifierInfo { /** Only for printing messages. */ const char *display_name; const char *xkb_id; GHOST_TKey key_l, key_r; GHOST_TModifierKey mod_l, mod_r; }; static const GWL_ModifierInfo g_modifier_info_table[MOD_INDEX_NUM] = { /*MOD_INDEX_SHIFT*/ { /*display_name*/ "Shift", /*xkb_id*/ XKB_MOD_NAME_SHIFT, /*key_l*/ GHOST_kKeyLeftShift, /*key_r*/ GHOST_kKeyRightShift, /*mod_l*/ GHOST_kModifierKeyLeftShift, /*mod_r*/ GHOST_kModifierKeyRightShift, }, /*MOD_INDEX_ALT*/ { /*display_name*/ "Alt", /*xkb_id*/ XKB_MOD_NAME_ALT, /*key_l*/ GHOST_kKeyLeftAlt, /*key_r*/ GHOST_kKeyRightAlt, /*mod_l*/ GHOST_kModifierKeyLeftAlt, /*mod_r*/ GHOST_kModifierKeyRightAlt, }, /*MOD_INDEX_CTRL*/ { /*display_name*/ "Control", /*xkb_id*/ XKB_MOD_NAME_CTRL, /*key_l*/ GHOST_kKeyLeftControl, /*key_r*/ GHOST_kKeyRightControl, /*mod_l*/ GHOST_kModifierKeyLeftControl, /*mod_r*/ GHOST_kModifierKeyRightControl, }, /*MOD_INDEX_OS*/ { /*display_name*/ "OS", /*xkb_id*/ XKB_MOD_NAME_LOGO, /*key_l*/ GHOST_kKeyLeftOS, /*key_r*/ GHOST_kKeyRightOS, /*mod_l*/ GHOST_kModifierKeyLeftOS, /*mod_r*/ GHOST_kModifierKeyRightOS, }, }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_SimpleBuffer Type * \{ */ struct GWL_SimpleBuffer { /** Constant data, but may be freed. */ const char *data = nullptr; size_t data_size = 0; }; static void gwl_simple_buffer_free_data(GWL_SimpleBuffer *buffer) { free(const_cast(buffer->data)); buffer->data = nullptr; buffer->data_size = 0; } static void gwl_simple_buffer_set_from_string(GWL_SimpleBuffer *buffer, const char *str) { free(const_cast(buffer->data)); buffer->data_size = strlen(str); char *data = static_cast(malloc(buffer->data_size)); std::memcpy(data, str, buffer->data_size); buffer->data = data; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_Cursor Type * \{ */ /** * From XKB internals, use for converting a scan-code from WAYLAND to a #xkb_keycode_t. * Ideally this wouldn't need a local define. */ #define EVDEV_OFFSET 8 /** * Data owned by the thread that updates the cursor. * Exposed so the #GWL_Seat can request the thread to exit & free itself. */ struct GWL_Cursor_AnimHandle { std::atomic exit_pending = false; }; struct GWL_Cursor { /** Wayland core types. */ struct { wl_surface *surface_cursor = nullptr; wl_buffer *buffer = nullptr; wl_cursor_image image = {0}; wl_cursor_theme *theme = nullptr; /** Only set when the cursor is from the theme (it may be animated). */ const wl_cursor *theme_cursor = nullptr; /** Needed so changing the theme scale can reload 'theme_cursor' at a new scale. */ const char *theme_cursor_name = nullptr; } wl; bool visible = false; /** * When false, hide the hardware cursor, while the cursor is still considered to be `visible`, * since the grab-mode determines the state of the software cursor, * this may change - removing the need for a software cursor and in this case it's important * the hardware cursor is used. */ bool is_hardware = true; /** When true, a custom image is used to display the cursor (stored in `wl_image`). */ bool is_custom = false; void *custom_data = nullptr; /** The size of `custom_data` in bytes. */ size_t custom_data_size = 0; /** Use for animated cursors. */ GWL_Cursor_AnimHandle *anim_handle = nullptr; /** * The name of the theme (set by an environment variable). * When disabled, leave as an empty string and the default theme will be used. */ std::string theme_name; /** * The size of the cursor (when looking up a cursor theme). * This must be scaled by the maximum output scale when passing to wl_cursor_theme_load. * See #update_cursor_scale. * */ int theme_size = 0; int custom_scale = 1; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_TabletTool Type * \{ */ /** * Collect tablet event data before the frame callback runs. */ enum class GWL_TabletTool_EventTypes { Unset = -1, Motion = 0, Pressure, Tilt, /* Left mouse button. */ Stylus0_Down, Stylus0_Up, /* Middle mouse button. */ Stylus1_Down, Stylus1_Up, /* Right mouse button. */ Stylus2_Down, Stylus2_Up, /* Mouse button number 4. */ Stylus3_Down, Stylus3_Up, Wheel, #define GWL_TabletTool_FrameTypes_NUM (int(GWL_TabletTool_EventTypes::Wheel) + 1) }; static const GHOST_TButton gwl_tablet_tool_ebutton[] = { GHOST_kButtonMaskLeft, /* `Stylus0_*`. */ GHOST_kButtonMaskMiddle, /* `Stylus1_*`. */ GHOST_kButtonMaskRight, /* `Stylus2_*`. */ GHOST_kButtonMaskButton4, /* `Stylus3_*`. */ }; /** * A single tablet can have multiple tools (pen, eraser, brush... etc). * WAYLAND exposes tools via #zwp_tablet_tool_v2. * Since are no API's to access properties of the tool, store the values here. */ struct GWL_TabletTool { /** Wayland native types. */ struct { /** * Tablets have a separate cursor to the 'pointer', * this surface is used for cursor drawing. */ wl_surface *surface_cursor = nullptr; } wl; GWL_Seat *seat = nullptr; /** Used to delay clearing tablet focused wl_surface until the frame is handled. */ bool proximity = false; GHOST_TabletData data = GHOST_TABLET_DATA_NONE; /** Motion. */ int32_t xy[2] = {0}; bool has_xy = false; /** * Collect data before the #zwp_tablet_tool_v2_listener::frame runs. */ struct { GWL_TabletTool_EventTypes frame_types[GWL_TabletTool_FrameTypes_NUM] = { GWL_TabletTool_EventTypes::Unset, }; int frame_types_num = 0; int frame_types_mask = 0; struct { int32_t clicks = 0; } wheel; } frame_pending; }; static void gwl_tablet_tool_frame_event_add(GWL_TabletTool *tablet_tool, const GWL_TabletTool_EventTypes ty) { const int ty_mask = 1 << int(ty); /* Motion callback may run multiple times. */ if (tablet_tool->frame_pending.frame_types_mask & ty_mask) { return; } tablet_tool->frame_pending.frame_types_mask |= ty_mask; int i = tablet_tool->frame_pending.frame_types_num++; tablet_tool->frame_pending.frame_types[i] = ty; } static void gwl_tablet_tool_frame_event_reset(GWL_TabletTool *tablet_tool) { tablet_tool->frame_pending.frame_types_num = 0; tablet_tool->frame_pending.frame_types_mask = 0; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_DataOffer Type * \{ */ /** * Data storage used for clipboard paste & drag-and-drop. */ struct GWL_DataOffer { /** Wayland native types. */ struct { wl_data_offer *id = nullptr; } wl; std::unordered_set types; struct { /** * Prevents freeing after #wl_data_device_listener.leave, * before #wl_data_device_listener.drop. */ bool in_use = false; /** * Bit-mask with available drop options. * #WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, #WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE.. etc. * The application that initializes the drag may set these depending on modifiers held * \note when dragging begins. Currently ghost doesn't make use of these. */ enum wl_data_device_manager_dnd_action source_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; enum wl_data_device_manager_dnd_action action = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE; /** Compatible with #GWL_Seat.xy coordinates. */ wl_fixed_t xy[2] = {0, 0}; } dnd; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_DataSource Type * \{ */ struct GWL_DataSource { /** Wayland core types. */ struct { wl_data_source *source = nullptr; } wl; GWL_SimpleBuffer buffer_out; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_Seat Type (#wl_seat wrapper & associated types) * \{ */ /** * Data used to implement client-side key-repeat. * * \note it's important not to store the target window here * as it can be closed while the key is repeating, * instead use the focused keyboard from #GWL_Seat which is cleared when windows are closed. * Therefor keyboard events must always check the window has not been cleared. */ struct GWL_KeyRepeatPlayload { GWL_Seat *seat = nullptr; xkb_keycode_t key_code; /** Time this timer started. */ uint64_t time_ms_init; /** * Don't cache the `utf8_buf` as this changes based on modifiers which may be pressed * while key repeat is enabled. */ struct { GHOST_TKey gkey; } key_data; }; /** Internal variables used to track grab-state. */ struct GWL_SeatStateGrab { bool use_lock = false; bool use_confine = false; }; /** * State of the pointing device (tablet or mouse). */ struct GWL_SeatStatePointer { /** Wayland core types. */ struct { /** * The wl_surface last used with this pointing device * (events with this pointing device will be sent here). */ wl_surface *surface_window = nullptr; } wl; /** * High precision coordinates. * * Mapping to pixels requires the window scale. * The following example converts these values to screen coordinates. * \code{.cc} * const wl_fixed_t scale = win->scale(); * const int event_xy[2] = { * wl_fixed_to_int(scale * seat_state_pointer->xy[0]), * wl_fixed_to_int(scale * seat_state_pointer->xy[1]), * }; * \endcode */ wl_fixed_t xy[2] = {0, 0}; /** Outputs on which the cursor is visible. */ std::unordered_set outputs; int theme_scale = 1; /** The serial of the last used pointer or tablet. */ uint32_t serial = 0; GHOST_Buttons buttons = GHOST_Buttons(); }; /** * Scroll state, applying to pointer (not tablet) events. * Otherwise this would be part of #GWL_SeatStatePointer. */ struct GWL_SeatStatePointerScroll { /** Smooth scrolling (handled & reset with pointer "frame" callback). */ wl_fixed_t smooth_xy[2] = {0, 0}; /** Discrete scrolling (handled & reset with pointer "frame" callback). */ int32_t discrete_xy[2] = {0, 0}; /** True when the axis is inverted (also known is "natural" scrolling). */ bool inverted_xy[2] = {false, false}; /** The source of scroll event. */ enum wl_pointer_axis_source axis_source = WL_POINTER_AXIS_SOURCE_WHEEL; /** * While the time should always be available, not all callbacks set the time * so account for it not being set. */ bool has_event_ms = false; /** Event time-stamp. */ uint64_t event_ms = 0; }; /** * Utility struct to access rounded values from a scaled `wl_fixed_t`, * without loosing information. * * As the rounded result is rounded to a lower precision integer, * the high precision value is accumulated and converted to an integer to * prevent the accumulation of rounded values giving an inaccurate result. * * \note This is simple but doesn't read well when expanded multiple times inline. */ struct GWL_ScaledFixedT { wl_fixed_t value = 0; wl_fixed_t factor = 1; }; static int gwl_scaled_fixed_t_add_and_calc_rounded_delta(GWL_ScaledFixedT *sf, const wl_fixed_t add) { const int result_prev = wl_fixed_to_int(sf->value * sf->factor); sf->value += add; const int result_curr = wl_fixed_to_int(sf->value * sf->factor); return result_curr - result_prev; } /** * Gesture state. * This is needed so the gesture values can be converted to deltas. */ struct GWL_SeatStatePointerGesture_Pinch { GWL_ScaledFixedT scale; GWL_ScaledFixedT rotation; }; /** * State of the keyboard (in #GWL_Seat). */ struct GWL_SeatStateKeyboard { /** Wayland core types. */ struct { /** * The wl_surface last used with this pointing device * (events with this pointing device will be sent here). */ wl_surface *surface_window = nullptr; } wl; /** The serial of the last used pointer or tablet. */ uint32_t serial = 0; }; /** * Store held keys (only modifiers), could store other keys in the future. * * Needed as #GWL_Seat.xkb_state doesn't store which modifier keys are held. */ struct GWL_KeyboardDepressedState { int16_t mods[GHOST_KEY_MODIFIER_NUM] = {0}; }; #ifdef WITH_GHOST_WAYLAND_LIBDECOR struct GWL_LibDecor_System { libdecor *context = nullptr; }; static void gwl_libdecor_system_destroy(GWL_LibDecor_System *decor) { if (decor->context) { libdecor_unref(decor->context); decor->context = nullptr; } delete decor; } #endif struct GWL_XDG_Decor_System { xdg_wm_base *shell = nullptr; uint32_t shell_name = WL_NAME_UNSET; zxdg_decoration_manager_v1 *manager = nullptr; uint32_t manager_name = WL_NAME_UNSET; }; static void gwl_xdg_decor_system_destroy(GWL_Display *display, GWL_XDG_Decor_System *decor) { if (decor->manager) { gwl_registry_entry_remove_by_name(display, decor->manager_name, nullptr); GHOST_ASSERT(decor->manager == nullptr, "Internal registry error"); } if (decor->shell) { gwl_registry_entry_remove_by_name(display, decor->shell_name, nullptr); GHOST_ASSERT(decor->shell == nullptr, "Internal registry error"); } delete decor; } struct GWL_PrimarySelection_DataOffer { /** Wayland native types. */ struct { zwp_primary_selection_offer_v1 *id = nullptr; } wp; std::unordered_set types; }; struct GWL_PrimarySelection_DataSource { /** Wayland native types. */ struct { zwp_primary_selection_source_v1 *source = nullptr; } wp; GWL_SimpleBuffer buffer_out; }; /** Primary selection support. */ struct GWL_PrimarySelection { GWL_PrimarySelection_DataSource *data_source = nullptr; std::mutex data_source_mutex; GWL_PrimarySelection_DataOffer *data_offer = nullptr; std::mutex data_offer_mutex; }; static void gwl_primary_selection_discard_offer(GWL_PrimarySelection *primary) { if (primary->data_offer == nullptr) { return; } zwp_primary_selection_offer_v1_destroy(primary->data_offer->wp.id); delete primary->data_offer; primary->data_offer = nullptr; } static void gwl_primary_selection_discard_source(GWL_PrimarySelection *primary) { GWL_PrimarySelection_DataSource *data_source = primary->data_source; if (data_source == nullptr) { return; } gwl_simple_buffer_free_data(&data_source->buffer_out); if (data_source->wp.source) { zwp_primary_selection_source_v1_destroy(data_source->wp.source); } delete primary->data_source; primary->data_source = nullptr; } #ifdef WITH_INPUT_IME struct GWL_SeatIME { wl_surface *surface_window = nullptr; GHOST_TEventImeData event_ime_data = { /*result_len*/ nullptr, /*composite_len*/ nullptr, /*result*/ nullptr, /*composite*/ nullptr, /*cursor_position*/ -1, /*target_start*/ -1, /*target_end*/ -1, }; /** When true, the client has indicated that IME input should be activated on text entry. */ bool is_enabled = false; /** * When true, some pre-edit text has been entered * (an IME popup may be showing however this isn't known). */ bool has_preedit = false; /** Storage for #GHOST_TEventImeData::result (the result of the `commit_string` callback). */ std::string result; /** Storage for #GHOST_TEventImeData::composite (the result of the `preedit_string` callback). */ std::string composite; /** #zwp_text_input_v3_listener::commit_string was called with a null text argument. */ bool result_is_null = false; /** #zwp_text_input_v3_listener::preedit_string was called with a null text argument. */ bool composite_is_null = false; /** #zwp_text_input_v3_listener::preedit_string was called. */ bool has_preedit_string_callback = false; /** #zwp_text_input_v3_listener::commit_string was called. */ bool has_commit_string_callback = false; /** Bounds (use for comparison). */ struct { int x = -1; int y = -1; int w = -1; int h = -1; } rect; }; #endif /* WITH_INPUT_IME */ struct GWL_Seat { /** Wayland core types. */ struct { wl_seat *seat = nullptr; wl_pointer *pointer = nullptr; wl_touch *touch = nullptr; wl_keyboard *keyboard = nullptr; wl_surface *surface_window_focus_dnd = nullptr; wl_data_device *data_device = nullptr; } wl; /** Wayland native types. */ struct { zwp_tablet_seat_v2 *tablet_seat = nullptr; #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE zwp_pointer_gesture_hold_v1 *pointer_gesture_hold = nullptr; #endif #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE zwp_pointer_gesture_pinch_v1 *pointer_gesture_pinch = nullptr; #endif #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE zwp_pointer_gesture_swipe_v1 *pointer_gesture_swipe = nullptr; #endif zwp_relative_pointer_v1 *relative_pointer = nullptr; zwp_locked_pointer_v1 *locked_pointer = nullptr; zwp_confined_pointer_v1 *confined_pointer = nullptr; zwp_primary_selection_device_v1 *primary_selection_device = nullptr; /** All currently active tablet tools (needed for changing the cursor). */ std::unordered_set tablet_tools; #ifdef WITH_INPUT_IME zwp_text_input_v3 *text_input = nullptr; #endif } wp; /** XKB native types. */ struct { xkb_context *context = nullptr; /** The compose key table (check for null before use). */ xkb_compose_table *compose_table = nullptr; /** The compose state is expected to use the keyboard `state` (check for null before use). */ xkb_compose_state *compose_state = nullptr; xkb_state *state = nullptr; /** * Keep a state with no modifiers active, use for symbol lookups. */ xkb_state *state_empty = nullptr; /** * Keep a state with shift enabled, use to access predictable number access for AZERTY keymaps. * If shift is not supported by the key-map, this is set to nullptr. */ xkb_state *state_empty_with_shift = nullptr; /** * Keep a state with number-lock enabled, use to access predictable key-pad symbols. * If number-lock is not supported by the key-map, this is set to nullptr. */ xkb_state *state_empty_with_numlock = nullptr; /** * The active layout, where a single #xkb_keymap may have multiple layouts. */ xkb_layout_index_t layout_active = 0; } xkb; #ifdef WITH_INPUT_IME GWL_SeatIME ime; #endif GHOST_SystemWayland *system = nullptr; std::string name; /** Use to check if the last cursor input was tablet or pointer. */ uint32_t cursor_source_serial = 0; GWL_SeatStatePointer pointer; GWL_SeatStatePointerScroll pointer_scroll; GWL_SeatStatePointerGesture_Pinch pointer_gesture_pinch; /** Mostly this can be interchanged with `pointer` however it can't be locked/confined. */ GWL_SeatStatePointer tablet; GWL_SeatStateKeyboard keyboard; #ifdef USE_GNOME_CONFINE_HACK bool use_pointer_software_confine = false; #endif /** The cursor location (in pixel-space) when hidden grab started (#GHOST_kGrabHide). */ wl_fixed_t grab_lock_xy[2] = {0, 0}; GWL_Cursor cursor; #ifdef USE_NON_LATIN_KB_WORKAROUND bool xkb_use_non_latin_workaround = false; #endif /** Keys held matching `xkb_state`. */ GWL_KeyboardDepressedState key_depressed; /** * Cache result of `xkb_keymap_mod_get_index` * so every time a modifier is accessed a string lookup isn't required. * Be sure to check for #XKB_MOD_INVALID before using. */ xkb_mod_index_t xkb_keymap_mod_index[MOD_INDEX_NUM] = {XKB_MOD_INVALID}; /* Cache result for other modifiers which aren't stored in `xkb_keymap_mod_index` * since their depressed state isn't tracked. */ /** Cache result of `xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM)`. */ xkb_mod_index_t xkb_keymap_mod_index_mod2 = XKB_MOD_INVALID; /** Cache result of `xkb_keymap_mod_get_index(keymap, "NumLock")`. */ xkb_mod_index_t xkb_keymap_mod_index_numlock = XKB_MOD_INVALID; struct { /** Key repetition in character per second. */ int32_t rate = 0; /** Time (milliseconds) after which to start repeating keys. */ int32_t delay = 0; /** * Timer for key repeats. * * \note For as long as #USE_EVENT_BACKGROUND_THREAD is defined, any access to this * (including null checks, must lock `timer_mutex` first. */ GHOST_ITimerTask *timer = nullptr; } key_repeat; /** Drag & Drop. */ GWL_DataOffer *data_offer_dnd = nullptr; std::mutex data_offer_dnd_mutex; /** Copy & Paste. */ GWL_DataOffer *data_offer_copy_paste = nullptr; std::mutex data_offer_copy_paste_mutex; GWL_DataSource *data_source = nullptr; std::mutex data_source_mutex; GWL_PrimarySelection primary_selection; /** * Last input device that was active (pointer/tablet/keyboard). * * \note Multi-touch gestures don't set this value, * if there is some use-case where this is needed - assignments can be added. */ uint32_t data_source_serial = 0; }; static GWL_SeatStatePointer *gwl_seat_state_pointer_active(GWL_Seat *seat) { if (seat->pointer.serial == seat->cursor_source_serial) { return &seat->pointer; } if (seat->tablet.serial == seat->cursor_source_serial) { return &seat->tablet; } return nullptr; } static GWL_SeatStatePointer *gwl_seat_state_pointer_from_cursor_surface( GWL_Seat *seat, const wl_surface *wl_surface) { if (ghost_wl_surface_own_cursor_pointer(wl_surface)) { return &seat->pointer; } if (ghost_wl_surface_own_cursor_tablet(wl_surface)) { return &seat->tablet; } GHOST_ASSERT(0, "Surface found without pointer/tablet tag"); return nullptr; } /** * Account for changes to #GWL_Seat::xkb::layout_active by running #xkb_state_update_mask * on static states which are used for lookups. * * This is also used when initializing the states. */ static void gwl_seat_key_layout_active_state_update_mask(GWL_Seat *seat) { const xkb_layout_index_t group = seat->xkb.layout_active; const xkb_mod_index_t mod_shift = seat->xkb_keymap_mod_index[MOD_INDEX_SHIFT]; const xkb_mod_index_t mod_mod2 = seat->xkb_keymap_mod_index_mod2; const xkb_mod_index_t mod_numlock = seat->xkb_keymap_mod_index_numlock; /* Handle `state_empty`. */ xkb_state_update_mask(seat->xkb.state_empty, 0, 0, 0, 0, 0, group); /* Handle `state_empty_with_shift`. */ GHOST_ASSERT((mod_shift == XKB_MOD_INVALID) == (seat->xkb.state_empty_with_shift == nullptr), "Invalid state for SHIFT"); if (seat->xkb.state_empty_with_shift) { xkb_state_update_mask(seat->xkb.state_empty_with_shift, 1 << mod_shift, 0, 0, 0, 0, group); } /* Handle `state_empty_with_shift`. */ GHOST_ASSERT((mod_mod2 == XKB_MOD_INVALID || mod_numlock == XKB_MOD_INVALID) == (seat->xkb.state_empty_with_numlock == nullptr), "Invalid state for NUMLOCK"); if (seat->xkb.state_empty_with_numlock) { xkb_state_update_mask( seat->xkb.state_empty_with_numlock, 1 << mod_mod2, 0, 1 << mod_numlock, 0, 0, group); } } /** * \note Caller must lock `timer_mutex`. */ static void gwl_seat_key_repeat_timer_add(GWL_Seat *seat, GHOST_TimerProcPtr key_repeat_fn, GHOST_TUserDataPtr payload, const bool use_delay) { GHOST_SystemWayland *system = seat->system; const uint64_t time_now = system->getMilliSeconds(); const uint64_t time_step = 1000 / seat->key_repeat.rate; const uint64_t time_start = use_delay ? seat->key_repeat.delay : time_step; static_cast(payload)->time_ms_init = time_now; #ifdef USE_EVENT_BACKGROUND_THREAD GHOST_TimerTask *timer = new GHOST_TimerTask( time_now + time_start, time_step, key_repeat_fn, payload); seat->key_repeat.timer = timer; system->ghost_timer_manager()->addTimer(timer); #else seat->key_repeat.timer = system->installTimer(time_start, time_step, key_repeat_fn, payload); #endif } /** * \note The caller must lock `timer_mutex`. */ static void gwl_seat_key_repeat_timer_remove(GWL_Seat *seat) { GHOST_SystemWayland *system = seat->system; #ifdef USE_EVENT_BACKGROUND_THREAD system->ghost_timer_manager()->removeTimer( static_cast(seat->key_repeat.timer)); #else system->removeTimer(seat->key_repeat.timer); #endif seat->key_repeat.timer = nullptr; } #ifdef WITH_INPUT_IME static void gwl_seat_ime_full_reset(GWL_Seat *seat) { const GWL_SeatIME ime_default{}; /* Preserve the following members since they represent the state of the connection to Wayland. * or which callbacks have run (which shouldn't be reset). */ wl_surface *surface_window = seat->ime.surface_window; const bool is_enabled = seat->ime.is_enabled; const bool has_preedit_string_callback = seat->ime.has_preedit_string_callback; const bool has_commit_string_callback = seat->ime.has_commit_string_callback; seat->ime = ime_default; seat->ime.surface_window = surface_window; seat->ime.is_enabled = is_enabled; seat->ime.has_preedit_string_callback = has_preedit_string_callback; seat->ime.has_commit_string_callback = has_commit_string_callback; } static void gwl_seat_ime_result_reset(GWL_Seat *seat) { seat->ime.result.clear(); seat->ime.result_is_null = false; GHOST_TEventImeData &event_ime_data = seat->ime.event_ime_data; event_ime_data.result_len = nullptr; event_ime_data.result = nullptr; } static void gwl_seat_ime_preedit_reset(GWL_Seat *seat) { seat->ime.composite.clear(); seat->ime.composite_is_null = false; GHOST_TEventImeData &event_ime_data = seat->ime.event_ime_data; event_ime_data.composite_len = nullptr; event_ime_data.composite = nullptr; event_ime_data.cursor_position = -1; event_ime_data.target_start = -1; event_ime_data.target_end = -1; } static void gwl_seat_ime_rect_reset(GWL_Seat *seat) { seat->ime.rect.x = -1; seat->ime.rect.y = -1; seat->ime.rect.w = -1; seat->ime.rect.h = -1; } #endif /* WITH_INPUT_IME */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_Display Type (#wl_display & #wl_compositor wrapper) * \{ */ struct GWL_RegistryEntry; /** * Variables for converting input timestamps, * see: #GHOST_SystemWayland::milliseconds_from_input_time. */ struct GWL_DisplayTimeStamp { /** * When true, the GHOST & WAYLAND time-stamps are compatible, * in most cases this means they will be equal however for systems with long up-times * it may equal `uint32(GHOST_SystemWayland::getMilliSeconds())`, * the `offsets` member is set to account for this case. */ bool exact_match = false; uint32_t last = 0; uint64_t offset = 0; }; struct GWL_Display { /** Wayland core types. */ struct { wl_registry *registry = nullptr; wl_display *display = nullptr; wl_compositor *compositor = nullptr; wl_shm *shm = nullptr; /* Managers. */ wl_data_device_manager *data_device_manager = nullptr; } wl; /** Wayland native types. */ struct { /* Managers. */ zwp_tablet_manager_v2 *tablet_manager = nullptr; zwp_relative_pointer_manager_v1 *relative_pointer_manager = nullptr; zwp_primary_selection_device_manager_v1 *primary_selection_device_manager = nullptr; wp_fractional_scale_manager_v1 *fractional_scale_manager = nullptr; wp_viewporter *viewporter = nullptr; zwp_pointer_constraints_v1 *pointer_constraints = nullptr; zwp_pointer_gestures_v1 *pointer_gestures = nullptr; #ifdef WITH_INPUT_IME zwp_text_input_manager_v3 *text_input_manager = nullptr; #endif } wp; /** Wayland XDG types. */ struct { /* Managers. */ zxdg_output_manager_v1 *output_manager = nullptr; xdg_activation_v1 *activation_manager = nullptr; } xdg; GWL_DisplayTimeStamp input_timestamp; GHOST_SystemWayland *system = nullptr; /** * True when initializing registration, while updating all other entries wont cause problems, * it will preform many redundant update calls. */ bool registry_skip_update_all = false; /** Registry entries, kept to allow updating & removal at run-time. */ GWL_RegistryEntry *registry_entry = nullptr; #ifdef WITH_GHOST_WAYLAND_LIBDECOR GWL_LibDecor_System *libdecor = nullptr; bool libdecor_required = false; #endif GWL_XDG_Decor_System *xdg_decor = nullptr; /** * NOTE(@ideasman42): Handling of outputs must account for this vector to be empty. * While this seems like something which might not ever happen, it can occur when * unplugging monitors (presumably from dodgy cables/connections too). */ std::vector outputs; std::vector seats; /** * Support a single active seat at once, this isn't an exact or correct mapping from WAYLAND. * Only allow input from different seats, not full concurrent multi-seat support. * * The main purpose of having an active seat is an alternative from always using the first * seat which prevents events from any other seat. * * NOTE(@ideasman42): This could be extended and developed further extended to support * an active seat per window (for e.g.), basic support is sufficient for now as currently isn't * a widely used feature. */ int seats_active_index = 0; /* Threaded event handling. */ #ifdef USE_EVENT_BACKGROUND_THREAD /** * Run a thread that consumes events in the background. * Use `pthread` because `std::thread` leaks memory. */ pthread_t events_pthread = 0; /** Use to exit the event reading loop. */ bool events_pthread_is_active = false; /** * Events added from the event reading thread. * Added into the main event queue when on #GHOST_SystemWayland::processEvents. */ std::vector events_pending; /** Guard against multiple threads accessing `events_pending` at once. */ std::mutex events_pending_mutex; /** * A separate timer queue, needed so the WAYLAND thread can lock access. * Using the system's #GHOST_Sysem::getTimerManager is not thread safe because * access to the timer outside of WAYLAND specific logic will not lock. * * Needed because #GHOST_System::dispatchEvents fires timers * outside of WAYLAND (without locking the `timer_mutex`). */ GHOST_TimerManager *ghost_timer_manager = nullptr; #endif /* USE_EVENT_BACKGROUND_THREAD */ }; /** * Free the #GWL_Display and it's related members. * * \note This may run on a partially initialized struct, * so it can't be assumed all members are set. */ static void gwl_display_destroy(GWL_Display *display) { #ifdef USE_EVENT_BACKGROUND_THREAD if (display->events_pthread) { ghost_wl_display_lock_without_input(display->wl.display, display->system->server_mutex); display->events_pthread_is_active = false; } #endif /* Stop all animated cursors (freeing their #GWL_Cursor_AnimHandle). */ for (GWL_Seat *seat : display->seats) { gwl_seat_cursor_anim_end(seat); } /* For typical WAYLAND use this will always be set. * However when WAYLAND isn't running, this will early-exit and be null. */ if (display->wl.registry) { wl_registry_destroy(display->wl.registry); display->wl.registry = nullptr; } /* Unregister items in reverse order. */ gwl_registry_entry_remove_all(display); #ifdef WITH_GHOST_WAYLAND_LIBDECOR if (use_libdecor) { if (display->libdecor) { gwl_libdecor_system_destroy(display->libdecor); display->libdecor = nullptr; } } else #endif { if (display->xdg_decor) { gwl_xdg_decor_system_destroy(display, display->xdg_decor); display->xdg_decor = nullptr; } } #ifdef WITH_OPENGL_BACKEND if (eglGetDisplay) { ::eglTerminate(eglGetDisplay(EGLNativeDisplayType(display->wl.display))); } #endif #ifdef USE_EVENT_BACKGROUND_THREAD if (display->events_pthread) { gwl_display_event_thread_destroy(display); display->system->server_mutex->unlock(); } /* Important to remove after the seats which may have key repeat timers active. */ if (display->ghost_timer_manager) { delete display->ghost_timer_manager; display->ghost_timer_manager = nullptr; } /* Pending events may be left unhandled. */ for (const GHOST_IEvent *event : display->events_pending) { delete event; } #endif /* USE_EVENT_BACKGROUND_THREAD */ if (display->wl.display) { wl_display_disconnect(display->wl.display); } delete display; } static int gwl_display_seat_index(GWL_Display *display, const GWL_Seat *seat) { std::vector::iterator iter = std::find( display->seats.begin(), display->seats.end(), seat); const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) : -1; GHOST_ASSERT(index != -1, "invalid internal state"); return index; } static GWL_Seat *gwl_display_seat_active_get(const GWL_Display *display) { if (UNLIKELY(display->seats.empty())) { return nullptr; } return display->seats[display->seats_active_index]; } static bool gwl_display_seat_active_set(GWL_Display *display, const GWL_Seat *seat) { if (UNLIKELY(display->seats.empty())) { return false; } const int index = gwl_display_seat_index(display, seat); if (index == display->seats_active_index) { return false; } display->seats_active_index = index; return true; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_RegistryHandler * \{ */ struct GWL_RegisteryAdd_Params { uint32_t name = 0; /** Index within `gwl_registry_handlers`. */ int interface_slot = 0; uint32_t version = 0; }; /** * Add callback for object registry. * \note Any operations that depend on other interfaces being registered must be performed in the * #GWL_RegistryHandler_UpdateFn callback as the order interfaces are added is out of our control. * * \param display: The display which holes a reference to the global object. * \param params: Various arguments needed for registration. */ using GWL_RegistryHandler_AddFn = void (*)(GWL_Display *display, const GWL_RegisteryAdd_Params *params); struct GWL_RegisteryUpdate_Params { uint32_t name = 0; /** Index within `gwl_registry_handlers`. */ int interface_slot = 0; uint32_t version = 0; /** Set to #GWL_RegistryEntry.user_data. */ void *user_data = nullptr; }; /** * Optional update callback to refresh internal data when another interface has been added/removed. * * \param display: The display which holes a reference to the global object. * \param params: Various arguments needed for updating. */ using GWL_RegistryHandler_UpdateFn = void (*)(GWL_Display *display, const GWL_RegisteryUpdate_Params *params); /** * Remove callback for object registry. * \param display: The display which holes a reference to the global object. * \param user_data: Optional reference to a sub element of `display`, * use for outputs or seats for e.g. when the display may hold multiple references. * \param on_exit: Enabled when freeing on exit. * When true the consistency of references between objects should be kept valid. * Otherwise it can be assumed that all objects will be freed and none will be used again, * so there is no need to ensure a valid state. */ using GWL_RegistryEntry_RemoveFn = void (*)(GWL_Display *display, void *user_data, bool on_exit); struct GWL_RegistryHandler { /** Pointer to the name (not the name itself), needed as the values aren't set on startup. */ const char *const *interface_p = nullptr; /** Add the interface. */ GWL_RegistryHandler_AddFn add_fn = nullptr; /** Optional update the interface (when other interfaces have been added/removed). */ GWL_RegistryHandler_UpdateFn update_fn = nullptr; /** Remove the interface. */ GWL_RegistryEntry_RemoveFn remove_fn = nullptr; }; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal #GWL_RegistryEntry * \{ */ /** * Registered global objects can be removed by the compositor, * these entries are a registry of objects and callbacks to properly remove them. * These are also used to remove all registered objects before exiting. */ struct GWL_RegistryEntry { GWL_RegistryEntry *next = nullptr; /** * Optional pointer passed to `remove_fn`, typically the container in #GWL_Display * in cases multiple instances of the same interface are supported. */ void *user_data = nullptr; /** * A unique identifier used as a handle by `wl_registry_listener.global_remove`. */ uint32_t name = WL_NAME_UNSET; /** * Version passed by the add callback. */ uint32_t version; /** * The index in `gwl_registry_handlers`, * useful for accessing the interface name (for logging for example). */ int interface_slot = 0; }; static void gwl_registry_entry_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params, void *user_data) { GWL_RegistryEntry *reg = new GWL_RegistryEntry; reg->interface_slot = params->interface_slot; reg->name = params->name; reg->version = params->version; reg->user_data = user_data; reg->next = display->registry_entry; display->registry_entry = reg; } static bool gwl_registry_entry_remove_by_name(GWL_Display *display, uint32_t name, int *r_interface_slot) { GWL_RegistryEntry *reg = display->registry_entry; GWL_RegistryEntry **reg_link_p = &display->registry_entry; bool found = false; if (r_interface_slot) { *r_interface_slot = -1; } while (reg) { if (reg->name == name) { GWL_RegistryEntry *reg_next = reg->next; const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot( reg->interface_slot); handler->remove_fn(display, reg->user_data, false); if (r_interface_slot) { *r_interface_slot = reg->interface_slot; } delete reg; *reg_link_p = reg_next; found = true; break; } reg_link_p = ®->next; reg = reg->next; } return found; } static bool gwl_registry_entry_remove_by_interface_slot(GWL_Display *display, int interface_slot, bool on_exit) { GWL_RegistryEntry *reg = display->registry_entry; GWL_RegistryEntry **reg_link_p = &display->registry_entry; bool found = false; while (reg) { if (reg->interface_slot == interface_slot) { GWL_RegistryEntry *reg_next = reg->next; const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot( interface_slot); handler->remove_fn(display, reg->user_data, on_exit); delete reg; *reg_link_p = reg_next; reg = reg_next; found = true; continue; } reg_link_p = ®->next; reg = reg->next; } return found; } /** * Remove all global objects (on exit). */ static void gwl_registry_entry_remove_all(GWL_Display *display) { const bool on_exit = true; /* NOTE(@ideasman42): Free by slot instead of simply looping over * `display->registry_entry` so the order of freeing is always predictable. * Otherwise global objects would be feed in the order they are registered. * While this works in my tests, it could cause difficult to reproduce bugs * where lesser used compositors or changes to existing compositors could * crash on exit based on the order of freeing objects is out of our control. * * To give a concrete example of how this could fail, it's possible removing * a tablet interface could reference the pointer interface, or the output interface. * Even though references between interfaces shouldn't be necessary in most cases * when `on_exit` is enabled. */ int interface_slot = gwl_registry_handler_interface_slot_max(); while (interface_slot--) { gwl_registry_entry_remove_by_interface_slot(display, interface_slot, on_exit); } GHOST_ASSERT(display->registry_entry == nullptr, "Failed to remove all entries!"); display->registry_entry = nullptr; } /** * Run GWL_RegistryHandler.update_fn an all registered interface instances. * This is needed to refresh the state of interfaces that may reference other interfaces. * Called when interfaces are added/removed. * * \param interface_slot_exclude: Skip updating slots of this type. * Note that while harmless dependencies only exist between different types, * so there is no reason to update all other outputs that an output was removed (for e.g.). * Pass as -1 to update all slots. * * NOTE(@ideasman42): Updating all other items on a single change is typically worth avoiding. * In practice this isn't a problem as so there are so few elements in `display->registry_entry`, * so few use update functions and adding/removal at runtime is rarely called (plugging/unplugging) * hardware for e.g. So while it's possible to store dependency links to avoid unnecessary * looping over data - it ends up being a non issue. */ static void gwl_registry_entry_update_all(GWL_Display *display, const int interface_slot_exclude) { GHOST_ASSERT(interface_slot_exclude == -1 || (uint(interface_slot_exclude) < uint(gwl_registry_handler_interface_slot_max())), "Invalid exclude slot"); for (GWL_RegistryEntry *reg = display->registry_entry; reg; reg = reg->next) { if (reg->interface_slot == interface_slot_exclude) { continue; } const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot( reg->interface_slot); if (handler->update_fn == nullptr) { continue; } GWL_RegisteryUpdate_Params params{}; params.name = reg->name; params.interface_slot = reg->interface_slot; params.version = reg->version; params.user_data = reg->user_data; handler->update_fn(display, ¶ms); } } /** \} */ /* -------------------------------------------------------------------- */ /** \name Private Utility Functions * \{ */ static uint64_t sub_abs_u64(const uint64_t a, const uint64_t b) { return a > b ? a - b : b - a; } /** * Return milliseconds from a microsecond uint32 pair (used by some wayland functions). */ static uint64_t ghost_wl_ms_from_utime_pair(uint32_t utime_hi, uint32_t utime_lo) { return ((uint64_t(utime_hi) << 32) | uint64_t(utime_lo)) / 1000; } /** * Access the LOCALE (with a fallback). */ static const char *ghost_wl_locale_from_env_with_default() { const char *locale = getenv("LC_ALL"); if (!locale || !*locale) { locale = getenv("LC_CTYPE"); if (!locale || !*locale) { locale = getenv("LANG"); if (!locale || !*locale) { locale = "C"; } } } return locale; } static void ghost_wl_display_report_error(wl_display *display) { int ecode = wl_display_get_error(display); GHOST_ASSERT(ecode, "Error not set!"); if (ELEM(ecode, EPIPE, ECONNRESET)) { fprintf(stderr, "The Wayland connection broke. Did the Wayland compositor die?\n"); } else { fprintf(stderr, "The Wayland connection experienced a fatal error: %s\n", strerror(ecode)); } /* NOTE(@ideasman42): The application is running, * however an error closes all windows and most importantly: * shuts down the GPU context (loosing all GPU state - shaders, bind codes etc), * so recovering from this effectively involves restarting. * * Keeping the GPU state alive doesn't seem to be supported as windows EGL context must use the * same connection as the used for all other WAYLAND interactions (see #wl_display_connect). * So in practice re-connecting to the display server isn't an option. * * Exit since leaving the process open will simply flood the output and do nothing. * Although as the process is in a valid state, auto-save for e.g. is possible, see: #100855. */ ::exit(-1); } #ifdef __GNUC__ static void ghost_wayland_log_handler(const char *msg, va_list arg) __attribute__((format(printf, 1, 0))); #endif /** * Callback for WAYLAND to run when there is an error. * * \note It's useful to set a break-point on this function as some errors are fatal * (for all intents and purposes) but don't crash the process. */ static void ghost_wayland_log_handler(const char *msg, va_list arg) { fprintf(stderr, "GHOST/Wayland: "); vfprintf(stderr, msg, arg); /* Includes newline. */ GHOST_TBacktraceFn backtrace_fn = GHOST_ISystem::getBacktraceFn(); if (backtrace_fn) { backtrace_fn(stderr); /* Includes newline. */ } } #if defined(WITH_GHOST_X11) && defined(WITH_GHOST_WAYLAND_LIBDECOR) /** * Check if the system is running X11. * This is not intended to be a fool-proof check (the `DISPLAY` is not validated for e.g.). * Just check `DISPLAY` is set and not-empty. */ static bool ghost_wayland_is_x11_available() { const char *x11_display = getenv("DISPLAY"); if (x11_display && x11_display[0]) { return true; } return false; } #endif /* WITH_GHOST_X11 && WITH_GHOST_WAYLAND_LIBDECOR */ static GHOST_TKey xkb_map_gkey(const xkb_keysym_t sym) { GHOST_TKey gkey; if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) { gkey = GHOST_TKey(sym); } else if (sym >= XKB_KEY_KP_0 && sym <= XKB_KEY_KP_9) { gkey = GHOST_TKey(GHOST_kKeyNumpad0 + sym - XKB_KEY_KP_0); } else if (sym >= XKB_KEY_A && sym <= XKB_KEY_Z) { gkey = GHOST_TKey(sym); } else if (sym >= XKB_KEY_a && sym <= XKB_KEY_z) { gkey = GHOST_TKey(sym - XKB_KEY_a + XKB_KEY_A); } else if (sym >= XKB_KEY_F1 && sym <= XKB_KEY_F24) { gkey = GHOST_TKey(GHOST_kKeyF1 + sym - XKB_KEY_F1); } else { #define GXMAP(k, x, y) \ case x: \ k = y; \ break switch (sym) { GXMAP(gkey, XKB_KEY_BackSpace, GHOST_kKeyBackSpace); GXMAP(gkey, XKB_KEY_Tab, GHOST_kKeyTab); GXMAP(gkey, XKB_KEY_Linefeed, GHOST_kKeyLinefeed); GXMAP(gkey, XKB_KEY_Clear, GHOST_kKeyClear); GXMAP(gkey, XKB_KEY_Return, GHOST_kKeyEnter); GXMAP(gkey, XKB_KEY_Escape, GHOST_kKeyEsc); GXMAP(gkey, XKB_KEY_space, GHOST_kKeySpace); GXMAP(gkey, XKB_KEY_apostrophe, GHOST_kKeyQuote); GXMAP(gkey, XKB_KEY_comma, GHOST_kKeyComma); GXMAP(gkey, XKB_KEY_minus, GHOST_kKeyMinus); GXMAP(gkey, XKB_KEY_plus, GHOST_kKeyPlus); GXMAP(gkey, XKB_KEY_period, GHOST_kKeyPeriod); GXMAP(gkey, XKB_KEY_slash, GHOST_kKeySlash); GXMAP(gkey, XKB_KEY_semicolon, GHOST_kKeySemicolon); GXMAP(gkey, XKB_KEY_equal, GHOST_kKeyEqual); GXMAP(gkey, XKB_KEY_bracketleft, GHOST_kKeyLeftBracket); GXMAP(gkey, XKB_KEY_bracketright, GHOST_kKeyRightBracket); GXMAP(gkey, XKB_KEY_backslash, GHOST_kKeyBackslash); GXMAP(gkey, XKB_KEY_grave, GHOST_kKeyAccentGrave); GXMAP(gkey, XKB_KEY_Shift_L, GHOST_kKeyLeftShift); GXMAP(gkey, XKB_KEY_Shift_R, GHOST_kKeyRightShift); GXMAP(gkey, XKB_KEY_Control_L, GHOST_kKeyLeftControl); GXMAP(gkey, XKB_KEY_Control_R, GHOST_kKeyRightControl); GXMAP(gkey, XKB_KEY_Alt_L, GHOST_kKeyLeftAlt); GXMAP(gkey, XKB_KEY_Alt_R, GHOST_kKeyRightAlt); GXMAP(gkey, XKB_KEY_Super_L, GHOST_kKeyLeftOS); GXMAP(gkey, XKB_KEY_Super_R, GHOST_kKeyRightOS); GXMAP(gkey, XKB_KEY_Menu, GHOST_kKeyApp); GXMAP(gkey, XKB_KEY_Caps_Lock, GHOST_kKeyCapsLock); GXMAP(gkey, XKB_KEY_Num_Lock, GHOST_kKeyNumLock); GXMAP(gkey, XKB_KEY_Scroll_Lock, GHOST_kKeyScrollLock); GXMAP(gkey, XKB_KEY_Left, GHOST_kKeyLeftArrow); GXMAP(gkey, XKB_KEY_Right, GHOST_kKeyRightArrow); GXMAP(gkey, XKB_KEY_Up, GHOST_kKeyUpArrow); GXMAP(gkey, XKB_KEY_Down, GHOST_kKeyDownArrow); GXMAP(gkey, XKB_KEY_Print, GHOST_kKeyPrintScreen); GXMAP(gkey, XKB_KEY_Pause, GHOST_kKeyPause); GXMAP(gkey, XKB_KEY_Insert, GHOST_kKeyInsert); GXMAP(gkey, XKB_KEY_Delete, GHOST_kKeyDelete); GXMAP(gkey, XKB_KEY_Home, GHOST_kKeyHome); GXMAP(gkey, XKB_KEY_End, GHOST_kKeyEnd); GXMAP(gkey, XKB_KEY_Page_Up, GHOST_kKeyUpPage); GXMAP(gkey, XKB_KEY_Page_Down, GHOST_kKeyDownPage); GXMAP(gkey, XKB_KEY_KP_Decimal, GHOST_kKeyNumpadPeriod); GXMAP(gkey, XKB_KEY_KP_Enter, GHOST_kKeyNumpadEnter); GXMAP(gkey, XKB_KEY_KP_Add, GHOST_kKeyNumpadPlus); GXMAP(gkey, XKB_KEY_KP_Subtract, GHOST_kKeyNumpadMinus); GXMAP(gkey, XKB_KEY_KP_Multiply, GHOST_kKeyNumpadAsterisk); GXMAP(gkey, XKB_KEY_KP_Divide, GHOST_kKeyNumpadSlash); GXMAP(gkey, XKB_KEY_XF86AudioPlay, GHOST_kKeyMediaPlay); GXMAP(gkey, XKB_KEY_XF86AudioStop, GHOST_kKeyMediaStop); GXMAP(gkey, XKB_KEY_XF86AudioPrev, GHOST_kKeyMediaFirst); GXMAP(gkey, XKB_KEY_XF86AudioNext, GHOST_kKeyMediaLast); /* Additional keys for non US layouts. */ /* Uses the same physical key as #XKB_KEY_KP_Decimal for QWERTZ layout, see: #102287. */ GXMAP(gkey, XKB_KEY_KP_Separator, GHOST_kKeyNumpadPeriod); default: /* Rely on #xkb_map_gkey_or_scan_code to report when no key can be found. */ gkey = GHOST_kKeyUnknown; } #undef GXMAP } return gkey; } /** * Map the keys using the users keyboard layout, if that fails fall back to physical locations. * This is needed so users with keyboard layouts that don't expose #GHOST_kKeyAccentGrave * (typically the key under escape) in the layout can still use this key in keyboard shortcuts. * * \param key: The key's scan-code, compatible with values in `linux/input-event-codes.h`. */ static GHOST_TKey xkb_map_gkey_or_scan_code(const xkb_keysym_t sym, const uint32_t key) { GHOST_TKey gkey = xkb_map_gkey(sym); if (UNLIKELY(gkey == GHOST_kKeyUnknown)) { /* Fall back to physical location for keys that would otherwise do nothing. */ switch (key) { case KEY_GRAVE: { gkey = GHOST_kKeyAccentGrave; break; } default: { GHOST_PRINT( /* Key-code. */ "unhandled key: " << std::hex << std::showbase << sym << /* Hex. */ std::dec << " (" << sym << "), " << /* Decimal. */ /* Scan-code. */ "scan-code: " << std::hex << std::showbase << key << /* Hex. */ std::dec << " (" << key << ")" << /* Decimal. */ std::endl); break; } } } return gkey; } static int pointer_axis_as_index(const uint32_t axis) { switch (axis) { case WL_POINTER_AXIS_HORIZONTAL_SCROLL: { return 0; } case WL_POINTER_AXIS_VERTICAL_SCROLL: { return 1; } default: { return -1; } } } static GHOST_TTabletMode tablet_tool_map_type(enum zwp_tablet_tool_v2_type wp_tablet_tool_type) { switch (wp_tablet_tool_type) { case ZWP_TABLET_TOOL_V2_TYPE_ERASER: { return GHOST_kTabletModeEraser; } case ZWP_TABLET_TOOL_V2_TYPE_PEN: case ZWP_TABLET_TOOL_V2_TYPE_BRUSH: case ZWP_TABLET_TOOL_V2_TYPE_PENCIL: case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH: case ZWP_TABLET_TOOL_V2_TYPE_FINGER: case ZWP_TABLET_TOOL_V2_TYPE_MOUSE: case ZWP_TABLET_TOOL_V2_TYPE_LENS: { return GHOST_kTabletModeStylus; } } GHOST_PRINT("unknown tablet tool: " << wp_tablet_tool_type << std::endl); return GHOST_kTabletModeStylus; } static const int default_cursor_size = 24; struct GWL_Cursor_ShapeInfo { const char *names[GHOST_kStandardCursorNumCursors] = {nullptr}; }; static const GWL_Cursor_ShapeInfo ghost_wl_cursors = []() -> GWL_Cursor_ShapeInfo { GWL_Cursor_ShapeInfo info{}; #define CASE_CURSOR(shape_id, shape_name_in_theme) \ case shape_id: \ info.names[int(shape_id)] = shape_name_in_theme; /* Use a switch to ensure missing values show a compiler warning. */ switch (GHOST_kStandardCursorDefault) { CASE_CURSOR(GHOST_kStandardCursorDefault, "left_ptr"); CASE_CURSOR(GHOST_kStandardCursorRightArrow, "right_ptr"); CASE_CURSOR(GHOST_kStandardCursorLeftArrow, "left_ptr"); CASE_CURSOR(GHOST_kStandardCursorInfo, "left_ptr_help"); CASE_CURSOR(GHOST_kStandardCursorDestroy, "pirate"); CASE_CURSOR(GHOST_kStandardCursorHelp, "question_arrow"); CASE_CURSOR(GHOST_kStandardCursorWait, "watch"); CASE_CURSOR(GHOST_kStandardCursorText, "xterm"); CASE_CURSOR(GHOST_kStandardCursorCrosshair, "crosshair"); CASE_CURSOR(GHOST_kStandardCursorCrosshairA, ""); CASE_CURSOR(GHOST_kStandardCursorCrosshairB, ""); CASE_CURSOR(GHOST_kStandardCursorCrosshairC, ""); CASE_CURSOR(GHOST_kStandardCursorPencil, "pencil"); CASE_CURSOR(GHOST_kStandardCursorUpArrow, "sb_up_arrow"); CASE_CURSOR(GHOST_kStandardCursorDownArrow, "sb_down_arrow"); CASE_CURSOR(GHOST_kStandardCursorVerticalSplit, "split_v"); CASE_CURSOR(GHOST_kStandardCursorHorizontalSplit, "split_h"); CASE_CURSOR(GHOST_kStandardCursorEraser, ""); CASE_CURSOR(GHOST_kStandardCursorKnife, ""); CASE_CURSOR(GHOST_kStandardCursorEyedropper, "color-picker"); CASE_CURSOR(GHOST_kStandardCursorZoomIn, "zoom-in"); CASE_CURSOR(GHOST_kStandardCursorZoomOut, "zoom-out"); CASE_CURSOR(GHOST_kStandardCursorMove, "move"); CASE_CURSOR(GHOST_kStandardCursorNSEWScroll, "all-scroll"); CASE_CURSOR(GHOST_kStandardCursorNSScroll, "size_ver"); CASE_CURSOR(GHOST_kStandardCursorEWScroll, "size_hor"); CASE_CURSOR(GHOST_kStandardCursorStop, "not-allowed"); CASE_CURSOR(GHOST_kStandardCursorUpDown, "sb_v_double_arrow"); CASE_CURSOR(GHOST_kStandardCursorLeftRight, "sb_h_double_arrow"); CASE_CURSOR(GHOST_kStandardCursorTopSide, "top_side"); CASE_CURSOR(GHOST_kStandardCursorBottomSide, "bottom_side"); CASE_CURSOR(GHOST_kStandardCursorLeftSide, "left_side"); CASE_CURSOR(GHOST_kStandardCursorRightSide, "right_side"); CASE_CURSOR(GHOST_kStandardCursorTopLeftCorner, "top_left_corner"); CASE_CURSOR(GHOST_kStandardCursorTopRightCorner, "top_right_corner"); CASE_CURSOR(GHOST_kStandardCursorBottomRightCorner, "bottom_right_corner"); CASE_CURSOR(GHOST_kStandardCursorBottomLeftCorner, "bottom_left_corner"); CASE_CURSOR(GHOST_kStandardCursorCopy, "copy"); CASE_CURSOR(GHOST_kStandardCursorCustom, ""); } #undef CASE_CURSOR return info; }(); static constexpr const char *ghost_wl_mime_text_plain = "text/plain"; static constexpr const char *ghost_wl_mime_text_utf8 = "text/plain;charset=utf-8"; static constexpr const char *ghost_wl_mime_text_uri = "text/uri-list"; static const char *ghost_wl_mime_preference_order[] = { ghost_wl_mime_text_uri, ghost_wl_mime_text_utf8, ghost_wl_mime_text_plain, }; /* Aligned to `ghost_wl_mime_preference_order`. */ static const GHOST_TDragnDropTypes ghost_wl_mime_preference_order_type[] = { GHOST_kDragnDropTypeString, GHOST_kDragnDropTypeString, GHOST_kDragnDropTypeFilenames, }; static const char *ghost_wl_mime_send[] = { "UTF8_STRING", "COMPOUND_TEXT", "TEXT", "STRING", "text/plain;charset=utf-8", "text/plain", }; #ifdef USE_EVENT_BACKGROUND_THREAD static void pthread_set_min_priority(pthread_t handle) { int policy; sched_param sch_params; if (pthread_getschedparam(handle, &policy, &sch_params) == 0) { sch_params.sched_priority = sched_get_priority_min(policy); pthread_setschedparam(handle, policy, &sch_params); } } static void thread_set_min_priority(std::thread &thread) { constexpr bool is_pthread = std::is_same(); if (!is_pthread) { return; } /* The cast is "safe" as non-matching types will have returned already. * This cast might be avoided with clever template use. */ pthread_set_min_priority(reinterpret_cast(thread.native_handle())); } #endif /* USE_EVENT_BACKGROUND_THREAD */ static int memfd_create_sealed(const char *name) { #ifdef HAVE_MEMFD_CREATE const int fd = memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING); if (fd >= 0) { fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL); } return fd; #else /* HAVE_MEMFD_CREATE */ char *path = getenv("XDG_RUNTIME_DIR"); if (!path) { errno = ENOENT; return -1; } char *tmpname; asprintf(&tmpname, "%s/%s-XXXXXX", path, name); const int fd = mkostemp(tmpname, O_CLOEXEC); if (fd >= 0) { unlink(tmpname); } free(tmpname); return fd; #endif /* !HAVE_MEMFD_CREATE */ } enum { GWL_IOR_READ = 1 << 0, GWL_IOR_WRITE = 1 << 1, GWL_IOR_NO_RETRY = 1 << 2, }; static int file_descriptor_is_io_ready(int fd, const int flags, const int timeout_ms) { int result; GHOST_ASSERT(flags & (GWL_IOR_READ | GWL_IOR_WRITE), "X"); /* Note: We don't bother to account for elapsed time if we get EINTR */ do { #ifdef HAVE_POLL pollfd info; info.fd = fd; info.events = 0; if (flags & GWL_IOR_READ) { info.events |= POLLIN | POLLPRI; } if (flags & GWL_IOR_WRITE) { info.events |= POLLOUT; } result = poll(&info, 1, timeout_ms); #else fd_set rfdset, *rfdp = nullptr; fd_set wfdset, *wfdp = nullptr; struct timeval tv, *tvp = nullptr; /* If this assert triggers we'll corrupt memory here */ GHOST_ASSERT(fd >= 0 && fd < FD_SETSIZE, "X"); if (flags & GWL_IOR_READ) { FD_ZERO(&rfdset); FD_SET(fd, &rfdset); rfdp = &rfdset; } if (flags & GWL_IOR_WRITE) { FD_ZERO(&wfdset); FD_SET(fd, &wfdset); wfdp = &wfdset; } if (timeout_ms >= 0) { tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; tvp = &tv; } result = select(fd + 1, rfdp, wfdp, nullptr, tvp); #endif /* !HAVE_POLL */ } while (result < 0 && errno == EINTR && !(flags & GWL_IOR_NO_RETRY)); return result; } static int ghost_wl_display_event_pump(wl_display *wl_display) { /* Based on SDL's `Wayland_PumpEvents`. */ int err; /* NOTE: Without this, interactions with window borders via LIBDECOR doesn't function. */ wl_display_flush(wl_display); if (wl_display_prepare_read(wl_display) == 0) { /* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */ if (file_descriptor_is_io_ready( wl_display_get_fd(wl_display), GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0) { err = wl_display_read_events(wl_display); } else { wl_display_cancel_read(wl_display); err = 0; } } else { err = wl_display_dispatch_pending(wl_display); } return err; } #ifdef USE_EVENT_BACKGROUND_THREAD static void ghost_wl_display_lock_without_input(wl_display *wl_display, std::mutex *server_mutex) { const int fd = wl_display_get_fd(wl_display); int state; do { state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0); /* Re-check `state` with a lock held, needed to avoid holding the lock. */ if (state == 0) { server_mutex->lock(); state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0); if (state == 0) { break; } } } while (state == 0); } static int ghost_wl_display_event_pump_from_thread(wl_display *wl_display, const int fd, std::mutex *server_mutex) { /* Based on SDL's `Wayland_PumpEvents`. */ server_mutex->lock(); int err = 0; if (wl_display_prepare_read(wl_display) == 0) { bool wait_on_fd = false; /* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */ if (file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0) { err = wl_display_read_events(wl_display); } else { wl_display_cancel_read(wl_display); /* Without this, the thread will loop continuously, 100% CPU. */ wait_on_fd = true; } server_mutex->unlock(); if (wait_on_fd) { /* Important this runs after unlocking. */ file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX); } } else { server_mutex->unlock(); /* Wait for input (unlocked, so as not to block other threads). */ int state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX); /* Re-check `state` with a lock held, needed to avoid holding the lock. */ if (state > 0) { server_mutex->lock(); state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0); if (state > 0) { err = wl_display_dispatch_pending(wl_display); } server_mutex->unlock(); } } return err; } #endif /* USE_EVENT_BACKGROUND_THREAD */ static size_t ghost_wl_shm_format_as_size(enum wl_shm_format format) { switch (format) { case WL_SHM_FORMAT_ARGB8888: { return 4; } default: { /* Support other formats as needed. */ GHOST_ASSERT(0, "Unexpected format passed in!"); return 4; } } } /** * Return a #wl_buffer, ready to have it's data filled in or nullptr in case of failure. * The caller is responsible for calling `unmap(buffer_data, buffer_size)`. * * \param r_buffer_data: The buffer to be filled. * \param r_buffer_data_size: The size of `r_buffer_data` in bytes. */ static wl_buffer *ghost_wl_buffer_create_for_image(wl_shm *shm, const int32_t size_xy[2], enum wl_shm_format format, void **r_buffer_data, size_t *r_buffer_data_size) { const int fd = memfd_create_sealed("ghost-wl-buffer"); wl_buffer *buffer = nullptr; if (fd >= 0) { const int32_t buffer_stride = size_xy[0] * ghost_wl_shm_format_as_size(format); const int32_t buffer_size = buffer_stride * size_xy[1]; if (posix_fallocate(fd, 0, buffer_size) == 0) { void *buffer_data = mmap(nullptr, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (buffer_data != MAP_FAILED) { wl_shm_pool *pool = wl_shm_create_pool(shm, fd, buffer_size); buffer = wl_shm_pool_create_buffer(pool, 0, UNPACK2(size_xy), buffer_stride, format); wl_shm_pool_destroy(pool); if (buffer) { *r_buffer_data = buffer_data; *r_buffer_data_size = size_t(buffer_size); } else { /* Highly unlikely. */ munmap(buffer_data, buffer_size); } } } close(fd); } return buffer; } /** * A version of `read` which will read `nbytes` or as many bytes as possible, * useful as the LIBC version may `read` less than requested. */ static ssize_t read_exhaustive(const int fd, void *data, size_t nbytes) { ssize_t nbytes_read = read(fd, data, nbytes); if (nbytes_read > 0) { while (nbytes_read < nbytes) { const ssize_t nbytes_extra = read( fd, static_cast(data) + nbytes_read, nbytes - nbytes_read); if (nbytes_extra > 0) { nbytes_read += nbytes_extra; } else { if (UNLIKELY(nbytes_extra < 0)) { nbytes_read = nbytes_extra; /* Error. */ } break; } } } return nbytes_read; } /** * Read from `fd` into a buffer which is returned. * Use for files where seeking to determine the final size isn't supported (pipes for e.g.). * * \return the buffer or null on failure. * On failure `errno` will be set. */ static char *read_file_as_buffer(const int fd, const bool nil_terminate, size_t *r_len) { struct ByteChunk { ByteChunk *next; char data[4096 - sizeof(ByteChunk *)]; }; bool ok = true; size_t len = 0; ByteChunk *chunk_first = static_cast(malloc(sizeof(ByteChunk))); { ByteChunk **chunk_link_p = &chunk_first; ByteChunk *chunk = chunk_first; while (true) { if (UNLIKELY(chunk == nullptr)) { errno = ENOMEM; ok = false; break; } chunk->next = nullptr; /* Using `read` causes issues with GNOME, see: #106040). */ const ssize_t len_chunk = read_exhaustive(fd, chunk->data, sizeof(ByteChunk::data)); if (len_chunk <= 0) { if (UNLIKELY(len_chunk < 0)) { ok = false; } free(chunk); break; } *chunk_link_p = chunk; chunk_link_p = &chunk->next; len += len_chunk; if (len_chunk != sizeof(ByteChunk::data)) { break; } chunk = static_cast(malloc(sizeof(ByteChunk))); } } char *buf = nullptr; if (ok) { buf = static_cast(malloc(len + (nil_terminate ? 1 : 0))); if (UNLIKELY(buf == nullptr)) { errno = ENOMEM; ok = false; } } if (ok) { *r_len = len; if (nil_terminate) { buf[len] = '\0'; } } else { *r_len = 0; } char *buf_stride = buf; while (chunk_first) { if (ok) { const size_t len_chunk = std::min(len, sizeof(chunk_first->data)); memcpy(buf_stride, chunk_first->data, len_chunk); buf_stride += len_chunk; len -= len_chunk; } ByteChunk *chunk = chunk_first->next; free(chunk_first); chunk_first = chunk; } return buf; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Private Cursor API * \{ */ static void cursor_buffer_set_surface_impl(const wl_cursor_image *wl_image, wl_buffer *buffer, wl_surface *wl_surface, const int scale) { const int32_t image_size_x = int32_t(wl_image->width); const int32_t image_size_y = int32_t(wl_image->height); GHOST_ASSERT((image_size_x % scale) == 0 && (image_size_y % scale) == 0, "The size must be a multiple of the scale!"); 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); } /** * Needed to ensure the cursor size is always a multiple of scale. */ static int cursor_buffer_compatible_scale_from_image(const wl_cursor_image *wl_image, int scale) { const int32_t image_size_x = int32_t(wl_image->width); const int32_t image_size_y = int32_t(wl_image->height); while (scale > 1) { if ((image_size_x % scale) == 0 && (image_size_y % scale) == 0) { break; } scale -= 1; } return scale; } static const wl_cursor *gwl_seat_cursor_find_from_shape(GWL_Seat *seat, const GHOST_TStandardCursor shape, const char **r_cursor_name) { /* Caller must lock `server_mutex`. */ GWL_Cursor *cursor = &seat->cursor; wl_cursor *wl_cursor = nullptr; const char *cursor_name = ghost_wl_cursors.names[shape]; if (cursor_name[0] != '\0') { if (!cursor->wl.theme) { /* The cursor wl_surface hasn't entered an output yet. Initialize theme with scale 1. */ cursor->wl.theme = wl_cursor_theme_load( cursor->theme_name.c_str(), cursor->theme_size, seat->system->wl_shm_get()); } if (cursor->wl.theme) { wl_cursor = wl_cursor_theme_get_cursor(cursor->wl.theme, cursor_name); if (!wl_cursor) { GHOST_PRINT("cursor '" << cursor_name << "' does not exist" << std::endl); } } } if (r_cursor_name && wl_cursor) { *r_cursor_name = cursor_name; } return wl_cursor; } /** * Show the buffer defined by #gwl_seat_cursor_buffer_set without changing anything else, * so #gwl_seat_cursor_buffer_hide can be used to display it again. * * The caller is responsible for setting `seat->cursor.visible`. */ static void gwl_seat_cursor_buffer_show(GWL_Seat *seat) { const GWL_Cursor *cursor = &seat->cursor; if (seat->wl.pointer) { const int scale = cursor->is_custom ? cursor->custom_scale : seat->pointer.theme_scale; const int32_t hotspot_x = int32_t(cursor->wl.image.hotspot_x) / scale; const int32_t hotspot_y = int32_t(cursor->wl.image.hotspot_y) / scale; wl_pointer_set_cursor( seat->wl.pointer, seat->pointer.serial, cursor->wl.surface_cursor, hotspot_x, hotspot_y); } if (!seat->wp.tablet_tools.empty()) { const int scale = cursor->is_custom ? cursor->custom_scale : seat->tablet.theme_scale; const int32_t hotspot_x = int32_t(cursor->wl.image.hotspot_x) / scale; const int32_t hotspot_y = int32_t(cursor->wl.image.hotspot_y) / scale; for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) { GWL_TabletTool *tablet_tool = static_cast( zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, seat->tablet.serial, tablet_tool->wl.surface_cursor, hotspot_x, hotspot_y); #ifdef USE_KDE_TABLET_HIDDEN_CURSOR_HACK wl_surface_commit(tablet_tool->wl.surface_cursor); #endif } } gwl_seat_cursor_anim_reset(seat); } /** * Hide the buffer defined by #gwl_seat_cursor_buffer_set without changing anything else, * so #gwl_seat_cursor_buffer_show can be used to display it again. * * The caller is responsible for setting `seat->cursor.visible`. */ static void gwl_seat_cursor_buffer_hide(GWL_Seat *seat) { gwl_seat_cursor_anim_end(seat); wl_pointer_set_cursor(seat->wl.pointer, seat->pointer.serial, nullptr, 0, 0); for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) { zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, seat->tablet.serial, nullptr, 0, 0); } } static void gwl_seat_cursor_buffer_set(const GWL_Seat *seat, const wl_cursor_image *wl_image, wl_buffer *buffer) { const GWL_Cursor *cursor = &seat->cursor; const bool visible = (cursor->visible && cursor->is_hardware); /* This is a requirement of WAYLAND, when this isn't the case, * it causes Blender's window to close intermittently. */ if (seat->wl.pointer) { const int scale = cursor_buffer_compatible_scale_from_image( wl_image, cursor->is_custom ? cursor->custom_scale : seat->pointer.theme_scale); const int32_t hotspot_x = int32_t(wl_image->hotspot_x) / scale; const int32_t hotspot_y = int32_t(wl_image->hotspot_y) / scale; cursor_buffer_set_surface_impl(wl_image, buffer, cursor->wl.surface_cursor, scale); wl_pointer_set_cursor(seat->wl.pointer, seat->pointer.serial, visible ? cursor->wl.surface_cursor : nullptr, hotspot_x, hotspot_y); } /* Set the cursor for all tablet tools as well. */ if (!seat->wp.tablet_tools.empty()) { const int scale = cursor_buffer_compatible_scale_from_image( wl_image, cursor->is_custom ? cursor->custom_scale : seat->tablet.theme_scale); const int32_t hotspot_x = int32_t(wl_image->hotspot_x) / scale; const int32_t hotspot_y = int32_t(wl_image->hotspot_y) / scale; for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) { GWL_TabletTool *tablet_tool = static_cast( zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); cursor_buffer_set_surface_impl(wl_image, buffer, tablet_tool->wl.surface_cursor, scale); zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2, seat->tablet.serial, visible ? tablet_tool->wl.surface_cursor : nullptr, hotspot_x, hotspot_y); } } } static void gwl_seat_cursor_buffer_set_current(GWL_Seat *seat) { const GWL_Cursor *cursor = &seat->cursor; gwl_seat_cursor_anim_end(seat); gwl_seat_cursor_buffer_set(seat, &cursor->wl.image, cursor->wl.buffer); gwl_seat_cursor_anim_begin_if_needed(seat); } enum eCursorSetMode { CURSOR_VISIBLE_ALWAYS_SET = 1, CURSOR_VISIBLE_ONLY_HIDE, CURSOR_VISIBLE_ONLY_SHOW, }; static void gwl_seat_cursor_visible_set(GWL_Seat *seat, const bool visible, const bool is_hardware, const enum eCursorSetMode set_mode) { GWL_Cursor *cursor = &seat->cursor; const bool was_visible = cursor->is_hardware && cursor->visible; const bool use_visible = is_hardware && visible; if (set_mode == CURSOR_VISIBLE_ALWAYS_SET) { /* Pass. */ } else if (set_mode == CURSOR_VISIBLE_ONLY_SHOW) { if (!use_visible) { return; } } else if (set_mode == CURSOR_VISIBLE_ONLY_HIDE) { if (use_visible) { return; } } if (use_visible) { if (!was_visible) { gwl_seat_cursor_buffer_show(seat); } } else { if (was_visible) { gwl_seat_cursor_buffer_hide(seat); } } cursor->visible = visible; cursor->is_hardware = is_hardware; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Private Cursor Animation API * \{ */ #ifdef USE_EVENT_BACKGROUND_THREAD static bool gwl_seat_cursor_anim_check(GWL_Seat *seat) { const wl_cursor *wl_cursor = seat->cursor.wl.theme_cursor; if (!wl_cursor) { return false; } /* NOTE: return true to stress test animated cursor, * to ensure (otherwise rare) issues are triggered more frequently. */ // return true; return wl_cursor->image_count > 1; } static void gwl_seat_cursor_anim_begin(GWL_Seat *seat) { /* Caller must lock `server_mutex`. */ GHOST_ASSERT(seat->cursor.anim_handle == nullptr, "Must be cleared"); /* Callback for updating the cursor animation. */ auto cursor_anim_frame_step_fn = [](GWL_Seat *seat, GWL_Cursor_AnimHandle *anim_handle, int delay) { /* It's possible the `wl_cursor` is reloaded while the cursor is animating. * Don't access outside the lock, that's why the `delay` is passed in. */ std::mutex *server_mutex = seat->system->server_mutex; int frame = 0; while (!anim_handle->exit_pending.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(delay)); if (!anim_handle->exit_pending.load()) { std::lock_guard lock_server_guard{*server_mutex}; if (!anim_handle->exit_pending.load()) { const struct wl_cursor *wl_cursor = seat->cursor.wl.theme_cursor; frame = (frame + 1) % wl_cursor->image_count; wl_cursor_image *image = wl_cursor->images[frame]; wl_buffer *buffer = wl_cursor_image_get_buffer(image); gwl_seat_cursor_buffer_set(seat, image, buffer); delay = wl_cursor->images[frame]->delay; /* Without this the cursor won't update when other processes are occupied. */ wl_display_flush(seat->system->wl_display_get()); } } } delete anim_handle; }; /* Allocate so this can be set before the thread begins. */ GWL_Cursor_AnimHandle *anim_handle = new GWL_Cursor_AnimHandle; seat->cursor.anim_handle = anim_handle; const int delay = seat->cursor.wl.theme_cursor->images[0]->delay; std::thread cursor_anim_thread(cursor_anim_frame_step_fn, seat, anim_handle, delay); /* Application logic should take priority. */ thread_set_min_priority(cursor_anim_thread); cursor_anim_thread.detach(); } static void gwl_seat_cursor_anim_begin_if_needed(GWL_Seat *seat) { if (gwl_seat_cursor_anim_check(seat)) { gwl_seat_cursor_anim_begin(seat); } } static void gwl_seat_cursor_anim_end(GWL_Seat *seat) { GWL_Cursor *cursor = &seat->cursor; if (cursor->anim_handle) { GWL_Cursor_AnimHandle *anim_handle = cursor->anim_handle; cursor->anim_handle = nullptr; anim_handle->exit_pending.store(true); } } static void gwl_seat_cursor_anim_reset(GWL_Seat *seat) { gwl_seat_cursor_anim_end(seat); gwl_seat_cursor_anim_begin_if_needed(seat); } #else /* Unfortunately cursor animation requires a background thread. */ [[maybe_unused]] static bool gwl_seat_cursor_anim_check(GWL_Seat * /*seat*/) { return false; } [[maybe_unused]] static void gwl_seat_cursor_anim_begin(GWL_Seat * /*seat*/) {} [[maybe_unused]] static void gwl_seat_cursor_anim_begin_if_needed(GWL_Seat * /*seat*/) {} [[maybe_unused]] static void gwl_seat_cursor_anim_end(GWL_Seat * /*seat*/) {} [[maybe_unused]] static void gwl_seat_cursor_anim_reset(GWL_Seat * /*seat*/) {} #endif /* !USE_EVENT_BACKGROUND_THREAD */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Private Keyboard Depressed Key Tracking * * Don't track physical key-codes because there may be multiple keyboards connected. * Instead, count the number of #GHOST_kKey are pressed. * This may seem susceptible to bugs with sticky-keys however XKB works this way internally. * \{ */ static CLG_LogRef LOG_WL_KEYBOARD_DEPRESSED_STATE = {"ghost.wl.keyboard.depressed"}; #define LOG (&LOG_WL_KEYBOARD_DEPRESSED_STATE) static void keyboard_depressed_state_reset(GWL_Seat *seat) { for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) { seat->key_depressed.mods[i] = 0; } } static void keyboard_depressed_state_key_event(GWL_Seat *seat, const GHOST_TKey gkey, const GHOST_TEventType etype) { if (GHOST_KEY_MODIFIER_CHECK(gkey)) { const int index = GHOST_KEY_MODIFIER_TO_INDEX(gkey); int16_t &value = seat->key_depressed.mods[index]; if (etype == GHOST_kEventKeyUp) { value -= 1; if (UNLIKELY(value < 0)) { CLOG_WARN(LOG, "modifier (%d) has negative keys held (%d)!", index, value); value = 0; } } else { value += 1; } } } static void keyboard_depressed_state_push_events_from_change( GWL_Seat *seat, const GWL_KeyboardDepressedState &key_depressed_prev) { GHOST_IWindow *win = ghost_wl_surface_user_data(seat->keyboard.wl.surface_window); const GHOST_SystemWayland *system = seat->system; /* Caller has no time-stamp, set from system. */ const uint64_t event_ms = system->getMilliSeconds(); /* Separate key up and down into separate passes so key down events always come after key up. * Do this so users of GHOST can use the last pressed or released modifier to check * if the modifier is held instead of counting modifiers pressed as is done here, * this isn't perfect but works well enough in practice. */ for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) { for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d < 0; d++) { const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i); seat->system->pushEvent_maybe_pending( new GHOST_EventKey(event_ms, GHOST_kEventKeyUp, win, gkey, false)); CLOG_INFO(LOG, 2, "modifier (%d) up", i); } } for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) { for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d > 0; d--) { const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i); seat->system->pushEvent_maybe_pending( new GHOST_EventKey(event_ms, GHOST_kEventKeyDown, win, gkey, false)); CLOG_INFO(LOG, 2, "modifier (%d) down", i); } } } #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Relative Motion), #zwp_relative_pointer_v1_listener * * These callbacks are registered for Wayland interfaces and called when * an event is received from the compositor. * \{ */ static CLG_LogRef LOG_WL_RELATIVE_POINTER = {"ghost.wl.handle.relative_pointer"}; #define LOG (&LOG_WL_RELATIVE_POINTER) /** * The caller is responsible for setting the value of `seat->xy`. */ static void relative_pointer_handle_relative_motion_impl(GWL_Seat *seat, GHOST_WindowWayland *win, const wl_fixed_t xy[2], const uint64_t event_ms) { seat->pointer.xy[0] = xy[0]; seat->pointer.xy[1] = xy[1]; #ifdef USE_GNOME_CONFINE_HACK if (seat->use_pointer_software_confine) { GHOST_Rect bounds; win->getClientBounds(bounds); /* Needed or the cursor is considered outside the window and doesn't restore the location. */ bounds.m_r -= 1; bounds.m_b -= 1; bounds.m_l = win->wl_fixed_from_window(wl_fixed_from_int(bounds.m_l)); bounds.m_t = win->wl_fixed_from_window(wl_fixed_from_int(bounds.m_t)); bounds.m_r = win->wl_fixed_from_window(wl_fixed_from_int(bounds.m_r)); bounds.m_b = win->wl_fixed_from_window(wl_fixed_from_int(bounds.m_b)); bounds.clampPoint(UNPACK2(seat->pointer.xy)); } #endif const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)}; seat->system->pushEvent_maybe_pending(new GHOST_EventCursor( event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), GHOST_TABLET_DATA_NONE)); } static void relative_pointer_handle_relative_motion( void *data, zwp_relative_pointer_v1 * /*zwp_relative_pointer_v1*/, const uint32_t utime_hi, const uint32_t utime_lo, const wl_fixed_t dx, const wl_fixed_t dy, const wl_fixed_t /*dx_unaccel*/, const wl_fixed_t /*dy_unaccel*/) { GWL_Seat *seat = static_cast(data); const uint64_t time = ghost_wl_ms_from_utime_pair(utime_hi, utime_lo); const uint64_t event_ms = seat->system->ms_from_input_time(time); if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { CLOG_INFO(LOG, 2, "relative_motion"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const wl_fixed_t xy_next[2] = { seat->pointer.xy[0] + win->wl_fixed_from_window(dx), seat->pointer.xy[1] + win->wl_fixed_from_window(dy), }; relative_pointer_handle_relative_motion_impl(seat, win, xy_next, event_ms); } else { CLOG_INFO(LOG, 2, "relative_motion (skipped)"); } } static const zwp_relative_pointer_v1_listener relative_pointer_listener = { relative_pointer_handle_relative_motion, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Data Source), #wl_data_source_listener * \{ */ static CLG_LogRef LOG_WL_DATA_SOURCE = {"ghost.wl.handle.data_source"}; #define LOG (&LOG_WL_DATA_SOURCE) static void dnd_events(const GWL_Seat *const seat, const GHOST_TEventType event, const uint64_t event_ms) { /* NOTE: `seat->data_offer_dnd_mutex` must already be locked. */ if (wl_surface *wl_surface_focus = seat->wl.surface_window_focus_dnd) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->data_offer_dnd->dnd.xy)}; for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order_type); i++) { const GHOST_TDragnDropTypes type = ghost_wl_mime_preference_order_type[i]; seat->system->pushEvent_maybe_pending( new GHOST_EventDragnDrop(event_ms, event, type, win, UNPACK2(event_xy), nullptr)); } } } static char *read_buffer_from_data_offer(GWL_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, const bool nil_terminate, size_t *r_len) { int pipefd[2]; const bool pipefd_ok = pipe(pipefd) == 0; if (pipefd_ok) { wl_data_offer_receive(data_offer->wl.id, mime_receive, pipefd[1]); close(pipefd[1]); } else { CLOG_WARN(LOG, "error creating pipe: %s", std::strerror(errno)); } /* Only for DND (A no-op to disable for clipboard data-offer). */ data_offer->dnd.in_use = false; if (mutex) { mutex->unlock(); } /* WARNING: `data_offer` may be freed from now on. */ char *buf = nullptr; if (pipefd_ok) { buf = read_file_as_buffer(pipefd[0], nil_terminate, r_len); if (buf == nullptr) { CLOG_WARN(LOG, "unable to pipe into buffer: %s", std::strerror(errno)); } close(pipefd[0]); } return buf; } static char *read_buffer_from_primary_selection_offer(GWL_PrimarySelection_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, const bool nil_terminate, size_t *r_len) { int pipefd[2]; const bool pipefd_ok = pipe(pipefd) == 0; if (pipefd_ok) { zwp_primary_selection_offer_v1_receive(data_offer->wp.id, mime_receive, pipefd[1]); close(pipefd[1]); } else { CLOG_WARN(LOG, "error creating pipe: %s", std::strerror(errno)); } if (mutex) { mutex->unlock(); } /* WARNING: `data_offer` may be freed from now on. */ char *buf = nullptr; if (pipefd_ok) { buf = read_file_as_buffer(pipefd[0], nil_terminate, r_len); if (buf == nullptr) { CLOG_WARN(LOG, "unable to pipe into buffer: %s", std::strerror(errno)); } close(pipefd[0]); } return buf; } /** * A target accepts an offered mime type. * * Sent when a target accepts pointer_focus or motion events. If * a target does not accept any of the offered types, type is nullptr. */ static void data_source_handle_target(void * /*data*/, wl_data_source * /*wl_data_source*/, const char * /*mime_type*/) { CLOG_INFO(LOG, 2, "target"); } static void data_source_handle_send(void *data, wl_data_source * /*wl_data_source*/, const char * /*mime_type*/, const int32_t fd) { GWL_Seat *seat = static_cast(data); CLOG_INFO(LOG, 2, "send"); auto write_file_fn = [](GWL_Seat *seat, const int fd) { if (UNLIKELY(write(fd, seat->data_source->buffer_out.data, seat->data_source->buffer_out.data_size) < 0)) { CLOG_WARN(LOG, "error writing to clipboard: %s", std::strerror(errno)); } close(fd); seat->data_source_mutex.unlock(); }; seat->data_source_mutex.lock(); std::thread write_thread(write_file_fn, seat, fd); write_thread.detach(); } static void data_source_handle_cancelled(void *data, wl_data_source *wl_data_source) { CLOG_INFO(LOG, 2, "cancelled"); GWL_Seat *seat = static_cast(data); GWL_DataSource *data_source = seat->data_source; if (seat->data_source->wl.source == wl_data_source) { data_source->wl.source = nullptr; } wl_data_source_destroy(wl_data_source); } /** * The drag-and-drop operation physically finished. * * The user performed the drop action. This event does not * indicate acceptance, #wl_data_source.cancelled may still be * emitted afterwards if the drop destination does not accept any mime type. */ static void data_source_handle_dnd_drop_performed(void * /*data*/, wl_data_source * /*wl_data_source*/) { CLOG_INFO(LOG, 2, "dnd_drop_performed"); } /** * The drag-and-drop operation concluded. * * The drop destination finished interoperating with this data * source, so the client is now free to destroy this data source * and free all associated data. */ static void data_source_handle_dnd_finished(void * /*data*/, wl_data_source * /*wl_data_source*/) { CLOG_INFO(LOG, 2, "dnd_finished"); } /** * Notify the selected action. * * This event indicates the action selected by the compositor * after matching the source/destination side actions. Only one * action (or none) will be offered here. */ static void data_source_handle_action(void * /*data*/, wl_data_source * /*wl_data_source*/, const uint32_t dnd_action) { CLOG_INFO(LOG, 2, "handle_action (dnd_action=%u)", dnd_action); } static const wl_data_source_listener data_source_listener = { /*target*/ data_source_handle_target, /*send*/ data_source_handle_send, /*cancelled*/ data_source_handle_cancelled, /*dnd_drop_performed*/ data_source_handle_dnd_drop_performed, /*dnd_finished*/ data_source_handle_dnd_finished, /*action*/ data_source_handle_action, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Data Offer), #wl_data_offer_listener * \{ */ static CLG_LogRef LOG_WL_DATA_OFFER = {"ghost.wl.handle.data_offer"}; #define LOG (&LOG_WL_DATA_OFFER) static void data_offer_handle_offer(void *data, wl_data_offer * /*wl_data_offer*/, const char *mime_type) { CLOG_INFO(LOG, 2, "offer (mime_type=%s)", mime_type); GWL_DataOffer *data_offer = static_cast(data); data_offer->types.insert(mime_type); } static void data_offer_handle_source_actions(void *data, wl_data_offer * /*wl_data_offer*/, const uint32_t source_actions) { CLOG_INFO(LOG, 2, "source_actions (%u)", source_actions); GWL_DataOffer *data_offer = static_cast(data); data_offer->dnd.source_actions = (enum wl_data_device_manager_dnd_action)source_actions; } static void data_offer_handle_action(void *data, wl_data_offer * /*wl_data_offer*/, const uint32_t dnd_action) { CLOG_INFO(LOG, 2, "actions (%u)", dnd_action); GWL_DataOffer *data_offer = static_cast(data); data_offer->dnd.action = (enum wl_data_device_manager_dnd_action)dnd_action; } static const wl_data_offer_listener data_offer_listener = { /*offer*/ data_offer_handle_offer, /*source_actions*/ data_offer_handle_source_actions, /*action*/ data_offer_handle_action, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Data Device), #wl_data_device_listener * \{ */ static CLG_LogRef LOG_WL_DATA_DEVICE = {"ghost.wl.handle.data_device"}; #define LOG (&LOG_WL_DATA_DEVICE) static void data_device_handle_data_offer(void * /*data*/, wl_data_device * /*wl_data_device*/, wl_data_offer *id) { CLOG_INFO(LOG, 2, "data_offer"); /* The ownership of data-offer isn't so obvious: * At this point it's not known if this will be used for drag & drop or selection. * * The API docs state that the following callbacks run immediately after this callback: * - #wl_data_device_listener::enter (for drag & drop). * - #wl_data_device_listener::selection (for copy & paste). * * In the case of GHOST, this means they will be assigned to either: * - #GWL_Seat::data_offer_dnd * - #GWL_Seat::data_offer_copy_paste */ GWL_DataOffer *data_offer = new GWL_DataOffer; data_offer->wl.id = id; wl_data_offer_add_listener(id, &data_offer_listener, data_offer); } static void data_device_handle_enter(void *data, wl_data_device * /*wl_data_device*/, const uint32_t serial, wl_surface *wl_surface, const wl_fixed_t x, const wl_fixed_t y, wl_data_offer *id) { /* Always clear the current data-offer no matter what else happens. */ GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->getMilliSeconds(); std::lock_guard lock{seat->data_offer_dnd_mutex}; if (seat->data_offer_dnd) { wl_data_offer_destroy(seat->data_offer_dnd->wl.id); delete seat->data_offer_dnd; seat->data_offer_dnd = nullptr; } /* Clearing complete. */ /* Handle the new offer. */ GWL_DataOffer *data_offer = static_cast(wl_data_offer_get_user_data(id)); if (!ghost_wl_surface_own_with_null_check(wl_surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); wl_data_offer_destroy(data_offer->wl.id); delete data_offer; return; } CLOG_INFO(LOG, 2, "enter"); /* Transfer ownership of the `data_offer`. */ seat->data_offer_dnd = data_offer; data_offer->dnd.in_use = true; data_offer->dnd.xy[0] = x; data_offer->dnd.xy[1] = y; wl_data_offer_set_actions(id, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order); i++) { const char *type = ghost_wl_mime_preference_order[i]; wl_data_offer_accept(id, serial, type); } seat->wl.surface_window_focus_dnd = wl_surface; seat->system->seat_active_set(seat); dnd_events(seat, GHOST_kEventDraggingEntered, event_ms); } static void data_device_handle_leave(void *data, wl_data_device * /*wl_data_device*/) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->getMilliSeconds(); std::lock_guard lock{seat->data_offer_dnd_mutex}; /* The user may have only dragged over the window decorations. */ if (seat->data_offer_dnd == nullptr) { return; } CLOG_INFO(LOG, 2, "leave"); dnd_events(seat, GHOST_kEventDraggingExited, event_ms); seat->wl.surface_window_focus_dnd = nullptr; if (seat->data_offer_dnd && !seat->data_offer_dnd->dnd.in_use) { wl_data_offer_destroy(seat->data_offer_dnd->wl.id); delete seat->data_offer_dnd; seat->data_offer_dnd = nullptr; } } static void data_device_handle_motion(void *data, wl_data_device * /*wl_data_device*/, const uint32_t time, const wl_fixed_t x, const wl_fixed_t y) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->ms_from_input_time(time); std::lock_guard lock{seat->data_offer_dnd_mutex}; /* The user may have only dragged over the window decorations. */ if (seat->data_offer_dnd == nullptr) { return; } CLOG_INFO(LOG, 2, "motion"); seat->data_offer_dnd->dnd.xy[0] = x; seat->data_offer_dnd->dnd.xy[1] = y; dnd_events(seat, GHOST_kEventDraggingUpdated, event_ms); } static void data_device_handle_drop(void *data, wl_data_device * /*wl_data_device*/) { GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_dnd_mutex}; /* No need to check this for null (as other callbacks do). * because the the data-offer has not been accepted (actions set... etc). */ GWL_DataOffer *data_offer = seat->data_offer_dnd; /* Use a blank string for `mime_receive` to prevent crashes, although could also be `nullptr`. * Failure to set this to a known type just means the file won't have any special handling. * GHOST still generates a dropped file event. * NOTE: this string can be compared with `mime_text_plain`, `mime_text_uri` etc... * as the this always points to the same values. */ const char *mime_receive = ""; for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_preference_order); i++) { const char *type = ghost_wl_mime_preference_order[i]; if (data_offer->types.count(type)) { mime_receive = type; break; } } CLOG_INFO(LOG, 2, "drop mime_recieve=%s", mime_receive); auto read_uris_fn = [](GWL_Seat *const seat, GWL_DataOffer *data_offer, wl_surface *wl_surface_window, const char *mime_receive) { const uint64_t event_ms = seat->system->getMilliSeconds(); const wl_fixed_t xy[2] = {UNPACK2(data_offer->dnd.xy)}; size_t data_buf_len = 0; const char *data_buf = read_buffer_from_data_offer( data_offer, mime_receive, nullptr, false, &data_buf_len); std::string data = data_buf ? std::string(data_buf, data_buf_len) : ""; free(const_cast(data_buf)); CLOG_INFO(LOG, 2, "drop_read_uris mime_receive=%s, data=%s", mime_receive, data.c_str()); wl_data_offer_finish(data_offer->wl.id); wl_data_offer_destroy(data_offer->wl.id); if (seat->data_offer_dnd == data_offer) { seat->data_offer_dnd = nullptr; } delete data_offer; data_offer = nullptr; GHOST_SystemWayland *const system = seat->system; if (mime_receive == ghost_wl_mime_text_uri) { static constexpr const char *file_proto = "file://"; /* NOTE: some applications CRLF (`\r\n`) GTK3 for e.g. & others don't `pcmanfm-qt`. * So support both, once `\n` is found, strip the preceding `\r` if found. */ static constexpr const char *lf = "\n"; GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_window); std::vector uris; size_t pos = 0; while (pos != std::string::npos) { pos = data.find(file_proto, pos); if (pos == std::string::npos) { break; } const size_t start = pos + sizeof(file_proto) - 1; pos = data.find(lf, pos); size_t end = pos; if (UNLIKELY(end == std::string::npos)) { /* Note that most well behaved file managers will add a trailing newline, * Gnome's web browser (44.3) doesn't, so support reading up until the last byte. */ end = data.size(); } /* Account for 'CRLF' case. */ if (data[end - 1] == '\r') { end -= 1; } uris.push_back(data.substr(start, end - start)); CLOG_INFO(LOG, 2, "drop_read_uris pos=%zu, text_uri=\"%s\"", start, uris.back().c_str()); } GHOST_TStringArray *flist = static_cast( malloc(sizeof(GHOST_TStringArray))); flist->count = int(uris.size()); flist->strings = static_cast(malloc(uris.size() * sizeof(uint8_t *))); for (size_t i = 0; i < uris.size(); i++) { flist->strings[i] = reinterpret_cast(GHOST_URL_decode_alloc(uris[i].c_str())); } CLOG_INFO(LOG, 2, "drop_read_uris_fn file_count=%d", flist->count); const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, xy)}; system->pushEvent_maybe_pending(new GHOST_EventDragnDrop(event_ms, GHOST_kEventDraggingDropDone, GHOST_kDragnDropTypeFilenames, win, UNPACK2(event_xy), flist)); } else if (ELEM(mime_receive, ghost_wl_mime_text_plain, ghost_wl_mime_text_utf8)) { /* TODO: enable use of internal functions 'txt_insert_buf' and * 'text_update_edited' to behave like dropped text was pasted. */ CLOG_INFO(LOG, 2, "drop_read_uris_fn (text_plain, text_utf8), unhandled!"); } wl_display_roundtrip(system->wl_display_get()); }; /* Pass in `seat->wl_surface_window_focus_dnd` instead of accessing it from `seat` since the * leave callback (#data_device_handle_leave) will clear the value once this function starts. */ std::thread read_thread( read_uris_fn, seat, data_offer, seat->wl.surface_window_focus_dnd, mime_receive); read_thread.detach(); } static void data_device_handle_selection(void *data, wl_data_device * /*wl_data_device*/, wl_data_offer *id) { /* Always clear the current data-offer no matter what else happens. */ GWL_Seat *seat = static_cast(data); std::lock_guard lock{seat->data_offer_copy_paste_mutex}; if (seat->data_offer_copy_paste) { wl_data_offer_destroy(seat->data_offer_copy_paste->wl.id); delete seat->data_offer_copy_paste; seat->data_offer_copy_paste = nullptr; } /* Clearing complete. */ /* Handle the new offer. */ if (id == nullptr) { CLOG_INFO(LOG, 2, "selection: (skipped)"); return; } CLOG_INFO(LOG, 2, "selection"); GWL_DataOffer *data_offer = static_cast(wl_data_offer_get_user_data(id)); /* Transfer ownership of the `data_offer`. */ seat->data_offer_copy_paste = data_offer; } static const wl_data_device_listener data_device_listener = { /*data_offer*/ data_device_handle_data_offer, /*enter*/ data_device_handle_enter, /*leave*/ data_device_handle_leave, /*motion*/ data_device_handle_motion, /*drop*/ data_device_handle_drop, /*selection*/ data_device_handle_selection, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Buffer), #wl_buffer_listener * \{ */ static CLG_LogRef LOG_WL_CURSOR_BUFFER = {"ghost.wl.handle.cursor_buffer"}; #define LOG (&LOG_WL_CURSOR_BUFFER) static void cursor_buffer_handle_release(void *data, wl_buffer *wl_buffer) { CLOG_INFO(LOG, 2, "release"); GWL_Cursor *cursor = static_cast(data); wl_buffer_destroy(wl_buffer); if (wl_buffer == cursor->wl.buffer) { /* The mapped buffer was from a custom cursor. */ cursor->wl.buffer = nullptr; } } static const wl_buffer_listener cursor_buffer_listener = { /*release*/ cursor_buffer_handle_release, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Surface), #wl_surface_listener * \{ */ static CLG_LogRef LOG_WL_CURSOR_SURFACE = {"ghost.wl.handle.cursor_surface"}; #define LOG (&LOG_WL_CURSOR_SURFACE) static bool update_cursor_scale(GWL_Cursor &cursor, wl_shm *shm, GWL_SeatStatePointer *seat_state_pointer, wl_surface *wl_surface_cursor) { int scale = 0; for (const GWL_Output *output : seat_state_pointer->outputs) { if (output->scale > scale) { scale = output->scale; } } if (scale > 0 && seat_state_pointer->theme_scale != scale) { seat_state_pointer->theme_scale = scale; if (!cursor.is_custom) { wl_surface_set_buffer_scale(wl_surface_cursor, scale); } wl_cursor_theme_destroy(cursor.wl.theme); cursor.wl.theme = wl_cursor_theme_load( cursor.theme_name.c_str(), scale * cursor.theme_size, shm); if (cursor.wl.theme_cursor) { cursor.wl.theme_cursor = wl_cursor_theme_get_cursor(cursor.wl.theme, cursor.wl.theme_cursor_name); } return true; } return false; } static void cursor_surface_handle_enter(void *data, wl_surface *wl_surface, wl_output *wl_output) { if (!ghost_wl_output_own(wl_output)) { CLOG_INFO(LOG, 2, "handle_enter (skipped)"); return; } CLOG_INFO(LOG, 2, "handle_enter"); GWL_Seat *seat = static_cast(data); GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_from_cursor_surface( seat, wl_surface); const GWL_Output *reg_output = ghost_wl_output_user_data(wl_output); seat_state_pointer->outputs.insert(reg_output); update_cursor_scale(seat->cursor, seat->system->wl_shm_get(), seat_state_pointer, wl_surface); } static void cursor_surface_handle_leave(void *data, wl_surface *wl_surface, wl_output *wl_output) { if (!(wl_output && ghost_wl_output_own(wl_output))) { CLOG_INFO(LOG, 2, "handle_leave (skipped)"); return; } CLOG_INFO(LOG, 2, "handle_leave"); GWL_Seat *seat = static_cast(data); GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_from_cursor_surface( seat, wl_surface); const GWL_Output *reg_output = ghost_wl_output_user_data(wl_output); seat_state_pointer->outputs.erase(reg_output); update_cursor_scale(seat->cursor, seat->system->wl_shm_get(), seat_state_pointer, wl_surface); } static const wl_surface_listener cursor_surface_listener = { /*enter*/ cursor_surface_handle_enter, /*leave*/ cursor_surface_handle_leave, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer), #wl_pointer_listener * \{ */ static CLG_LogRef LOG_WL_POINTER = {"ghost.wl.handle.pointer"}; #define LOG (&LOG_WL_POINTER) static void pointer_handle_enter(void *data, wl_pointer * /*wl_pointer*/, const uint32_t serial, wl_surface *wl_surface, const wl_fixed_t surface_x, const wl_fixed_t surface_y) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->getMilliSeconds(); /* Null when just destroyed. */ if (!ghost_wl_surface_own_with_null_check(wl_surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface); seat->cursor_source_serial = serial; seat->pointer.serial = serial; seat->pointer.xy[0] = surface_x; seat->pointer.xy[1] = surface_y; /* Resetting scroll events is likely unnecessary, * do this to avoid any possible problems as it's harmless. */ seat->pointer_scroll = GWL_SeatStatePointerScroll{}; seat->pointer.wl.surface_window = wl_surface; seat->system->seat_active_set(seat); seat->system->cursor_shape_set(win->getCursorShape()); const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)}; seat->system->pushEvent_maybe_pending(new GHOST_EventCursor( event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), GHOST_TABLET_DATA_NONE)); } static void pointer_handle_leave(void *data, wl_pointer * /*wl_pointer*/, const uint32_t /*serial*/, wl_surface *wl_surface) { /* First clear the `pointer.wl_surface`, since the window won't exist when closing the window. */ static_cast(data)->pointer.wl.surface_window = nullptr; if (!ghost_wl_surface_own_with_null_check(wl_surface)) { CLOG_INFO(LOG, 2, "leave (skipped)"); return; } CLOG_INFO(LOG, 2, "leave"); } static void pointer_handle_motion(void *data, wl_pointer * /*wl_pointer*/, const uint32_t time, const wl_fixed_t surface_x, const wl_fixed_t surface_y) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->ms_from_input_time(time); seat->pointer.xy[0] = surface_x; seat->pointer.xy[1] = surface_y; if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { CLOG_INFO(LOG, 2, "motion"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)}; seat->system->pushEvent_maybe_pending(new GHOST_EventCursor( event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), GHOST_TABLET_DATA_NONE)); } else { CLOG_INFO(LOG, 2, "motion (skipped)"); } } static void pointer_handle_button(void *data, wl_pointer * /*wl_pointer*/, const uint32_t serial, const uint32_t time, const uint32_t button, const uint32_t state) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->ms_from_input_time(time); CLOG_INFO(LOG, 2, "button (button=%u, state=%u)", button, state); GHOST_TEventType etype = GHOST_kEventUnknown; switch (state) { case WL_POINTER_BUTTON_STATE_RELEASED: etype = GHOST_kEventButtonUp; break; case WL_POINTER_BUTTON_STATE_PRESSED: etype = GHOST_kEventButtonDown; break; } GHOST_TButton ebutton = GHOST_kButtonMaskLeft; switch (button) { case BTN_LEFT: ebutton = GHOST_kButtonMaskLeft; break; case BTN_MIDDLE: ebutton = GHOST_kButtonMaskMiddle; break; case BTN_RIGHT: ebutton = GHOST_kButtonMaskRight; break; case BTN_SIDE: ebutton = GHOST_kButtonMaskButton4; break; case BTN_EXTRA: ebutton = GHOST_kButtonMaskButton5; break; case BTN_FORWARD: ebutton = GHOST_kButtonMaskButton6; break; case BTN_BACK: ebutton = GHOST_kButtonMaskButton7; break; } seat->data_source_serial = serial; seat->pointer.buttons.set(ebutton, state == WL_POINTER_BUTTON_STATE_PRESSED); if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent_maybe_pending( new GHOST_EventButton(event_ms, etype, win, ebutton, GHOST_TABLET_DATA_NONE)); } } static void pointer_handle_axis(void *data, wl_pointer * /*wl_pointer*/, const uint32_t time, const uint32_t axis, const wl_fixed_t value) { GWL_Seat *seat = static_cast(data); seat->pointer_scroll.event_ms = seat->system->ms_from_input_time(time); seat->pointer_scroll.has_event_ms = true; /* NOTE: this is used for touch based scrolling - or other input that doesn't scroll with * discrete "steps". This allows supporting smooth-scrolling without "touch" gesture support. */ CLOG_INFO(LOG, 2, "axis (axis=%u, value=%d)", axis, value); const int index = pointer_axis_as_index(axis); if (UNLIKELY(index == -1)) { return; } seat->pointer_scroll.smooth_xy[index] = value; } static void pointer_handle_frame(void *data, wl_pointer * /*wl_pointer*/) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->pointer_scroll.has_event_ms ? seat->pointer_scroll.event_ms : seat->system->getMilliSeconds(); CLOG_INFO(LOG, 2, "frame"); /* Both discrete and smooth events may be set at once, never generate events for both * as this will be handling the same event in to different ways. * Prioritize discrete axis events for the mouse wheel, otherwise smooth scroll. */ if (seat->pointer_scroll.axis_source == WL_POINTER_AXIS_SOURCE_WHEEL) { if (seat->pointer_scroll.discrete_xy[0]) { seat->pointer_scroll.smooth_xy[0] = 0; } if (seat->pointer_scroll.discrete_xy[1]) { seat->pointer_scroll.smooth_xy[1] = 0; } } else { if (seat->pointer_scroll.smooth_xy[0]) { seat->pointer_scroll.discrete_xy[0] = 0; } if (seat->pointer_scroll.smooth_xy[1]) { seat->pointer_scroll.discrete_xy[1] = 0; } } /* Discrete X axis currently unsupported. */ if (seat->pointer_scroll.discrete_xy[1]) { if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const int32_t discrete = seat->pointer_scroll.discrete_xy[1]; seat->system->pushEvent_maybe_pending( new GHOST_EventWheel(event_ms, win, std::signbit(discrete) ? +1 : -1)); } seat->pointer_scroll.discrete_xy[0] = 0; seat->pointer_scroll.discrete_xy[1] = 0; } if (seat->pointer_scroll.smooth_xy[0] || seat->pointer_scroll.smooth_xy[1]) { if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)}; seat->system->pushEvent_maybe_pending(new GHOST_EventTrackpad( event_ms, win, GHOST_kTrackpadEventScroll, UNPACK2(event_xy), /* NOTE: scaling the delta doesn't seem necessary. * NOTE: inverting delta gives correct results, see: QTBUG-85767. * NOTE: the preference to invert scrolling (in GNOME at least) * has already been applied so there is no need to read this preference. */ -wl_fixed_to_int(seat->pointer_scroll.smooth_xy[0]), -wl_fixed_to_int(seat->pointer_scroll.smooth_xy[1]), /* NOTE: GHOST does not support per-axis inversion. * Assume inversion is used or not. */ seat->pointer_scroll.inverted_xy[0] || seat->pointer_scroll.inverted_xy[1])); } seat->pointer_scroll.smooth_xy[0] = 0; seat->pointer_scroll.smooth_xy[1] = 0; } seat->pointer_scroll.axis_source = WL_POINTER_AXIS_SOURCE_WHEEL; seat->pointer_scroll.inverted_xy[0] = false; seat->pointer_scroll.inverted_xy[1] = false; seat->pointer_scroll.event_ms = 0; seat->pointer_scroll.has_event_ms = false; } static void pointer_handle_axis_source(void *data, wl_pointer * /*wl_pointer*/, uint32_t axis_source) { CLOG_INFO(LOG, 2, "axis_source (axis_source=%u)", axis_source); GWL_Seat *seat = static_cast(data); seat->pointer_scroll.axis_source = (enum wl_pointer_axis_source)axis_source; } static void pointer_handle_axis_stop(void *data, wl_pointer * /*wl_pointer*/, uint32_t time, uint32_t axis) { GWL_Seat *seat = static_cast(data); seat->pointer_scroll.event_ms = seat->system->ms_from_input_time(time); seat->pointer_scroll.has_event_ms = true; CLOG_INFO(LOG, 2, "axis_stop (axis=%u)", axis); } static void pointer_handle_axis_discrete(void *data, wl_pointer * /*wl_pointer*/, uint32_t axis, int32_t discrete) { /* NOTE: a discrete axis are typically mouse wheel events. * The non-discrete version of this function is used for touch-pad. */ CLOG_INFO(LOG, 2, "axis_discrete (axis=%u, discrete=%d)", axis, discrete); const int index = pointer_axis_as_index(axis); if (UNLIKELY(index == -1)) { return; } GWL_Seat *seat = static_cast(data); seat->pointer_scroll.discrete_xy[index] = discrete; } static void pointer_handle_axis_value120(void * /*data*/, wl_pointer * /*wl_pointer*/, uint32_t axis, int32_t value120) { /* NOTE: the axis handler seems high resolution enough. * Nevertheless, we might want to support this. */ CLOG_INFO(LOG, 2, "axis_value120 (axis=%u, value120=%d)", axis, value120); } #ifdef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM /* Requires WAYLAND 1.22 or newer. */ static void pointer_handle_axis_relative_direction(void *data, wl_pointer * /*wl_pointer*/, uint32_t axis, uint32_t direction) { CLOG_INFO(LOG, 2, "axis_relative_direction (axis=%u, direction=%u)", axis, direction); const int index = pointer_axis_as_index(axis); if (UNLIKELY(index == -1)) { return; } GWL_Seat *seat = static_cast(data); seat->pointer_scroll.inverted_xy[index] = (direction == WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED); } #endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */ static const wl_pointer_listener pointer_listener = { /*enter*/ pointer_handle_enter, /*leave*/ pointer_handle_leave, /*motion*/ pointer_handle_motion, /*button*/ pointer_handle_button, /*axis*/ pointer_handle_axis, /*frame*/ pointer_handle_frame, /*axis_source*/ pointer_handle_axis_source, /*axis_stop*/ pointer_handle_axis_stop, /*axis_discrete*/ pointer_handle_axis_discrete, /*axis_value120*/ pointer_handle_axis_value120, #ifdef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM /*axis_relative_direction*/ pointer_handle_axis_relative_direction, #endif }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer Gesture: Hold), #zwp_pointer_gesture_hold_v1_listener * \{ */ #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE static CLG_LogRef LOG_WL_POINTER_GESTURE_HOLD = {"ghost.wl.handle.pointer_gesture.hold"}; # define LOG (&LOG_WL_POINTER_GESTURE_HOLD) static void gesture_hold_handle_begin( void * /*data*/, zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/, uint32_t /*serial*/, uint32_t /*time*/, wl_surface * /*surface*/, uint32_t fingers) { CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers); } static void gesture_hold_handle_end(void * /*data*/, zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t cancelled) { CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled); } static const zwp_pointer_gesture_hold_v1_listener gesture_hold_listener = { /*begin*/ gesture_hold_handle_begin, /*end*/ gesture_hold_handle_end, }; # undef LOG #endif /* ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer Gesture: Pinch), #zwp_pointer_gesture_pinch_v1_listener * \{ */ #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE static CLG_LogRef LOG_WL_POINTER_GESTURE_PINCH = {"ghost.wl.handle.pointer_gesture.pinch"}; # define LOG (&LOG_WL_POINTER_GESTURE_PINCH) static void gesture_pinch_handle_begin(void *data, zwp_pointer_gesture_pinch_v1 * /*pinch*/, uint32_t /*serial*/, uint32_t time, wl_surface * /*surface*/, uint32_t fingers) { GWL_Seat *seat = static_cast(data); (void)seat->system->ms_from_input_time(time); /* Only update internal time. */ CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers); /* Reset defaults. */ seat->pointer_gesture_pinch = GWL_SeatStatePointerGesture_Pinch{}; GHOST_WindowWayland *win = nullptr; if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { win = ghost_wl_surface_user_data(wl_surface_focus); } /* NOTE(@ideasman42): Scale factors match Blender's operators & default preferences. * For these values to work correctly, operator logic will need to be changed not to scale input * by the region size (as with 3D view zoom) or preference for 3D view orbit sensitivity. * * By working "correctly" I mean that a rotation action where the users fingers rotate to * opposite locations should always rotate the viewport 180d, since users will expect the * physical location of their fingers to match the viewport. * Similarly with zoom, the scale value from the pinch action can be mapped to a zoom level * although unlike rotation, an inexact mapping is less noticeable. * Users may even prefer the zoom level to be scaled - which could be a preference. */ seat->pointer_gesture_pinch.scale.value = wl_fixed_from_int(1); /* The value 300 matches a value used in clip & image zoom operators. * It seems OK for the 3D view too. */ seat->pointer_gesture_pinch.scale.factor = 300; /* The value 5 is used on macOS and roughly maps 1:1 with turntable rotation, * although preferences can scale the sensitivity (which would be skipped ideally). */ seat->pointer_gesture_pinch.rotation.factor = 5; if (win) { /* NOTE(@ideasman42): Blender's use of trackpad coordinates is inconsistent and needs work. * This isn't specific to WAYLAND, in practice they tend to work well enough in most cases. * Some operators scale by the UI scale, some don't. * Even though the window scale is correct, it doesn't account for the UI scale preference * (which GHOST doesn't know about). * * If support for this were all that was needed it could be handled in GHOST, * however as the operators are not even using coordinates compatible with each other, * it would be better to resolve this by passing rotation & zoom levels directly, * instead of attempting to handle them as cursor coordinates. */ const GWL_WindowScaleParams &scale_params = win->scale_params_get(); seat->pointer_gesture_pinch.scale.factor = gwl_window_scale_int_to( scale_params, seat->pointer_gesture_pinch.scale.factor); seat->pointer_gesture_pinch.rotation.factor = gwl_window_scale_int_to( scale_params, seat->pointer_gesture_pinch.rotation.factor); } } static void gesture_pinch_handle_update(void *data, zwp_pointer_gesture_pinch_v1 * /*pinch*/, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->ms_from_input_time(time); CLOG_INFO(LOG, 2, "update (dx=%.3f, dy=%.3f, scale=%.3f, rotation=%.3f)", wl_fixed_to_double(dx), wl_fixed_to_double(dy), wl_fixed_to_double(scale), wl_fixed_to_double(rotation)); GHOST_WindowWayland *win = nullptr; if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { win = ghost_wl_surface_user_data(wl_surface_focus); } /* Scale defaults to `wl_fixed_from_int(1)` which may change while pinching. * This needs to be converted to a delta. */ const wl_fixed_t scale_delta = scale - seat->pointer_gesture_pinch.scale.value; const int scale_as_delta_px = gwl_scaled_fixed_t_add_and_calc_rounded_delta( &seat->pointer_gesture_pinch.scale, scale_delta); /* Rotation in degrees, unlike scale this is a delta. */ const int rotation_as_delta_px = gwl_scaled_fixed_t_add_and_calc_rounded_delta( &seat->pointer_gesture_pinch.rotation, rotation); if (win) { const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, seat->pointer.xy)}; if (scale_as_delta_px) { seat->system->pushEvent_maybe_pending(new GHOST_EventTrackpad(event_ms, win, GHOST_kTrackpadEventMagnify, event_xy[0], event_xy[1], scale_as_delta_px, 0, false)); } if (rotation_as_delta_px) { seat->system->pushEvent_maybe_pending(new GHOST_EventTrackpad(event_ms, win, GHOST_kTrackpadEventRotate, event_xy[0], event_xy[1], rotation_as_delta_px, 0, false)); } } } static void gesture_pinch_handle_end(void *data, zwp_pointer_gesture_pinch_v1 * /*pinch*/, uint32_t /*serial*/, uint32_t time, int32_t cancelled) { GWL_Seat *seat = static_cast(data); (void)seat->system->ms_from_input_time(time); /* Only update internal time. */ CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled); } static const zwp_pointer_gesture_pinch_v1_listener gesture_pinch_listener = { /*begin*/ gesture_pinch_handle_begin, /*update*/ gesture_pinch_handle_update, /*end*/ gesture_pinch_handle_end, }; # undef LOG #endif /* ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Pointer Gesture: Swipe), #zwp_pointer_gesture_swipe_v1 * * \note In both Gnome-Shell & KDE this gesture isn't emitted at time of writing, * instead, high resolution 2D #wl_pointer_listener.axis data is generated which works well. * There may be some situations where WAYLAND compositors generate this gesture * (swiping with 3+ fingers, for e.g.). So keep this to allow logging & testing gestures. * \{ */ #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE static CLG_LogRef LOG_WL_POINTER_GESTURE_SWIPE = {"ghost.wl.handle.pointer_gesture.swipe"}; # define LOG (&LOG_WL_POINTER_GESTURE_SWIPE) static void gesture_swipe_handle_begin( void * /*data*/, zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/, uint32_t /*serial*/, uint32_t /*time*/, wl_surface * /*surface*/, uint32_t fingers) { CLOG_INFO(LOG, 2, "begin (fingers=%u)", fingers); } static void gesture_swipe_handle_update( void * /*data*/, zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/, uint32_t /*time*/, wl_fixed_t dx, wl_fixed_t dy) { CLOG_INFO(LOG, 2, "update (dx=%.3f, dy=%.3f)", wl_fixed_to_double(dx), wl_fixed_to_double(dy)); } static void gesture_swipe_handle_end( void * /*data*/, zwp_pointer_gesture_swipe_v1 * /*zwp_pointer_gesture_swipe_v1*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t cancelled) { CLOG_INFO(LOG, 2, "end (cancelled=%i)", cancelled); } static const zwp_pointer_gesture_swipe_v1_listener gesture_swipe_listener = { /*begin*/ gesture_swipe_handle_begin, /*update*/ gesture_swipe_handle_update, /*end*/ gesture_swipe_handle_end, }; # undef LOG #endif /* ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Touch Seat), #wl_touch_listener * * NOTE(@ideasman42): It's not clear if this interface is used by popular compositors. * It looks like GNOME/KDE only support `zwp_pointer_gestures_v1_interface`. * If this isn't used anywhere, it could be removed. * \{ */ static CLG_LogRef LOG_WL_TOUCH = {"ghost.wl.handle.touch"}; #define LOG (&LOG_WL_TOUCH) static void touch_seat_handle_down(void * /*data*/, wl_touch * /*wl_touch*/, uint32_t /*serial*/, uint32_t /*time*/, wl_surface * /*wl_surface*/, int32_t /*id*/, wl_fixed_t /*x*/, wl_fixed_t /*y*/) { CLOG_INFO(LOG, 2, "down"); } static void touch_seat_handle_up(void * /*data*/, wl_touch * /*wl_touch*/, uint32_t /*serial*/, uint32_t /*time*/, int32_t /*id*/) { CLOG_INFO(LOG, 2, "up"); } static void touch_seat_handle_motion(void * /*data*/, wl_touch * /*wl_touch*/, uint32_t /*time*/, int32_t /*id*/, wl_fixed_t /*x*/, wl_fixed_t /*y*/) { CLOG_INFO(LOG, 2, "motion"); } static void touch_seat_handle_frame(void * /*data*/, wl_touch * /*wl_touch*/) { CLOG_INFO(LOG, 2, "frame"); } static void touch_seat_handle_cancel(void * /*data*/, wl_touch * /*wl_touch*/) { CLOG_INFO(LOG, 2, "cancel"); } static void touch_seat_handle_shape(void * /*data*/, wl_touch * /*touch*/, int32_t /*id*/, wl_fixed_t /*major*/, wl_fixed_t /*minor*/) { CLOG_INFO(LOG, 2, "shape"); } static void touch_seat_handle_orientation(void * /*data*/, wl_touch * /*touch*/, int32_t /*id*/, wl_fixed_t /*orientation*/) { CLOG_INFO(LOG, 2, "orientation"); } static const wl_touch_listener touch_seat_listener = { /*down*/ touch_seat_handle_down, /*up*/ touch_seat_handle_up, /*motion*/ touch_seat_handle_motion, /*frame*/ touch_seat_handle_frame, /*cancel*/ touch_seat_handle_cancel, /*shape*/ touch_seat_handle_shape, /*orientation*/ touch_seat_handle_orientation, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Tablet Tool), #zwp_tablet_tool_v2_listener * \{ */ static CLG_LogRef LOG_WL_TABLET_TOOL = {"ghost.wl.handle.tablet_tool"}; #define LOG (&LOG_WL_TABLET_TOOL) static void tablet_tool_handle_type(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t tool_type) { CLOG_INFO(LOG, 2, "type (type=%u)", tool_type); GWL_TabletTool *tablet_tool = static_cast(data); tablet_tool->data.Active = tablet_tool_map_type((enum zwp_tablet_tool_v2_type)tool_type); } static void tablet_tool_handle_hardware_serial(void * /*data*/, 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*/, 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*/, 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*/, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) { CLOG_INFO(LOG, 2, "done"); } static void tablet_tool_handle_removed(void *data, zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { CLOG_INFO(LOG, 2, "removed"); GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; if (tablet_tool->wl.surface_cursor) { wl_surface_destroy(tablet_tool->wl.surface_cursor); } seat->wp.tablet_tools.erase(zwp_tablet_tool_v2); delete tablet_tool; } static void tablet_tool_handle_proximity_in(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t serial, zwp_tablet_v2 * /*tablet*/, wl_surface *wl_surface) { if (!ghost_wl_surface_own_with_null_check(wl_surface)) { CLOG_INFO(LOG, 2, "proximity_in (skipped)"); return; } CLOG_INFO(LOG, 2, "proximity_in"); GWL_TabletTool *tablet_tool = static_cast(data); tablet_tool->proximity = true; GWL_Seat *seat = tablet_tool->seat; seat->cursor_source_serial = serial; seat->tablet.wl.surface_window = wl_surface; seat->tablet.serial = serial; seat->data_source_serial = serial; seat->system->seat_active_set(seat); /* Update #GHOST_TabletData. */ GHOST_TabletData &td = tablet_tool->data; /* Reset, to avoid using stale tilt/pressure. */ td.Xtilt = 0.0f; td.Ytilt = 0.0f; /* In case pressure isn't supported. */ td.Pressure = 1.0f; const GHOST_WindowWayland *win = ghost_wl_surface_user_data(seat->tablet.wl.surface_window); seat->system->cursor_shape_set(win->getCursorShape()); } static void tablet_tool_handle_proximity_out(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) { CLOG_INFO(LOG, 2, "proximity_out"); GWL_TabletTool *tablet_tool = static_cast(data); /* Defer clearing the wl_surface until the frame is handled. * Without this, the frame can not access the wl_surface. */ tablet_tool->proximity = false; } static void tablet_tool_handle_down(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t serial) { GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; CLOG_INFO(LOG, 2, "down"); seat->data_source_serial = serial; gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Stylus0_Down); } static void tablet_tool_handle_up(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/) { GWL_TabletTool *tablet_tool = static_cast(data); CLOG_INFO(LOG, 2, "up"); gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Stylus0_Up); } static void tablet_tool_handle_motion(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const wl_fixed_t x, const wl_fixed_t y) { GWL_TabletTool *tablet_tool = static_cast(data); CLOG_INFO(LOG, 2, "motion"); tablet_tool->xy[0] = x; tablet_tool->xy[1] = y; tablet_tool->has_xy = true; gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Motion); } static void tablet_tool_handle_pressure(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t pressure) { const float pressure_unit = float(pressure) / 65535; CLOG_INFO(LOG, 2, "pressure (%.4f)", pressure_unit); GWL_TabletTool *tablet_tool = static_cast(data); GHOST_TabletData &td = tablet_tool->data; td.Pressure = pressure_unit; gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Pressure); } static void tablet_tool_handle_distance(void * /*data*/, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t distance) { CLOG_INFO(LOG, 2, "distance (distance=%u)", distance); } static void tablet_tool_handle_tilt(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const wl_fixed_t tilt_x, const wl_fixed_t tilt_y) { /* Map degrees to `-1.0..1.0`. */ const float tilt_unit[2] = { float(wl_fixed_to_double(tilt_x) / 90.0), float(wl_fixed_to_double(tilt_y) / 90.0), }; CLOG_INFO(LOG, 2, "tilt (x=%.4f, y=%.4f)", UNPACK2(tilt_unit)); GWL_TabletTool *tablet_tool = static_cast(data); GHOST_TabletData &td = tablet_tool->data; td.Xtilt = tilt_unit[0]; td.Ytilt = tilt_unit[1]; CLAMP(td.Xtilt, -1.0f, 1.0f); CLAMP(td.Ytilt, -1.0f, 1.0f); gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Tilt); } static void tablet_tool_handle_rotation(void * /*data*/, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const wl_fixed_t degrees) { CLOG_INFO(LOG, 2, "rotation (degrees=%.4f)", wl_fixed_to_double(degrees)); } static void tablet_tool_handle_slider(void * /*data*/, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const int32_t position) { CLOG_INFO(LOG, 2, "slider (position=%d)", position); } static void tablet_tool_handle_wheel(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const wl_fixed_t /*degrees*/, const int32_t clicks) { if (clicks == 0) { return; } GWL_TabletTool *tablet_tool = static_cast(data); CLOG_INFO(LOG, 2, "wheel (clicks=%d)", clicks); tablet_tool->frame_pending.wheel.clicks = clicks; gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Wheel); } static void tablet_tool_handle_button(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t serial, const uint32_t button, const uint32_t state) { GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; CLOG_INFO(LOG, 2, "button (button=%u, state=%u)", button, state); bool is_press = false; switch (state) { case WL_POINTER_BUTTON_STATE_RELEASED: is_press = false; break; case WL_POINTER_BUTTON_STATE_PRESSED: is_press = true; break; } seat->data_source_serial = serial; GWL_TabletTool_EventTypes ty = GWL_TabletTool_EventTypes::Unset; switch (button) { case BTN_STYLUS: { ty = is_press ? GWL_TabletTool_EventTypes::Stylus1_Down : GWL_TabletTool_EventTypes::Stylus1_Up; break; } case BTN_STYLUS2: { ty = is_press ? GWL_TabletTool_EventTypes::Stylus2_Down : GWL_TabletTool_EventTypes::Stylus2_Up; break; } case BTN_STYLUS3: { ty = is_press ? GWL_TabletTool_EventTypes::Stylus3_Down : GWL_TabletTool_EventTypes::Stylus3_Up; break; } } if (ty != GWL_TabletTool_EventTypes::Unset) { gwl_tablet_tool_frame_event_add(tablet_tool, ty); } } static void tablet_tool_handle_frame(void *data, zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/, const uint32_t time) { GWL_TabletTool *tablet_tool = static_cast(data); GWL_Seat *seat = tablet_tool->seat; const uint64_t event_ms = seat->system->ms_from_input_time(time); CLOG_INFO(LOG, 2, "frame"); /* No need to check the surfaces origin, it's already known to be owned by GHOST. */ if (wl_surface *wl_surface_focus = seat->tablet.wl.surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); bool has_motion = false; for (int i = 0; i < tablet_tool->frame_pending.frame_types_num; i++) { const GWL_TabletTool_EventTypes ty = tablet_tool->frame_pending.frame_types[i]; switch (ty) { case GWL_TabletTool_EventTypes::Unset: { GHOST_ASSERT(0, "Event types should never be Unset"); break; } /* Use motion for pressure and tilt as there are no explicit event types for these. */ case GWL_TabletTool_EventTypes::Motion: case GWL_TabletTool_EventTypes::Pressure: case GWL_TabletTool_EventTypes::Tilt: { /* Only one motion event per frame. */ if (has_motion) { break; } /* Can happen when there is pressure/tilt without motion. */ if (tablet_tool->has_xy == false) { break; } seat->tablet.xy[0] = tablet_tool->xy[0]; seat->tablet.xy[1] = tablet_tool->xy[1]; const int event_xy[2] = {WL_FIXED_TO_INT_FOR_WINDOW_V2(win, tablet_tool->xy)}; seat->system->pushEvent_maybe_pending(new GHOST_EventCursor( event_ms, GHOST_kEventCursorMove, win, UNPACK2(event_xy), tablet_tool->data)); has_motion = true; break; } case GWL_TabletTool_EventTypes::Stylus0_Down: case GWL_TabletTool_EventTypes::Stylus0_Up: case GWL_TabletTool_EventTypes::Stylus1_Down: case GWL_TabletTool_EventTypes::Stylus1_Up: case GWL_TabletTool_EventTypes::Stylus2_Down: case GWL_TabletTool_EventTypes::Stylus2_Up: case GWL_TabletTool_EventTypes::Stylus3_Down: case GWL_TabletTool_EventTypes::Stylus3_Up: { const int button_enum_offset = int(ty) - int(GWL_TabletTool_EventTypes::Stylus0_Down); const int button_index = button_enum_offset / 2; const bool button_down = (button_index * 2) == button_enum_offset; const GHOST_TButton ebutton = gwl_tablet_tool_ebutton[button_index]; const GHOST_TEventType etype = button_down ? GHOST_kEventButtonDown : GHOST_kEventButtonUp; seat->tablet.buttons.set(ebutton, button_down); seat->system->pushEvent_maybe_pending( new GHOST_EventButton(event_ms, etype, win, ebutton, tablet_tool->data)); break; } case GWL_TabletTool_EventTypes::Wheel: { seat->system->pushEvent_maybe_pending( new GHOST_EventWheel(event_ms, win, tablet_tool->frame_pending.wheel.clicks)); break; } } } if (tablet_tool->proximity == false) { seat->system->cursor_shape_set(win->getCursorShape()); } } if (tablet_tool->proximity == false) { seat->tablet.wl.surface_window = nullptr; } gwl_tablet_tool_frame_event_reset(tablet_tool); } static const zwp_tablet_tool_v2_listener tablet_tool_listner = { /*type*/ tablet_tool_handle_type, /*hardware_serial*/ tablet_tool_handle_hardware_serial, /*hardware_id_wacom*/ tablet_tool_handle_hardware_id_wacom, /*capability*/ tablet_tool_handle_capability, /*done*/ tablet_tool_handle_done, /*removed*/ tablet_tool_handle_removed, /*proximity_in*/ tablet_tool_handle_proximity_in, /*proximity_out*/ tablet_tool_handle_proximity_out, /*down*/ tablet_tool_handle_down, /*up*/ tablet_tool_handle_up, /*motion*/ tablet_tool_handle_motion, /*pressure*/ tablet_tool_handle_pressure, /*distance*/ tablet_tool_handle_distance, /*tilt*/ tablet_tool_handle_tilt, /*rotation*/ tablet_tool_handle_rotation, /*slider*/ tablet_tool_handle_slider, /*wheel*/ tablet_tool_handle_wheel, /*button*/ tablet_tool_handle_button, /*frame*/ tablet_tool_handle_frame, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Table Seat), #zwp_tablet_seat_v2_listener * \{ */ static CLG_LogRef LOG_WL_TABLET_SEAT = {"ghost.wl.handle.tablet_seat"}; #define LOG (&LOG_WL_TABLET_SEAT) static void tablet_seat_handle_tablet_added(void * /*data*/, zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, zwp_tablet_v2 *id) { CLOG_INFO(LOG, 2, "tablet_added (id=%p)", id); } static void tablet_seat_handle_tool_added(void *data, zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, zwp_tablet_tool_v2 *id) { CLOG_INFO(LOG, 2, "tool_added (id=%p)", id); GWL_Seat *seat = static_cast(data); GWL_TabletTool *tablet_tool = new GWL_TabletTool(); tablet_tool->seat = seat; /* Every tool has its own cursor wl_surface. */ tablet_tool->wl.surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor_get()); ghost_wl_surface_tag_cursor_tablet(tablet_tool->wl.surface_cursor); wl_surface_add_listener( tablet_tool->wl.surface_cursor, &cursor_surface_listener, static_cast(seat)); zwp_tablet_tool_v2_add_listener(id, &tablet_tool_listner, tablet_tool); seat->wp.tablet_tools.insert(id); } static void tablet_seat_handle_pad_added(void * /*data*/, zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/, zwp_tablet_pad_v2 *id) { CLOG_INFO(LOG, 2, "pad_added (id=%p)", id); } static const zwp_tablet_seat_v2_listener tablet_seat_listener = { /*tablet_added*/ tablet_seat_handle_tablet_added, /*tool_added*/ tablet_seat_handle_tool_added, /*pad_added*/ tablet_seat_handle_pad_added, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Keyboard), #wl_keyboard_listener * \{ */ static CLG_LogRef LOG_WL_KEYBOARD = {"ghost.wl.handle.keyboard"}; #define LOG (&LOG_WL_KEYBOARD) static void keyboard_handle_keymap(void *data, wl_keyboard * /*wl_keyboard*/, const uint32_t format, const int32_t fd, const uint32_t size) { GWL_Seat *seat = static_cast(data); if ((!data) || (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)) { CLOG_INFO(LOG, 2, "keymap (no data or wrong version)"); close(fd); return; } char *map_str = static_cast(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0)); if (map_str == MAP_FAILED) { close(fd); CLOG_INFO(LOG, 2, "keymap mmap failed: %s", std::strerror(errno)); return; } xkb_keymap *keymap = xkb_keymap_new_from_string( seat->xkb.context, map_str, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); munmap(map_str, size); close(fd); if (!keymap) { CLOG_INFO(LOG, 2, "keymap (not found)"); return; } CLOG_INFO(LOG, 2, "keymap"); /* Reset in case there was a previous non-zero active layout for the the last key-map. * Note that this is set later by `wl_keyboard_listener::modifiers`, it's possible that handling * the first modifier will run #xkb_state_update_mask again (if the active layout is non-zero) * however as this is only done when the layout changed, it's harmless. * With a single layout - in practice the active layout will be zero. */ seat->xkb.layout_active = 0; if (seat->xkb.compose_state) { xkb_compose_state_reset(seat->xkb.compose_state); } else if (seat->xkb.compose_table) { seat->xkb.compose_state = xkb_compose_state_new(seat->xkb.compose_table, XKB_COMPOSE_STATE_NO_FLAGS); } /* In practice we can assume `xkb_state_new` always succeeds. */ xkb_state_unref(seat->xkb.state); seat->xkb.state = xkb_state_new(keymap); xkb_state_unref(seat->xkb.state_empty); seat->xkb.state_empty = xkb_state_new(keymap); for (int i = 0; i < MOD_INDEX_NUM; i++) { const GWL_ModifierInfo &mod_info = g_modifier_info_table[i]; seat->xkb_keymap_mod_index[i] = xkb_keymap_mod_get_index(keymap, mod_info.xkb_id); } seat->xkb_keymap_mod_index_mod2 = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_NUM); seat->xkb_keymap_mod_index_numlock = xkb_keymap_mod_get_index(keymap, "NumLock"); xkb_state_unref(seat->xkb.state_empty_with_shift); seat->xkb.state_empty_with_shift = nullptr; if (seat->xkb_keymap_mod_index[MOD_INDEX_SHIFT] != XKB_MOD_INVALID) { seat->xkb.state_empty_with_shift = xkb_state_new(keymap); } xkb_state_unref(seat->xkb.state_empty_with_numlock); seat->xkb.state_empty_with_numlock = nullptr; if ((seat->xkb_keymap_mod_index_mod2 != XKB_MOD_INVALID) && (seat->xkb_keymap_mod_index_numlock != XKB_MOD_INVALID)) { seat->xkb.state_empty_with_numlock = xkb_state_new(keymap); } gwl_seat_key_layout_active_state_update_mask(seat); #ifdef USE_NON_LATIN_KB_WORKAROUND seat->xkb_use_non_latin_workaround = false; if (seat->xkb.state_empty_with_shift) { seat->xkb_use_non_latin_workaround = true; for (xkb_keycode_t key_code = KEY_1 + EVDEV_OFFSET; key_code <= KEY_0 + EVDEV_OFFSET; key_code++) { const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(seat->xkb.state_empty_with_shift, key_code); if (!(sym_test >= XKB_KEY_0 && sym_test <= XKB_KEY_9)) { seat->xkb_use_non_latin_workaround = false; break; } } } #endif keyboard_depressed_state_reset(seat); xkb_keymap_unref(keymap); } /** * Enter event. * * Notification that this seat's keyboard focus is on a certain wl_surface. */ static void keyboard_handle_enter(void *data, wl_keyboard * /*wl_keyboard*/, const uint32_t serial, wl_surface *wl_surface, wl_array *keys) { /* Null when just destroyed. */ if (!ghost_wl_surface_own_with_null_check(wl_surface)) { CLOG_INFO(LOG, 2, "enter (skipped)"); return; } CLOG_INFO(LOG, 2, "enter"); GWL_Seat *seat = static_cast(data); seat->keyboard.serial = serial; seat->keyboard.wl.surface_window = wl_surface; seat->system->seat_active_set(seat); /* If there are any keys held when activating the window, * modifiers will be compared against the seat state, * only enabling modifiers that were previously disabled. */ GWL_KeyboardDepressedState key_depressed_prev = seat->key_depressed; keyboard_depressed_state_reset(seat); uint32_t *key; WL_ARRAY_FOR_EACH (key, keys) { const xkb_keycode_t key_code = *key + EVDEV_OFFSET; CLOG_INFO(LOG, 2, "enter (key_held=%d)", int(key_code)); const xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->xkb.state, key_code); const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, *key); if (gkey != GHOST_kKeyUnknown) { keyboard_depressed_state_key_event(seat, gkey, GHOST_kEventKeyDown); } } keyboard_depressed_state_push_events_from_change(seat, key_depressed_prev); } /** * Leave event. * * Notification that this seat's keyboard focus is no longer on a certain wl_surface. */ static void keyboard_handle_leave(void *data, wl_keyboard * /*wl_keyboard*/, const uint32_t /*serial*/, wl_surface *wl_surface) { if (!ghost_wl_surface_own_with_null_check(wl_surface)) { CLOG_INFO(LOG, 2, "leave (skipped)"); return; } CLOG_INFO(LOG, 2, "leave"); GWL_Seat *seat = static_cast(data); seat->keyboard.wl.surface_window = nullptr; { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_timer_guard{*seat->system->timer_mutex}; #endif /* Losing focus must stop repeating text. */ if (seat->key_repeat.timer) { keyboard_handle_key_repeat_cancel(seat); } } } /** * A version of #xkb_state_key_get_one_sym which returns the key without any modifiers pressed. * Needed because #GHOST_TKey uses these values as key-codes. */ static xkb_keysym_t xkb_state_key_get_one_sym_without_modifiers( xkb_state *xkb_state_empty, xkb_state *xkb_state_empty_with_numlock, xkb_state *xkb_state_empty_with_shift, const bool xkb_use_non_latin_workaround, const xkb_keycode_t key) { /* Use an empty keyboard state to access key symbol without modifiers. */ xkb_keysym_t sym = xkb_state_key_get_one_sym(xkb_state_empty, key); /* NOTE(@ideasman42): Only perform the number-locked lookup as a fallback * when a number-pad key has been pressed. This is important as some key-maps use number lock * for switching other layers (in particular `de(neo_qwertz)` turns on layer-4), see: #96170. * Alternative solutions could be to inspect the layout however this could get involved * and turning on the number-lock is only needed for a limited set of keys. */ /* Accounts for key-pad keys typically swapped for numbers when number-lock is enabled: * `Home Left Up Right Down Prior Page_Up Next Page_Dow End Begin Insert Delete`. */ if (sym >= XKB_KEY_KP_Home && sym <= XKB_KEY_KP_Delete) { if (xkb_state_empty_with_numlock) { const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(xkb_state_empty_with_numlock, key); if (sym_test != XKB_KEY_NoSymbol) { sym = sym_test; } } } else { #ifdef USE_NON_LATIN_KB_WORKAROUND if (key >= (KEY_1 + EVDEV_OFFSET) && key <= (KEY_0 + EVDEV_OFFSET)) { if (xkb_state_empty_with_shift && xkb_use_non_latin_workaround) { const xkb_keysym_t sym_test = xkb_state_key_get_one_sym(xkb_state_empty_with_shift, key); if (sym_test != XKB_KEY_NoSymbol) { /* Should never happen as enabling `xkb_use_non_latin_workaround` checks this. */ GHOST_ASSERT(sym_test >= XKB_KEY_0 && sym_test <= XKB_KEY_9, "Unexpected key"); sym = sym_test; } } } #else (void)xkb_state_empty_with_shift; (void)xkb_use_non_latin_workaround; #endif } return sym; } static bool xkb_compose_state_feed_and_get_utf8( xkb_compose_state *compose_state, xkb_state *state, const xkb_keycode_t key, char r_utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)]) { const xkb_keysym_t sym = xkb_state_key_get_one_sym(state, key); const xkb_compose_feed_result result = xkb_compose_state_feed(compose_state, sym); bool handled = false; if (result == XKB_COMPOSE_FEED_ACCEPTED) { switch (xkb_compose_state_get_status(compose_state)) { case XKB_COMPOSE_NOTHING: { break; } case XKB_COMPOSE_COMPOSING: { r_utf8_buf[0] = '\0'; handled = true; break; } case XKB_COMPOSE_COMPOSED: { char utf8_buf_compose[sizeof(GHOST_TEventKeyData::utf8_buf) + 1] = {'\0'}; const int utf8_buf_compose_len = xkb_compose_state_get_utf8( compose_state, utf8_buf_compose, sizeof(utf8_buf_compose)); if (utf8_buf_compose_len > 0) { memcpy(r_utf8_buf, utf8_buf_compose, utf8_buf_compose_len); handled = true; } break; } case XKB_COMPOSE_CANCELLED: { /* NOTE(@ideasman42): QT & GTK ignore these events as well as not inputting any text * so `` for e.g. causes a cancel and *not* back-space. * This isn't supported under GHOST at the moment. * The key-event could also be ignored but this means tracking held state of * keys wont work properly, so don't do any input and pass in the key-symbol. */ r_utf8_buf[0] = '\0'; handled = true; break; } } } return handled; } /** * \note Caller must lock `timer_mutex`. */ static void keyboard_handle_key_repeat_cancel(GWL_Seat *seat) { GHOST_ASSERT(seat->key_repeat.timer != nullptr, "Caller much check for timer"); delete static_cast(seat->key_repeat.timer->getUserData()); gwl_seat_key_repeat_timer_remove(seat); } /** * Restart the key-repeat timer. * \param use_delay: When false, use the interval * (prevents pause when the setting changes while the key is held). * * \note Caller must lock `timer_mutex`. */ static void keyboard_handle_key_repeat_reset(GWL_Seat *seat, const bool use_delay) { GHOST_ASSERT(seat->key_repeat.timer != nullptr, "Caller much check for timer"); GHOST_TimerProcPtr key_repeat_fn = seat->key_repeat.timer->getTimerProc(); GHOST_TUserDataPtr payload = seat->key_repeat.timer->getUserData(); gwl_seat_key_repeat_timer_remove(seat); gwl_seat_key_repeat_timer_add(seat, key_repeat_fn, payload, use_delay); } static void keyboard_handle_key(void *data, wl_keyboard * /*wl_keyboard*/, const uint32_t serial, const uint32_t time, const uint32_t key, const uint32_t state) { GWL_Seat *seat = static_cast(data); const uint64_t event_ms = seat->system->ms_from_input_time(time); const xkb_keycode_t key_code = key + EVDEV_OFFSET; const xkb_keysym_t sym = xkb_state_key_get_one_sym_without_modifiers( seat->xkb.state_empty, seat->xkb.state_empty_with_numlock, seat->xkb.state_empty_with_shift, #ifdef USE_NON_LATIN_KB_WORKAROUND seat->xkb_use_non_latin_workaround, #else false, #endif key_code); if (sym == XKB_KEY_NoSymbol) { CLOG_INFO(LOG, 2, "key (code=%d, state=%u, no symbol, skipped)", int(key_code), state); return; } CLOG_INFO(LOG, 2, "key (code=%d, state=%u)", int(key_code), state); GHOST_TEventType etype = GHOST_kEventUnknown; switch (state) { case WL_KEYBOARD_KEY_STATE_RELEASED: etype = GHOST_kEventKeyUp; break; case WL_KEYBOARD_KEY_STATE_PRESSED: etype = GHOST_kEventKeyDown; break; } #ifdef USE_EVENT_BACKGROUND_THREAD /* Any access to `seat->key_repeat.timer` must lock. */ std::lock_guard lock_timer_guard{*seat->system->timer_mutex}; #endif GWL_KeyRepeatPlayload *key_repeat_payload = nullptr; /* Delete previous timer. */ if (seat->key_repeat.timer) { enum { NOP = 1, RESET, CANCEL } timer_action = NOP; key_repeat_payload = static_cast( seat->key_repeat.timer->getUserData()); if (seat->key_repeat.rate == 0) { /* Repeat was disabled (unlikely but possible). */ timer_action = CANCEL; } else if (key_code == key_repeat_payload->key_code) { /* Releasing the key that was held always cancels. */ timer_action = CANCEL; } else if (xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb.state), key_code)) { if (etype == GHOST_kEventKeyDown) { /* Any other key-down always cancels (and may start its own repeat timer). */ timer_action = CANCEL; } else { /* Key-up from keys that were not repeating cause the repeat timer to pause. * * NOTE(@ideasman42): This behavior isn't universal, some text input systems will * stop the repeat entirely. Choose to pause repeat instead as this is what GTK/WIN32 do, * and it fits better for keyboard input that isn't related to text entry. */ timer_action = RESET; } } switch (timer_action) { case NOP: { /* Don't add a new timer, leave the existing timer owning this `key_repeat_payload`. */ key_repeat_payload = nullptr; break; } case RESET: { /* The payload will be added again. */ gwl_seat_key_repeat_timer_remove(seat); break; } case CANCEL: { delete key_repeat_payload; key_repeat_payload = nullptr; gwl_seat_key_repeat_timer_remove(seat); break; } } } const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, key); char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'}; if (etype == GHOST_kEventKeyDown) { /* Handle key-compose (dead-keys). */ if (seat->xkb.compose_state && xkb_compose_state_feed_and_get_utf8( seat->xkb.compose_state, seat->xkb.state, key_code, utf8_buf)) { /* `utf8_buf` has been filled by a compose action. */ } else { xkb_state_key_get_utf8(seat->xkb.state, key_code, utf8_buf, sizeof(utf8_buf)); } } seat->data_source_serial = serial; keyboard_depressed_state_key_event(seat, gkey, etype); if (wl_surface *wl_surface_focus = seat->keyboard.wl.surface_window) { GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus); seat->system->pushEvent_maybe_pending( new GHOST_EventKey(event_ms, etype, win, gkey, false, utf8_buf)); } /* An existing payload means the key repeat timer is reset and will be added again. */ if (key_repeat_payload == nullptr) { /* Start timer for repeating key, if applicable. */ if ((seat->key_repeat.rate > 0) && (etype == GHOST_kEventKeyDown) && xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb.state), key_code)) { key_repeat_payload = new GWL_KeyRepeatPlayload(); key_repeat_payload->seat = seat; key_repeat_payload->key_code = key_code; key_repeat_payload->key_data.gkey = gkey; } } if (key_repeat_payload) { auto key_repeat_fn = [](GHOST_ITimerTask *task, uint64_t time_ms) { GWL_KeyRepeatPlayload *payload = static_cast(task->getUserData()); GWL_Seat *seat = payload->seat; if (wl_surface *wl_surface_focus = seat->keyboard.wl.surface_window) { GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus); GHOST_SystemWayland *system = seat->system; const uint64_t event_ms = payload->time_ms_init + time_ms; /* Calculate this value every time in case modifier keys are pressed. */ char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'}; if (seat->xkb.compose_state && xkb_compose_state_feed_and_get_utf8( seat->xkb.compose_state, seat->xkb.state, payload->key_code, utf8_buf)) { /* `utf8_buf` has been filled by a compose action. */ } else { xkb_state_key_get_utf8(seat->xkb.state, payload->key_code, utf8_buf, sizeof(utf8_buf)); } system->pushEvent_maybe_pending(new GHOST_EventKey( event_ms, GHOST_kEventKeyDown, win, payload->key_data.gkey, true, utf8_buf)); } }; gwl_seat_key_repeat_timer_add(seat, key_repeat_fn, key_repeat_payload, true); } } static void keyboard_handle_modifiers(void *data, wl_keyboard * /*wl_keyboard*/, const uint32_t serial, const uint32_t mods_depressed, const uint32_t mods_latched, const uint32_t mods_locked, const uint32_t group) { CLOG_INFO(LOG, 2, "modifiers (depressed=%u, latched=%u, locked=%u, group=%u)", mods_depressed, mods_latched, mods_locked, group); GWL_Seat *seat = static_cast(data); xkb_state_update_mask(seat->xkb.state, mods_depressed, mods_latched, mods_locked, 0, 0, group); /* Account for the active layout changing within the same key-map, * needed so modifiers are detected from the expected layout, see: #115160. */ if (group != seat->xkb.layout_active) { seat->xkb.layout_active = group; gwl_seat_key_layout_active_state_update_mask(seat); } /* A modifier changed so reset the timer, * see comment in #keyboard_handle_key regarding this behavior. */ { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_timer_guard{*seat->system->timer_mutex}; #endif if (seat->key_repeat.timer) { keyboard_handle_key_repeat_reset(seat, true); } } seat->data_source_serial = serial; } static void keyboard_handle_repeat_info(void *data, wl_keyboard * /*wl_keyboard*/, const int32_t rate, const int32_t delay) { CLOG_INFO(LOG, 2, "info (rate=%d, delay=%d)", rate, delay); GWL_Seat *seat = static_cast(data); seat->key_repeat.rate = rate; seat->key_repeat.delay = delay; { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_timer_guard{*seat->system->timer_mutex}; #endif /* Unlikely possible this setting changes while repeating. */ if (seat->key_repeat.timer) { keyboard_handle_key_repeat_reset(seat, false); } } } static const wl_keyboard_listener keyboard_listener = { /*keymap*/ keyboard_handle_keymap, /*enter*/ keyboard_handle_enter, /*leave*/ keyboard_handle_leave, /*key*/ keyboard_handle_key, /*modifiers*/ keyboard_handle_modifiers, /*repeat_info*/ keyboard_handle_repeat_info, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Primary Selection Offer), #zwp_primary_selection_offer_v1_listener * \{ */ static CLG_LogRef LOG_WL_PRIMARY_SELECTION_OFFER = {"ghost.wl.handle.primary_selection_offer"}; #define LOG (&LOG_WL_PRIMARY_SELECTION_OFFER) static void primary_selection_offer_offer(void *data, zwp_primary_selection_offer_v1 *id, const char *type) { GWL_PrimarySelection_DataOffer *data_offer = static_cast(data); if (data_offer->wp.id != id) { CLOG_INFO(LOG, 2, "offer: %p: offer for unknown selection %p of %s (skipped)", data, id, type); return; } data_offer->types.insert(std::string(type)); } static const zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { /*offer*/ primary_selection_offer_offer, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Primary Selection Device), #zwp_primary_selection_device_v1_listener * \{ */ static CLG_LogRef LOG_WL_PRIMARY_SELECTION_DEVICE = {"ghost.wl.handle.primary_selection_device"}; #define LOG (&LOG_WL_PRIMARY_SELECTION_DEVICE) static void primary_selection_device_handle_data_offer( void * /*data*/, zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/, zwp_primary_selection_offer_v1 *id) { CLOG_INFO(LOG, 2, "data_offer"); GWL_PrimarySelection_DataOffer *data_offer = new GWL_PrimarySelection_DataOffer; data_offer->wp.id = id; zwp_primary_selection_offer_v1_add_listener(id, &primary_selection_offer_listener, data_offer); } static void primary_selection_device_handle_selection( void *data, zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/, zwp_primary_selection_offer_v1 *id) { GWL_PrimarySelection *primary = static_cast(data); std::lock_guard lock{primary->data_offer_mutex}; /* Delete old data offer. */ if (primary->data_offer != nullptr) { gwl_primary_selection_discard_offer(primary); } if (id == nullptr) { CLOG_INFO(LOG, 2, "selection: (skipped)"); return; } CLOG_INFO(LOG, 2, "selection"); /* Get new data offer. */ GWL_PrimarySelection_DataOffer *data_offer = static_cast( zwp_primary_selection_offer_v1_get_user_data(id)); primary->data_offer = data_offer; } static const zwp_primary_selection_device_v1_listener primary_selection_device_listener = { /*data_offer*/ primary_selection_device_handle_data_offer, /*selection*/ primary_selection_device_handle_selection, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Primary Selection Source), #zwp_primary_selection_source_v1_listener * \{ */ static CLG_LogRef LOG_WL_PRIMARY_SELECTION_SOURCE = {"ghost.wl.handle.primary_selection_source"}; #define LOG (&LOG_WL_PRIMARY_SELECTION_SOURCE) static void primary_selection_source_send(void *data, zwp_primary_selection_source_v1 * /*source*/, const char * /*mime_type*/, int32_t fd) { CLOG_INFO(LOG, 2, "send"); GWL_PrimarySelection *primary = static_cast(data); auto write_file_fn = [](GWL_PrimarySelection *primary, const int fd) { if (UNLIKELY(write(fd, primary->data_source->buffer_out.data, primary->data_source->buffer_out.data_size) < 0)) { CLOG_WARN(LOG, "error writing to primary clipboard: %s", std::strerror(errno)); } close(fd); primary->data_source_mutex.unlock(); }; primary->data_source_mutex.lock(); std::thread write_thread(write_file_fn, primary, fd); write_thread.detach(); } static void primary_selection_source_cancelled(void *data, zwp_primary_selection_source_v1 *source) { CLOG_INFO(LOG, 2, "cancelled"); GWL_PrimarySelection *primary = static_cast(data); if (source == primary->data_source->wp.source) { gwl_primary_selection_discard_source(primary); } } static const zwp_primary_selection_source_v1_listener primary_selection_source_listener = { /*send*/ primary_selection_source_send, /*cancelled*/ primary_selection_source_cancelled, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Text Input), #zwp_text_input_manager_v3 * \{ */ #ifdef WITH_INPUT_IME class GHOST_EventIME : public GHOST_Event { public: /** * Constructor. * \param msec: The time this event was generated. * \param type: The type of key event. * \param key: The key code of the key. */ GHOST_EventIME(uint64_t msec, GHOST_TEventType type, GHOST_IWindow *window, void *customdata) : GHOST_Event(msec, type, window) { this->m_data = customdata; } }; static CLG_LogRef LOG_WL_TEXT_INPUT = {"ghost.wl.handle.text_input"}; # define LOG (&LOG_WL_TEXT_INPUT) static void text_input_handle_enter(void *data, zwp_text_input_v3 * /*zwp_text_input_v3*/, wl_surface *surface) { if (!ghost_wl_surface_own(surface)) { return; } CLOG_INFO(LOG, 2, "enter"); GWL_Seat *seat = static_cast(data); seat->ime.surface_window = surface; } static void text_input_handle_leave(void *data, zwp_text_input_v3 * /*zwp_text_input_v3*/, wl_surface *surface) { /* Can be null when closing a window. */ if (!ghost_wl_surface_own_with_null_check(surface)) { return; } CLOG_INFO(LOG, 2, "leave"); GWL_Seat *seat = static_cast(data); if (seat->ime.surface_window == surface) { seat->ime.surface_window = nullptr; } } static void text_input_handle_preedit_string(void *data, zwp_text_input_v3 * /*zwp_text_input_v3*/, const char *text, int32_t cursor_begin, int32_t cursor_end) { CLOG_INFO(LOG, 2, "preedit_string (text=\"%s\", cursor_begin=%d, cursor_end=%d)", text ? text : "", cursor_begin, cursor_end); GWL_Seat *seat = static_cast(data); if (UNLIKELY(seat->ime.surface_window == nullptr)) { return; } if (seat->ime.has_preedit == false) { /* Starting IME input. */ gwl_seat_ime_full_reset(seat); } seat->ime.composite_is_null = (text == nullptr); if (!seat->ime.composite_is_null) { seat->ime.composite = text; seat->ime.event_ime_data.composite = (void *)seat->ime.composite.c_str(); seat->ime.event_ime_data.composite_len = (void *)seat->ime.composite.size(); seat->ime.event_ime_data.cursor_position = cursor_begin; seat->ime.event_ime_data.target_start = cursor_begin; seat->ime.event_ime_data.target_end = cursor_end; } seat->ime.has_preedit_string_callback = true; } static void text_input_handle_commit_string(void *data, zwp_text_input_v3 * /*zwp_text_input_v3*/, const char *text) { CLOG_INFO(LOG, 2, "commit_string (text=\"%s\")", text ? text : ""); GWL_Seat *seat = static_cast(data); if (UNLIKELY(seat->ime.surface_window == nullptr)) { return; } seat->ime.result_is_null = (text == nullptr); if (seat->ime.result_is_null) { seat->ime.result = ""; } else { seat->ime.result = text; } seat->ime.result_is_null = (text == nullptr); seat->ime.event_ime_data.result = (void *)seat->ime.result.c_str(); seat->ime.event_ime_data.result_len = (void *)seat->ime.result.size(); seat->ime.event_ime_data.cursor_position = seat->ime.result.size(); seat->ime.has_commit_string_callback = true; } static void text_input_handle_delete_surrounding_text(void * /*data*/, zwp_text_input_v3 * /*zwp_text_input_v3*/, uint32_t before_length, uint32_t after_length) { CLOG_INFO(LOG, 2, "delete_surrounding_text (before_length=%u, after_length=%u)", before_length, after_length); /* NOTE: Currently unused, do we care about this event? * SDL ignores this event. */ } static void text_input_handle_done(void *data, zwp_text_input_v3 * /*zwp_text_input_v3*/, uint32_t /*serial*/) { GWL_Seat *seat = static_cast(data); GHOST_SystemWayland *system = seat->system; const uint64_t event_ms = seat->system->getMilliSeconds(); CLOG_INFO(LOG, 2, "done"); GHOST_WindowWayland *win = ghost_wl_surface_user_data(seat->ime.surface_window); if (seat->ime.has_commit_string_callback) { if (seat->ime.has_preedit) { const bool is_end = seat->ime.composite_is_null; if (is_end) { seat->ime.has_preedit = false; /* `commit_string` (end). */ system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data)); system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data)); } else { /* `commit_string` (continues). */ system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data)); } } else { /* `commit_string` ran with no active IME popup, start & end to insert text. */ system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeCompositionStart, win, &seat->ime.event_ime_data)); system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data)); system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data)); } if (seat->ime.has_preedit == false) { gwl_seat_ime_preedit_reset(seat); } } else if (seat->ime.has_preedit_string_callback) { const bool is_end = seat->ime.composite_is_null; if (is_end) { /* `preedit_string` (end). */ seat->ime.has_preedit = false; system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data)); } else { const bool is_start = seat->ime.has_preedit == false; /* `preedit_string` (start or continue). */ seat->ime.has_preedit = true; system->pushEvent_maybe_pending(new GHOST_EventIME( event_ms, is_start ? GHOST_kEventImeCompositionStart : GHOST_kEventImeComposition, win, &seat->ime.event_ime_data)); } } seat->ime.has_preedit_string_callback = false; seat->ime.has_commit_string_callback = false; } static zwp_text_input_v3_listener text_input_listener = { /*enter*/ text_input_handle_enter, /*leave*/ text_input_handle_leave, /*preedit_string*/ text_input_handle_preedit_string, /*commit_string*/ text_input_handle_commit_string, /*delete_surrounding_text*/ text_input_handle_delete_surrounding_text, /*done*/ text_input_handle_done, }; # undef LOG #endif /* WITH_INPUT_IME. */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Seat), #wl_seat_listener * \{ */ static CLG_LogRef LOG_WL_SEAT = {"ghost.wl.handle.seat"}; #define LOG (&LOG_WL_SEAT) static void gwl_seat_capability_pointer_enable(GWL_Seat *seat) { if (seat->wl.pointer) { return; } seat->wl.pointer = wl_seat_get_pointer(seat->wl.seat); seat->cursor.wl.surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor_get()); seat->cursor.visible = true; seat->cursor.wl.buffer = nullptr; { /* Use environment variables, falling back to defaults. * These environment variables are used by enough WAYLAND applications * that it makes sense to check them (see `Xcursor` man page). */ const char *env; env = getenv("XCURSOR_THEME"); seat->cursor.theme_name = std::string(env ? env : ""); env = getenv("XCURSOR_SIZE"); seat->cursor.theme_size = default_cursor_size; if (env && (*env != '\0')) { char *env_end = nullptr; /* While clamping is not needed on the WAYLAND side, * GHOST's internal logic may get confused by negative values, so ensure it's at least 1. */ const long value = strtol(env, &env_end, 10); if ((*env_end == '\0') && (value > 0)) { seat->cursor.theme_size = int(value); } } } wl_pointer_add_listener(seat->wl.pointer, &pointer_listener, seat); wl_surface_add_listener(seat->cursor.wl.surface_cursor, &cursor_surface_listener, seat); ghost_wl_surface_tag_cursor_pointer(seat->cursor.wl.surface_cursor); zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get(); if (pointer_gestures) { const uint pointer_gestures_version = zwp_pointer_gestures_v1_get_version(pointer_gestures); #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE if (pointer_gestures_version >= ZWP_POINTER_GESTURES_V1_GET_HOLD_GESTURE_SINCE_VERSION) { /* Hold gesture. */ zwp_pointer_gesture_hold_v1 *gesture = zwp_pointer_gestures_v1_get_hold_gesture( pointer_gestures, seat->wl.pointer); zwp_pointer_gesture_hold_v1_set_user_data(gesture, seat); zwp_pointer_gesture_hold_v1_add_listener(gesture, &gesture_hold_listener, seat); seat->wp.pointer_gesture_hold = gesture; } #endif #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE { /* Pinch gesture. */ zwp_pointer_gesture_pinch_v1 *gesture = zwp_pointer_gestures_v1_get_pinch_gesture( pointer_gestures, seat->wl.pointer); zwp_pointer_gesture_pinch_v1_set_user_data(gesture, seat); zwp_pointer_gesture_pinch_v1_add_listener(gesture, &gesture_pinch_listener, seat); seat->wp.pointer_gesture_pinch = gesture; } #endif #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE { /* Swipe gesture. */ zwp_pointer_gesture_swipe_v1 *gesture = zwp_pointer_gestures_v1_get_swipe_gesture( pointer_gestures, seat->wl.pointer); zwp_pointer_gesture_swipe_v1_set_user_data(gesture, seat); zwp_pointer_gesture_swipe_v1_add_listener(gesture, &gesture_swipe_listener, seat); seat->wp.pointer_gesture_swipe = gesture; } #endif } } static void gwl_seat_capability_pointer_disable(GWL_Seat *seat) { if (!seat->wl.pointer) { return; } const zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get(); if (pointer_gestures) { #ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE { /* Hold gesture. */ zwp_pointer_gesture_hold_v1 **gesture_p = &seat->wp.pointer_gesture_hold; if (*gesture_p) { zwp_pointer_gesture_hold_v1_destroy(*gesture_p); *gesture_p = nullptr; } } #endif #ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE { /* Pinch gesture. */ zwp_pointer_gesture_pinch_v1 **gesture_p = &seat->wp.pointer_gesture_pinch; if (*gesture_p) { zwp_pointer_gesture_pinch_v1_destroy(*gesture_p); *gesture_p = nullptr; } } #endif #ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE { /* Swipe gesture. */ zwp_pointer_gesture_swipe_v1 **gesture_p = &seat->wp.pointer_gesture_swipe; if (*gesture_p) { zwp_pointer_gesture_swipe_v1_destroy(*gesture_p); *gesture_p = nullptr; } } #endif } if (seat->cursor.wl.surface_cursor) { wl_surface_destroy(seat->cursor.wl.surface_cursor); seat->cursor.wl.surface_cursor = nullptr; } if (seat->cursor.wl.theme) { wl_cursor_theme_destroy(seat->cursor.wl.theme); seat->cursor.wl.theme = nullptr; } wl_pointer_destroy(seat->wl.pointer); seat->wl.pointer = nullptr; } static void gwl_seat_capability_keyboard_enable(GWL_Seat *seat) { if (seat->wl.keyboard) { return; } seat->wl.keyboard = wl_seat_get_keyboard(seat->wl.seat); wl_keyboard_add_listener(seat->wl.keyboard, &keyboard_listener, seat); } static void gwl_seat_capability_keyboard_disable(GWL_Seat *seat) { if (!seat->wl.keyboard) { return; } { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_timer_guard{*seat->system->timer_mutex}; #endif if (seat->key_repeat.timer) { keyboard_handle_key_repeat_cancel(seat); } } wl_keyboard_destroy(seat->wl.keyboard); seat->wl.keyboard = nullptr; } static void gwl_seat_capability_touch_enable(GWL_Seat *seat) { if (seat->wl.touch) { return; } seat->wl.touch = wl_seat_get_touch(seat->wl.seat); wl_touch_set_user_data(seat->wl.touch, seat); wl_touch_add_listener(seat->wl.touch, &touch_seat_listener, seat); } static void gwl_seat_capability_touch_disable(GWL_Seat *seat) { if (!seat->wl.touch) { return; } wl_touch_destroy(seat->wl.touch); seat->wl.touch = nullptr; } static void seat_handle_capabilities(void *data, /* Only used in an assert. */ [[maybe_unused]] wl_seat *wl_seat, const uint32_t capabilities) { CLOG_INFO(LOG, 2, "capabilities (pointer=%d, keyboard=%d, touch=%d)", (capabilities & WL_SEAT_CAPABILITY_POINTER) != 0, (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0, (capabilities & WL_SEAT_CAPABILITY_TOUCH) != 0); GWL_Seat *seat = static_cast(data); GHOST_ASSERT(seat->wl.seat == wl_seat, "Seat mismatch"); if (capabilities & WL_SEAT_CAPABILITY_POINTER) { gwl_seat_capability_pointer_enable(seat); } else { gwl_seat_capability_pointer_disable(seat); } if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { gwl_seat_capability_keyboard_enable(seat); } else { gwl_seat_capability_keyboard_disable(seat); } if (capabilities & WL_SEAT_CAPABILITY_TOUCH) { gwl_seat_capability_touch_enable(seat); } else { gwl_seat_capability_touch_disable(seat); } } static void seat_handle_name(void *data, wl_seat * /*wl_seat*/, const char *name) { CLOG_INFO(LOG, 2, "name (name=\"%s\")", name); static_cast(data)->name = std::string(name); } static const wl_seat_listener seat_listener = { /*capabilities*/ seat_handle_capabilities, /*name*/ seat_handle_name, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (XDG Output), #zxdg_output_v1_listener * \{ */ static CLG_LogRef LOG_WL_XDG_OUTPUT = {"ghost.wl.handle.xdg_output"}; #define LOG (&LOG_WL_XDG_OUTPUT) static void xdg_output_handle_logical_position(void *data, zxdg_output_v1 * /*xdg_output*/, const int32_t x, const int32_t y) { CLOG_INFO(LOG, 2, "logical_position [%d, %d]", x, y); GWL_Output *output = static_cast(data); output->position_logical[0] = x; output->position_logical[1] = y; output->has_position_logical = true; } static void xdg_output_handle_logical_size(void *data, zxdg_output_v1 * /*xdg_output*/, const int32_t width, const int32_t height) { CLOG_INFO(LOG, 2, "logical_size [%d, %d]", width, height); GWL_Output *output = static_cast(data); if (output->size_logical[0] != 0 && output->size_logical[1] != 0) { /* Original comment from SDL. */ /* FIXME(@flibit): GNOME has a bug where the logical size does not account for * scale, resulting in bogus viewport sizes. * * Until this is fixed, validate that _some_ kind of scaling is being * done (we can't match exactly because fractional scaling can't be * detected otherwise), then override if necessary. */ if ((output->size_logical[0] == width) && (output->scale_fractional == (1 * FRACTIONAL_DENOMINATOR))) { GHOST_PRINT("xdg_output scale did not match, overriding with wl_output scale\n"); #ifdef USE_GNOME_CONFINE_HACK /* Use a bug in GNOME to check GNOME is in use. If the bug is fixed this won't cause an issue * as #98793 has been fixed up-stream too, but not in a release at time of writing. */ use_gnome_confine_hack = true; #endif return; } } output->size_logical[0] = width; output->size_logical[1] = height; output->has_size_logical = true; } static void xdg_output_handle_done(void *data, zxdg_output_v1 * /*xdg_output*/) { CLOG_INFO(LOG, 2, "done"); /* NOTE: `xdg-output.done` events are deprecated and only apply below version 3 of the protocol. * `wl-output.done` event will be emitted in version 3 or higher. */ GWL_Output *output = static_cast(data); if (zxdg_output_v1_get_version(output->xdg.output) < 3) { output_handle_done(data, output->wl.output); } } static void xdg_output_handle_name(void * /*data*/, zxdg_output_v1 * /*xdg_output*/, const char *name) { CLOG_INFO(LOG, 2, "name (name=\"%s\")", name); } static void xdg_output_handle_description(void * /*data*/, zxdg_output_v1 * /*xdg_output*/, const char *description) { CLOG_INFO(LOG, 2, "description (description=\"%s\")", description); } static const zxdg_output_v1_listener xdg_output_listener = { /*logical_position*/ xdg_output_handle_logical_position, /*logical_size*/ xdg_output_handle_logical_size, /*done*/ xdg_output_handle_done, /*name*/ xdg_output_handle_name, /*description*/ xdg_output_handle_description, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Output), #wl_output_listener * \{ */ static CLG_LogRef LOG_WL_OUTPUT = {"ghost.wl.handle.output"}; #define LOG (&LOG_WL_OUTPUT) static void output_handle_geometry(void *data, wl_output * /*wl_output*/, const int32_t /*x*/, const int32_t /*y*/, const int32_t physical_width, const int32_t physical_height, const int32_t /*subpixel*/, const char *make, const char *model, const int32_t transform) { CLOG_INFO(LOG, 2, "geometry (make=\"%s\", model=\"%s\", transform=%d, size=[%d, %d])", make, model, transform, physical_width, physical_height); GWL_Output *output = static_cast(data); output->transform = transform; output->make = std::string(make); output->model = std::string(model); output->size_mm[0] = physical_width; output->size_mm[1] = physical_height; } static void output_handle_mode(void *data, wl_output * /*wl_output*/, const uint32_t flags, const int32_t width, const int32_t height, const int32_t /*refresh*/) { if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) { CLOG_INFO(LOG, 2, "mode (skipped)"); return; } CLOG_INFO(LOG, 2, "mode (size=[%d, %d], flags=%u)", width, height, flags); GWL_Output *output = static_cast(data); output->size_native[0] = width; output->size_native[1] = height; /* Don't rotate this yet, `wl-output` coordinates are transformed in * handle_done and `xdg-output` coordinates are pre-transformed. */ if (!output->has_size_logical) { output->size_logical[0] = width; output->size_logical[1] = height; } } /** * Sent all information about output. * * This event is sent after all other properties have been sent * after binding to the output object and after any other property * changes done after that. This allows changes to the output * properties to be seen as atomic, even if they happen via multiple events. */ static void output_handle_done(void *data, wl_output * /*wl_output*/) { CLOG_INFO(LOG, 2, "done"); GWL_Output *output = static_cast(data); int32_t size_native[2] = {UNPACK2(output->size_native)}; if (ELEM(output->transform, WL_OUTPUT_TRANSFORM_90, WL_OUTPUT_TRANSFORM_270)) { std::swap(size_native[0], size_native[1]); } /* If `xdg-output` is present, calculate the true scale of the desktop */ if (output->has_size_logical) { /* NOTE: it's not necessary to divide these values by their greatest-common-denominator * as even a 64k screen resolution doesn't approach overflowing an `int32_t`. */ GHOST_ASSERT(size_native[0] && output->size_logical[0], "Screen size values were not set when they were expected to be."); output->scale_fractional = (size_native[0] * FRACTIONAL_DENOMINATOR) / output->size_logical[0]; output->has_scale_fractional = true; } } static void output_handle_scale(void *data, wl_output * /*wl_output*/, const int32_t factor) { CLOG_INFO(LOG, 2, "scale"); GWL_Output *output = static_cast(data); output->scale = factor; output->system->output_scale_update(output); } static const wl_output_listener output_listener = { /*geometry*/ output_handle_geometry, /*mode*/ output_handle_mode, /*done*/ output_handle_done, /*scale*/ output_handle_scale, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (XDG WM Base), #xdg_wm_base_listener * \{ */ static CLG_LogRef LOG_WL_XDG_WM_BASE = {"ghost.wl.handle.xdg_wm_base"}; #define LOG (&LOG_WL_XDG_WM_BASE) static void shell_handle_ping(void * /*data*/, 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 xdg_wm_base_listener shell_listener = { /*ping*/ shell_handle_ping, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (LibDecor), #libdecor_interface * \{ */ #ifdef WITH_GHOST_WAYLAND_LIBDECOR static CLG_LogRef LOG_WL_LIBDECOR = {"ghost.wl.handle.libdecor"}; # define LOG (&LOG_WL_LIBDECOR) static void decor_handle_error(libdecor * /*context*/, enum libdecor_error error, const char *message) { CLOG_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 libdecor_interface libdecor_interface = { decor_handle_error, }; # undef LOG #endif /* WITH_GHOST_WAYLAND_LIBDECOR. */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Listener (Registry), #wl_registry_listener * \{ */ static CLG_LogRef LOG_WL_REGISTRY = {"ghost.wl.handle.registry"}; #define LOG (&LOG_WL_REGISTRY) /* #GWL_Display.wl_compositor */ static void gwl_registry_compositor_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wl.compositor = static_cast( wl_registry_bind(display->wl.registry, params->name, &wl_compositor_interface, 3)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_compositor_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { wl_compositor **value_p = &display->wl.compositor; wl_compositor_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.xdg_decor.shell */ static void gwl_registry_xdg_wm_base_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_XDG_Decor_System &decor = *display->xdg_decor; decor.shell = static_cast( wl_registry_bind(display->wl.registry, params->name, &xdg_wm_base_interface, 1)); xdg_wm_base_add_listener(decor.shell, &shell_listener, nullptr); decor.shell_name = params->name; gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_wm_base_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { GWL_XDG_Decor_System &decor = *display->xdg_decor; xdg_wm_base **value_p = &decor.shell; uint32_t *name_p = &decor.shell_name; xdg_wm_base_destroy(*value_p); *value_p = nullptr; *name_p = WL_NAME_UNSET; } /* #GWL_Display.xdg_decor.manager */ static void gwl_registry_xdg_decoration_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_XDG_Decor_System &decor = *display->xdg_decor; decor.manager = static_cast(wl_registry_bind( display->wl.registry, params->name, &zxdg_decoration_manager_v1_interface, 1)); decor.manager_name = params->name; gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_decoration_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { GWL_XDG_Decor_System &decor = *display->xdg_decor; zxdg_decoration_manager_v1 **value_p = &decor.manager; uint32_t *name_p = &decor.manager_name; zxdg_decoration_manager_v1_destroy(*value_p); *value_p = nullptr; *name_p = WL_NAME_UNSET; } /* #GWL_Display.xdg_output_manager */ static void gwl_registry_xdg_output_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->xdg.output_manager = static_cast( wl_registry_bind(display->wl.registry, params->name, &zxdg_output_manager_v1_interface, 2)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_output_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zxdg_output_manager_v1 **value_p = &display->xdg.output_manager; zxdg_output_manager_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wl_output */ static void gwl_registry_wl_output_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_Output *output = new GWL_Output; output->system = display->system; output->wl.output = static_cast( wl_registry_bind(display->wl.registry, params->name, &wl_output_interface, 2)); ghost_wl_output_tag(output->wl.output); wl_output_set_user_data(output->wl.output, output); display->outputs.push_back(output); wl_output_add_listener(output->wl.output, &output_listener, output); gwl_registry_entry_add(display, params, static_cast(output)); } static void gwl_registry_wl_output_update(GWL_Display *display, const GWL_RegisteryUpdate_Params *params) { GWL_Output *output = static_cast(params->user_data); if (display->xdg.output_manager) { if (output->xdg.output == nullptr) { output->xdg.output = zxdg_output_manager_v1_get_xdg_output(display->xdg.output_manager, output->wl.output); zxdg_output_v1_add_listener(output->xdg.output, &xdg_output_listener, output); } } else { output->xdg.output = nullptr; } } static void gwl_registry_wl_output_remove(GWL_Display *display, void *user_data, const bool on_exit) { /* While windows & cursors hold references to outputs, there is no need to manually remove * these references as the compositor will remove references via #wl_surface_listener.leave. * * WARNING: this is not the case for WLROOTS based compositors which have a (bug?) * where surface leave events don't run. So `system->output_leave(..)` is needed * until the issue is resolved in WLROOTS. */ GWL_Output *output = static_cast(user_data); if (!on_exit) { /* Needed for WLROOTS, does nothing if surface leave callbacks have already run. */ if (output->system->output_unref(output->wl.output)) { CLOG_WARN(LOG, "mis-behaving compositor failed to call \"surface_listener.leave\" " "window scale may be invalid!"); } } if (output->xdg.output) { zxdg_output_v1_destroy(output->xdg.output); } wl_output_destroy(output->wl.output); std::vector::iterator iter = std::find( display->outputs.begin(), display->outputs.end(), output); const int index = (iter != display->outputs.cend()) ? std::distance(display->outputs.begin(), iter) : -1; GHOST_ASSERT(index != -1, "invalid internal state"); /* NOTE: always erase even when `on_exit` because `output->xdg_output` is cleared later. */ display->outputs.erase(display->outputs.begin() + index); delete output; } /* #GWL_Display.seats */ static void gwl_registry_wl_seat_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { GWL_Seat *seat = new GWL_Seat; seat->system = display->system; seat->xkb.context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); /* May be null (skip dead-key support in this case). */ seat->xkb.compose_table = xkb_compose_table_new_from_locale( seat->xkb.context, ghost_wl_locale_from_env_with_default(), XKB_COMPOSE_COMPILE_NO_FLAGS); seat->data_source = new GWL_DataSource; seat->wl.seat = static_cast( wl_registry_bind(display->wl.registry, params->name, &wl_seat_interface, 5)); display->seats.push_back(seat); wl_seat_add_listener(seat->wl.seat, &seat_listener, seat); gwl_registry_entry_add(display, params, static_cast(seat)); } static void gwl_registry_wl_seat_update(GWL_Display *display, const GWL_RegisteryUpdate_Params *params) { GWL_Seat *seat = static_cast(params->user_data); /* Register data device per seat for IPC between WAYLAND clients. */ if (display->wl.data_device_manager) { if (seat->wl.data_device == nullptr) { seat->wl.data_device = wl_data_device_manager_get_data_device( display->wl.data_device_manager, seat->wl.seat); wl_data_device_add_listener(seat->wl.data_device, &data_device_listener, seat); } } else { seat->wl.data_device = nullptr; } if (display->wp.tablet_manager) { if (seat->wp.tablet_seat == nullptr) { seat->wp.tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(display->wp.tablet_manager, seat->wl.seat); zwp_tablet_seat_v2_add_listener(seat->wp.tablet_seat, &tablet_seat_listener, seat); } } else { seat->wp.tablet_seat = nullptr; } if (display->wp.primary_selection_device_manager) { if (seat->wp.primary_selection_device == nullptr) { seat->wp.primary_selection_device = zwp_primary_selection_device_manager_v1_get_device( display->wp.primary_selection_device_manager, seat->wl.seat); zwp_primary_selection_device_v1_add_listener(seat->wp.primary_selection_device, &primary_selection_device_listener, &seat->primary_selection); } } else { seat->wp.primary_selection_device = nullptr; } #ifdef WITH_INPUT_IME if (display->wp.text_input_manager) { if (seat->wp.text_input == nullptr) { seat->wp.text_input = zwp_text_input_manager_v3_get_text_input( display->wp.text_input_manager, seat->wl.seat); zwp_text_input_v3_set_user_data(seat->wp.text_input, seat); zwp_text_input_v3_add_listener(seat->wp.text_input, &text_input_listener, seat); } } else { seat->wp.text_input = nullptr; } #endif /* WITH_INPUT_IME */ } static void gwl_registry_wl_seat_remove(GWL_Display *display, void *user_data, const bool on_exit) { GWL_Seat *seat = static_cast(user_data); /* First handle members that require locking. * While highly unlikely, it's possible they are being used while this function runs. */ { std::lock_guard lock{seat->data_source_mutex}; if (seat->data_source) { gwl_simple_buffer_free_data(&seat->data_source->buffer_out); if (seat->data_source->wl.source) { wl_data_source_destroy(seat->data_source->wl.source); } delete seat->data_source; } } { std::lock_guard lock{seat->data_offer_dnd_mutex}; if (seat->data_offer_dnd) { wl_data_offer_destroy(seat->data_offer_dnd->wl.id); delete seat->data_offer_dnd; } } { std::lock_guard lock{seat->data_offer_copy_paste_mutex}; if (seat->data_offer_copy_paste) { wl_data_offer_destroy(seat->data_offer_copy_paste->wl.id); delete seat->data_offer_copy_paste; } } { GWL_PrimarySelection *primary = &seat->primary_selection; std::lock_guard lock{primary->data_offer_mutex}; gwl_primary_selection_discard_offer(primary); } { GWL_PrimarySelection *primary = &seat->primary_selection; std::lock_guard lock{primary->data_source_mutex}; gwl_primary_selection_discard_source(primary); } if (seat->wp.primary_selection_device) { zwp_primary_selection_device_v1_destroy(seat->wp.primary_selection_device); } if (seat->wl.data_device) { wl_data_device_release(seat->wl.data_device); } if (seat->cursor.custom_data) { munmap(seat->cursor.custom_data, seat->cursor.custom_data_size); } /* Disable all capabilities as a way to free: * - `seat.wl_pointer` (and related cursor variables). * - `seat.wl_touch`. * - `seat.wl_keyboard`. */ gwl_seat_capability_pointer_disable(seat); gwl_seat_capability_keyboard_disable(seat); gwl_seat_capability_touch_disable(seat); /* Un-referencing checks for nullptr case. */ xkb_state_unref(seat->xkb.state); xkb_state_unref(seat->xkb.state_empty); xkb_state_unref(seat->xkb.state_empty_with_shift); xkb_state_unref(seat->xkb.state_empty_with_numlock); xkb_compose_state_unref(seat->xkb.compose_state); xkb_compose_table_unref(seat->xkb.compose_table); xkb_context_unref(seat->xkb.context); /* Remove the seat. */ wl_seat_destroy(seat->wl.seat); std::vector::iterator iter = std::find( display->seats.begin(), display->seats.end(), seat); const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) : -1; GHOST_ASSERT(index != -1, "invalid internal state"); if (!on_exit) { if (display->seats_active_index >= index) { display->seats_active_index -= 1; } display->seats.erase(display->seats.begin() + index); } delete seat; } /* #GWL_Display.wl_shm */ static void gwl_registry_wl_shm_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wl.shm = static_cast( wl_registry_bind(display->wl.registry, params->name, &wl_shm_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wl_shm_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { wl_shm **value_p = &display->wl.shm; wl_shm_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wl_data_device_manager */ static void gwl_registry_wl_data_device_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wl.data_device_manager = static_cast( wl_registry_bind(display->wl.registry, params->name, &wl_data_device_manager_interface, 3)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wl_data_device_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { wl_data_device_manager **value_p = &display->wl.data_device_manager; wl_data_device_manager_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_tablet_manager */ static void gwl_registry_wp_tablet_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.tablet_manager = static_cast( wl_registry_bind(display->wl.registry, params->name, &zwp_tablet_manager_v2_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_tablet_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zwp_tablet_manager_v2 **value_p = &display->wp.tablet_manager; zwp_tablet_manager_v2_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_relative_pointer_manager */ static void gwl_registry_wp_relative_pointer_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.relative_pointer_manager = static_cast( wl_registry_bind( display->wl.registry, params->name, &zwp_relative_pointer_manager_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_relative_pointer_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zwp_relative_pointer_manager_v1 **value_p = &display->wp.relative_pointer_manager; zwp_relative_pointer_manager_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_pointer_constraints */ static void gwl_registry_wp_pointer_constraints_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.pointer_constraints = static_cast(wl_registry_bind( display->wl.registry, params->name, &zwp_pointer_constraints_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_pointer_constraints_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zwp_pointer_constraints_v1 **value_p = &display->wp.pointer_constraints; zwp_pointer_constraints_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_pointer_gestures */ static void gwl_registry_wp_pointer_gestures_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.pointer_gestures = static_cast( wl_registry_bind(display->wl.registry, params->name, &zwp_pointer_gestures_v1_interface, std::min(params->version, 3u))); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_pointer_gestures_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zwp_pointer_gestures_v1 **value_p = &display->wp.pointer_gestures; zwp_pointer_gestures_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.xdg_activation */ static void gwl_registry_xdg_activation_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->xdg.activation_manager = static_cast( wl_registry_bind(display->wl.registry, params->name, &xdg_activation_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_xdg_activation_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { xdg_activation_v1 **value_p = &display->xdg.activation_manager; xdg_activation_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_fractional_scale_manger */ static void gwl_registry_wp_fractional_scale_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.fractional_scale_manager = static_cast( wl_registry_bind( display->wl.registry, params->name, &wp_fractional_scale_manager_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_fractional_scale_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { wp_fractional_scale_manager_v1 **value_p = &display->wp.fractional_scale_manager; wp_fractional_scale_manager_v1_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wl_viewport */ static void gwl_registry_wp_viewporter_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.viewporter = static_cast( wl_registry_bind(display->wl.registry, params->name, &wp_viewporter_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_viewporter_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { wp_viewporter **value_p = &display->wp.viewporter; wp_viewporter_destroy(*value_p); *value_p = nullptr; } /* #GWL_Display.wp_primary_selection_device_manager */ static void gwl_registry_wp_primary_selection_device_manager_add( GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.primary_selection_device_manager = static_cast( wl_registry_bind(display->wl.registry, params->name, &zwp_primary_selection_device_manager_v1_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_primary_selection_device_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zwp_primary_selection_device_manager_v1 **value_p = &display->wp.primary_selection_device_manager; zwp_primary_selection_device_manager_v1_destroy(*value_p); *value_p = nullptr; } #ifdef WITH_INPUT_IME /* #GWL_Display.wp_text_input_manager */ static void gwl_registry_wp_text_input_manager_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params) { display->wp.text_input_manager = static_cast(wl_registry_bind( display->wl.registry, params->name, &zwp_text_input_manager_v3_interface, 1)); gwl_registry_entry_add(display, params, nullptr); } static void gwl_registry_wp_text_input_manager_remove(GWL_Display *display, void * /*user_data*/, const bool /*on_exit*/) { zwp_text_input_manager_v3 **value_p = &display->wp.text_input_manager; zwp_text_input_manager_v3_destroy(*value_p); *value_p = nullptr; } #endif /* WITH_INPUT_IME */ /** * Map interfaces to initialization functions. * * \note This list also defines the order interfaces are removed. * On exit interface removal runs from last to first to avoid potential bugs * caused by undefined order of removal. * * In general fundamental, low level objects such as the compositor and shared memory * should be declared earlier and other interfaces that may use them should be declared later. */ static const GWL_RegistryHandler gwl_registry_handlers[] = { /* Low level interfaces. */ { /*interface_p*/ &wl_compositor_interface.name, /*add_fn*/ gwl_registry_compositor_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_compositor_remove, }, { /*interface_p*/ &wl_shm_interface.name, /*add_fn*/ gwl_registry_wl_shm_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wl_shm_remove, }, { /*interface_p*/ &xdg_wm_base_interface.name, /*add_fn*/ gwl_registry_xdg_wm_base_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_xdg_wm_base_remove, }, /* Managers. */ { /*interface_p*/ &zxdg_decoration_manager_v1_interface.name, /*add_fn*/ gwl_registry_xdg_decoration_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_xdg_decoration_manager_remove, }, { /*interface_p*/ &zxdg_output_manager_v1_interface.name, /*add_fn*/ gwl_registry_xdg_output_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_xdg_output_manager_remove, }, { /*interface_p*/ &wl_data_device_manager_interface.name, /*add_fn*/ gwl_registry_wl_data_device_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wl_data_device_manager_remove, }, { /*interface_p*/ &zwp_primary_selection_device_manager_v1_interface.name, /*add_fn*/ gwl_registry_wp_primary_selection_device_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_primary_selection_device_manager_remove, }, { /*interface_p*/ &zwp_tablet_manager_v2_interface.name, /*add_fn*/ gwl_registry_wp_tablet_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_tablet_manager_remove, }, { /*interface_p*/ &zwp_relative_pointer_manager_v1_interface.name, /*add_fn*/ gwl_registry_wp_relative_pointer_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_relative_pointer_manager_remove, }, #ifdef WITH_INPUT_IME { /*interface_p*/ &zwp_text_input_manager_v3_interface.name, /*add_fn*/ gwl_registry_wp_text_input_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_text_input_manager_remove, }, #endif /* Higher level interfaces. */ { /*interface_p*/ &zwp_pointer_constraints_v1_interface.name, /*add_fn*/ gwl_registry_wp_pointer_constraints_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_pointer_constraints_remove, }, { /*interface_p*/ &zwp_pointer_gestures_v1_interface.name, /*add_fn*/ gwl_registry_wp_pointer_gestures_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_pointer_gestures_remove, }, { /*interface_p*/ &xdg_activation_v1_interface.name, /*add_fn*/ gwl_registry_xdg_activation_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_xdg_activation_remove, }, { /*interface_p*/ &wp_fractional_scale_manager_v1_interface.name, /*add_fn*/ gwl_registry_wp_fractional_scale_manager_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_fractional_scale_manager_remove, }, { /*interface_p*/ &wp_viewporter_interface.name, /*add_fn*/ gwl_registry_wp_viewporter_add, /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_viewporter_remove, }, /* Display outputs. */ { /*interface_p*/ &wl_output_interface.name, /*add_fn*/ gwl_registry_wl_output_add, /*update_fn*/ gwl_registry_wl_output_update, /*remove_fn*/ gwl_registry_wl_output_remove, }, /* Seats. * Keep the seat near the end to ensure other types are created first. * as the seat creates data based on other interfaces. */ { /*interface_p*/ &wl_seat_interface.name, /*add_fn*/ gwl_registry_wl_seat_add, /*update_fn*/ gwl_registry_wl_seat_update, /*remove_fn*/ gwl_registry_wl_seat_remove, }, {nullptr}, }; /** * Workaround for `gwl_registry_handlers` order of declaration, * preventing `ARRAY_SIZE(gwl_registry_handlers) - 1` being used. */ static int gwl_registry_handler_interface_slot_max() { return ARRAY_SIZE(gwl_registry_handlers) - 1; } static int gwl_registry_handler_interface_slot_from_string(const char *interface) { for (const GWL_RegistryHandler *handler = gwl_registry_handlers; handler->interface_p != nullptr; handler++) { if (STREQ(interface, *handler->interface_p)) { return int(handler - gwl_registry_handlers); } } return -1; } static const GWL_RegistryHandler *gwl_registry_handler_from_interface_slot(int interface_slot) { GHOST_ASSERT(uint32_t(interface_slot) < uint32_t(gwl_registry_handler_interface_slot_max()), "Index out of range"); return &gwl_registry_handlers[interface_slot]; } static void global_handle_add(void *data, [[maybe_unused]] wl_registry *wl_registry, const uint32_t name, const char *interface, const uint32_t version) { /* Log last since it's useful to know if the interface was handled or not. */ GWL_Display *display = static_cast(data); GHOST_ASSERT(display->wl.registry == wl_registry, "Registry argument must match!"); const int interface_slot = gwl_registry_handler_interface_slot_from_string(interface); bool added = false; if (interface_slot != -1) { const GWL_RegistryHandler *handler = &gwl_registry_handlers[interface_slot]; const GWL_RegistryEntry *registry_entry_prev = display->registry_entry; /* The interface name that is ensured not to be freed. */ GWL_RegisteryAdd_Params params{}; params.name = name; params.interface_slot = interface_slot; params.version = version; handler->add_fn(display, ¶ms); added = display->registry_entry != registry_entry_prev; } else { /* Not found. */ #ifdef USE_GNOME_NEEDS_LIBDECOR_HACK if (STRPREFIX(interface, "gtk_shell")) { /* `gtk_shell1` at time of writing. */ /* Only require `libdecor` when built with X11 support, * otherwise there is nothing to fall back on. */ display->libdecor_required = true; } #endif } CLOG_INFO(LOG, 2, "add %s(interface=%s, version=%u, name=%u)", (interface_slot != -1) ? (added ? "" : "(found but not added)") : "(skipped), ", interface, version, name); /* Initialization avoids excessive calls by calling update after all have been initialized. */ if (added) { if (display->registry_skip_update_all == false) { /* See doc-string for rationale on updating all on add/removal. */ gwl_registry_entry_update_all(display, interface_slot); } } } /** * Announce removal of global object. * * Notify the client of removed global objects. * * This event notifies the client that the global identified by * name is no longer available. If the client bound to the global * using the bind request, the client should now destroy that object. */ static void global_handle_remove(void *data, [[maybe_unused]] wl_registry *wl_registry, const uint32_t name) { GWL_Display *display = static_cast(data); GHOST_ASSERT(display->wl.registry == wl_registry, "Registry argument must match!"); int interface_slot = 0; const bool removed = gwl_registry_entry_remove_by_name(display, name, &interface_slot); CLOG_INFO(LOG, 2, "remove (name=%u, interface=%s)", name, removed ? *gwl_registry_handlers[interface_slot].interface_p : "(unknown)"); if (removed) { if (display->registry_skip_update_all == false) { /* See doc-string for rationale on updating all on add/removal. */ gwl_registry_entry_update_all(display, interface_slot); } } } static const wl_registry_listener registry_listener = { /*global*/ global_handle_add, /*global_remove*/ global_handle_remove, }; #undef LOG /** \} */ /* -------------------------------------------------------------------- */ /** \name Event Thread * \{ */ #ifdef USE_EVENT_BACKGROUND_THREAD static void *gwl_display_event_thread_fn(void *display_voidp) { GWL_Display *display = static_cast(display_voidp); const int fd = wl_display_get_fd(display->wl.display); while (display->events_pthread_is_active) { /* Wait for an event, this thread is dedicated to event handling. */ if (ghost_wl_display_event_pump_from_thread( display->wl.display, fd, display->system->server_mutex) == -1) { break; } } /* Wait until the main thread cancels this thread, otherwise this thread may exit * before cancel is called, causing a crash on exit. */ while (true) { pause(); } return nullptr; } /* Event reading thread. */ static void gwl_display_event_thread_create(GWL_Display *display) { GHOST_ASSERT(display->events_pthread == 0, "Only call once"); display->events_pending.reserve(events_pending_default_size); display->events_pthread_is_active = true; pthread_create(&display->events_pthread, nullptr, gwl_display_event_thread_fn, display); /* Application logic should take priority, this only ensures events don't accumulate when busy * which typically takes a while (5+ seconds of frantic mouse motion for e.g.). */ pthread_set_min_priority(display->events_pthread); pthread_detach(display->events_pthread); } static void gwl_display_event_thread_destroy(GWL_Display *display) { pthread_cancel(display->events_pthread); } #endif /* USE_EVENT_BACKGROUND_THREAD */ /** \} */ /* -------------------------------------------------------------------- */ /** \name GHOST Implementation * * WAYLAND specific implementation of the #GHOST_System interface. * \{ */ GHOST_SystemWayland::GHOST_SystemWayland(bool background) : GHOST_System(), #ifdef USE_EVENT_BACKGROUND_THREAD server_mutex(new std::mutex), timer_mutex(new std::mutex), main_thread_id(std::this_thread::get_id()), #endif display_(new GWL_Display) { wl_log_set_handler_client(ghost_wayland_log_handler); display_->system = this; /* Connect to the Wayland server. */ display_->wl.display = wl_display_connect(nullptr); if (!display_->wl.display) { display_destroy_and_free_all(); throw std::runtime_error("unable to connect to display!"); } /* This may be removed later if decorations are required, needed as part of registration. */ display_->xdg_decor = new GWL_XDG_Decor_System; /* Register interfaces. */ { display_->registry_skip_update_all = true; wl_registry *registry = wl_display_get_registry(display_->wl.display); display_->wl.registry = registry; wl_registry_add_listener(registry, ®istry_listener, display_); /* First round-trip to receive all registry objects. */ wl_display_roundtrip(display_->wl.display); /* Second round-trip to receive all output events. */ wl_display_roundtrip(display_->wl.display); /* Account for dependencies between interfaces. */ gwl_registry_entry_update_all(display_, -1); display_->registry_skip_update_all = false; } #ifdef WITH_GHOST_WAYLAND_LIBDECOR if (display_->libdecor_required) { /* Ignore windowing requirements when running in background mode, * as it doesn't make sense to fall back to X11 because of windowing functionality * in background mode, also LIBDECOR is crashing in background mode `blender -b -f 1` * for e.g. while it could be fixed, requiring the library at all makes no sense. */ if (background) { display_->libdecor_required = false; } # ifdef WITH_GHOST_X11 else if (!has_libdecor && !ghost_wayland_is_x11_available()) { /* Only require LIBDECOR when X11 is available, otherwise there is nothing to fall back to. * It's better to open without window decorations than failing entirely. */ display_->libdecor_required = false; } # endif /* WITH_GHOST_X11 */ } if (display_->libdecor_required) { gwl_xdg_decor_system_destroy(display_, display_->xdg_decor); display_->xdg_decor = nullptr; if (!has_libdecor) { # ifdef WITH_GHOST_X11 /* LIBDECOR was the only reason X11 was used, let the user know they need it installed. */ fprintf(stderr, "WAYLAND found but libdecor was not, install libdecor for Wayland support, " "falling back to X11\n"); # endif display_destroy_and_free_all(); throw std::runtime_error("unable to find libdecor!"); } } else { use_libdecor = false; } #endif /* WITH_GHOST_WAYLAND_LIBDECOR */ #ifdef WITH_GHOST_WAYLAND_LIBDECOR if (use_libdecor) { display_->libdecor = new GWL_LibDecor_System; GWL_LibDecor_System &decor = *display_->libdecor; decor.context = libdecor_new(display_->wl.display, &libdecor_interface); if (!decor.context) { display_destroy_and_free_all(); throw std::runtime_error("unable to create window decorations!"); } } else #else (void)background; #endif { const GWL_XDG_Decor_System &decor = *display_->xdg_decor; if (!decor.shell) { display_destroy_and_free_all(); throw std::runtime_error("unable to access xdg_shell!"); } } /* Without this, the output fractional size from `display->xdg.output_manager` isn't known, * while this isn't essential, the first window creation uses this for setting the size. * Supporting both XDG initialized/uninitialized outputs is possible it complicates logic. * see: #113328 for an example of size on startup issues. */ wl_display_roundtrip(display_->wl.display); #ifdef USE_EVENT_BACKGROUND_THREAD gwl_display_event_thread_create(display_); display_->ghost_timer_manager = new GHOST_TimerManager(); #endif } void GHOST_SystemWayland::display_destroy_and_free_all() { gwl_display_destroy(display_); #ifdef USE_EVENT_BACKGROUND_THREAD delete server_mutex; delete timer_mutex; #endif } GHOST_SystemWayland::~GHOST_SystemWayland() { display_destroy_and_free_all(); } GHOST_TSuccess GHOST_SystemWayland::init() { GHOST_TSuccess success = GHOST_System::init(); if (success) { #ifdef WITH_INPUT_NDOF m_ndofManager = new GHOST_NDOFManagerUnix(*this); #endif return GHOST_kSuccess; } return GHOST_kFailure; } #undef pushEvent bool GHOST_SystemWayland::processEvents(bool waitForEvent) { bool any_processed = false; #ifdef USE_EVENT_BACKGROUND_THREAD if (UNLIKELY(has_pending_actions_for_window.exchange(false))) { std::lock_guard lock_server_guard{*server_mutex}; for (GHOST_IWindow *iwin : getWindowManager()->getWindows()) { GHOST_WindowWayland *win = static_cast(iwin); win->pending_actions_handle(); } } { std::lock_guard lock{display_->events_pending_mutex}; for (const GHOST_IEvent *event : display_->events_pending) { /* Perform actions that aren't handled in a thread. */ switch (event->getType()) { case GHOST_kEventWindowActivate: { getWindowManager()->setActiveWindow(event->getWindow()); break; } case GHOST_kEventWindowDeactivate: { getWindowManager()->setWindowInactive(event->getWindow()); break; } default: { break; } } pushEvent(event); } display_->events_pending.clear(); if (UNLIKELY(display_->events_pending.capacity() > events_pending_default_size)) { /* Avoid over allocation in the case of occasional delay between processing events * causing many events to be collected and making this into a large array. */ display_->events_pending.shrink_to_fit(); display_->events_pending.reserve(events_pending_default_size); } } #endif /* USE_EVENT_BACKGROUND_THREAD */ { const uint64_t now = getMilliSeconds(); #ifdef USE_EVENT_BACKGROUND_THREAD { std::lock_guard lock_timer_guard{*display_->system->timer_mutex}; if (ghost_timer_manager()->fireTimers(now)) { any_processed = true; } } #endif if (getTimerManager()->fireTimers(now)) { any_processed = true; } } #ifdef WITH_INPUT_NDOF if (static_cast(m_ndofManager)->processEvents()) { /* As NDOF bypasses WAYLAND event handling, * never wait for an event when an NDOF event was found. */ waitForEvent = false; any_processed = true; } #endif /* WITH_INPUT_NDOF */ if (waitForEvent) { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif if (wl_display_dispatch(display_->wl.display) == -1) { ghost_wl_display_report_error(display_->wl.display); } } else { #ifdef USE_EVENT_BACKGROUND_THREAD /* NOTE: this works, but as the events are being read in a thread, * this could be removed and event handling still works.. */ if (server_mutex->try_lock()) { if (ghost_wl_display_event_pump(display_->wl.display) == -1) { ghost_wl_display_report_error(display_->wl.display); } server_mutex->unlock(); } #else if (ghost_wl_display_event_pump(display_->wl.display) == -1) { ghost_wl_display_report_error(display_->wl.display); } #endif /* !USE_EVENT_BACKGROUND_THREAD */ } if (getEventManager()->getNumEvents() > 0) { any_processed = true; } return any_processed; } bool GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*action*/) { return false; } GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } /* Only read the underlying `seat->xkb_state` when there is an active window. * Without this, the following situation occurs: * * - A window is activated (before the #wl_keyboard_listener::enter has run). * - The modifiers from `seat->xkb_state` don't match `seat->key_depressed`. * - Dummy values are written into `seat->key_depressed` to account for the discrepancy * (as `seat->xkb_state` is the source of truth), however the number of held modifiers * is not longer valid (because it's not known from dummy values). * - #wl_keyboard_listener::enter runs, however the events generated from the state change * may not match the physically held keys because the dummy values are not accurate. * * As this is an edge-case caused by the order of callbacks that run on window activation, * don't attempt to *fix* the values in `seat->key_depressed` before the keyboard enter * handler runs. This means the result of `getModifierKeys` may be momentarily incorrect * however it's corrected once #wl_keyboard_listener::enter runs. */ const bool is_keyboard_active = seat->keyboard.wl.surface_window != nullptr; const xkb_mod_mask_t state = is_keyboard_active ? xkb_state_serialize_mods(seat->xkb.state, XKB_STATE_MODS_DEPRESSED) : 0; /* Use local #GWL_KeyboardDepressedState to check which key is pressed. * Use XKB as the source of truth, if there is any discrepancy. */ for (int i = 0; i < MOD_INDEX_NUM; i++) { if (UNLIKELY(seat->xkb_keymap_mod_index[i] == XKB_MOD_INVALID)) { continue; } const GWL_ModifierInfo &mod_info = g_modifier_info_table[i]; /* NOTE(@ideasman42): it's important to write the XKB state back to #GWL_KeyboardDepressedState * otherwise changes to modifiers in the future wont generate events. * This can cause modifiers to be stuck when switching between windows in GNOME because * window activation is handled before the keyboard enter callback runs, see: #107314. * Now resolved upstream, keep this for GNOME 45 and older releases & misbehaving compositors * as the workaround doesn't have significant down-sides. */ int16_t &depressed_l = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_l)]; int16_t &depressed_r = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_r)]; bool val_l = depressed_l > 0; bool val_r = depressed_r > 0; if (is_keyboard_active) { const bool val = (state & (1 << seat->xkb_keymap_mod_index[i])) != 0; /* This shouldn't be needed, but guard against any possibility of modifiers being stuck. * Warn so if this happens it can be investigated. */ if (val) { if (UNLIKELY(!(val_l || val_r))) { CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE, "modifier (%s) state is inconsistent (GHOST held keys do not match XKB)", mod_info.display_name); /* Picking the left is arbitrary. */ val_l = true; depressed_l = 1; } } else { if (UNLIKELY(val_l || val_r)) { CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE, "modifier (%s) state is inconsistent (GHOST released keys do not match XKB)", mod_info.display_name); val_l = false; val_r = false; depressed_l = 0; depressed_r = 0; } } } keys.set(mod_info.mod_l, val_l); keys.set(mod_info.mod_r, val_r); } return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } const GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat); if (!seat_state_pointer) { return GHOST_kFailure; } buttons = seat_state_pointer->buttons; return GHOST_kSuccess; } /** * Return a mime type which is supported by GHOST and exists in `types` * (defined by the data offer). */ static const char *system_clipboard_text_mime_type( const std::unordered_set &data_offer_types) { const char *ghost_supported_types[] = {ghost_wl_mime_text_utf8, ghost_wl_mime_text_plain}; for (size_t i = 0; i < ARRAY_SIZE(ghost_supported_types); i++) { if (data_offer_types.count(ghost_supported_types[i])) { return ghost_supported_types[i]; } } return nullptr; } static char *system_clipboard_get_primary_selection(GWL_Display *display) { GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return nullptr; } GWL_PrimarySelection *primary = &seat->primary_selection; std::mutex &mutex = primary->data_offer_mutex; mutex.lock(); bool mutex_locked = true; char *data = nullptr; GWL_PrimarySelection_DataOffer *data_offer = primary->data_offer; if (data_offer != nullptr) { const char *mime_receive = system_clipboard_text_mime_type(data_offer->types); if (mime_receive) { /* Receive the clipboard in a thread, performing round-trips while waiting. * This is needed so pasting contents from our own `primary->data_source` doesn't hang. */ struct ThreadResult { char *data = nullptr; std::atomic done = false; } thread_result; auto read_clipboard_fn = [](GWL_PrimarySelection_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, ThreadResult *thread_result) { size_t data_len = 0; thread_result->data = read_buffer_from_primary_selection_offer( data_offer, mime_receive, mutex, true, &data_len); thread_result->done = true; }; std::thread read_thread(read_clipboard_fn, data_offer, mime_receive, &mutex, &thread_result); read_thread.detach(); while (!thread_result.done) { wl_display_roundtrip(display->wl.display); } data = thread_result.data; /* Reading the data offer unlocks the mutex. */ mutex_locked = false; } } if (mutex_locked) { mutex.unlock(); } return data; } static char *system_clipboard_get(GWL_Display *display) { GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return nullptr; } std::mutex &mutex = seat->data_offer_copy_paste_mutex; mutex.lock(); bool mutex_locked = true; char *data = nullptr; GWL_DataOffer *data_offer = seat->data_offer_copy_paste; if (data_offer != nullptr) { const char *mime_receive = system_clipboard_text_mime_type(data_offer->types); if (mime_receive) { /* Receive the clipboard in a thread, performing round-trips while waiting. * This is needed so pasting contents from our own `seat->data_source` doesn't hang. */ struct ThreadResult { char *data = nullptr; std::atomic done = false; } thread_result; auto read_clipboard_fn = [](GWL_DataOffer *data_offer, const char *mime_receive, std::mutex *mutex, ThreadResult *thread_result) { size_t data_len = 0; thread_result->data = read_buffer_from_data_offer( data_offer, mime_receive, mutex, true, &data_len); thread_result->done = true; }; std::thread read_thread(read_clipboard_fn, data_offer, mime_receive, &mutex, &thread_result); read_thread.detach(); while (!thread_result.done) { wl_display_roundtrip(display->wl.display); } data = thread_result.data; /* Reading the data offer unlocks the mutex. */ mutex_locked = false; } } if (mutex_locked) { mutex.unlock(); } return data; } char *GHOST_SystemWayland::getClipboard(bool selection) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif char *data = nullptr; if (selection) { data = system_clipboard_get_primary_selection(display_); } else { data = system_clipboard_get(display_); } return data; } static void system_clipboard_put_primary_selection(GWL_Display *display, const char *buffer) { if (!display->wp.primary_selection_device_manager) { return; } GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return; } GWL_PrimarySelection *primary = &seat->primary_selection; std::lock_guard lock{primary->data_source_mutex}; gwl_primary_selection_discard_source(primary); GWL_PrimarySelection_DataSource *data_source = new GWL_PrimarySelection_DataSource; primary->data_source = data_source; /* Copy buffer. */ gwl_simple_buffer_set_from_string(&data_source->buffer_out, buffer); data_source->wp.source = zwp_primary_selection_device_manager_v1_create_source( display->wp.primary_selection_device_manager); zwp_primary_selection_source_v1_add_listener( data_source->wp.source, &primary_selection_source_listener, primary); for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_send); i++) { zwp_primary_selection_source_v1_offer(data_source->wp.source, ghost_wl_mime_send[i]); } if (seat->wp.primary_selection_device) { zwp_primary_selection_device_v1_set_selection( seat->wp.primary_selection_device, data_source->wp.source, seat->data_source_serial); } } static void system_clipboard_put(GWL_Display *display, const char *buffer) { if (!display->wl.data_device_manager) { return; } GWL_Seat *seat = gwl_display_seat_active_get(display); if (UNLIKELY(!seat)) { return; } std::lock_guard lock{seat->data_source_mutex}; GWL_DataSource *data_source = seat->data_source; /* Copy buffer. */ gwl_simple_buffer_set_from_string(&data_source->buffer_out, buffer); data_source->wl.source = wl_data_device_manager_create_data_source( display->wl.data_device_manager); wl_data_source_add_listener(data_source->wl.source, &data_source_listener, seat); for (size_t i = 0; i < ARRAY_SIZE(ghost_wl_mime_send); i++) { wl_data_source_offer(data_source->wl.source, ghost_wl_mime_send[i]); } if (seat->wl.data_device) { wl_data_device_set_selection( seat->wl.data_device, data_source->wl.source, seat->data_source_serial); } } void GHOST_SystemWayland::putClipboard(const char *buffer, bool selection) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif if (selection) { system_clipboard_put_primary_selection(display_, buffer); } else { system_clipboard_put(display_, buffer); } } uint8_t GHOST_SystemWayland::getNumDisplays() const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif return display_ ? uint8_t(display_->outputs.size()) : 0; } uint64_t GHOST_SystemWayland::getMilliSeconds() const { /* Match the timing method used by LIBINPUT, so the result is closer to WAYLAND's time-stamps. */ struct timespec ts = {0, 0}; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t(ts.tv_sec) * 1000) + uint64_t(ts.tv_nsec / 1000000); } static GHOST_TSuccess getCursorPositionClientRelative_impl( const GWL_SeatStatePointer *seat_state_pointer, const GHOST_WindowWayland *win, int32_t &x, int32_t &y) { if (win->getCursorGrabModeIsWarp()) { /* As the cursor is restored at the warped location, * apply warping when requesting the cursor location. */ GHOST_Rect wrap_bounds{}; if (win->getCursorGrabBounds(wrap_bounds) == GHOST_kFailure) { win->getClientBounds(wrap_bounds); } int xy_wrap[2] = { seat_state_pointer->xy[0], seat_state_pointer->xy[1], }; GHOST_Rect wrap_bounds_scale; wrap_bounds_scale.m_l = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.m_l)); wrap_bounds_scale.m_t = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.m_t)); wrap_bounds_scale.m_r = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.m_r)); wrap_bounds_scale.m_b = win->wl_fixed_from_window(wl_fixed_from_int(wrap_bounds.m_b)); wrap_bounds_scale.wrapPoint(UNPACK2(xy_wrap), 0, win->getCursorGrabAxis()); x = wl_fixed_to_int(win->wl_fixed_to_window(xy_wrap[0])); y = wl_fixed_to_int(win->wl_fixed_to_window(xy_wrap[1])); } else { x = wl_fixed_to_int(win->wl_fixed_to_window(seat_state_pointer->xy[0])); y = wl_fixed_to_int(win->wl_fixed_to_window(seat_state_pointer->xy[1])); } return GHOST_kSuccess; } static GHOST_TSuccess setCursorPositionClientRelative_impl(GWL_Seat *seat, GHOST_WindowWayland *win, const int32_t x, const int32_t y) { /* NOTE: WAYLAND doesn't support warping the cursor. * However when grab is enabled, we already simulate a cursor location * so that can be set to a new location. */ if (!seat->wp.relative_pointer) { return GHOST_kFailure; } const wl_fixed_t xy_next[2]{ win->wl_fixed_from_window(wl_fixed_from_int(x)), win->wl_fixed_from_window(wl_fixed_from_int(y)), }; /* As the cursor was "warped" generate an event at the new location. */ const uint64_t event_ms = seat->system->getMilliSeconds(); relative_pointer_handle_relative_motion_impl(seat, win, xy_next, event_ms); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::getCursorPositionClientRelative(const GHOST_IWindow *window, int32_t &x, int32_t &y) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat); if (!seat_state_pointer || !seat_state_pointer->wl.surface_window) { return GHOST_kFailure; } const GHOST_WindowWayland *win = static_cast(window); return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y); } GHOST_TSuccess GHOST_SystemWayland::setCursorPositionClientRelative(GHOST_IWindow *window, const int32_t x, const int32_t y) { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GHOST_WindowWayland *win = static_cast(window); return setCursorPositionClientRelative_impl(seat, win, x, y); } GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat); if (!seat_state_pointer) { return GHOST_kFailure; } if (wl_surface *wl_surface_focus = seat_state_pointer->wl.surface_window) { const GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y); } return GHOST_kFailure; } GHOST_TSuccess GHOST_SystemWayland::setCursorPosition(const int32_t x, const int32_t y) { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } /* Intentionally different from `getCursorPosition` which supports both tablet & pointer. * In the case of setting the cursor location, tablets don't support this. */ if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) { GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_focus); return setCursorPositionClientRelative_impl(seat, win, x, y); } return GHOST_kFailure; } void GHOST_SystemWayland::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif if (!display_->outputs.empty()) { /* We assume first output as main. */ const GWL_Output *output = display_->outputs[0]; int32_t size_native[2] = {UNPACK2(output->size_native)}; if (ELEM(output->transform, WL_OUTPUT_TRANSFORM_90, WL_OUTPUT_TRANSFORM_270)) { std::swap(size_native[0], size_native[1]); } width = uint32_t(size_native[0]); height = uint32_t(size_native[1]); } } void GHOST_SystemWayland::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif if (!display_->outputs.empty()) { int32_t xy_min[2] = {INT32_MAX, INT32_MAX}; int32_t xy_max[2] = {INT32_MIN, INT32_MIN}; for (const GWL_Output *output : display_->outputs) { int32_t xy[2] = {0, 0}; int32_t size_native[2] = {UNPACK2(output->size_native)}; if (output->has_position_logical) { xy[0] = output->position_logical[0]; xy[1] = output->position_logical[1]; } if (ELEM(output->transform, WL_OUTPUT_TRANSFORM_90, WL_OUTPUT_TRANSFORM_270)) { std::swap(size_native[0], size_native[1]); } xy_min[0] = std::min(xy_min[0], xy[0]); xy_min[1] = std::min(xy_min[1], xy[1]); xy_max[0] = std::max(xy_max[0], xy[0] + size_native[0]); xy_max[1] = std::max(xy_max[1], xy[1] + size_native[1]); } width = xy_max[0] - xy_min[0]; height = xy_max[1] - xy_min[1]; } } GHOST_IContext *GHOST_SystemWayland::createOffscreenContext(GHOST_GPUSettings gpuSettings) { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif const bool debug_context = (gpuSettings.flags & GHOST_gpuDebugContext) != 0; switch (gpuSettings.context_type) { #ifdef WITH_VULKAN_BACKEND case GHOST_kDrawingContextTypeVulkan: { /* Create new off-screen surface only for vulkan. */ wl_surface *wl_surface = wl_compositor_create_surface(wl_compositor_get()); GHOST_Context *context = new GHOST_ContextVK(false, GHOST_kVulkanPlatformWayland, 0, nullptr, wl_surface, display_->wl.display, nullptr, 1, 2, debug_context); if (context->initializeDrawingContext()) { context->setUserData(wl_surface); return context; } delete context; if (wl_surface) { wl_surface_destroy(wl_surface); } return nullptr; } #endif /* WITH_VULKAN_BACKEND */ #ifdef WITH_OPENGL_BACKEND case GHOST_kDrawingContextTypeOpenGL: { /* Create new off-screen window. */ wl_surface *wl_surface = wl_compositor_create_surface(wl_compositor_get()); wl_egl_window *egl_window = wl_surface ? wl_egl_window_create(wl_surface, 1, 1) : nullptr; for (int minor = 6; minor >= 3; --minor) { /* Caller must lock `system->server_mutex`. */ GHOST_Context *context = new GHOST_ContextEGL( this, false, EGLNativeWindowType(egl_window), EGLNativeDisplayType(display_->wl.display), EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, 4, minor, GHOST_OPENGL_EGL_CONTEXT_FLAGS | (debug_context ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0), GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY, EGL_OPENGL_API); if (context->initializeDrawingContext()) { wl_surface_set_user_data(wl_surface, egl_window); context->setUserData(wl_surface); return context; } delete context; } GHOST_PRINT("Cannot create off-screen EGL context" << std::endl); if (wl_surface) { wl_surface_destroy(wl_surface); } if (egl_window) { wl_egl_window_destroy(egl_window); } return nullptr; } #endif /* WITH_OPENGL_BACKEND */ default: /* Unsupported backend. */ return nullptr; } } GHOST_TSuccess GHOST_SystemWayland::disposeContext(GHOST_IContext *context) { #ifdef USE_EVENT_BACKGROUND_THREAD std::lock_guard lock_server_guard{*server_mutex}; #endif GHOST_TDrawingContextType type = GHOST_kDrawingContextTypeNone; #ifdef WITH_OPENGL_BACKEND if (dynamic_cast(context)) { type = GHOST_kDrawingContextTypeOpenGL; } #endif /* WITH_OPENGL_BACKEND */ #ifdef WITH_VULKAN_BACKEND if (dynamic_cast(context)) { type = GHOST_kDrawingContextTypeVulkan; } #endif /* WITH_VULKAN_BACKEND */ wl_surface *wl_surface = static_cast( (static_cast(context))->getUserData()); /* Delete the context before the window so the context is able to release * native resources (such as the #EGLSurface) before WAYLAND frees them. */ delete context; #ifdef WITH_OPENGL_BACKEND if (type == GHOST_kDrawingContextTypeOpenGL) { wl_egl_window *egl_window = static_cast(wl_surface_get_user_data(wl_surface)); if (egl_window != nullptr) { wl_egl_window_destroy(egl_window); } } #endif /* WITH_OPENGL_BACKEND */ wl_surface_destroy(wl_surface); (void)type; /* Maybe unused. */ return GHOST_kSuccess; } GHOST_IWindow *GHOST_SystemWayland::createWindow(const char *title, const int32_t left, const int32_t top, const uint32_t width, const uint32_t height, const GHOST_TWindowState state, const GHOST_GPUSettings gpuSettings, const bool exclusive, const bool is_dialog, const GHOST_IWindow *parentWindow) { /* Globally store pointer to window manager. */ GHOST_WindowWayland *window = new GHOST_WindowWayland( this, title, left, top, width, height, state, parentWindow, gpuSettings.context_type, is_dialog, ((gpuSettings.flags & GHOST_gpuStereoVisual) != 0), exclusive, (gpuSettings.flags & GHOST_gpuDebugContext) != 0); if (window) { if (window->getValid()) { m_windowManager->addWindow(window); m_windowManager->setActiveWindow(window); const uint64_t event_ms = getMilliSeconds(); pushEvent(new GHOST_Event(event_ms, GHOST_kEventWindowSize, window)); } else { delete window; window = nullptr; } } return window; } static bool cursor_is_software(const GHOST_TGrabCursorMode mode, const bool use_software_confine) { if (mode == GHOST_kGrabWrap) { return true; } #ifdef USE_GNOME_CONFINE_HACK if (mode == GHOST_kGrabNormal) { if (use_software_confine) { return true; } } #else (void)use_software_confine; #endif return false; } GHOST_TSuccess GHOST_SystemWayland::cursor_shape_set(const GHOST_TStandardCursor shape) { /* Caller must lock `server_mutex`. */ GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } const char *cursor_name = nullptr; const wl_cursor *wl_cursor = gwl_seat_cursor_find_from_shape(seat, shape, &cursor_name); if (wl_cursor == nullptr) { return GHOST_kFailure; } GWL_Cursor *cursor = &seat->cursor; wl_cursor_image *image = wl_cursor->images[0]; wl_buffer *buffer = wl_cursor_image_get_buffer(image); if (!buffer) { return GHOST_kFailure; } cursor->visible = true; cursor->is_custom = false; cursor->wl.buffer = buffer; cursor->wl.image = *image; cursor->wl.theme_cursor = wl_cursor; cursor->wl.theme_cursor_name = cursor_name; gwl_seat_cursor_buffer_set_current(seat); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::cursor_shape_check(const GHOST_TStandardCursor cursorShape) { /* No need to lock `server_mutex`. */ GWL_Seat *seat = gwl_display_seat_active_get(display_); const wl_cursor *wl_cursor = gwl_seat_cursor_find_from_shape(seat, cursorShape, nullptr); if (wl_cursor == nullptr) { return GHOST_kFailure; } return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::cursor_shape_custom_set(const uint8_t *bitmap, const uint8_t *mask, const int sizex, const int sizey, const int hotX, const int hotY, const bool /*canInvertColor*/) { /* Caller needs to lock `server_mutex`. */ GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_Cursor *cursor = &seat->cursor; if (cursor->custom_data) { munmap(cursor->custom_data, cursor->custom_data_size); cursor->custom_data = nullptr; cursor->custom_data_size = 0; /* Not needed, but the value is no longer meaningful. */ } const int32_t size_xy[2] = {sizex, sizey}; wl_buffer *buffer = ghost_wl_buffer_create_for_image(display_->wl.shm, size_xy, WL_SHM_FORMAT_ARGB8888, &cursor->custom_data, &cursor->custom_data_size); if (buffer == nullptr) { return GHOST_kFailure; } wl_buffer_add_listener(buffer, &cursor_buffer_listener, cursor); static constexpr uint32_t black = 0xFF000000; static constexpr uint32_t white = 0xFFFFFFFF; static constexpr uint32_t transparent = 0x00000000; uint8_t datab = 0, maskb = 0; for (int y = 0; y < sizey; ++y) { uint32_t *pixel = &static_cast(cursor->custom_data)[y * sizex]; for (int x = 0; x < sizex; ++x) { if ((x % 8) == 0) { datab = *bitmap++; maskb = *mask++; /* Reverse bit order. */ datab = uint8_t((datab * 0x0202020202ULL & 0x010884422010ULL) % 1023); maskb = uint8_t((maskb * 0x0202020202ULL & 0x010884422010ULL) % 1023); } if (maskb & 0x80) { *pixel++ = (datab & 0x80) ? white : black; } else { *pixel++ = (datab & 0x80) ? white : transparent; } datab <<= 1; maskb <<= 1; } } cursor->visible = true; cursor->is_custom = true; cursor->custom_scale = 1; /* TODO: support Hi-DPI custom cursors. */ cursor->wl.buffer = buffer; cursor->wl.image.width = uint32_t(sizex); cursor->wl.image.height = uint32_t(sizey); cursor->wl.image.hotspot_x = uint32_t(hotX); cursor->wl.image.hotspot_y = uint32_t(hotY); cursor->wl.theme_cursor = nullptr; cursor->wl.theme_cursor_name = nullptr; gwl_seat_cursor_buffer_set_current(seat); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::cursor_bitmap_get(GHOST_CursorBitmapRef *bitmap) { /* Caller must lock `server_mutex`. */ GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } GWL_Cursor *cursor = &seat->cursor; if (cursor->custom_data == nullptr) { return GHOST_kFailure; } if (!cursor->is_custom) { return GHOST_kFailure; } bitmap->data_size[0] = cursor->wl.image.width; bitmap->data_size[1] = cursor->wl.image.height; bitmap->hot_spot[0] = cursor->wl.image.hotspot_x; bitmap->hot_spot[1] = cursor->wl.image.hotspot_y; bitmap->data = static_cast(cursor->custom_data); return GHOST_kSuccess; } GHOST_TSuccess GHOST_SystemWayland::cursor_visibility_set(const bool visible) { /* Caller must lock `server_mutex`. */ GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return GHOST_kFailure; } gwl_seat_cursor_visible_set(seat, visible, seat->cursor.is_hardware, CURSOR_VISIBLE_ALWAYS_SET); return GHOST_kSuccess; } GHOST_TCapabilityFlag GHOST_SystemWayland::getCapabilities() const { return GHOST_TCapabilityFlag( GHOST_CAPABILITY_FLAG_ALL & ~( /* WAYLAND doesn't support accessing the window position. */ GHOST_kCapabilityWindowPosition | /* WAYLAND doesn't support setting the cursor position directly, * this is an intentional choice, forcing us to use a software cursor in this case. */ GHOST_kCapabilityCursorWarp | /* Some drivers don't support front-buffer reading, see: #98462 & #106264. * * NOTE(@ideasman42): the EGL flag `EGL_BUFFER_PRESERVED` is intended request support for * front-buffer reading however in my tests requesting the flag didn't work with AMD, * and it's not even requirement - so we can't rely on this feature being supported. * * Instead of assuming this is not supported, the graphics card driver could be inspected * (enable for NVIDIA for e.g.), but the advantage in supporting this is minimal. * In practice it means an off-screen buffer is used to redraw the window for the * screen-shot and eye-dropper sampling logic, both operations where the overhead * is negligible. */ GHOST_kCapabilityGPUReadFrontBuffer | /* This WAYLAND back-end has not yet implemented desktop color sample. */ GHOST_kCapabilityDesktopSample | /* This WAYLAND back-end has not yet implemented image copy/paste. */ GHOST_kCapabilityClipboardImages)); } bool GHOST_SystemWayland::cursor_grab_use_software_display_get(const GHOST_TGrabCursorMode mode) { /* Caller must lock `server_mutex`. */ const GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return false; } #ifdef USE_GNOME_CONFINE_HACK const bool use_software_confine = seat->use_pointer_software_confine; #else const bool use_software_confine = false; #endif return cursor_is_software(mode, use_software_confine); } #ifdef USE_GNOME_CONFINE_HACK static bool setCursorGrab_use_software_confine(const GHOST_TGrabCursorMode mode, wl_surface *wl_surface) { # ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON if (use_gnome_confine_hack == false) { return false; } # endif if (mode != GHOST_kGrabNormal) { return false; } const GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface); if (!win) { return false; } # ifndef USE_GNOME_CONFINE_HACK_ALWAYS_ON if (win->scale_get() <= 1) { return false; } # endif return true; } #endif static GWL_SeatStateGrab seat_grab_state_from_mode(const GHOST_TGrabCursorMode mode, const bool use_software_confine) { /* Initialize all members. */ GWL_SeatStateGrab grab_state; /* Warping happens to require software cursor which also hides. */ grab_state.use_lock = ELEM(mode, GHOST_kGrabWrap, GHOST_kGrabHide) || use_software_confine; grab_state.use_confine = (mode == GHOST_kGrabNormal) && (use_software_confine == false); return grab_state; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Proxy Ownership API * \{ */ static const char *ghost_wl_output_tag_id = "GHOST-output"; static const char *ghost_wl_surface_tag_id = "GHOST-window"; static const char *ghost_wl_surface_cursor_pointer_tag_id = "GHOST-cursor-pointer"; static const char *ghost_wl_surface_cursor_tablet_tag_id = "GHOST-cursor-tablet"; bool ghost_wl_output_own(const wl_output *wl_output) { const wl_proxy *proxy = reinterpret_cast(wl_output); return wl_proxy_get_tag(const_cast(proxy)) == &ghost_wl_output_tag_id; } bool ghost_wl_surface_own(const wl_surface *wl_surface) { const wl_proxy *proxy = reinterpret_cast(wl_surface); return wl_proxy_get_tag(const_cast(proxy)) == &ghost_wl_surface_tag_id; } bool ghost_wl_surface_own_with_null_check(const wl_surface *wl_surface) { return wl_surface && ghost_wl_surface_own(wl_surface); } bool ghost_wl_surface_own_cursor_pointer(const wl_surface *wl_surface) { const wl_proxy *proxy = reinterpret_cast(wl_surface); return wl_proxy_get_tag(const_cast(proxy)) == &ghost_wl_surface_cursor_pointer_tag_id; } bool ghost_wl_surface_own_cursor_tablet(const wl_surface *wl_surface) { const wl_proxy *proxy = reinterpret_cast(wl_surface); return wl_proxy_get_tag(const_cast(proxy)) == &ghost_wl_surface_cursor_tablet_tag_id; } void ghost_wl_output_tag(wl_output *wl_output) { wl_proxy *proxy = reinterpret_cast(wl_output); wl_proxy_set_tag(proxy, &ghost_wl_output_tag_id); } void ghost_wl_surface_tag(wl_surface *wl_surface) { wl_proxy *proxy = reinterpret_cast(wl_surface); wl_proxy_set_tag(proxy, &ghost_wl_surface_tag_id); } void ghost_wl_surface_tag_cursor_pointer(wl_surface *wl_surface) { wl_proxy *proxy = reinterpret_cast(wl_surface); wl_proxy_set_tag(proxy, &ghost_wl_surface_cursor_pointer_tag_id); } void ghost_wl_surface_tag_cursor_tablet(wl_surface *wl_surface) { wl_proxy *proxy = reinterpret_cast(wl_surface); wl_proxy_set_tag(proxy, &ghost_wl_surface_cursor_tablet_tag_id); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Direct Data Access * * Expose some members via methods. * \{ */ wl_display *GHOST_SystemWayland::wl_display_get() { return display_->wl.display; } wl_compositor *GHOST_SystemWayland::wl_compositor_get() { return display_->wl.compositor; } zwp_primary_selection_device_manager_v1 *GHOST_SystemWayland::wp_primary_selection_manager_get() { return display_->wp.primary_selection_device_manager; } xdg_activation_v1 *GHOST_SystemWayland::xdg_activation_manager_get() { return display_->xdg.activation_manager; } wp_fractional_scale_manager_v1 *GHOST_SystemWayland::wp_fractional_scale_manager_get() { return display_->wp.fractional_scale_manager; } wp_viewporter *GHOST_SystemWayland::wp_viewporter_get() { return display_->wp.viewporter; } zwp_pointer_gestures_v1 *GHOST_SystemWayland::wp_pointer_gestures_get() { return display_->wp.pointer_gestures; } /* This value is expected to match the base name of the `.desktop` file. see #101805. * * NOTE: the XDG desktop-entry-spec defines that this should follow the "reverse DNS" convention. * For e.g. `org.blender.Blender` - however the `.desktop` file distributed with Blender is * simply called `blender.desktop`, so the it's important to follow that name. * Other distributions such as SNAP & FLATPAK may need to change this value #101779. * Currently there isn't a way to configure this, we may want to support that. */ static const char *ghost_wl_app_id = ( #ifdef WITH_GHOST_WAYLAND_APP_ID STRINGIFY(WITH_GHOST_WAYLAND_APP_ID) #else "blender" #endif ); const char *GHOST_SystemWayland::xdg_app_id_get() { return ghost_wl_app_id; } #ifdef WITH_GHOST_WAYLAND_LIBDECOR libdecor *GHOST_SystemWayland::libdecor_context_get() { return display_->libdecor->context; } #endif /* !WITH_GHOST_WAYLAND_LIBDECOR */ xdg_wm_base *GHOST_SystemWayland::xdg_decor_shell_get() { return display_->xdg_decor->shell; } zxdg_decoration_manager_v1 *GHOST_SystemWayland::xdg_decor_manager_get() { return display_->xdg_decor->manager; } /* End `xdg_decor`. */ const std::vector &GHOST_SystemWayland::outputs_get() const { return display_->outputs; } wl_shm *GHOST_SystemWayland::wl_shm_get() const { return display_->wl.shm; } #ifdef USE_EVENT_BACKGROUND_THREAD GHOST_TimerManager *GHOST_SystemWayland::ghost_timer_manager() { return display_->ghost_timer_manager; } #endif /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Text Input (IME) Functions * * Functionality only used for the WAYLAND implementation. * \{ */ #ifdef WITH_INPUT_IME void GHOST_SystemWayland::ime_begin(const GHOST_WindowWayland *win, int32_t x, int32_t y, int32_t w, int32_t h, bool completed) const { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return; } if (seat->wp.text_input == nullptr) { return; } /* Prevent a feedback loop because the commits from this function cause * #zwp_text_input_v3_listener::preedit_string to run again which sends an event, * refreshing the position, running this function again. */ gwl_seat_ime_result_reset(seat); /* Don't re-enable if we're already enabled. */ if (seat->ime.is_enabled && completed) { return; } bool force_rect_update = false; if (seat->ime.is_enabled == false) { seat->ime.has_preedit = false; seat->ime.is_enabled = true; /* NOTE(@flibit): For some reason this has to be done twice, * it appears to be a bug in mutter? Maybe? */ zwp_text_input_v3_enable(seat->wp.text_input); zwp_text_input_v3_commit(seat->wp.text_input); zwp_text_input_v3_enable(seat->wp.text_input); zwp_text_input_v3_commit(seat->wp.text_input); /* Now that it's enabled, set the input properties. */ zwp_text_input_v3_set_content_type(seat->wp.text_input, ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL); gwl_seat_ime_rect_reset(seat); force_rect_update = true; } if ((force_rect_update == false) && /* Was just created, always update. */ (seat->ime.rect.x == x) && /* X. */ (seat->ime.rect.y == y) && /* Y. */ (seat->ime.rect.w == w) && /* W. */ (seat->ime.rect.h == h)) /* H. */ { /* Only re-update the rectangle as needed. */ } else { const int rect_x = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(x))); const int rect_y = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(y))); const int rect_w = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(w))) + 1; const int rect_h = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(h))) + 1; zwp_text_input_v3_set_cursor_rectangle(seat->wp.text_input, rect_x, rect_y, rect_w, rect_h); zwp_text_input_v3_commit(seat->wp.text_input); seat->ime.rect.x = x; seat->ime.rect.y = y; seat->ime.rect.w = w; seat->ime.rect.h = h; } } void GHOST_SystemWayland::ime_end(const GHOST_WindowWayland * /*window*/) const { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return; } seat->ime.is_enabled = false; gwl_seat_ime_rect_reset(seat); if (seat->wp.text_input == nullptr) { return; } zwp_text_input_v3_disable(seat->wp.text_input); zwp_text_input_v3_commit(seat->wp.text_input); } #endif /* WITH_INPUT_IME */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Query Access * \{ */ GWL_Output *ghost_wl_output_user_data(wl_output *wl_output) { GHOST_ASSERT(wl_output, "output must not be nullptr"); GHOST_ASSERT(ghost_wl_output_own(wl_output), "output is not owned by GHOST"); GWL_Output *output = static_cast(wl_output_get_user_data(wl_output)); return output; } GHOST_WindowWayland *ghost_wl_surface_user_data(wl_surface *wl_surface) { GHOST_ASSERT(wl_surface, "wl_surface must not be nullptr"); GHOST_ASSERT(ghost_wl_surface_own(wl_surface), "wl_surface is not owned by GHOST"); GHOST_WindowWayland *win = static_cast( wl_surface_get_user_data(wl_surface)); return win; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Utility Functions * * Functionality only used for the WAYLAND implementation. * \{ */ uint64_t GHOST_SystemWayland::ms_from_input_time(const uint32_t timestamp_as_uint) { /* NOTE(@ideasman42): Return a time compatible with `getMilliSeconds()`, * this is needed as WAYLAND time-stamps don't have a well defined beginning * use `timestamp_as_uint` to calculate an offset which is applied to future events. * This is updated because time may have passed between generating the time-stamp and `now`. * The method here is used by SDL. */ uint64_t timestamp = uint64_t(timestamp_as_uint); GWL_DisplayTimeStamp &input_timestamp = display_->input_timestamp; if (UNLIKELY(timestamp_as_uint < input_timestamp.last)) { /* NOTE(@ideasman42): Sometimes event times are out of order, * while this should _never_ happen, it occasionally does when resizing the window then * clicking on the window with GNOME+LIBDECOR. * Accept events must occur within ~25 days, out-of-order time-stamps above this time-frame * will be treated as a wrapped integer. */ if (input_timestamp.last - timestamp_as_uint > std::numeric_limits::max() / 2) { /* Finally check to avoid invalid rollover, * ensure the rolled over time is closer to "now" than it is currently. */ const uint64_t offset_test = input_timestamp.offset + uint64_t(std::numeric_limits::max()) + 1; const uint64_t now = getMilliSeconds(); if (sub_abs_u64(now, timestamp + offset_test) < sub_abs_u64(now, timestamp + input_timestamp.offset)) { /* 32-bit timer rollover, bump the offset. */ input_timestamp.offset = offset_test; } } } input_timestamp.last = timestamp_as_uint; if (input_timestamp.exact_match) { timestamp += input_timestamp.offset; } else { const uint64_t now = getMilliSeconds(); const uint32_t now_as_uint32 = uint32_t(now); if (now_as_uint32 == timestamp_as_uint) { input_timestamp.exact_match = true; /* For systems with up times exceeding 47 days * it's possible we need to begin with an offset. */ input_timestamp.offset = now - uint64_t(now_as_uint32); timestamp = now; } else { if (!input_timestamp.offset) { input_timestamp.offset = (now - timestamp); } timestamp += input_timestamp.offset; if (timestamp > now) { input_timestamp.offset -= (timestamp - now); timestamp = now; } } } return timestamp; } GHOST_TSuccess GHOST_SystemWayland::pushEvent_maybe_pending(const GHOST_IEvent *event) { #ifdef USE_EVENT_BACKGROUND_THREAD if (main_thread_id != std::this_thread::get_id()) { std::lock_guard lock{display_->events_pending_mutex}; display_->events_pending.push_back(event); return GHOST_kSuccess; } #endif return pushEvent(event); } void GHOST_SystemWayland::seat_active_set(const GWL_Seat *seat) { gwl_display_seat_active_set(display_, seat); } wl_seat *GHOST_SystemWayland::wl_seat_active_get_with_input_serial(uint32_t &serial) { GWL_Seat *seat = gwl_display_seat_active_get(display_); if (seat) { serial = seat->data_source_serial; return seat->wl.seat; } return nullptr; } bool GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface) { bool changed = false; #define SURFACE_CLEAR_PTR(surface_test) \ if (surface_test == wl_surface) { \ surface_test = nullptr; \ changed = true; \ } \ ((void)0); /* Only clear window surfaces (not cursors, off-screen surfaces etc). */ for (GWL_Seat *seat : display_->seats) { SURFACE_CLEAR_PTR(seat->pointer.wl.surface_window); SURFACE_CLEAR_PTR(seat->tablet.wl.surface_window); SURFACE_CLEAR_PTR(seat->keyboard.wl.surface_window); SURFACE_CLEAR_PTR(seat->wl.surface_window_focus_dnd); #ifdef WITH_INPUT_IME SURFACE_CLEAR_PTR(seat->ime.surface_window); #endif } #undef SURFACE_CLEAR_PTR return changed; } bool GHOST_SystemWayland::output_unref(wl_output *wl_output) { bool changed = false; if (!ghost_wl_output_own(wl_output)) { return changed; } /* NOTE: keep in sync with `output_scale_update`. */ GWL_Output *output = ghost_wl_output_user_data(wl_output); const GHOST_WindowManager *window_manager = getWindowManager(); if (window_manager) { for (GHOST_IWindow *iwin : window_manager->getWindows()) { GHOST_WindowWayland *win = static_cast(iwin); if (win->outputs_leave(output)) { win->outputs_changed_update_scale_tag(); changed = true; } } } for (GWL_Seat *seat : display_->seats) { if (seat->pointer.outputs.erase(output)) { changed = true; } if (seat->tablet.outputs.erase(output)) { changed = true; } } return changed; } void GHOST_SystemWayland::output_scale_update(GWL_Output *output) { /* NOTE: keep in sync with `output_unref`. */ GHOST_WindowManager *window_manager = getWindowManager(); if (window_manager) { for (GHOST_IWindow *iwin : window_manager->getWindows()) { GHOST_WindowWayland *win = static_cast(iwin); const std::vector &outputs = win->outputs_get(); if (!(std::find(outputs.begin(), outputs.end(), output) == outputs.cend())) { win->outputs_changed_update_scale_tag(); } } } for (GWL_Seat *seat : display_->seats) { if (seat->pointer.outputs.count(output)) { if (seat->cursor.wl.surface_cursor != nullptr) { update_cursor_scale(seat->cursor, seat->system->wl_shm_get(), &seat->pointer, seat->cursor.wl.surface_cursor); } } if (seat->tablet.outputs.count(output)) { for (zwp_tablet_tool_v2 *zwp_tablet_tool_v2 : seat->wp.tablet_tools) { GWL_TabletTool *tablet_tool = static_cast( zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2)); if (tablet_tool->wl.surface_cursor != nullptr) { update_cursor_scale(seat->cursor, seat->system->wl_shm_get(), &seat->tablet, tablet_tool->wl.surface_cursor); } } } } } bool GHOST_SystemWayland::window_cursor_grab_set(const GHOST_TGrabCursorMode mode, const GHOST_TGrabCursorMode mode_current, int32_t init_grab_xy[2], const GHOST_Rect *wrap_bounds, const GHOST_TAxisFlag wrap_axis, wl_surface *wl_surface, const GWL_WindowScaleParams &scale_params) { /* Caller must lock `server_mutex`. */ /* Ignore, if the required protocols are not supported. */ if (UNLIKELY(!display_->wp.relative_pointer_manager || !display_->wp.pointer_constraints)) { return false; } GWL_Seat *seat = gwl_display_seat_active_get(display_); if (UNLIKELY(!seat)) { return false; } /* No change, success. */ if (mode == mode_current) { return true; } #ifdef USE_GNOME_CONFINE_HACK const bool was_software_confine = seat->use_pointer_software_confine; const bool use_software_confine = setCursorGrab_use_software_confine(mode, wl_surface); #else const bool was_software_confine = false; const bool use_software_confine = false; #endif const GWL_SeatStateGrab grab_state_prev = seat_grab_state_from_mode(mode_current, was_software_confine); const GWL_SeatStateGrab grab_state_next = seat_grab_state_from_mode(mode, use_software_confine); /* Check for wrap as #GHOST_kCapabilityCursorWarp isn't supported. */ const bool use_visible = !(ELEM(mode, GHOST_kGrabHide, GHOST_kGrabWrap) || use_software_confine); const bool is_hardware_cursor = !cursor_is_software(mode, use_software_confine); /* Only hide so the cursor is not made visible before it's location is restored. * This function is called again at the end of this function which only shows. */ gwl_seat_cursor_visible_set(seat, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_HIDE); /* Switching from one grab mode to another, * in this case disable the current locks as it makes logic confusing, * postpone changing the cursor to avoid flickering. */ if (!grab_state_next.use_lock) { if (seat->wp.relative_pointer) { zwp_relative_pointer_v1_destroy(seat->wp.relative_pointer); seat->wp.relative_pointer = nullptr; } if (seat->wp.locked_pointer) { /* Potentially add a motion event so the application has updated X/Y coordinates. */ int32_t xy_motion[2] = {0, 0}; bool xy_motion_create_event = false; /* Request location to restore to. */ if (mode_current == GHOST_kGrabWrap) { /* Since this call is initiated by Blender, we can be sure the window wasn't closed * by logic outside this function - as the window was needed to make this call. */ int32_t xy_next[2] = {UNPACK2(seat->pointer.xy)}; GHOST_Rect bounds_scale; bounds_scale.m_l = gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(wrap_bounds->m_l)); bounds_scale.m_t = gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(wrap_bounds->m_t)); bounds_scale.m_r = gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(wrap_bounds->m_r)); bounds_scale.m_b = gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(wrap_bounds->m_b)); bounds_scale.wrapPoint(UNPACK2(xy_next), 0, wrap_axis); /* Push an event so the new location is registered. */ if ((xy_next[0] != seat->pointer.xy[0]) || (xy_next[1] != seat->pointer.xy[1])) { xy_motion[0] = xy_next[0]; xy_motion[1] = xy_next[1]; xy_motion_create_event = true; } seat->pointer.xy[0] = xy_next[0]; seat->pointer.xy[1] = xy_next[1]; zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp.locked_pointer, UNPACK2(xy_next)); wl_surface_commit(wl_surface); } else if (mode_current == GHOST_kGrabHide) { if ((init_grab_xy[0] != seat->grab_lock_xy[0]) || (init_grab_xy[1] != seat->grab_lock_xy[1])) { const wl_fixed_t xy_next[2] = { gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(init_grab_xy[0])), gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(init_grab_xy[1])), }; zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp.locked_pointer, UNPACK2(xy_next)); wl_surface_commit(wl_surface); /* NOTE(@ideasman42): The new cursor position is a hint, * it's possible the hint is ignored. It doesn't seem like there is a good way to * know if the hint will be used or not, at least not immediately. */ xy_motion[0] = xy_next[0]; xy_motion[1] = xy_next[1]; xy_motion_create_event = true; } } #ifdef USE_GNOME_CONFINE_HACK else if (mode_current == GHOST_kGrabNormal) { if (was_software_confine) { zwp_locked_pointer_v1_set_cursor_position_hint(seat->wp.locked_pointer, UNPACK2(seat->pointer.xy)); wl_surface_commit(wl_surface); } } #endif if (xy_motion_create_event) { /* Caller has no time-stamp. */ const uint64_t event_ms = getMilliSeconds(); seat->system->pushEvent_maybe_pending(new GHOST_EventCursor( event_ms, GHOST_kEventCursorMove, ghost_wl_surface_user_data(wl_surface), wl_fixed_to_int(gwl_window_scale_wl_fixed_to(scale_params, xy_motion[0])), wl_fixed_to_int(gwl_window_scale_wl_fixed_to(scale_params, xy_motion[1])), GHOST_TABLET_DATA_NONE)); } zwp_locked_pointer_v1_destroy(seat->wp.locked_pointer); seat->wp.locked_pointer = nullptr; } } if (!grab_state_next.use_confine) { if (seat->wp.confined_pointer) { zwp_confined_pointer_v1_destroy(seat->wp.confined_pointer); seat->wp.confined_pointer = nullptr; } } if (mode != GHOST_kGrabDisable) { if (grab_state_next.use_lock) { if (!grab_state_prev.use_lock) { /* As WAYLAND does not support setting the cursor coordinates programmatically, * #GHOST_kGrabWrap cannot be supported by positioning the cursor directly. * Instead the cursor is locked in place, using a software cursor that is warped. * Then WAYLAND's #zwp_locked_pointer_v1_set_cursor_position_hint is used to restore * the cursor to the warped location. */ seat->wp.relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer( display_->wp.relative_pointer_manager, seat->wl.pointer); zwp_relative_pointer_v1_add_listener( seat->wp.relative_pointer, &relative_pointer_listener, seat); seat->wp.locked_pointer = zwp_pointer_constraints_v1_lock_pointer( display_->wp.pointer_constraints, wl_surface, seat->wl.pointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); } if (mode == GHOST_kGrabHide) { /* Set the initial position to detect any changes when un-grabbing, * otherwise the unlocked cursor defaults to un-locking in-place. */ init_grab_xy[0] = wl_fixed_to_int( gwl_window_scale_wl_fixed_to(scale_params, seat->pointer.xy[0])); init_grab_xy[1] = wl_fixed_to_int( gwl_window_scale_wl_fixed_to(scale_params, seat->pointer.xy[1])); seat->grab_lock_xy[0] = init_grab_xy[0]; seat->grab_lock_xy[1] = init_grab_xy[1]; } } else if (grab_state_next.use_confine) { if (!grab_state_prev.use_confine) { seat->wp.confined_pointer = zwp_pointer_constraints_v1_confine_pointer( display_->wp.pointer_constraints, wl_surface, seat->wl.pointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); } } } /* Only show so the cursor is made visible as the last step. */ gwl_seat_cursor_visible_set(seat, use_visible, is_hardware_cursor, CURSOR_VISIBLE_ONLY_SHOW); #ifdef USE_GNOME_CONFINE_HACK seat->use_pointer_software_confine = use_software_confine; #endif return true; } #ifdef WITH_GHOST_WAYLAND_LIBDECOR bool GHOST_SystemWayland::use_libdecor_runtime() { return use_libdecor; } #endif #ifdef WITH_GHOST_WAYLAND_DYNLOAD bool ghost_wl_dynload_libraries_init() { # ifdef WITH_GHOST_X11 /* When running in WAYLAND, let the user know when a missing library is the only reason * WAYLAND could not be used. Otherwise it's not obvious why X11 is used as a fallback. * Otherwise when X11 is used, reporting WAYLAND library warnings is unwelcome noise. */ bool verbose = getenv("WAYLAND_DISPLAY") != nullptr; # else bool verbose = true; # endif /* !WITH_GHOST_X11 */ if (wayland_dynload_client_init(verbose) && /* `libwayland-client`. */ wayland_dynload_cursor_init(verbose) && /* `libwayland-cursor`. */ # ifdef WITH_OPENGL_BACKEND wayland_dynload_egl_init(verbose) /* `libwayland-egl`. */ # else true # endif ) { # ifdef WITH_GHOST_WAYLAND_LIBDECOR has_libdecor = wayland_dynload_libdecor_init(verbose); /* `libdecor-0`. */ # endif return true; } wayland_dynload_client_exit(); wayland_dynload_cursor_exit(); # ifdef WITH_OPENGL_BACKEND wayland_dynload_egl_exit(); # endif return false; } void ghost_wl_dynload_libraries_exit() { wayland_dynload_client_exit(); wayland_dynload_cursor_exit(); # ifdef WITH_OPENGL_BACKEND wayland_dynload_egl_exit(); # endif # ifdef WITH_GHOST_WAYLAND_LIBDECOR wayland_dynload_libdecor_exit(); # endif } #endif /* WITH_GHOST_WAYLAND_DYNLOAD */ /** \} */