/* SPDX-FileCopyrightText: 2020-2023 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup GHOST
 */

#include "GHOST_SystemWayland.hh"
#include "GHOST_Context.hh"
#include "GHOST_Event.hh"
#include "GHOST_EventButton.hh"
#include "GHOST_EventCursor.hh"
#include "GHOST_EventDragnDrop.hh"
#include "GHOST_EventKey.hh"
#include "GHOST_EventTrackpad.hh"
#include "GHOST_EventWheel.hh"
#include "GHOST_PathUtils.hh"
#include "GHOST_TimerManager.hh"
#include "GHOST_WaylandUtils.hh"
#include "GHOST_WindowManager.hh"
#include "GHOST_utildefines.hh"

#ifdef WITH_OPENGL_BACKEND
#  include "GHOST_ContextEGL.hh"
#endif

#ifdef WITH_VULKAN_BACKEND
#  include "GHOST_ContextVK.hh"
#endif

#ifdef WITH_INPUT_NDOF
#  include "GHOST_NDOFManagerUnix.hh"
#endif

#ifdef WITH_GHOST_WAYLAND_DYNLOAD
#  include <wayland_dynload_API.h> /* For `ghost_wl_dynload_libraries`. */
#endif

#ifdef WITH_OPENGL_BACKEND
#  ifdef WITH_GHOST_WAYLAND_DYNLOAD
#    include <wayland_dynload_egl.h>
#  endif
#  include <wayland-egl.h>
#endif /* WITH_OPENGL_BACKEND */

#include <algorithm>
#include <atomic>
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <unordered_set>

#ifdef WITH_GHOST_WAYLAND_DYNLOAD
#  include <wayland_dynload_cursor.h>
#endif
#include <wayland-cursor.h>

#include <xkbcommon/xkbcommon-compose.h>
#include <xkbcommon/xkbcommon.h>

/* Generated by `wayland-scanner`. */
#include <fractional-scale-v1-client-protocol.h>
#include <pointer-constraints-unstable-v1-client-protocol.h>
#include <pointer-gestures-unstable-v1-client-protocol.h>
#include <primary-selection-unstable-v1-client-protocol.h>
#include <relative-pointer-unstable-v1-client-protocol.h>
#include <tablet-unstable-v2-client-protocol.h>
#include <viewporter-client-protocol.h>
#include <xdg-activation-v1-client-protocol.h>
#include <xdg-output-unstable-v1-client-protocol.h>
#ifdef WITH_INPUT_IME
#  include <text-input-unstable-v3-client-protocol.h>
#endif

/* Decorations `xdg_decor`. */
#include <xdg-decoration-unstable-v1-client-protocol.h>
#include <xdg-shell-client-protocol.h>
/* End `xdg_decor`. */

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#include <cstdlib> /* For `exit`. */
#include <cstring>
#include <mutex>

#include <pthread.h> /* For setting the thread priority. */

#ifdef HAVE_POLL
#  include <poll.h>
#endif

/* Logging, use `ghost.wl.*` prefix. */
#include "CLG_log.h"

#ifdef USE_EVENT_BACKGROUND_THREAD
#  include "GHOST_TimerTask.hh"
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
static bool use_libdecor = true;
#  ifdef WITH_GHOST_WAYLAND_DYNLOAD
static bool has_libdecor = false;
#  else
static bool has_libdecor = true;
#  endif
#endif

/* -------------------------------------------------------------------- */
/** \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);

static bool xkb_compose_state_feed_and_get_utf8(
    xkb_compose_state *compose_state,
    xkb_state *state,
    const xkb_keycode_t key,
    char r_utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)]);

#ifdef USE_EVENT_BACKGROUND_THREAD
static void gwl_display_event_thread_destroy(GWL_Display *display);

static void ghost_wl_display_lock_without_input(wl_display *wl_display, std::mutex *server_mutex);

/** Default size for pending event vector. */
constexpr size_t events_pending_default_size = 4096 / sizeof(void *);

#endif /* USE_EVENT_BACKGROUND_THREAD */

/** In nearly all cases use `pushEvent_maybe_pending`
 * at least when called from WAYLAND callbacks. */
#define pushEvent DONT_USE

/** \} */

/* -------------------------------------------------------------------- */
/** \name Workaround Compositor Specific Bugs
 * \{ */

/**
 * GNOME (mutter 42.2 had a bug with confine not respecting scale - Hi-DPI), See: #98793.
 * Even though this has been fixed, at time of writing it's not yet in a release.
 * Workaround the problem by implementing confine with a software cursor.
 * While this isn't ideal, it's not adding a lot of overhead as software
 * cursors are already used for warping (which WAYLAND doesn't support).
 */
#define USE_GNOME_CONFINE_HACK
/**
 * Always use software confine (not just in GNOME).
 * Useful for developing with compositors that don't need this workaround.
 */
// #define USE_GNOME_CONFINE_HACK_ALWAYS_ON

#ifdef USE_GNOME_CONFINE_HACK
static bool use_gnome_confine_hack = false;
#endif

/**
 * KDE (plasma 5.26.1) has a bug where the cursor surface needs to be committed
 * (via `wl_surface_commit`) when it was hidden and is being set to visible again, see: #102048.
 * See: https://bugs.kde.org/show_bug.cgi?id=461001
 */
#define USE_KDE_TABLET_HIDDEN_CURSOR_HACK

/**
 * 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<char *>(buffer->data));
  buffer->data = nullptr;
  buffer->data_size = 0;
}

static void gwl_simple_buffer_set_from_string(GWL_SimpleBuffer *buffer, const char *str)
{
  free(const_cast<char *>(buffer->data));
  buffer->data_size = strlen(str);
  char *data = static_cast<char *>(malloc(buffer->data_size));
  std::memcpy(data, str, buffer->data_size);
  buffer->data = data;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Cursor Type
 * \{ */

/**
 * From XKB internals, use for converting a scan-code from WAYLAND to a #xkb_keycode_t.
 * Ideally this wouldn't need a local define.
 */
#define EVDEV_OFFSET 8

/**
 * 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<bool> 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<std::string> types;

  struct {
    /**
     * Prevents freeing after #wl_data_device_listener.leave,
     * before #wl_data_device_listener.drop.
     */
    bool in_use = false;
    /**
     * Bit-mask with available drop options.
     * #WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, #WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE.. etc.
     * The application that initializes the drag may set these depending on modifiers held
     * \note when dragging begins. Currently ghost doesn't make use of these.
     */
    enum wl_data_device_manager_dnd_action source_actions = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE;
    enum wl_data_device_manager_dnd_action action = WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE;
    /** Compatible with #GWL_Seat.xy coordinates. */
    wl_fixed_t xy[2] = {0, 0};
  } dnd;
};

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_DataSource Type
 * \{ */

struct GWL_DataSource {

  /** Wayland core types. */
  struct {
    wl_data_source *source = nullptr;
  } wl;

  GWL_SimpleBuffer buffer_out;
};

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Seat Type (#wl_seat wrapper & associated types)
 * \{ */

/**
 * Data used to implement client-side key-repeat.
 *
 * \note it's important not to store the target window here
 * as it can be closed while the key is repeating,
 * instead use the focused keyboard from #GWL_Seat which is cleared when windows are closed.
 * Therefor keyboard events must always check the window has not been cleared.
 */
struct GWL_KeyRepeatPlayload {
  GWL_Seat *seat = nullptr;

  xkb_keycode_t key_code;

  /** Time this timer started. */
  uint64_t time_ms_init;

  /**
   * Don't cache the `utf8_buf` as this changes based on modifiers which may be pressed
   * while key repeat is enabled.
   */
  struct {
    GHOST_TKey gkey;
  } key_data;
};

/** Internal variables used to track grab-state. */
struct GWL_SeatStateGrab {
  bool use_lock = false;
  bool use_confine = false;
};

/**
 * State of the pointing device (tablet or mouse).
 */
struct GWL_SeatStatePointer {

  /** Wayland core types. */
  struct {
    /**
     * The wl_surface last used with this pointing device
     * (events with this pointing device will be sent here).
     */
    wl_surface *surface_window = nullptr;
  } wl;

  /**
   * High precision coordinates.
   *
   * Mapping to pixels requires the window scale.
   * The following example converts these values to screen coordinates.
   * \code{.cc}
   * const wl_fixed_t scale = win->scale();
   * const int event_xy[2] = {
   *   wl_fixed_to_int(scale * seat_state_pointer->xy[0]),
   *   wl_fixed_to_int(scale * seat_state_pointer->xy[1]),
   * };
   * \endcode
   */
  wl_fixed_t xy[2] = {0, 0};

  /** Outputs on which the cursor is visible. */
  std::unordered_set<const GWL_Output *> outputs;

  int 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<std::string> types;
};

struct GWL_PrimarySelection_DataSource {

  /** Wayland native types. */
  struct {
    zwp_primary_selection_source_v1 *source = nullptr;
  } wp;

  GWL_SimpleBuffer buffer_out;
};

/** Primary selection support. */
struct GWL_PrimarySelection {

  GWL_PrimarySelection_DataSource *data_source = nullptr;
  std::mutex data_source_mutex;

  GWL_PrimarySelection_DataOffer *data_offer = nullptr;
  std::mutex data_offer_mutex;
};

static void gwl_primary_selection_discard_offer(GWL_PrimarySelection *primary)
{
  if (primary->data_offer == nullptr) {
    return;
  }
  zwp_primary_selection_offer_v1_destroy(primary->data_offer->wp.id);
  delete primary->data_offer;
  primary->data_offer = nullptr;
}

static void gwl_primary_selection_discard_source(GWL_PrimarySelection *primary)
{
  GWL_PrimarySelection_DataSource *data_source = primary->data_source;
  if (data_source == nullptr) {
    return;
  }
  gwl_simple_buffer_free_data(&data_source->buffer_out);
  if (data_source->wp.source) {
    zwp_primary_selection_source_v1_destroy(data_source->wp.source);
  }
  delete primary->data_source;
  primary->data_source = nullptr;
}

#ifdef WITH_INPUT_IME
struct GWL_SeatIME {
  /**
   * The surface associated with this text input method.
   *
   * \note Even when null, IME callbacks run and events are generated to ensure
   * the IME state remains consistent & allow for the compositor to assign the surface
   * at any point in time which is then defines `window` IME events are associated with.
   */
  wl_surface *surface_window = nullptr;
  GHOST_TEventImeData event_ime_data = {
      /*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<zwp_tablet_tool_v2 *> tablet_tools;

#ifdef WITH_INPUT_IME
    zwp_text_input_v3 *text_input = nullptr;
#endif
  } wp;

  /** XKB native types. */
  struct {
    xkb_context *context = nullptr;

    /** The compose key table (check for null before use). */
    xkb_compose_table *compose_table = nullptr;

    /** The compose state is expected to use the keyboard `state` (check for null before use). */
    xkb_compose_state *compose_state = nullptr;

    xkb_state *state = nullptr;
    /**
     * Keep a state with no modifiers active, use for symbol lookups.
     */
    xkb_state *state_empty = nullptr;

    /**
     * Keep a state with shift enabled, use to access predictable number access for AZERTY keymaps.
     * If shift is not supported by the key-map, this is set to nullptr.
     */
    xkb_state *state_empty_with_shift = nullptr;
    /**
     * Keep a state with number-lock enabled, use to access predictable key-pad symbols.
     * If number-lock is not supported by the key-map, this is set to nullptr.
     */
    xkb_state *state_empty_with_numlock = nullptr;

    /**
     * The active layout, where a single #xkb_keymap may have multiple layouts.
     */
    xkb_layout_index_t layout_active = 0;
  } xkb;

#ifdef WITH_INPUT_IME
  GWL_SeatIME ime;
#endif

  GHOST_SystemWayland *system = nullptr;

  std::string name;

  /** Use to check if the last cursor input was tablet or pointer. */
  uint32_t cursor_source_serial = 0;

  GWL_SeatStatePointer pointer;
  GWL_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);
  }
}

/** Callback that runs from GHOST's timer. */
static void gwl_seat_key_repeat_timer_fn(GHOST_ITimerTask *task, uint64_t time_ms)
{
  GWL_KeyRepeatPlayload *payload = static_cast<GWL_KeyRepeatPlayload *>(task->getUserData());

  GWL_Seat *seat = payload->seat;
  wl_surface *wl_surface_focus = seat->keyboard.wl.surface_window;
  if (UNLIKELY(wl_surface_focus == nullptr)) {
    return;
  }

  GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus);
  GHOST_SystemWayland *system = seat->system;
  const uint64_t event_ms = payload->time_ms_init + time_ms;
  /* Calculate this value every time in case modifier keys are pressed. */

  char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'};
  if (seat->xkb.compose_state &&
      xkb_compose_state_feed_and_get_utf8(
          seat->xkb.compose_state, seat->xkb.state, payload->key_code, utf8_buf))
  {
    /* `utf8_buf` has been filled by a compose action. */
  }
  else {
    xkb_state_key_get_utf8(seat->xkb.state, payload->key_code, utf8_buf, sizeof(utf8_buf));
  }

  system->pushEvent_maybe_pending(new GHOST_EventKey(
      event_ms, GHOST_kEventKeyDown, win, payload->key_data.gkey, true, utf8_buf));
}

/**
 * \note Caller must lock `timer_mutex`.
 */
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<GWL_KeyRepeatPlayload *>(payload)->time_ms_init = time_now;

#ifdef USE_EVENT_BACKGROUND_THREAD
  GHOST_TimerTask *timer = new GHOST_TimerTask(
      time_now + time_start, time_step, key_repeat_fn, payload);
  seat->key_repeat.timer = timer;
  system->ghost_timer_manager()->addTimer(timer);
#else
  seat->key_repeat.timer = system->installTimer(time_start, time_step, key_repeat_fn, payload);
#endif
}

/**
 * \note The caller must lock `timer_mutex`.
 */
static void gwl_seat_key_repeat_timer_remove(GWL_Seat *seat)
{
  GHOST_SystemWayland *system = seat->system;
#ifdef USE_EVENT_BACKGROUND_THREAD
  system->ghost_timer_manager()->removeTimer(
      static_cast<GHOST_TimerTask *>(seat->key_repeat.timer));
#else
  system->removeTimer(seat->key_repeat.timer);
#endif
  seat->key_repeat.timer = nullptr;
}

#ifdef WITH_INPUT_IME

static void gwl_seat_ime_full_reset(GWL_Seat *seat)
{
  const GWL_SeatIME ime_default{};
  /* Preserve the following members since they represent the state of the connection to Wayland.
   * or which callbacks have run (which shouldn't be reset). */
  wl_surface *surface_window = seat->ime.surface_window;
  const bool is_enabled = seat->ime.is_enabled;
  const bool has_preedit_string_callback = seat->ime.has_preedit_string_callback;
  const bool has_commit_string_callback = seat->ime.has_commit_string_callback;

  seat->ime = ime_default;

  seat->ime.surface_window = surface_window;
  seat->ime.is_enabled = is_enabled;
  seat->ime.has_preedit_string_callback = has_preedit_string_callback;
  seat->ime.has_commit_string_callback = has_commit_string_callback;
}

static void gwl_seat_ime_result_reset(GWL_Seat *seat)
{
  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<GWL_Output *> outputs;
  std::vector<GWL_Seat *> seats;
  /**
   * Support a single active seat at once, this isn't an exact or correct mapping from WAYLAND.
   * Only allow input from different seats, not full concurrent multi-seat support.
   *
   * The main purpose of having an active seat is an alternative from always using the first
   * seat which prevents events from any other seat.
   *
   * NOTE(@ideasman42): This could be extended and developed further extended to support
   * an active seat per window (for 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<const GHOST_IEvent *> events_pending;
  /** Guard against multiple threads accessing `events_pending` at once. */
  std::mutex events_pending_mutex;

  /**
   * A separate timer queue, needed so the WAYLAND thread can lock access.
   * Using the system's #GHOST_Sysem::getTimerManager is not thread safe because
   * access to the timer outside of WAYLAND specific logic will not lock.
   *
   * Needed because #GHOST_System::dispatchEvents fires timers
   * outside of WAYLAND (without locking the `timer_mutex`).
   */
  GHOST_TimerManager *ghost_timer_manager = nullptr;

#endif /* USE_EVENT_BACKGROUND_THREAD */
};

/**
 * Free the #GWL_Display and its related members.
 *
 * \note This may run on a partially initialized struct,
 * so it can't be assumed all members are set.
 */
static void gwl_display_destroy(GWL_Display *display)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  if (display->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<GWL_Seat *>::iterator iter = std::find(
      display->seats.begin(), display->seats.end(), seat);
  const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) :
                                                      -1;
  GHOST_ASSERT(index != -1, "invalid internal state");
  return index;
}

static GWL_Seat *gwl_display_seat_active_get(const GWL_Display *display)
{
  if (UNLIKELY(display->seats.empty())) {
    return nullptr;
  }
  return display->seats[display->seats_active_index];
}

static bool gwl_display_seat_active_set(GWL_Display *display, const GWL_Seat *seat)
{
  if (UNLIKELY(display->seats.empty())) {
    return false;
  }
  const int index = gwl_display_seat_index(display, seat);
  if (index == display->seats_active_index) {
    return false;
  }
  display->seats_active_index = index;
  return true;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_RegistryHandler
 * \{ */

struct GWL_RegisteryAdd_Params {
  uint32_t name = 0;
  /** Index within `gwl_registry_handlers`. */
  int interface_slot = 0;
  uint32_t version = 0;
};

/**
 * Add callback for object registry.
 * \note Any operations that depend on other interfaces being registered must be performed in the
 * #GWL_RegistryHandler_UpdateFn callback as the order interfaces are added is out of our control.
 *
 * \param display: The display which holes a reference to the global object.
 * \param params: Various arguments needed for registration.
 */
using GWL_RegistryHandler_AddFn = void (*)(GWL_Display *display,
                                           const GWL_RegisteryAdd_Params *params);

struct GWL_RegisteryUpdate_Params {
  uint32_t name = 0;
  /** Index within `gwl_registry_handlers`. */
  int interface_slot = 0;
  uint32_t version = 0;

  /** Set to #GWL_RegistryEntry.user_data. */
  void *user_data = nullptr;
};

/**
 * Optional update callback to refresh internal data when another interface has been added/removed.
 *
 * \param display: The display which holes a reference to the global object.
 * \param params: Various arguments needed for updating.
 */
using GWL_RegistryHandler_UpdateFn = void (*)(GWL_Display *display,
                                              const GWL_RegisteryUpdate_Params *params);

/**
 * Remove callback for object registry.
 * \param display: The display which holes a reference to the global object.
 * \param user_data: Optional reference to a sub element of `display`,
 * use for outputs or seats for e.g. when the display may hold multiple references.
 * \param on_exit: Enabled when freeing on exit.
 * When true the consistency of references between objects should be kept valid.
 * Otherwise it can be assumed that all objects will be freed and none will be used again,
 * so there is no need to ensure a valid state.
 */
using GWL_RegistryEntry_RemoveFn = void (*)(GWL_Display *display, void *user_data, bool on_exit);

struct GWL_RegistryHandler {
  /** Pointer to the name (not the name itself), needed as the values aren't set on startup. */
  const char *const *interface_p = nullptr;

  /** Add the interface. */
  GWL_RegistryHandler_AddFn add_fn = nullptr;
  /** Optional update the interface (when other interfaces have been added/removed). */
  GWL_RegistryHandler_UpdateFn update_fn = nullptr;
  /** Remove the interface. */
  GWL_RegistryEntry_RemoveFn remove_fn = nullptr;
};

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_RegistryEntry
 * \{ */

/**
 * Registered global objects can be removed by the compositor,
 * these entries are a registry of objects and callbacks to properly remove them.
 * These are also used to  remove all registered objects before exiting.
 */
struct GWL_RegistryEntry {
  GWL_RegistryEntry *next = nullptr;
  /**
   * Optional pointer passed to `remove_fn`, typically the container in #GWL_Display
   * in cases multiple instances of the same interface are supported.
   */
  void *user_data = nullptr;
  /**
   * A unique identifier used as a handle by `wl_registry_listener.global_remove`.
   */
  uint32_t name = WL_NAME_UNSET;
  /**
   * Version passed by the add callback.
   */
  uint32_t version;
  /**
   * The index in `gwl_registry_handlers`,
   * useful for accessing the interface name (for logging for example).
   */
  int interface_slot = 0;
};

static void gwl_registry_entry_add(GWL_Display *display,
                                   const GWL_RegisteryAdd_Params *params,
                                   void *user_data)
{
  GWL_RegistryEntry *reg = new GWL_RegistryEntry;

  reg->interface_slot = params->interface_slot;
  reg->name = params->name;
  reg->version = params->version;
  reg->user_data = user_data;

  reg->next = display->registry_entry;
  display->registry_entry = reg;
}

static bool gwl_registry_entry_remove_by_name(GWL_Display *display,
                                              uint32_t name,
                                              int *r_interface_slot)
{
  GWL_RegistryEntry *reg = display->registry_entry;
  GWL_RegistryEntry **reg_link_p = &display->registry_entry;
  bool found = false;

  if (r_interface_slot) {
    *r_interface_slot = -1;
  }

  while (reg) {
    if (reg->name == name) {
      GWL_RegistryEntry *reg_next = reg->next;
      const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
          reg->interface_slot);
      handler->remove_fn(display, reg->user_data, false);
      if (r_interface_slot) {
        *r_interface_slot = reg->interface_slot;
      }
      delete reg;
      *reg_link_p = reg_next;
      found = true;
      break;
    }
    reg_link_p = &reg->next;
    reg = reg->next;
  }
  return found;
}

static bool gwl_registry_entry_remove_by_interface_slot(GWL_Display *display,
                                                        int interface_slot,
                                                        bool on_exit)
{
  GWL_RegistryEntry *reg = display->registry_entry;
  GWL_RegistryEntry **reg_link_p = &display->registry_entry;
  bool found = false;

  while (reg) {
    if (reg->interface_slot == interface_slot) {
      GWL_RegistryEntry *reg_next = reg->next;
      const GWL_RegistryHandler *handler = gwl_registry_handler_from_interface_slot(
          interface_slot);
      handler->remove_fn(display, reg->user_data, on_exit);
      delete reg;
      *reg_link_p = reg_next;
      reg = reg_next;
      found = true;
      continue;
    }
    reg_link_p = &reg->next;
    reg = reg->next;
  }
  return found;
}

/**
 * Remove all global objects (on exit).
 */
static void gwl_registry_entry_remove_all(GWL_Display *display)
{
  const bool on_exit = true;

  /* NOTE(@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, &params);
  }
}

/** \} */

/* -------------------------------------------------------------------- */
/** \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

static bool ghost_wayland_log_handler_is_background = false;

/**
 * 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)
{
  /* This is fine in background mode, we will try to fall back to headless GPU context.
   * Happens when render farm process runs without user login session. */
  if (ghost_wayland_log_handler_is_background &&
      (strstr(msg, "error: XDG_RUNTIME_DIR not set in the environment") ||
       strstr(msg, "error: XDG_RUNTIME_DIR is invalid or not set in the environment")))
  {
    return;
  }

  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<std::thread::native_handle_type, pthread_t>();
  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<pthread_t>(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 */
}

#if defined(WITH_GHOST_WAYLAND_LIBDECOR) && defined(WITH_VULKAN_BACKEND)
int memfd_create_sealed_for_vulkan_hack(const char *name)
{
  return memfd_create_sealed(name);
}
#endif

enum {
  GWL_IOR_READ = 1 << 0,
  GWL_IOR_WRITE = 1 << 1,
  GWL_IOR_NO_RETRY = 1 << 2,
};

static int file_descriptor_is_io_ready(int fd, const int flags, const int timeout_ms)
{
  int result;

  GHOST_ASSERT(flags & (GWL_IOR_READ | GWL_IOR_WRITE), "X");

  /* Note: We don't bother to account for elapsed time if we get EINTR */
  do {
#ifdef HAVE_POLL
    pollfd info;

    info.fd = fd;
    info.events = 0;
    if (flags & GWL_IOR_READ) {
      info.events |= POLLIN | POLLPRI;
    }
    if (flags & GWL_IOR_WRITE) {
      info.events |= POLLOUT;
    }
    result = poll(&info, 1, timeout_ms);
#else
    fd_set rfdset, *rfdp = nullptr;
    fd_set wfdset, *wfdp = nullptr;
    struct timeval tv, *tvp = nullptr;

    /* If this assert triggers we'll corrupt memory here */
    GHOST_ASSERT(fd >= 0 && fd < FD_SETSIZE, "X");

    if (flags & GWL_IOR_READ) {
      FD_ZERO(&rfdset);
      FD_SET(fd, &rfdset);
      rfdp = &rfdset;
    }
    if (flags & GWL_IOR_WRITE) {
      FD_ZERO(&wfdset);
      FD_SET(fd, &wfdset);
      wfdp = &wfdset;
    }

    if (timeout_ms >= 0) {
      tv.tv_sec = timeout_ms / 1000;
      tv.tv_usec = (timeout_ms % 1000) * 1000;
      tvp = &tv;
    }

    result = select(fd + 1, rfdp, wfdp, nullptr, tvp);
#endif /* !HAVE_POLL */
  } while (result < 0 && errno == EINTR && !(flags & GWL_IOR_NO_RETRY));

  return result;
}

static int ghost_wl_display_event_pump(wl_display *wl_display)
{
  /* Based on SDL's `Wayland_PumpEvents`. */
  int err;

  /* NOTE: Without this, interactions with window borders via LIBDECOR doesn't function. */
  wl_display_flush(wl_display);

  if (wl_display_prepare_read(wl_display) == 0) {
    /* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */
    if (file_descriptor_is_io_ready(
            wl_display_get_fd(wl_display), GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0)
    {
      err = wl_display_read_events(wl_display);
    }
    else {
      wl_display_cancel_read(wl_display);
      err = 0;
    }
  }
  else {
    err = wl_display_dispatch_pending(wl_display);
  }
  return err;
}

#ifdef USE_EVENT_BACKGROUND_THREAD

static void ghost_wl_display_lock_without_input(wl_display *wl_display, std::mutex *server_mutex)
{
  const int fd = wl_display_get_fd(wl_display);
  int state;
  do {
    state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
    /* Re-check `state` with a lock held, needed to avoid holding the lock. */
    if (state == 0) {
      server_mutex->lock();
      state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
      if (state == 0) {
        break;
      }
    }
  } while (state == 0);
}

static int ghost_wl_display_event_pump_from_thread(wl_display *wl_display,
                                                   const int fd,
                                                   std::mutex *server_mutex)
{
  /* Based on SDL's `Wayland_PumpEvents`. */
  server_mutex->lock();
  int err = 0;
  if (wl_display_prepare_read(wl_display) == 0) {
    bool wait_on_fd = false;
    /* Use #GWL_IOR_NO_RETRY to ensure #SIGINT will break us out of our wait. */
    if (file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0) > 0) {
      err = wl_display_read_events(wl_display);
    }
    else {
      wl_display_cancel_read(wl_display);
      /* Without this, the thread will loop continuously, 100% CPU. */
      wait_on_fd = true;
    }

    server_mutex->unlock();

    if (wait_on_fd) {
      /* Important this runs after unlocking. */
      file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX);
    }
  }
  else {
    server_mutex->unlock();

    /* Wait for input (unlocked, so as not to block other threads). */
    int state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, INT32_MAX);
    /* Re-check `state` with a lock held, needed to avoid holding the lock. */
    if (state > 0) {
      server_mutex->lock();
      state = file_descriptor_is_io_ready(fd, GWL_IOR_READ | GWL_IOR_NO_RETRY, 0);
      if (state > 0) {
        err = wl_display_dispatch_pending(wl_display);
      }
      server_mutex->unlock();
    }
  }

  return err;
}
#endif /* USE_EVENT_BACKGROUND_THREAD */

static size_t ghost_wl_shm_format_as_size(enum wl_shm_format format)
{
  switch (format) {
    case WL_SHM_FORMAT_ARGB8888: {
      return 4;
    }
    default: {
      /* Support other formats as needed. */
      GHOST_ASSERT(0, "Unexpected format passed in!");
      return 4;
    }
  }
}

/**
 * Return a #wl_buffer, ready to have it's data filled in or nullptr in case of failure.
 * The caller is responsible for calling `unmap(buffer_data, buffer_size)`.
 *
 * \param r_buffer_data: The buffer to be filled.
 * \param r_buffer_data_size: The size of `r_buffer_data` in bytes.
 */
static wl_buffer *ghost_wl_buffer_create_for_image(wl_shm *shm,
                                                   const int32_t size_xy[2],
                                                   enum wl_shm_format format,
                                                   void **r_buffer_data,
                                                   size_t *r_buffer_data_size)
{
  const int fd = memfd_create_sealed("ghost-wl-buffer");
  wl_buffer *buffer = nullptr;
  if (fd >= 0) {
    const int32_t buffer_stride = size_xy[0] * ghost_wl_shm_format_as_size(format);
    const int32_t buffer_size = buffer_stride * size_xy[1];
    if (posix_fallocate(fd, 0, buffer_size) == 0) {
      void *buffer_data = mmap(nullptr, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      if (buffer_data != MAP_FAILED) {
        wl_shm_pool *pool = wl_shm_create_pool(shm, fd, buffer_size);
        buffer = wl_shm_pool_create_buffer(pool, 0, UNPACK2(size_xy), buffer_stride, format);
        wl_shm_pool_destroy(pool);
        if (buffer) {
          *r_buffer_data = buffer_data;
          *r_buffer_data_size = size_t(buffer_size);
        }
        else {
          /* Highly unlikely. */
          munmap(buffer_data, buffer_size);
        }
      }
    }
    close(fd);
  }
  return buffer;
}

/**
 * A version of `read` which will read `nbytes` or as many bytes as possible,
 * useful as the LIBC version may `read` less than requested.
 */
static ssize_t read_exhaustive(const int fd, void *data, size_t nbytes)
{
  ssize_t nbytes_read = read(fd, data, nbytes);
  if (nbytes_read > 0) {
    while (nbytes_read < nbytes) {
      const ssize_t nbytes_extra = read(
          fd, static_cast<char *>(data) + nbytes_read, nbytes - nbytes_read);
      if (nbytes_extra > 0) {
        nbytes_read += nbytes_extra;
      }
      else {
        if (UNLIKELY(nbytes_extra < 0)) {
          nbytes_read = nbytes_extra; /* Error. */
        }
        break;
      }
    }
  }
  return nbytes_read;
}

/**
 * Read from `fd` into a buffer which is returned.
 * Use for files where seeking to determine the final size isn't supported (pipes for 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<ByteChunk *>(malloc(sizeof(ByteChunk)));
  {
    ByteChunk **chunk_link_p = &chunk_first;
    ByteChunk *chunk = chunk_first;
    while (true) {
      if (UNLIKELY(chunk == nullptr)) {
        errno = ENOMEM;
        ok = false;
        break;
      }
      chunk->next = nullptr;
      /* Using `read` causes issues with GNOME, see: #106040). */
      const ssize_t len_chunk = read_exhaustive(fd, chunk->data, sizeof(ByteChunk::data));
      if (len_chunk <= 0) {
        if (UNLIKELY(len_chunk < 0)) {
          ok = false;
        }
        if (chunk == chunk_first) {
          chunk_first = nullptr;
        }
        free(chunk);
        break;
      }
      *chunk_link_p = chunk;
      chunk_link_p = &chunk->next;
      len += len_chunk;

      if (len_chunk != sizeof(ByteChunk::data)) {
        break;
      }
      chunk = static_cast<ByteChunk *>(malloc(sizeof(ByteChunk)));
    }
  }

  char *buf = nullptr;
  if (ok) {
    buf = static_cast<char *>(malloc(len + (nil_terminate ? 1 : 0)));
    if (UNLIKELY(buf == nullptr)) {
      errno = ENOMEM;
      ok = false;
    }
  }

  if (ok) {
    *r_len = len;
    if (nil_terminate) {
      buf[len] = '\0';
    }
  }
  else {
    *r_len = 0;
  }

  char *buf_stride = buf;
  while (chunk_first) {
    if (ok) {
      const size_t len_chunk = std::min(len, sizeof(chunk_first->data));
      memcpy(buf_stride, chunk_first->data, len_chunk);
      buf_stride += len_chunk;
      len -= len_chunk;
    }
    ByteChunk *chunk = chunk_first->next;
    free(chunk_first);
    chunk_first = chunk;
  }

  return buf;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Private Cursor API
 * \{ */

static void cursor_buffer_set_surface_impl(const wl_cursor_image *wl_image,
                                           wl_buffer *buffer,
                                           wl_surface *wl_surface,
                                           const int scale)
{
  const int32_t image_size_x = int32_t(wl_image->width);
  const int32_t image_size_y = int32_t(wl_image->height);
  GHOST_ASSERT((image_size_x % scale) == 0 && (image_size_y % scale) == 0,
               "The size must be a multiple of the scale!");

  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<GWL_TabletTool *>(
          zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
      zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2,
                                    seat->tablet.serial,
                                    tablet_tool->wl.surface_cursor,
                                    hotspot_x,
                                    hotspot_y);
#ifdef USE_KDE_TABLET_HIDDEN_CURSOR_HACK
      wl_surface_commit(tablet_tool->wl.surface_cursor);
#endif
    }
  }

  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<GWL_TabletTool *>(
          zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
      cursor_buffer_set_surface_impl(wl_image, buffer, tablet_tool->wl.surface_cursor, scale);
      zwp_tablet_tool_v2_set_cursor(zwp_tablet_tool_v2,
                                    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 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,
    GHOST_IWindow *win,
    const uint64_t event_ms,
    const GWL_KeyboardDepressedState &key_depressed_prev)
{
  /* Separate key up and down into separate passes so key down events always come after key up.
   * Do this so users of GHOST can use the last pressed or released modifier to check
   * if the modifier is held instead of counting modifiers pressed as is done here,
   * this isn't perfect but works well enough in practice. */
  for (int i = 0; i < GHOST_KEY_MODIFIER_NUM; i++) {
    for (int d = seat->key_depressed.mods[i] - key_depressed_prev.mods[i]; d < 0; d++) {
      const GHOST_TKey gkey = GHOST_KEY_MODIFIER_FROM_INDEX(i);
      seat->system->pushEvent_maybe_pending(
          new GHOST_EventKey(event_ms, GHOST_kEventKeyUp, win, gkey, false));

      CLOG_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<GWL_Seat *>(data);
  const uint64_t time = ghost_wl_ms_from_utime_pair(utime_hi, utime_lo);
  const uint64_t event_ms = seat->system->ms_from_input_time(time);

  if (wl_surface *wl_surface_focus = seat->pointer.wl.surface_window) {
    CLOG_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<GWL_Seat *>(data);

  CLOG_INFO(LOG, 2, "send");

  auto write_file_fn = [](GWL_Seat *seat, const int fd) {
    if (UNLIKELY(write(fd,
                       seat->data_source->buffer_out.data,
                       seat->data_source->buffer_out.data_size) < 0))
    {
      CLOG_WARN(LOG, "error writing to clipboard: %s", std::strerror(errno));
    }
    close(fd);
    seat->data_source_mutex.unlock();
  };

  seat->data_source_mutex.lock();
  std::thread write_thread(write_file_fn, seat, fd);
  write_thread.detach();
}

static void data_source_handle_cancelled(void *data, wl_data_source *wl_data_source)
{
  CLOG_INFO(LOG, 2, "cancelled");
  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  GWL_DataSource *data_source = seat->data_source;
  if (seat->data_source->wl.source == wl_data_source) {
    data_source->wl.source = nullptr;
  }

  wl_data_source_destroy(wl_data_source);
}

/**
 * The drag-and-drop operation physically finished.
 *
 * The user performed the drop action. This event does not
 * indicate acceptance, #wl_data_source.cancelled may still be
 * emitted afterwards if the drop destination does not accept any mime type.
 */
static void data_source_handle_dnd_drop_performed(void * /*data*/,
                                                  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<GWL_DataOffer *>(data);
  data_offer->types.insert(mime_type);
}

static void data_offer_handle_source_actions(void *data,
                                             wl_data_offer * /*wl_data_offer*/,
                                             const uint32_t source_actions)
{
  CLOG_INFO(LOG, 2, "source_actions (%u)", source_actions);
  GWL_DataOffer *data_offer = static_cast<GWL_DataOffer *>(data);
  data_offer->dnd.source_actions = (enum wl_data_device_manager_dnd_action)source_actions;
}

static void data_offer_handle_action(void *data,
                                     wl_data_offer * /*wl_data_offer*/,
                                     const uint32_t dnd_action)
{
  CLOG_INFO(LOG, 2, "actions (%u)", dnd_action);
  GWL_DataOffer *data_offer = static_cast<GWL_DataOffer *>(data);
  data_offer->dnd.action = (enum wl_data_device_manager_dnd_action)dnd_action;
}

static const 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<GWL_Seat *>(data);
  const uint64_t event_ms = seat->system->getMilliSeconds();

  std::lock_guard lock{seat->data_offer_dnd_mutex};
  if (seat->data_offer_dnd) {
    wl_data_offer_destroy(seat->data_offer_dnd->wl.id);
    delete seat->data_offer_dnd;
    seat->data_offer_dnd = nullptr;
  }
  /* Clearing complete. */

  /* Handle the new offer. */
  GWL_DataOffer *data_offer = static_cast<GWL_DataOffer *>(wl_data_offer_get_user_data(id));
  if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
    CLOG_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<GWL_Seat *>(data);
  const uint64_t event_ms = seat->system->getMilliSeconds();

  std::lock_guard lock{seat->data_offer_dnd_mutex};
  /* The user may have only dragged over the window decorations. */
  if (seat->data_offer_dnd == nullptr) {
    return;
  }
  CLOG_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<GWL_Seat *>(data);
  const uint64_t event_ms = seat->system->ms_from_input_time(time);

  std::lock_guard lock{seat->data_offer_dnd_mutex};
  /* The user may have only dragged over the window decorations. */
  if (seat->data_offer_dnd == nullptr) {
    return;
  }

  CLOG_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<GWL_Seat *>(data);
  std::lock_guard lock{seat->data_offer_dnd_mutex};

  /* No need to check this for null (as other callbacks do).
   * because the 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<char *>(data_buf));

    CLOG_INFO(LOG, 2, "drop_read_uris mime_receive=%s, data=%s", mime_receive, data.c_str());

    wl_data_offer_finish(data_offer->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) {
      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. */
      const char lf = '\n';

      GHOST_WindowWayland *win = ghost_wl_surface_user_data(wl_surface_window);
      std::vector<std::string> uris;

      size_t pos = 0;
      while (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<GHOST_TStringArray *>(
          malloc(sizeof(GHOST_TStringArray)));
      flist->count = int(uris.size());
      flist->strings = static_cast<uint8_t **>(malloc(uris.size() * sizeof(uint8_t *)));
      for (size_t i = 0; i < uris.size(); i++) {
        flist->strings[i] = reinterpret_cast<uint8_t *>(GHOST_URL_decode_alloc(uris[i].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<GWL_Seat *>(data);
  std::lock_guard lock{seat->data_offer_copy_paste_mutex};
  if (seat->data_offer_copy_paste) {
    wl_data_offer_destroy(seat->data_offer_copy_paste->wl.id);
    delete seat->data_offer_copy_paste;
    seat->data_offer_copy_paste = nullptr;
  }
  /* 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<GWL_DataOffer *>(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<GWL_Cursor *>(data);
  wl_buffer_destroy(wl_buffer);

  if (wl_buffer == cursor->wl.buffer) {
    /* The mapped buffer was from a custom cursor. */
    cursor->wl.buffer = nullptr;
  }
}

static const wl_buffer_listener cursor_buffer_listener = {
    /*release*/ cursor_buffer_handle_release,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (Surface), #wl_surface_listener
 * \{ */

static CLG_LogRef LOG_WL_CURSOR_SURFACE = {"ghost.wl.handle.cursor_surface"};
#define LOG (&LOG_WL_CURSOR_SURFACE)

static bool update_cursor_scale(GWL_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<GWL_Seat *>(data);
  GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_from_cursor_surface(
      seat, wl_surface);
  const GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
  seat_state_pointer->outputs.insert(reg_output);
  update_cursor_scale(seat->cursor, seat->system->wl_shm_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<GWL_Seat *>(data);
  GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_from_cursor_surface(
      seat, wl_surface);
  const GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
  seat_state_pointer->outputs.erase(reg_output);
  update_cursor_scale(seat->cursor, seat->system->wl_shm_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<GWL_Seat *>(data);
  const uint64_t event_ms = seat->system->getMilliSeconds();

  /* Null when just destroyed. */
  if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
    CLOG_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<GWL_Seat *>(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<GWL_Seat *>(data);
  const uint64_t event_ms = seat->system->ms_from_input_time(time);

  seat->pointer.xy[0] = surface_x;
  seat->pointer.xy[1] = surface_y;

  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<GWL_Seat *>(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<GWL_Seat *>(data);
  seat->pointer_scroll.event_ms = seat->system->ms_from_input_time(time);
  seat->pointer_scroll.has_event_ms = true;

  /* NOTE: this is used for touch based scrolling - or other input that doesn't scroll with
   * discrete "steps". This allows supporting smooth-scrolling without "touch" gesture support. */
  CLOG_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<GWL_Seat *>(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<GWL_Seat *>(data);
  seat->pointer_scroll.axis_source = (enum wl_pointer_axis_source)axis_source;
}
static void pointer_handle_axis_stop(void *data,
                                     wl_pointer * /*wl_pointer*/,
                                     uint32_t time,
                                     uint32_t axis)
{
  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  seat->pointer_scroll.event_ms = seat->system->ms_from_input_time(time);
  seat->pointer_scroll.has_event_ms = true;

  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<GWL_Seat *>(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<GWL_Seat *>(data);
  seat->pointer_scroll.inverted_xy[index] = (direction ==
                                             WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED);
}
#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */

static const wl_pointer_listener pointer_listener = {
    /*enter*/ pointer_handle_enter,
    /*leave*/ pointer_handle_leave,
    /*motion*/ pointer_handle_motion,
    /*button*/ pointer_handle_button,
    /*axis*/ pointer_handle_axis,
    /*frame*/ pointer_handle_frame,
    /*axis_source*/ pointer_handle_axis_source,
    /*axis_stop*/ pointer_handle_axis_stop,
    /*axis_discrete*/ pointer_handle_axis_discrete,
    /*axis_value120*/ pointer_handle_axis_value120,
#ifdef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM
    /*axis_relative_direction*/ pointer_handle_axis_relative_direction,
#endif
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (Pointer Gesture: Hold), #zwp_pointer_gesture_hold_v1_listener
 * \{ */

#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
static CLG_LogRef LOG_WL_POINTER_GESTURE_HOLD = {"ghost.wl.handle.pointer_gesture.hold"};
#  define LOG (&LOG_WL_POINTER_GESTURE_HOLD)

static void gesture_hold_handle_begin(
    void * /*data*/,
    zwp_pointer_gesture_hold_v1 * /*zwp_pointer_gesture_hold_v1*/,
    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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_Seat *>(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<GWL_TabletTool *>(data);

  tablet_tool->data.Active = tablet_tool_map_type((enum zwp_tablet_tool_v2_type)tool_type);
}

static void tablet_tool_handle_hardware_serial(void * /*data*/,
                                               zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
                                               const uint32_t /*hardware_serial_hi*/,
                                               const uint32_t /*hardware_serial_lo*/)
{
  CLOG_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<GWL_TabletTool *>(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<GWL_TabletTool *>(data);
  tablet_tool->proximity = true;

  GWL_Seat *seat = tablet_tool->seat;
  seat->cursor_source_serial = serial;
  seat->tablet.wl.surface_window = wl_surface;
  seat->tablet.serial = serial;

  seat->data_source_serial = serial;

  seat->system->seat_active_set(seat);

  /* Update #GHOST_TabletData. */
  GHOST_TabletData &td = tablet_tool->data;
  /* Reset, to avoid using stale tilt/pressure. */
  td.Xtilt = 0.0f;
  td.Ytilt = 0.0f;
  /* In case pressure isn't supported. */
  td.Pressure = 1.0f;

  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<GWL_TabletTool *>(data);
  /* Defer clearing the wl_surface until the frame is handled.
   * Without this, the frame can not access the wl_surface. */
  tablet_tool->proximity = false;
}

static void tablet_tool_handle_down(void *data,
                                    zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
                                    const uint32_t serial)
{
  GWL_TabletTool *tablet_tool = static_cast<GWL_TabletTool *>(data);
  GWL_Seat *seat = tablet_tool->seat;

  CLOG_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<GWL_TabletTool *>(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<GWL_TabletTool *>(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<GWL_TabletTool *>(data);
  GHOST_TabletData &td = tablet_tool->data;
  td.Pressure = pressure_unit;

  gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Pressure);
}

static void tablet_tool_handle_distance(void * /*data*/,
                                        zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
                                        const uint32_t distance)
{
  CLOG_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<GWL_TabletTool *>(data);
  GHOST_TabletData &td = tablet_tool->data;
  td.Xtilt = std::clamp(tilt_unit[0], -1.0f, 1.0f);
  td.Ytilt = std::clamp(tilt_unit[1], -1.0f, 1.0f);

  gwl_tablet_tool_frame_event_add(tablet_tool, GWL_TabletTool_EventTypes::Tilt);
}

static void tablet_tool_handle_rotation(void * /*data*/,
                                        zwp_tablet_tool_v2 * /*zwp_tablet_tool_v2*/,
                                        const wl_fixed_t degrees)
{
  CLOG_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<GWL_TabletTool *>(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<GWL_TabletTool *>(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<GWL_TabletTool *>(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<GWL_Seat *>(data);
  GWL_TabletTool *tablet_tool = new GWL_TabletTool();
  tablet_tool->seat = seat;

  /* Every tool has its own cursor wl_surface. */
  tablet_tool->wl.surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor_get());
  ghost_wl_surface_tag_cursor_tablet(tablet_tool->wl.surface_cursor);

  wl_surface_add_listener(
      tablet_tool->wl.surface_cursor, &cursor_surface_listener, static_cast<void *>(seat));

  zwp_tablet_tool_v2_add_listener(id, &tablet_tool_listner, tablet_tool);

  seat->wp.tablet_tools.insert(id);
}

static void tablet_seat_handle_pad_added(void * /*data*/,
                                         zwp_tablet_seat_v2 * /*zwp_tablet_seat_v2*/,
                                         zwp_tablet_pad_v2 *id)
{
  CLOG_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<GWL_Seat *>(data);

  if ((!data) || (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)) {
    CLOG_INFO(LOG, 2, "keymap (no data or wrong version)");
    close(fd);
    return;
  }

  char *map_str = static_cast<char *>(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0));
  if (map_str == MAP_FAILED) {
    close(fd);
    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<GWL_Seat *>(data);
  GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface);

  seat->keyboard.serial = serial;
  seat->keyboard.wl.surface_window = wl_surface;

  seat->system->seat_active_set(seat);

  /* If there are any keys held when activating the window,
   * modifiers will be compared against the seat state,
   * only enabling modifiers that were previously disabled. */
  GWL_KeyboardDepressedState key_depressed_prev = seat->key_depressed;
  keyboard_depressed_state_reset(seat);

  /* Keep track of the last held repeating key, start the repeat timer if one exists. */
  struct {
    uint32_t key = std::numeric_limits<uint32_t>::max();
    xkb_keysym_t sym = 0;
  } repeat;

  uint32_t *key;
  WL_ARRAY_FOR_EACH (key, keys) {
    const xkb_keycode_t key_code = *key + EVDEV_OFFSET;
    CLOG_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);
    }

    if (xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb.state), key_code)) {
      repeat.key = *key;
      repeat.sym = sym;
    }
  }

  /* Caller has no time-stamp, set from system. */
  const uint64_t event_ms = seat->system->getMilliSeconds();
  keyboard_depressed_state_push_events_from_change(seat, win, event_ms, key_depressed_prev);

  if ((repeat.key != std::numeric_limits<uint32_t>::max()) && (seat->key_repeat.rate > 0)) {
    /* Since the key has been held, immediately send a press event.
     * This also ensures the key will be registered as pressed, see #117896. */
#ifdef USE_EVENT_BACKGROUND_THREAD
    std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
    /* Should have been cleared on leave, set here just in case. */
    if (UNLIKELY(seat->key_repeat.timer)) {
      keyboard_handle_key_repeat_cancel(seat);
    }

    const xkb_keycode_t key_code = repeat.key + EVDEV_OFFSET;
    const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(repeat.sym, repeat.key);

    GWL_KeyRepeatPlayload *key_repeat_payload = new GWL_KeyRepeatPlayload();
    key_repeat_payload->seat = seat;
    key_repeat_payload->key_code = key_code;
    key_repeat_payload->key_data.gkey = gkey;

    gwl_seat_key_repeat_timer_add(seat, gwl_seat_key_repeat_timer_fn, key_repeat_payload, false);
    /* Ensure there is a press event on enter so this is known to be held before any mouse
     * button events which may use a key-binding that depends on this key being held. */
    gwl_seat_key_repeat_timer_fn(seat->key_repeat.timer, 0);
  }
}

/**
 * Leave event.
 *
 * Notification that this seat's keyboard focus is no longer on a certain wl_surface.
 */
static void keyboard_handle_leave(void *data,
                                  wl_keyboard * /*wl_keyboard*/,
                                  const uint32_t /*serial*/,
                                  wl_surface *wl_surface)
{
  if (!ghost_wl_surface_own_with_null_check(wl_surface)) {
    CLOG_INFO(LOG, 2, "leave (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "leave");

  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  seat->keyboard.wl.surface_window = nullptr;

  {
#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 `<Compose><Backspace>` 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<GWL_KeyRepeatPlayload *>(seat->key_repeat.timer->getUserData());

  gwl_seat_key_repeat_timer_remove(seat);
}

/**
 * Restart the key-repeat timer.
 * \param use_delay: When false, use the interval
 * (prevents pause when the setting changes while the key is held).
 *
 * \note Caller must lock `timer_mutex`.
 */
static void keyboard_handle_key_repeat_reset(GWL_Seat *seat, const bool use_delay)
{
  GHOST_ASSERT(seat->key_repeat.timer != nullptr, "Caller much check for timer");
  GHOST_TimerProcPtr key_repeat_fn = seat->key_repeat.timer->getTimerProc();
  GHOST_TUserDataPtr payload = seat->key_repeat.timer->getUserData();

  gwl_seat_key_repeat_timer_remove(seat);
  gwl_seat_key_repeat_timer_add(seat, key_repeat_fn, payload, use_delay);
}

static void keyboard_handle_key(void *data,
                                wl_keyboard * /*wl_keyboard*/,
                                const uint32_t serial,
                                const uint32_t time,
                                const uint32_t key,
                                const uint32_t state)
{
  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  const uint64_t event_ms = seat->system->ms_from_input_time(time);

  const xkb_keycode_t key_code = key + EVDEV_OFFSET;

  const xkb_keysym_t sym = xkb_state_key_get_one_sym_without_modifiers(
      seat->xkb.state_empty,
      seat->xkb.state_empty_with_numlock,
      seat->xkb.state_empty_with_shift,
#ifdef USE_NON_LATIN_KB_WORKAROUND
      seat->xkb_use_non_latin_workaround,
#else
      false,
#endif
      key_code);
  if (sym == XKB_KEY_NoSymbol) {
    CLOG_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<GWL_KeyRepeatPlayload *>(
        seat->key_repeat.timer->getUserData());

    if (seat->key_repeat.rate == 0) {
      /* Repeat was disabled (unlikely but possible). */
      timer_action = CANCEL;
    }
    else if (key_code == key_repeat_payload->key_code) {
      /* Releasing the key that was held always cancels. */
      timer_action = CANCEL;
    }
    else if (xkb_keymap_key_repeats(xkb_state_get_keymap(seat->xkb.state), key_code)) {
      if (etype == GHOST_kEventKeyDown) {
        /* Any other key-down always cancels (and may start its own repeat timer). */
        timer_action = CANCEL;
      }
      else {
        /* Key-up from keys that were not repeating cause the repeat timer to pause.
         *
         * NOTE(@ideasman42): This behavior isn't universal, some text input systems will
         * stop the repeat entirely. Choose to pause repeat instead as this is what GTK/WIN32 do,
         * and it fits better for keyboard input that isn't related to text entry. */
        timer_action = RESET;
      }
    }

    switch (timer_action) {
      case NOP: {
        /* Don't add a new timer, leave the existing timer owning this `key_repeat_payload`. */
        key_repeat_payload = nullptr;
        break;
      }
      case RESET: {
        /* The payload will be added again. */
        gwl_seat_key_repeat_timer_remove(seat);
        break;
      }
      case CANCEL: {
        delete key_repeat_payload;
        key_repeat_payload = nullptr;
        gwl_seat_key_repeat_timer_remove(seat);
        break;
      }
    }
  }

  const GHOST_TKey gkey = xkb_map_gkey_or_scan_code(sym, key);

  char utf8_buf[sizeof(GHOST_TEventKeyData::utf8_buf)] = {'\0'};
  if (etype == GHOST_kEventKeyDown) {
    /* Handle key-compose (dead-keys). */
    if (seat->xkb.compose_state &&
        xkb_compose_state_feed_and_get_utf8(
            seat->xkb.compose_state, seat->xkb.state, key_code, utf8_buf))
    {
      /* `utf8_buf` has been filled by a compose action. */
    }
    else {
      xkb_state_key_get_utf8(seat->xkb.state, key_code, utf8_buf, sizeof(utf8_buf));
    }
  }

  seat->data_source_serial = serial;

  keyboard_depressed_state_key_event(seat, gkey, etype);

  if (wl_surface *wl_surface_focus = seat->keyboard.wl.surface_window) {
    GHOST_IWindow *win = ghost_wl_surface_user_data(wl_surface_focus);
    seat->system->pushEvent_maybe_pending(
        new GHOST_EventKey(event_ms, etype, win, gkey, 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) {
    gwl_seat_key_repeat_timer_add(seat, gwl_seat_key_repeat_timer_fn, key_repeat_payload, true);
  }
}

static void keyboard_handle_modifiers(void *data,
                                      wl_keyboard * /*wl_keyboard*/,
                                      const uint32_t serial,
                                      const uint32_t mods_depressed,
                                      const uint32_t mods_latched,
                                      const uint32_t mods_locked,
                                      const uint32_t group)
{
  CLOG_INFO(LOG,
            2,
            "modifiers (depressed=%u, latched=%u, locked=%u, group=%u)",
            mods_depressed,
            mods_latched,
            mods_locked,
            group);

  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  xkb_state_update_mask(seat->xkb.state, mods_depressed, mods_latched, mods_locked, 0, 0, group);

  /* 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<GWL_Seat *>(data);
  seat->key_repeat.rate = rate;
  seat->key_repeat.delay = delay;

  {
#ifdef USE_EVENT_BACKGROUND_THREAD
    std::lock_guard lock_timer_guard{*seat->system->timer_mutex};
#endif
    /* Unlikely possible this setting changes while repeating. */
    if (seat->key_repeat.timer) {
      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<GWL_PrimarySelection_DataOffer *>(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<GWL_PrimarySelection *>(data);

  std::lock_guard lock{primary->data_offer_mutex};

  /* Delete old data offer. */
  if (primary->data_offer != nullptr) {
    gwl_primary_selection_discard_offer(primary);
  }

  if (id == nullptr) {
    CLOG_INFO(LOG, 2, "selection: (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "selection");
  /* Get new data offer. */
  GWL_PrimarySelection_DataOffer *data_offer = static_cast<GWL_PrimarySelection_DataOffer *>(
      zwp_primary_selection_offer_v1_get_user_data(id));
  primary->data_offer = data_offer;
}

static const 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<GWL_PrimarySelection *>(data);

  auto write_file_fn = [](GWL_PrimarySelection *primary, const int fd) {
    if (UNLIKELY(write(fd,
                       primary->data_source->buffer_out.data,
                       primary->data_source->buffer_out.data_size) < 0))
    {
      CLOG_WARN(LOG, "error writing to primary clipboard: %s", std::strerror(errno));
    }
    close(fd);
    primary->data_source_mutex.unlock();
  };

  primary->data_source_mutex.lock();
  std::thread write_thread(write_file_fn, primary, fd);
  write_thread.detach();
}

static void primary_selection_source_cancelled(void *data, zwp_primary_selection_source_v1 *source)
{
  CLOG_INFO(LOG, 2, "cancelled");

  GWL_PrimarySelection *primary = static_cast<GWL_PrimarySelection *>(data);

  if (source == primary->data_source->wp.source) {
    gwl_primary_selection_discard_source(primary);
  }
}

static const 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<GWL_Seat *>(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<GWL_Seat *>(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 : "<null>",
            cursor_begin,
            cursor_end);

  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  if (seat->ime.has_preedit == false) {
    /* Starting IME input. */
    gwl_seat_ime_full_reset(seat);
  }

  seat->ime.composite_is_null = (text == nullptr);
  if (!seat->ime.composite_is_null) {
    seat->ime.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 : "<null>");

  GWL_Seat *seat = static_cast<GWL_Seat *>(data);
  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<GWL_Seat *>(data);
  GHOST_SystemWayland *system = seat->system;
  const uint64_t event_ms = seat->system->getMilliSeconds();

  CLOG_INFO(LOG, 2, "done");

  GHOST_WindowWayland *win = seat->ime.surface_window ?
                                 ghost_wl_surface_user_data(seat->ime.surface_window) :
                                 nullptr;
  if (seat->ime.has_commit_string_callback) {
    if (seat->ime.has_preedit) {
      const bool is_end = seat->ime.composite_is_null;
      if (is_end) {
        seat->ime.has_preedit = false;
        /* `commit_string` (end). */
        system->pushEvent_maybe_pending(new GHOST_EventIME(
            event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data));
        system->pushEvent_maybe_pending(new GHOST_EventIME(
            event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data));
      }
      else {
        /* `commit_string` (continues). */
        system->pushEvent_maybe_pending(new GHOST_EventIME(
            event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data));
      }
    }
    else {
      /* `commit_string` ran with no active IME popup, start & end to insert text. */
      system->pushEvent_maybe_pending(new GHOST_EventIME(
          event_ms, GHOST_kEventImeCompositionStart, win, &seat->ime.event_ime_data));
      system->pushEvent_maybe_pending(new GHOST_EventIME(
          event_ms, GHOST_kEventImeComposition, win, &seat->ime.event_ime_data));
      system->pushEvent_maybe_pending(new GHOST_EventIME(
          event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data));
    }

    if (seat->ime.has_preedit == false) {
      gwl_seat_ime_preedit_reset(seat);
    }
  }
  else if (seat->ime.has_preedit_string_callback) {
    const bool is_end = seat->ime.composite_is_null;
    if (is_end) {
      /* `preedit_string` (end). */
      seat->ime.has_preedit = false;
      system->pushEvent_maybe_pending(new GHOST_EventIME(
          event_ms, GHOST_kEventImeCompositionEnd, win, &seat->ime.event_ime_data));
    }
    else {
      const bool is_start = seat->ime.has_preedit == false;
      /* `preedit_string` (start or continue). */
      seat->ime.has_preedit = true;
      system->pushEvent_maybe_pending(new GHOST_EventIME(
          event_ms,
          is_start ? GHOST_kEventImeCompositionStart : GHOST_kEventImeComposition,
          win,
          &seat->ime.event_ime_data));
    }
  }

  seat->ime.has_preedit_string_callback = false;
  seat->ime.has_commit_string_callback = false;
}

static zwp_text_input_v3_listener text_input_listener = {
    /*enter*/ text_input_handle_enter,
    /*leave*/ text_input_handle_leave,
    /*preedit_string*/ text_input_handle_preedit_string,
    /*commit_string*/ text_input_handle_commit_string,
    /*delete_surrounding_text*/ text_input_handle_delete_surrounding_text,
    /*done*/ text_input_handle_done,
};

#  undef LOG

#endif /* WITH_INPUT_IME. */

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (Seat), #wl_seat_listener
 * \{ */

static CLG_LogRef LOG_WL_SEAT = {"ghost.wl.handle.seat"};
#define LOG (&LOG_WL_SEAT)

static bool gwl_seat_capability_pointer_multitouch_check(const GWL_Seat *seat, const bool fallback)
{
  zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get();
  if (pointer_gestures == nullptr) {
    return fallback;
  }

  bool found = false;
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
  if (seat->wp.pointer_gesture_hold) {
    return true;
  }
  found = true;
#endif
#ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE
  if (seat->wp.pointer_gesture_pinch) {
    return true;
  }
  found = true;
#endif
#ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE
  if (seat->wp.pointer_gesture_swipe) {
    return true;
  }
  found = true;
#endif
  if (found == false) {
    return fallback;
  }
  return false;
}

static void gwl_seat_capability_pointer_multitouch_enable(GWL_Seat *seat)
{
  zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get();
  if (pointer_gestures == nullptr) {
    return;
  }

  const uint pointer_gestures_version = zwp_pointer_gestures_v1_get_version(pointer_gestures);
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
  if (pointer_gestures_version >= ZWP_POINTER_GESTURES_V1_GET_HOLD_GESTURE_SINCE_VERSION)
  { /* Hold gesture. */
    zwp_pointer_gesture_hold_v1 *gesture = zwp_pointer_gestures_v1_get_hold_gesture(
        pointer_gestures, seat->wl.pointer);
    zwp_pointer_gesture_hold_v1_set_user_data(gesture, seat);
    zwp_pointer_gesture_hold_v1_add_listener(gesture, &gesture_hold_listener, seat);
    seat->wp.pointer_gesture_hold = gesture;
  }
#endif
#ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE
  { /* Pinch gesture. */
    zwp_pointer_gesture_pinch_v1 *gesture = zwp_pointer_gestures_v1_get_pinch_gesture(
        pointer_gestures, seat->wl.pointer);
    zwp_pointer_gesture_pinch_v1_set_user_data(gesture, seat);
    zwp_pointer_gesture_pinch_v1_add_listener(gesture, &gesture_pinch_listener, seat);
    seat->wp.pointer_gesture_pinch = gesture;
  }
#endif
#ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE
  { /* Swipe gesture. */
    zwp_pointer_gesture_swipe_v1 *gesture = zwp_pointer_gestures_v1_get_swipe_gesture(
        pointer_gestures, seat->wl.pointer);
    zwp_pointer_gesture_swipe_v1_set_user_data(gesture, seat);
    zwp_pointer_gesture_swipe_v1_add_listener(gesture, &gesture_swipe_listener, seat);
    seat->wp.pointer_gesture_swipe = gesture;
  }
#endif
}

static void gwl_seat_capability_pointer_multitouch_disable(GWL_Seat *seat)
{
  const zwp_pointer_gestures_v1 *pointer_gestures = seat->system->wp_pointer_gestures_get();
  if (pointer_gestures == nullptr) {
    return;
  }
#ifdef ZWP_POINTER_GESTURE_HOLD_V1_INTERFACE
  { /* Hold gesture. */
    zwp_pointer_gesture_hold_v1 **gesture_p = &seat->wp.pointer_gesture_hold;
    if (*gesture_p) {
      zwp_pointer_gesture_hold_v1_destroy(*gesture_p);
      *gesture_p = nullptr;
    }
  }
#endif
#ifdef ZWP_POINTER_GESTURE_PINCH_V1_INTERFACE
  { /* Pinch gesture. */
    zwp_pointer_gesture_pinch_v1 **gesture_p = &seat->wp.pointer_gesture_pinch;
    if (*gesture_p) {
      zwp_pointer_gesture_pinch_v1_destroy(*gesture_p);
      *gesture_p = nullptr;
    }
  }
#endif
#ifdef ZWP_POINTER_GESTURE_SWIPE_V1_INTERFACE
  { /* Swipe gesture. */
    zwp_pointer_gesture_swipe_v1 **gesture_p = &seat->wp.pointer_gesture_swipe;
    if (*gesture_p) {
      zwp_pointer_gesture_swipe_v1_destroy(*gesture_p);
      *gesture_p = nullptr;
    }
  }
#endif
}

static void gwl_seat_capability_pointer_enable(GWL_Seat *seat)
{
  if (seat->wl.pointer) {
    return;
  }
  seat->wl.pointer = wl_seat_get_pointer(seat->wl.seat);
  seat->cursor.wl.surface_cursor = wl_compositor_create_surface(seat->system->wl_compositor_get());
  seat->cursor.visible = true;
  seat->cursor.wl.buffer = nullptr;
  {
    /* 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);

  gwl_seat_capability_pointer_multitouch_enable(seat);
}

static void gwl_seat_capability_pointer_disable(GWL_Seat *seat)
{
  if (!seat->wl.pointer) {
    return;
  }

  gwl_seat_capability_pointer_multitouch_disable(seat);

  if (seat->cursor.wl.surface_cursor) {
    wl_surface_destroy(seat->cursor.wl.surface_cursor);
    seat->cursor.wl.surface_cursor = nullptr;
  }
  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<GWL_Seat *>(data);
  GHOST_ASSERT(seat->wl.seat == wl_seat, "Seat mismatch");

  if (capabilities & WL_SEAT_CAPABILITY_POINTER) {
    gwl_seat_capability_pointer_enable(seat);
  }
  else {
    gwl_seat_capability_pointer_disable(seat);
  }

  if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
    gwl_seat_capability_keyboard_enable(seat);
  }
  else {
    gwl_seat_capability_keyboard_disable(seat);
  }

  if (capabilities & WL_SEAT_CAPABILITY_TOUCH) {
    gwl_seat_capability_touch_enable(seat);
  }
  else {
    gwl_seat_capability_touch_disable(seat);
  }
}

static void seat_handle_name(void *data, wl_seat * /*wl_seat*/, const char *name)
{
  CLOG_INFO(LOG, 2, "name (name=\"%s\")", name);
  static_cast<GWL_Seat *>(data)->name = std::string(name);
}

static const wl_seat_listener seat_listener = {
    /*capabilities*/ seat_handle_capabilities,
    /*name*/ seat_handle_name,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Output), #zxdg_output_v1_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_OUTPUT = {"ghost.wl.handle.xdg_output"};
#define LOG (&LOG_WL_XDG_OUTPUT)

static void xdg_output_handle_logical_position(void *data,
                                               zxdg_output_v1 * /*xdg_output*/,
                                               const int32_t x,
                                               const int32_t y)
{
  CLOG_INFO(LOG, 2, "logical_position [%d, %d]", x, y);

  GWL_Output *output = static_cast<GWL_Output *>(data);
  output->position_logical[0] = x;
  output->position_logical[1] = y;
  output->has_position_logical = true;
}

static void xdg_output_handle_logical_size(void *data,
                                           zxdg_output_v1 * /*xdg_output*/,
                                           const int32_t width,
                                           const int32_t height)
{
  CLOG_INFO(LOG, 2, "logical_size [%d, %d]", width, height);

  GWL_Output *output = static_cast<GWL_Output *>(data);
  if (output->size_logical[0] != 0 && output->size_logical[1] != 0) {
    /* Original comment from SDL. */
    /* FIXME(@flibit): GNOME has a bug where the logical size does not account for
     * scale, resulting in bogus viewport sizes.
     *
     * Until this is fixed, validate that _some_ kind of scaling is being
     * done (we can't match exactly because fractional scaling can't be
     * detected otherwise), then override if necessary. */
    if ((output->size_logical[0] == width) &&
        (output->scale_fractional == (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<GWL_Output *>(data);
  if (zxdg_output_v1_get_version(output->xdg.output) < 3) {
    output_handle_done(data, output->wl.output);
  }
}

static void xdg_output_handle_name(void * /*data*/,
                                   zxdg_output_v1 * /*xdg_output*/,
                                   const char *name)
{
  CLOG_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<GWL_Output *>(data);
  output->transform = transform;
  output->make = std::string(make);
  output->model = std::string(model);
  output->size_mm[0] = physical_width;
  output->size_mm[1] = physical_height;
}

static void output_handle_mode(void *data,
                               wl_output * /*wl_output*/,
                               const uint32_t flags,
                               const int32_t width,
                               const int32_t height,
                               const int32_t /*refresh*/)
{
  if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) {
    CLOG_INFO(LOG, 2, "mode (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "mode (size=[%d, %d], flags=%u)", width, height, flags);

  GWL_Output *output = static_cast<GWL_Output *>(data);
  output->size_native[0] = width;
  output->size_native[1] = height;

  /* Don't rotate this yet, `wl-output` coordinates are transformed in
   * handle_done and `xdg-output` coordinates are pre-transformed. */
  if (!output->has_size_logical) {
    output->size_logical[0] = width;
    output->size_logical[1] = height;
  }
}

/**
 * Sent all information about output.
 *
 * This event is sent after all other properties have been sent
 * after binding to the output object and after any other property
 * changes done after that. This allows changes to the output
 * properties to be seen as atomic, even if they happen via multiple events.
 */
static void output_handle_done(void *data, wl_output * /*wl_output*/)
{
  CLOG_INFO(LOG, 2, "done");

  GWL_Output *output = static_cast<GWL_Output *>(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<GWL_Output *>(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_compositor *>(
      wl_registry_bind(display->wl.registry, params->name, &wl_compositor_interface, 3));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_compositor_remove(GWL_Display *display,
                                           void * /*user_data*/,
                                           const bool /*on_exit*/)
{
  wl_compositor **value_p = &display->wl.compositor;
  wl_compositor_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.xdg_decor.shell */

static void gwl_registry_xdg_wm_base_add(GWL_Display *display,
                                         const GWL_RegisteryAdd_Params *params)
{
  GWL_XDG_Decor_System &decor = *display->xdg_decor;
  decor.shell = static_cast<xdg_wm_base *>(
      wl_registry_bind(display->wl.registry, params->name, &xdg_wm_base_interface, 1));
  xdg_wm_base_add_listener(decor.shell, &shell_listener, nullptr);
  decor.shell_name = params->name;
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_xdg_wm_base_remove(GWL_Display *display,
                                            void * /*user_data*/,
                                            const bool /*on_exit*/)
{
  GWL_XDG_Decor_System &decor = *display->xdg_decor;
  xdg_wm_base **value_p = &decor.shell;
  uint32_t *name_p = &decor.shell_name;
  xdg_wm_base_destroy(*value_p);
  *value_p = nullptr;
  *name_p = WL_NAME_UNSET;
}

/* #GWL_Display.xdg_decor.manager */

static void gwl_registry_xdg_decoration_manager_add(GWL_Display *display,
                                                    const GWL_RegisteryAdd_Params *params)
{
  GWL_XDG_Decor_System &decor = *display->xdg_decor;
  decor.manager = static_cast<zxdg_decoration_manager_v1 *>(wl_registry_bind(
      display->wl.registry, params->name, &zxdg_decoration_manager_v1_interface, 1));
  decor.manager_name = params->name;
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_xdg_decoration_manager_remove(GWL_Display *display,
                                                       void * /*user_data*/,
                                                       const bool /*on_exit*/)
{
  GWL_XDG_Decor_System &decor = *display->xdg_decor;
  zxdg_decoration_manager_v1 **value_p = &decor.manager;
  uint32_t *name_p = &decor.manager_name;
  zxdg_decoration_manager_v1_destroy(*value_p);
  *value_p = nullptr;
  *name_p = WL_NAME_UNSET;
}

/* #GWL_Display.xdg_output_manager */

static void gwl_registry_xdg_output_manager_add(GWL_Display *display,
                                                const GWL_RegisteryAdd_Params *params)
{
  display->xdg.output_manager = static_cast<zxdg_output_manager_v1 *>(
      wl_registry_bind(display->wl.registry, params->name, &zxdg_output_manager_v1_interface, 2));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_xdg_output_manager_remove(GWL_Display *display,
                                                   void * /*user_data*/,
                                                   const bool /*on_exit*/)
{
  zxdg_output_manager_v1 **value_p = &display->xdg.output_manager;
  zxdg_output_manager_v1_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.wl_output */

static void gwl_registry_wl_output_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params)
{
  GWL_Output *output = new GWL_Output;
  output->system = display->system;
  output->wl.output = static_cast<wl_output *>(
      wl_registry_bind(display->wl.registry, params->name, &wl_output_interface, 2));
  ghost_wl_output_tag(output->wl.output);
  wl_output_set_user_data(output->wl.output, output);

  display->outputs.push_back(output);
  wl_output_add_listener(output->wl.output, &output_listener, output);
  gwl_registry_entry_add(display, params, static_cast<void *>(output));
}
static void gwl_registry_wl_output_update(GWL_Display *display,
                                          const GWL_RegisteryUpdate_Params *params)
{
  GWL_Output *output = static_cast<GWL_Output *>(params->user_data);
  if (display->xdg.output_manager) {
    if (output->xdg.output == nullptr) {
      output->xdg.output = zxdg_output_manager_v1_get_xdg_output(display->xdg.output_manager,
                                                                 output->wl.output);
      zxdg_output_v1_add_listener(output->xdg.output, &xdg_output_listener, output);
    }
  }
  else {
    output->xdg.output = nullptr;
  }
}
static void gwl_registry_wl_output_remove(GWL_Display *display,
                                          void *user_data,
                                          const bool on_exit)
{
  /* While windows & cursors hold references to outputs, there is no need to manually remove
   * these references as the compositor will remove references via #wl_surface_listener.leave.
   *
   * WARNING: this is not the case for WLROOTS based compositors which have a (bug?)
   * where surface leave events don't run. So `system->output_leave(..)` is needed
   * until the issue is resolved in WLROOTS. */
  GWL_Output *output = static_cast<GWL_Output *>(user_data);

  if (!on_exit) {
    /* Needed for WLROOTS, does nothing if surface leave callbacks have already run. */
    if (output->system->output_unref(output->wl.output)) {
      CLOG_WARN(LOG,
                "mis-behaving compositor failed to call \"surface_listener.leave\" "
                "window scale may be invalid!");
    }
  }

  if (output->xdg.output) {
    zxdg_output_v1_destroy(output->xdg.output);
  }
  wl_output_destroy(output->wl.output);
  std::vector<GWL_Output *>::iterator iter = std::find(
      display->outputs.begin(), display->outputs.end(), output);
  const int index = (iter != display->outputs.cend()) ?
                        std::distance(display->outputs.begin(), iter) :
                        -1;
  GHOST_ASSERT(index != -1, "invalid internal state");
  /* NOTE: always erase even when `on_exit` because `output->xdg_output` is cleared later. */
  display->outputs.erase(display->outputs.begin() + index);
  delete output;
}

/* #GWL_Display.seats */

static void gwl_registry_wl_seat_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params)
{
  GWL_Seat *seat = new GWL_Seat;
  seat->system = display->system;
  seat->xkb.context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);

  /* May be null (skip dead-key support in this case). */
  seat->xkb.compose_table = xkb_compose_table_new_from_locale(
      seat->xkb.context, ghost_wl_locale_from_env_with_default(), XKB_COMPOSE_COMPILE_NO_FLAGS);

  seat->data_source = new GWL_DataSource;
  seat->wl.seat = static_cast<wl_seat *>(
      wl_registry_bind(display->wl.registry, params->name, &wl_seat_interface, 5));
  display->seats.push_back(seat);
  wl_seat_add_listener(seat->wl.seat, &seat_listener, seat);
  gwl_registry_entry_add(display, params, static_cast<void *>(seat));
}
static void gwl_registry_wl_seat_update(GWL_Display *display,
                                        const GWL_RegisteryUpdate_Params *params)
{
  GWL_Seat *seat = static_cast<GWL_Seat *>(params->user_data);

  /* Register data device per seat for IPC between WAYLAND clients. */
  if (display->wl.data_device_manager) {
    if (seat->wl.data_device == nullptr) {
      seat->wl.data_device = wl_data_device_manager_get_data_device(
          display->wl.data_device_manager, seat->wl.seat);
      wl_data_device_add_listener(seat->wl.data_device, &data_device_listener, seat);
    }
  }
  else {
    seat->wl.data_device = nullptr;
  }

  if (display->wp.tablet_manager) {
    if (seat->wp.tablet_seat == nullptr) {
      seat->wp.tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(display->wp.tablet_manager,
                                                                   seat->wl.seat);
      zwp_tablet_seat_v2_add_listener(seat->wp.tablet_seat, &tablet_seat_listener, seat);
    }
  }
  else {
    seat->wp.tablet_seat = nullptr;
  }

  if (display->wp.primary_selection_device_manager) {
    if (seat->wp.primary_selection_device == nullptr) {
      seat->wp.primary_selection_device = zwp_primary_selection_device_manager_v1_get_device(
          display->wp.primary_selection_device_manager, seat->wl.seat);

      zwp_primary_selection_device_v1_add_listener(seat->wp.primary_selection_device,
                                                   &primary_selection_device_listener,
                                                   &seat->primary_selection);
    }
  }
  else {
    seat->wp.primary_selection_device = nullptr;
  }

#ifdef WITH_INPUT_IME
  if (display->wp.text_input_manager) {
    if (seat->wp.text_input == nullptr) {
      seat->wp.text_input = zwp_text_input_manager_v3_get_text_input(
          display->wp.text_input_manager, seat->wl.seat);
      zwp_text_input_v3_set_user_data(seat->wp.text_input, seat);
      zwp_text_input_v3_add_listener(seat->wp.text_input, &text_input_listener, seat);
    }
  }
  else {
    seat->wp.text_input = nullptr;
  }
#endif /* WITH_INPUT_IME */
}
static void gwl_registry_wl_seat_remove(GWL_Display *display, void *user_data, const bool on_exit)
{
  GWL_Seat *seat = static_cast<GWL_Seat *>(user_data);

  /* First handle members that require locking.
   * While highly unlikely, it's possible they are being used while this function runs. */
  {
    std::lock_guard lock{seat->data_source_mutex};
    if (seat->data_source) {
      gwl_simple_buffer_free_data(&seat->data_source->buffer_out);
      if (seat->data_source->wl.source) {
        wl_data_source_destroy(seat->data_source->wl.source);
      }
      delete seat->data_source;
    }
  }

  {
    std::lock_guard lock{seat->data_offer_dnd_mutex};
    if (seat->data_offer_dnd) {
      wl_data_offer_destroy(seat->data_offer_dnd->wl.id);
      delete seat->data_offer_dnd;
    }
  }

  {
    std::lock_guard lock{seat->data_offer_copy_paste_mutex};
    if (seat->data_offer_copy_paste) {
      wl_data_offer_destroy(seat->data_offer_copy_paste->wl.id);
      delete seat->data_offer_copy_paste;
    }
  }

  {
    GWL_PrimarySelection *primary = &seat->primary_selection;
    std::lock_guard lock{primary->data_offer_mutex};
    gwl_primary_selection_discard_offer(primary);
  }

  {
    GWL_PrimarySelection *primary = &seat->primary_selection;
    std::lock_guard lock{primary->data_source_mutex};
    gwl_primary_selection_discard_source(primary);
  }

  if (seat->wp.primary_selection_device) {
    zwp_primary_selection_device_v1_destroy(seat->wp.primary_selection_device);
  }

  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<GWL_Seat *>::iterator iter = std::find(
      display->seats.begin(), display->seats.end(), seat);
  const int index = (iter != display->seats.cend()) ? std::distance(display->seats.begin(), iter) :
                                                      -1;
  GHOST_ASSERT(index != -1, "invalid internal state");

  if (!on_exit) {
    if (display->seats_active_index >= index) {
      display->seats_active_index -= 1;
    }
    display->seats.erase(display->seats.begin() + index);
  }
  delete seat;
}

/* #GWL_Display.wl_shm */

static void gwl_registry_wl_shm_add(GWL_Display *display, const GWL_RegisteryAdd_Params *params)
{
  display->wl.shm = static_cast<wl_shm *>(
      wl_registry_bind(display->wl.registry, params->name, &wl_shm_interface, 1));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wl_shm_remove(GWL_Display *display,
                                       void * /*user_data*/,
                                       const bool /*on_exit*/)
{
  wl_shm **value_p = &display->wl.shm;
  wl_shm_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.wl_data_device_manager */

static void gwl_registry_wl_data_device_manager_add(GWL_Display *display,
                                                    const GWL_RegisteryAdd_Params *params)
{
  display->wl.data_device_manager = static_cast<wl_data_device_manager *>(
      wl_registry_bind(display->wl.registry, params->name, &wl_data_device_manager_interface, 3));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wl_data_device_manager_remove(GWL_Display *display,
                                                       void * /*user_data*/,
                                                       const bool /*on_exit*/)
{
  wl_data_device_manager **value_p = &display->wl.data_device_manager;
  wl_data_device_manager_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.wp_tablet_manager */

static void gwl_registry_wp_tablet_manager_add(GWL_Display *display,
                                               const GWL_RegisteryAdd_Params *params)
{
  display->wp.tablet_manager = static_cast<zwp_tablet_manager_v2 *>(
      wl_registry_bind(display->wl.registry, params->name, &zwp_tablet_manager_v2_interface, 1));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_tablet_manager_remove(GWL_Display *display,
                                                  void * /*user_data*/,
                                                  const bool /*on_exit*/)
{
  zwp_tablet_manager_v2 **value_p = &display->wp.tablet_manager;
  zwp_tablet_manager_v2_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.wp_relative_pointer_manager */

static void gwl_registry_wp_relative_pointer_manager_add(GWL_Display *display,
                                                         const GWL_RegisteryAdd_Params *params)
{
  display->wp.relative_pointer_manager = static_cast<zwp_relative_pointer_manager_v1 *>(
      wl_registry_bind(
          display->wl.registry, params->name, &zwp_relative_pointer_manager_v1_interface, 1));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_relative_pointer_manager_remove(GWL_Display *display,
                                                            void * /*user_data*/,
                                                            const bool /*on_exit*/)
{
  zwp_relative_pointer_manager_v1 **value_p = &display->wp.relative_pointer_manager;
  zwp_relative_pointer_manager_v1_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.wp_pointer_constraints */

static void gwl_registry_wp_pointer_constraints_add(GWL_Display *display,
                                                    const GWL_RegisteryAdd_Params *params)
{
  display->wp.pointer_constraints = static_cast<zwp_pointer_constraints_v1 *>(wl_registry_bind(
      display->wl.registry, params->name, &zwp_pointer_constraints_v1_interface, 1));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_pointer_constraints_remove(GWL_Display *display,
                                                       void * /*user_data*/,
                                                       const bool /*on_exit*/)
{
  zwp_pointer_constraints_v1 **value_p = &display->wp.pointer_constraints;
  zwp_pointer_constraints_v1_destroy(*value_p);
  *value_p = nullptr;
}

/* #GWL_Display.wp_pointer_gestures */

static void gwl_registry_wp_pointer_gestures_add(GWL_Display *display,
                                                 const GWL_RegisteryAdd_Params *params)
{
  display->wp.pointer_gestures = static_cast<zwp_pointer_gestures_v1 *>(
      wl_registry_bind(display->wl.registry,
                       params->name,
                       &zwp_pointer_gestures_v1_interface,
                       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<xdg_activation_v1 *>(
      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<wp_fractional_scale_manager_v1 *>(
      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<wp_viewporter *>(
      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<zwp_primary_selection_device_manager_v1 *>(
          wl_registry_bind(display->wl.registry,
                           params->name,
                           &zwp_primary_selection_device_manager_v1_interface,
                           1));
  gwl_registry_entry_add(display, params, nullptr);
}
static void gwl_registry_wp_primary_selection_device_manager_remove(GWL_Display *display,
                                                                    void * /*user_data*/,
                                                                    const bool /*on_exit*/)
{
  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<zwp_text_input_manager_v3 *>(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<GWL_Display *>(data);
  GHOST_ASSERT(display->wl.registry == wl_registry, "Registry argument must match!");

  const int interface_slot = gwl_registry_handler_interface_slot_from_string(interface);
  bool added = false;

  if (interface_slot != -1) {
    const GWL_RegistryHandler *handler = &gwl_registry_handlers[interface_slot];
    const GWL_RegistryEntry *registry_entry_prev = display->registry_entry;

    /* The interface name that is ensured not to be freed. */
    GWL_RegisteryAdd_Params params{};
    params.name = name;
    params.interface_slot = interface_slot;
    params.version = version;

    handler->add_fn(display, &params);

    added = display->registry_entry != registry_entry_prev;
  }
  else {
    /* Not found. */
#ifdef USE_GNOME_NEEDS_LIBDECOR_HACK
    if (STRPREFIX(interface, "gtk_shell")) { /* `gtk_shell1` at time of writing. */
      /* Only require `libdecor` when built with X11 support,
       * otherwise there is nothing to fall back on. */
      display->libdecor_required = true;
    }
#endif
  }

  CLOG_INFO(LOG,
            2,
            "add %s(interface=%s, version=%u, name=%u)",
            (interface_slot != -1) ? (added ? "" : "(found but not added)") : "(skipped), ",
            interface,
            version,
            name);

  /* Initialization avoids excessive calls by calling update after all have been initialized. */
  if (added) {
    if (display->registry_skip_update_all == false) {
      /* See doc-string for rationale on updating all on add/removal. */
      gwl_registry_entry_update_all(display, interface_slot);
    }
  }
}

/**
 * Announce removal of global object.
 *
 * Notify the client of removed global objects.
 *
 * This event notifies the client that the global identified by
 * name is no longer available. If the client bound to the global
 * using the bind request, the client should now destroy that object.
 */
static void global_handle_remove(void *data,
                                 [[maybe_unused]] wl_registry *wl_registry,
                                 const uint32_t name)
{
  GWL_Display *display = static_cast<GWL_Display *>(data);
  GHOST_ASSERT(display->wl.registry == wl_registry, "Registry argument must match!");

  int interface_slot = 0;
  const bool removed = gwl_registry_entry_remove_by_name(display, name, &interface_slot);

  CLOG_INFO(LOG,
            2,
            "remove (name=%u, interface=%s)",
            name,
            removed ? *gwl_registry_handlers[interface_slot].interface_p : "(unknown)");

  if (removed) {
    if (display->registry_skip_update_all == false) {
      /* See doc-string for rationale on updating all on add/removal. */
      gwl_registry_entry_update_all(display, interface_slot);
    }
  }
}

static const wl_registry_listener registry_listener = {
    /*global*/ global_handle_add,
    /*global_remove*/ global_handle_remove,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Event Thread
 * \{ */

#ifdef USE_EVENT_BACKGROUND_THREAD

static void *gwl_display_event_thread_fn(void *display_voidp)
{
  GWL_Display *display = static_cast<GWL_Display *>(display_voidp);
  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)
{
  ghost_wayland_log_handler_is_background = background;
  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, &registry_listener, display_);
    /* First round-trip to receive all registry objects. */
    wl_display_roundtrip(display_->wl.display);
    /* Second round-trip to receive all output events. */
    wl_display_roundtrip(display_->wl.display);

    /* Account for dependencies between interfaces. */
    gwl_registry_entry_update_all(display_, -1);

    display_->registry_skip_update_all = false;
  }

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  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<GHOST_WindowWayland *>(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<GHOST_NDOFManagerUnix *>(m_ndofManager)->processEvents()) {
    /* As NDOF bypasses WAYLAND event handling,
     * never wait for an event when an NDOF event was found. */
    waitForEvent = false;
    any_processed = true;
  }
#endif /* WITH_INPUT_NDOF */

  if (waitForEvent) {
#ifdef USE_EVENT_BACKGROUND_THREAD
    std::lock_guard lock_server_guard{*server_mutex};
#endif
    if (wl_display_dispatch(display_->wl.display) == -1) {
      ghost_wl_display_report_error(display_->wl.display);
    }
  }
  else {
#ifdef USE_EVENT_BACKGROUND_THREAD
    /* NOTE: this works, but as the events are being read in a thread,
     * this could be removed and event handling still works.. */
    if (server_mutex->try_lock()) {
      if (ghost_wl_display_event_pump(display_->wl.display) == -1) {
        ghost_wl_display_report_error(display_->wl.display);
      }
      server_mutex->unlock();
    }
#else
    if (ghost_wl_display_event_pump(display_->wl.display) == -1) {
      ghost_wl_display_report_error(display_->wl.display);
    }
#endif /* !USE_EVENT_BACKGROUND_THREAD */
  }

  if (getEventManager()->getNumEvents() > 0) {
    any_processed = true;
  }

  return any_processed;
}

bool GHOST_SystemWayland::setConsoleWindowState(GHOST_TConsoleWindowState /*action*/)
{
  return false;
}

GHOST_TSuccess GHOST_SystemWayland::getModifierKeys(GHOST_ModifierKeys &keys) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*server_mutex};
#endif

  GWL_Seat *seat = gwl_display_seat_active_get(display_);
  if (UNLIKELY(!seat)) {
    return GHOST_kFailure;
  }

  /* Only read the underlying `seat->xkb_state` when there is an active window.
   * Without this, the following situation occurs:
   *
   * - A window is activated (before the #wl_keyboard_listener::enter has run).
   * - The modifiers from `seat->xkb_state` don't match `seat->key_depressed`.
   * - Dummy values are written into `seat->key_depressed` to account for the discrepancy
   *   (as `seat->xkb_state` is the source of truth), however the number of held modifiers
   *   is not longer valid (because it's not known from dummy values).
   * - #wl_keyboard_listener::enter runs, however the events generated from the state change
   *   may not match the physically held keys because the dummy values are not accurate.
   *
   * As this is an edge-case caused by the order of callbacks that run on window activation,
   * don't attempt to *fix* the values in `seat->key_depressed` before the keyboard enter
   * handler runs. This means the result of `getModifierKeys` may be momentarily incorrect
   * however it's corrected once #wl_keyboard_listener::enter runs.
   */
  const bool is_keyboard_active = seat->keyboard.wl.surface_window != nullptr;
  const xkb_mod_mask_t state = is_keyboard_active ?
                                   xkb_state_serialize_mods(seat->xkb.state,
                                                            XKB_STATE_MODS_DEPRESSED) :
                                   0;

  /* Use local #GWL_KeyboardDepressedState to check which key is pressed.
   * Use XKB as the source of truth, if there is any discrepancy. */
  for (int i = 0; i < MOD_INDEX_NUM; i++) {
    if (UNLIKELY(seat->xkb_keymap_mod_index[i] == XKB_MOD_INVALID)) {
      continue;
    }

    const GWL_ModifierInfo &mod_info = g_modifier_info_table[i];
    /* NOTE(@ideasman42): it's important to write the XKB state back to #GWL_KeyboardDepressedState
     * otherwise changes to modifiers in the future wont generate events.
     * This can cause modifiers to be stuck when switching between windows in GNOME because
     * window activation is handled before the keyboard enter callback runs, see: #107314.
     * Now resolved upstream, keep this for GNOME 45 and older releases & misbehaving compositors
     * as the workaround doesn't have significant down-sides. */
    int16_t &depressed_l = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_l)];
    int16_t &depressed_r = seat->key_depressed.mods[GHOST_KEY_MODIFIER_TO_INDEX(mod_info.key_r)];
    bool val_l = depressed_l > 0;
    bool val_r = depressed_r > 0;

    if (is_keyboard_active) {
      const bool val = (state & (1 << seat->xkb_keymap_mod_index[i])) != 0;
      /* This shouldn't be needed, but guard against any possibility of modifiers being stuck.
       * Warn so if this happens it can be investigated. */
      if (val) {
        if (UNLIKELY(!(val_l || val_r))) {
          CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE,
                    "modifier (%s) state is inconsistent (GHOST held keys do not match XKB)",
                    mod_info.display_name);

          /* Picking the left is arbitrary. */
          val_l = true;
          depressed_l = 1;
        }
      }
      else {
        if (UNLIKELY(val_l || val_r)) {
          CLOG_WARN(&LOG_WL_KEYBOARD_DEPRESSED_STATE,
                    "modifier (%s) state is inconsistent (GHOST released keys do not match XKB)",
                    mod_info.display_name);
          val_l = false;
          val_r = false;
          depressed_l = 0;
          depressed_r = 0;
        }
      }
    }

    keys.set(mod_info.mod_l, val_l);
    keys.set(mod_info.mod_r, val_r);
  }

  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*server_mutex};
#endif

  GWL_Seat *seat = gwl_display_seat_active_get(display_);
  if (UNLIKELY(!seat)) {
    return GHOST_kFailure;
  }
  const GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat);
  if (!seat_state_pointer) {
    return GHOST_kFailure;
  }

  buttons = seat_state_pointer->buttons;
  return GHOST_kSuccess;
}

/**
 * Return a mime type which is supported by GHOST and exists in `types`
 * (defined by the data offer).
 */
static const char *system_clipboard_text_mime_type(
    const std::unordered_set<std::string> &data_offer_types)
{
  const char *ghost_supported_types[] = {ghost_wl_mime_text_utf8, ghost_wl_mime_text_plain};
  for (size_t i = 0; i < ARRAY_SIZE(ghost_supported_types); i++) {
    if (data_offer_types.count(ghost_supported_types[i])) {
      return ghost_supported_types[i];
    }
  }
  return nullptr;
}

static char *system_clipboard_get_primary_selection(GWL_Display *display)
{
  GWL_Seat *seat = gwl_display_seat_active_get(display);
  if (UNLIKELY(!seat)) {
    return nullptr;
  }
  GWL_PrimarySelection *primary = &seat->primary_selection;
  std::mutex &mutex = primary->data_offer_mutex;

  mutex.lock();
  bool mutex_locked = true;
  char *data = nullptr;

  GWL_PrimarySelection_DataOffer *data_offer = primary->data_offer;
  if (data_offer != nullptr) {
    const char *mime_receive = system_clipboard_text_mime_type(data_offer->types);
    if (mime_receive) {
      /* Receive the clipboard in a thread, performing round-trips while waiting.
       * This is needed so pasting contents from our own `primary->data_source` doesn't hang. */
      struct ThreadResult {
        char *data = nullptr;
        std::atomic<bool> done = false;
      } thread_result;
      auto read_clipboard_fn = [](GWL_PrimarySelection_DataOffer *data_offer,
                                  const char *mime_receive,
                                  std::mutex *mutex,
                                  ThreadResult *thread_result) {
        size_t data_len = 0;
        thread_result->data = read_buffer_from_primary_selection_offer(
            data_offer, mime_receive, mutex, true, &data_len);
        thread_result->done = true;
      };
      std::thread read_thread(read_clipboard_fn, data_offer, mime_receive, &mutex, &thread_result);
      read_thread.detach();

      while (!thread_result.done) {
        wl_display_roundtrip(display->wl.display);
      }
      data = thread_result.data;

      /* Reading the data offer unlocks the mutex. */
      mutex_locked = false;
    }
  }
  if (mutex_locked) {
    mutex.unlock();
  }
  return data;
}

static char *system_clipboard_get(GWL_Display *display)
{
  GWL_Seat *seat = gwl_display_seat_active_get(display);
  if (UNLIKELY(!seat)) {
    return nullptr;
  }
  std::mutex &mutex = seat->data_offer_copy_paste_mutex;

  mutex.lock();
  bool mutex_locked = true;
  char *data = nullptr;

  GWL_DataOffer *data_offer = seat->data_offer_copy_paste;
  if (data_offer != nullptr) {
    const char *mime_receive = system_clipboard_text_mime_type(data_offer->types);
    if (mime_receive) {
      /* Receive the clipboard in a thread, performing round-trips while waiting.
       * This is needed so pasting contents from our own `seat->data_source` doesn't hang. */
      struct ThreadResult {
        char *data = nullptr;
        std::atomic<bool> done = false;
      } thread_result;
      auto read_clipboard_fn = [](GWL_DataOffer *data_offer,
                                  const char *mime_receive,
                                  std::mutex *mutex,
                                  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. */
  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<const GHOST_WindowWayland *>(window);
  return getCursorPositionClientRelative_impl(seat_state_pointer, win, x, y);
}

GHOST_TSuccess GHOST_SystemWayland::setCursorPositionClientRelative(GHOST_IWindow *window,
                                                                    const int32_t x,
                                                                    const int32_t y)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*server_mutex};
#endif

  GWL_Seat *seat = gwl_display_seat_active_get(display_);
  if (UNLIKELY(!seat)) {
    return GHOST_kFailure;
  }
  GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(window);
  return setCursorPositionClientRelative_impl(seat, win, x, y);
}

GHOST_TSuccess GHOST_SystemWayland::getCursorPosition(int32_t &x, int32_t &y) const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*server_mutex};
#endif

  GWL_Seat *seat = gwl_display_seat_active_get(display_);
  if (UNLIKELY(!seat)) {
    return GHOST_kFailure;
  }
  GWL_SeatStatePointer *seat_state_pointer = gwl_seat_state_pointer_active(seat);
  if (!seat_state_pointer) {
    return GHOST_kFailure;
  }

  if (wl_surface *wl_surface_focus = seat_state_pointer->wl.surface_window) {
    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<GHOST_ContextEGL *>(context)) {
    type = GHOST_kDrawingContextTypeOpenGL;
  }
#endif /* WITH_OPENGL_BACKEND */
#ifdef WITH_VULKAN_BACKEND
  if (dynamic_cast<GHOST_ContextVK *>(context)) {
    type = GHOST_kDrawingContextTypeVulkan;
  }
#endif /* WITH_VULKAN_BACKEND */

  wl_surface *wl_surface = static_cast<struct wl_surface *>(
      (static_cast<GHOST_Context *>(context))->getUserData());

  /* Delete the context before the window so the context is able to release
   * native resources (such as the #EGLSurface) before WAYLAND frees them. */
  delete context;

#ifdef WITH_OPENGL_BACKEND
  if (type == GHOST_kDrawingContextTypeOpenGL) {
    wl_egl_window *egl_window = static_cast<wl_egl_window *>(wl_surface_get_user_data(wl_surface));
    if (egl_window != nullptr) {
      wl_egl_window_destroy(egl_window);
    }
  }
#endif /* WITH_OPENGL_BACKEND */

  wl_surface_destroy(wl_surface);

  (void)type; /* Maybe unused. */

  return GHOST_kSuccess;
}

GHOST_IWindow *GHOST_SystemWayland::createWindow(const char *title,
                                                 const int32_t left,
                                                 const int32_t top,
                                                 const uint32_t width,
                                                 const uint32_t height,
                                                 const GHOST_TWindowState state,
                                                 const GHOST_GPUSettings 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<uint32_t *>(cursor->custom_data)[y * sizex];
    for (int x = 0; x < sizex; ++x) {
      if ((x % 8) == 0) {
        datab = *bitmap++;
        maskb = *mask++;

        /* Reverse bit order. */
        datab = uint8_t((datab * 0x0202020202ULL & 0x010884422010ULL) % 1023);
        maskb = uint8_t((maskb * 0x0202020202ULL & 0x010884422010ULL) % 1023);
      }

      if (maskb & 0x80) {
        *pixel++ = (datab & 0x80) ? white : black;
      }
      else {
        *pixel++ = (datab & 0x80) ? white : transparent;
      }
      datab <<= 1;
      maskb <<= 1;
    }
  }

  cursor->visible = true;
  cursor->is_custom = true;
  cursor->custom_scale = 1; /* TODO: support Hi-DPI custom cursors. */
  cursor->wl.buffer = buffer;
  cursor->wl.image.width = uint32_t(sizex);
  cursor->wl.image.height = uint32_t(sizey);
  cursor->wl.image.hotspot_x = uint32_t(hotX);
  cursor->wl.image.hotspot_y = uint32_t(hotY);
  cursor->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<uint8_t *>(cursor->custom_data);

  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_SystemWayland::cursor_visibility_set(const bool visible)
{
  /* Caller must lock `server_mutex`. */
  GWL_Seat *seat = gwl_display_seat_active_get(display_);
  if (UNLIKELY(!seat)) {
    return GHOST_kFailure;
  }

  gwl_seat_cursor_visible_set(seat, visible, seat->cursor.is_hardware, CURSOR_VISIBLE_ALWAYS_SET);
  return GHOST_kSuccess;
}

GHOST_TCapabilityFlag GHOST_SystemWayland::getCapabilities() const
{
  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;
}

void GHOST_SystemWayland::setMultitouchGestures(const bool use)
{
  if (m_multitouchGestures == use) {
    return;
  }
  m_multitouchGestures = use;

#ifdef USE_EVENT_BACKGROUND_THREAD
  /* Ensure this listeners aren't removed while events are generated. */
  std::lock_guard lock_server_guard{*server_mutex};
#endif
  for (GWL_Seat *seat : display_->seats) {
    if (use == gwl_seat_capability_pointer_multitouch_check(seat, use)) {
      continue;
    }
    if (use) {
      gwl_seat_capability_pointer_multitouch_enable(seat);
    }
    else {
      gwl_seat_capability_pointer_multitouch_disable(seat);
    }
  }
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Proxy Ownership API
 * \{ */

static const char *ghost_wl_output_tag_id = "GHOST-output";
static const char *ghost_wl_surface_tag_id = "GHOST-window";
static const char *ghost_wl_surface_cursor_pointer_tag_id = "GHOST-cursor-pointer";
static const char *ghost_wl_surface_cursor_tablet_tag_id = "GHOST-cursor-tablet";

bool ghost_wl_output_own(const wl_output *wl_output)
{
  const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_output);
  return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) == &ghost_wl_output_tag_id;
}

bool ghost_wl_surface_own(const wl_surface *wl_surface)
{
  const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_surface);
  return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) == &ghost_wl_surface_tag_id;
}

bool ghost_wl_surface_own_with_null_check(const wl_surface *wl_surface)
{
  return wl_surface && ghost_wl_surface_own(wl_surface);
}

bool ghost_wl_surface_own_cursor_pointer(const wl_surface *wl_surface)
{
  const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_surface);
  return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) ==
         &ghost_wl_surface_cursor_pointer_tag_id;
}

bool ghost_wl_surface_own_cursor_tablet(const wl_surface *wl_surface)
{
  const wl_proxy *proxy = reinterpret_cast<const wl_proxy *>(wl_surface);
  return wl_proxy_get_tag(const_cast<wl_proxy *>(proxy)) == &ghost_wl_surface_cursor_tablet_tag_id;
}

void ghost_wl_output_tag(wl_output *wl_output)
{
  wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_output);
  wl_proxy_set_tag(proxy, &ghost_wl_output_tag_id);
}

void ghost_wl_surface_tag(wl_surface *wl_surface)
{
  wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_surface);
  wl_proxy_set_tag(proxy, &ghost_wl_surface_tag_id);
}

void ghost_wl_surface_tag_cursor_pointer(wl_surface *wl_surface)
{
  wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_surface);
  wl_proxy_set_tag(proxy, &ghost_wl_surface_cursor_pointer_tag_id);
}

void ghost_wl_surface_tag_cursor_tablet(wl_surface *wl_surface)
{
  wl_proxy *proxy = reinterpret_cast<wl_proxy *>(wl_surface);
  wl_proxy_set_tag(proxy, &ghost_wl_surface_cursor_tablet_tag_id);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Direct Data Access
 *
 * Expose some members via methods.
 * \{ */

wl_display *GHOST_SystemWayland::wl_display_get()
{
  return display_->wl.display;
}

wl_compositor *GHOST_SystemWayland::wl_compositor_get()
{
  return display_->wl.compositor;
}

zwp_primary_selection_device_manager_v1 *GHOST_SystemWayland::wp_primary_selection_manager_get()
{
  return display_->wp.primary_selection_device_manager;
}

xdg_activation_v1 *GHOST_SystemWayland::xdg_activation_manager_get()
{
  return display_->xdg.activation_manager;
}

wp_fractional_scale_manager_v1 *GHOST_SystemWayland::wp_fractional_scale_manager_get()
{
  return display_->wp.fractional_scale_manager;
}
wp_viewporter *GHOST_SystemWayland::wp_viewporter_get()
{
  return display_->wp.viewporter;
}

zwp_pointer_gestures_v1 *GHOST_SystemWayland::wp_pointer_gestures_get()
{
  return display_->wp.pointer_gestures;
}

/* This value is expected to match the base name of the `.desktop` file. see #101805.
 *
 * NOTE: the XDG desktop-entry-spec defines that this should follow the "reverse DNS" convention.
 * For 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<GWL_Output *> &GHOST_SystemWayland::outputs_get() const
{
  return display_->outputs;
}

wl_shm *GHOST_SystemWayland::wl_shm_get() const
{
  return display_->wl.shm;
}

#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_TimerManager *GHOST_SystemWayland::ghost_timer_manager()
{
  return display_->ghost_timer_manager;
}
#endif

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Text Input (IME) Functions
 *
 * Functionality only used for the WAYLAND implementation.
 * \{ */

#ifdef WITH_INPUT_IME

void GHOST_SystemWayland::ime_begin(const GHOST_WindowWayland *win,
                                    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<GWL_Output *>(wl_output_get_user_data(wl_output));
  return output;
}

GHOST_WindowWayland *ghost_wl_surface_user_data(wl_surface *wl_surface)
{
  GHOST_ASSERT(wl_surface, "wl_surface must not be nullptr");
  GHOST_ASSERT(ghost_wl_surface_own(wl_surface), "wl_surface is not owned by GHOST");
  GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(
      wl_surface_get_user_data(wl_surface));
  return win;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Utility Functions
 *
 * Functionality only used for the WAYLAND implementation.
 * \{ */

uint64_t GHOST_SystemWayland::ms_from_input_time(const uint32_t timestamp_as_uint)
{
  /* NOTE(@ideasman42): Return a time compatible with `getMilliSeconds()`,
   * this is needed as WAYLAND time-stamps don't have a well defined beginning
   * use `timestamp_as_uint` to calculate an offset which is applied to future events.
   * This is updated because time may have passed between generating the time-stamp and `now`.
   * The method here is used by SDL. */
  uint64_t timestamp = uint64_t(timestamp_as_uint);

  GWL_DisplayTimeStamp &input_timestamp = display_->input_timestamp;
  if (UNLIKELY(timestamp_as_uint < input_timestamp.last)) {
    /* NOTE(@ideasman42): Sometimes event times are out of order,
     * while this should _never_ happen, it occasionally does:
     * - When resizing the window then clicking on the window with GNOME+LIBDECOR.
     * - With accepting IME text with GNOME-v45.2 the timestamp is in seconds, see:
     *   https://gitlab.gnome.org/GNOME/mutter/-/issues/3214
     * Accept events must occur within ~25 days, out-of-order time-stamps above this time-frame
     * will be treated as a wrapped integer. */
    if (input_timestamp.last - timestamp_as_uint > std::numeric_limits<uint32_t>::max() / 2) {
      /* Finally check to avoid invalid rollover,
       * ensure the rolled over time is closer to "now" than it is currently. */
      const uint64_t offset_test = input_timestamp.offset +
                                   uint64_t(std::numeric_limits<uint32_t>::max()) + 1;
      const uint64_t now = getMilliSeconds();
      if (sub_abs_u64(now, timestamp + offset_test) <
          sub_abs_u64(now, timestamp + input_timestamp.offset))
      {
        /* 32-bit timer rollover, bump the offset. */
        input_timestamp.offset = offset_test;
      }
    }
  }
  input_timestamp.last = timestamp_as_uint;

  if (input_timestamp.exact_match) {
    timestamp += input_timestamp.offset;
  }
  else {
    const uint64_t now = getMilliSeconds();
    const uint32_t now_as_uint32 = uint32_t(now);
    if (now_as_uint32 == timestamp_as_uint) {
      input_timestamp.exact_match = true;
      /* For systems with up times exceeding 47 days
       * it's possible we need to begin with an offset. */
      input_timestamp.offset = now - uint64_t(now_as_uint32);
      timestamp = now;
    }
    else {
      if (!input_timestamp.offset) {
        input_timestamp.offset = (now - timestamp);
      }
      timestamp += input_timestamp.offset;

      if (timestamp > now) {
        input_timestamp.offset -= (timestamp - now);
        timestamp = now;
      }
    }
  }

  return timestamp;
}

GHOST_TSuccess GHOST_SystemWayland::pushEvent_maybe_pending(const GHOST_IEvent *event)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  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<GHOST_WindowWayland *>(iwin);
      if (win->outputs_leave(output)) {
        win->outputs_changed_update_scale_tag();
        changed = true;
      }
    }
  }
  for (GWL_Seat *seat : display_->seats) {
    if (seat->pointer.outputs.erase(output)) {
      changed = true;
    }
    if (seat->tablet.outputs.erase(output)) {
      changed = true;
    }
  }
  return changed;
}

void GHOST_SystemWayland::output_scale_update(GWL_Output *output)
{
  /* NOTE: keep in sync with `output_unref`. */
  GHOST_WindowManager *window_manager = getWindowManager();
  if (window_manager) {
    for (GHOST_IWindow *iwin : window_manager->getWindows()) {
      GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(iwin);
      const std::vector<GWL_Output *> &outputs = win->outputs_get();
      if (!(std::find(outputs.begin(), outputs.end(), output) == outputs.cend())) {
        win->outputs_changed_update_scale_tag();
      }
    }
  }

  for (GWL_Seat *seat : display_->seats) {
    if (seat->pointer.outputs.count(output)) {
      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<GWL_TabletTool *>(
            zwp_tablet_tool_v2_get_user_data(zwp_tablet_tool_v2));
        if (tablet_tool->wl.surface_cursor != nullptr) {
          update_cursor_scale(seat->cursor,
                              seat->system->wl_shm_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 */

/** \} */
