// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "wayland_nogui_platform.h" #include "common/assert.h" #include "common/log.h" #include "common/string_util.h" #include "common/threading.h" #include "core/host.h" #include "nogui_host.h" #include "nogui_platform.h" #include #include #include #include Log_SetChannel(WaylandNoGUIPlatform); WaylandNoGUIPlatform::WaylandNoGUIPlatform() { m_message_loop_running.store(true, std::memory_order_release); } WaylandNoGUIPlatform::~WaylandNoGUIPlatform() { if (m_xkb_state) xkb_state_unref(m_xkb_state); if (m_xkb_keymap) xkb_keymap_unref(m_xkb_keymap); if (m_wl_keyboard) wl_keyboard_destroy(m_wl_keyboard); if (m_wl_pointer) wl_pointer_destroy(m_wl_pointer); if (m_wl_seat) wl_seat_destroy(m_wl_seat); if (m_xkb_context) xkb_context_unref(m_xkb_context); if (m_registry) wl_registry_destroy(m_registry); } bool WaylandNoGUIPlatform::Initialize() { m_xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (!m_xkb_context) { Panic("Failed to create XKB context"); return false; } m_display = wl_display_connect(nullptr); if (!m_display) { Panic("Failed to connect to Wayland display."); return false; } static const wl_registry_listener registry_listener = {GlobalRegistryHandler, GlobalRegistryRemover}; m_registry = wl_display_get_registry(m_display); wl_registry_add_listener(m_registry, ®istry_listener, this); // Call back to registry listener to get compositor/shell. wl_display_dispatch_pending(m_display); wl_display_roundtrip(m_display); // We need a shell/compositor, or at least one we understand. if (!m_compositor || !m_xdg_wm_base) { Panic("Missing Wayland shell/compositor\n"); return false; } static const xdg_wm_base_listener xdg_wm_base_listener = {XDGWMBasePing}; xdg_wm_base_add_listener(m_xdg_wm_base, &xdg_wm_base_listener, this); wl_display_dispatch_pending(m_display); wl_display_roundtrip(m_display); return true; } void WaylandNoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message) { // not implemented } bool WaylandNoGUIPlatform::ConfirmMessage(const std::string_view& title, const std::string_view& message) { // not implemented return true; } void WaylandNoGUIPlatform::SetDefaultConfig(SettingsInterface& si) {} bool WaylandNoGUIPlatform::CreatePlatformWindow(std::string title) { s32 window_x, window_y, window_width, window_height; bool has_window_pos = NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height); if (!has_window_pos) { window_x = 0; window_y = 0; window_width = DEFAULT_WINDOW_WIDTH; window_height = DEFAULT_WINDOW_HEIGHT; } // Create the compositor and shell surface. if (!(m_surface = wl_compositor_create_surface(m_compositor)) || !(m_xdg_surface = xdg_wm_base_get_xdg_surface(m_xdg_wm_base, m_surface)) || !(m_xdg_toplevel = xdg_surface_get_toplevel(m_xdg_surface))) { Log_ErrorPrintf("Failed to create compositor/shell surfaces"); return false; } static const xdg_surface_listener shell_surface_listener = {XDGSurfaceConfigure}; xdg_surface_add_listener(m_xdg_surface, &shell_surface_listener, this); static const xdg_toplevel_listener toplevel_listener = {TopLevelConfigure, TopLevelClose}; xdg_toplevel_add_listener(m_xdg_toplevel, &toplevel_listener, this); // Create region in the surface to draw into. m_region = wl_compositor_create_region(m_compositor); wl_region_add(m_region, 0, 0, window_width, window_height); wl_surface_set_opaque_region(m_surface, m_region); wl_surface_commit(m_surface); // This doesn't seem to have any effect on kwin... if (has_window_pos) { xdg_surface_set_window_geometry(m_xdg_surface, window_x, window_y, window_width, window_height); } if (m_decoration_manager) { m_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(m_decoration_manager, m_xdg_toplevel); if (m_toplevel_decoration) zxdg_toplevel_decoration_v1_set_mode(m_toplevel_decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); } m_window_info.surface_width = static_cast(window_width); m_window_info.surface_height = static_cast(window_height); m_window_info.surface_scale = 1.0f; m_window_info.type = WindowInfo::Type::Wayland; m_window_info.window_handle = m_surface; m_window_info.display_connection = m_display; wl_display_dispatch_pending(m_display); wl_display_roundtrip(m_display); return true; } bool WaylandNoGUIPlatform::HasPlatformWindow() const { return (m_surface != nullptr); } void WaylandNoGUIPlatform::DestroyPlatformWindow() { m_window_info = {}; if (m_toplevel_decoration) { zxdg_toplevel_decoration_v1_destroy(m_toplevel_decoration); m_toplevel_decoration = {}; } if (m_xdg_toplevel) { xdg_toplevel_destroy(m_xdg_toplevel); m_xdg_toplevel = {}; } if (m_xdg_surface) { xdg_surface_destroy(m_xdg_surface); m_xdg_surface = {}; } if (m_surface) { wl_surface_destroy(m_surface); m_surface = {}; } wl_display_dispatch_pending(m_display); wl_display_roundtrip(m_display); } std::optional WaylandNoGUIPlatform::GetPlatformWindowInfo() { if (m_window_info.type == WindowInfo::Type::Wayland) return m_window_info; else return std::nullopt; } void WaylandNoGUIPlatform::SetPlatformWindowTitle(std::string title) { if (m_xdg_toplevel) xdg_toplevel_set_title(m_xdg_toplevel, title.c_str()); } std::optional WaylandNoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str) { std::unique_lock lock(m_key_map_mutex); for (const auto& it : m_key_map) { if (StringUtil::Strncasecmp(it.second.c_str(), str.data(), str.length()) == 0) return it.first; } return std::nullopt; } std::optional WaylandNoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code) { std::unique_lock lock(m_key_map_mutex); const auto it = m_key_map.find(static_cast(code)); return (it != m_key_map.end()) ? std::optional(it->second) : std::nullopt; } void WaylandNoGUIPlatform::GlobalRegistryHandler(void* data, wl_registry* registry, uint32_t id, const char* interface, uint32_t version) { WaylandNoGUIPlatform* platform = static_cast(data); if (std::strcmp(interface, wl_compositor_interface.name) == 0) { platform->m_compositor = static_cast(wl_registry_bind(platform->m_registry, id, &wl_compositor_interface, 1)); } else if (std::strcmp(interface, xdg_wm_base_interface.name) == 0) { platform->m_xdg_wm_base = static_cast(wl_registry_bind(platform->m_registry, id, &xdg_wm_base_interface, 1)); } else if (std::strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) { platform->m_decoration_manager = static_cast( wl_registry_bind(platform->m_registry, id, &zxdg_decoration_manager_v1_interface, 1)); } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { static const wl_seat_listener seat_listener = {&WaylandNoGUIPlatform::SeatCapabilities}; platform->m_wl_seat = static_cast(wl_registry_bind(registry, id, &wl_seat_interface, 1)); wl_seat_add_listener(platform->m_wl_seat, &seat_listener, platform); } } void WaylandNoGUIPlatform::GlobalRegistryRemover(void* data, wl_registry* registry, uint32_t id) {} void WaylandNoGUIPlatform::XDGWMBasePing(void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial) { xdg_wm_base_pong(xdg_wm_base, serial); } void WaylandNoGUIPlatform::XDGSurfaceConfigure(void* data, struct xdg_surface* xdg_surface, uint32_t serial) { xdg_surface_ack_configure(xdg_surface, serial); } void WaylandNoGUIPlatform::TopLevelConfigure(void* data, struct xdg_toplevel* xdg_toplevel, int32_t width, int32_t height, struct wl_array* states) { // If this is zero, it's asking us to set the size. if (width == 0 || height == 0) return; WaylandNoGUIPlatform* platform = static_cast(data); platform->m_window_info.surface_width = width; platform->m_window_info.surface_height = height; NoGUIHost::ProcessPlatformWindowResize(width, height, platform->m_window_info.surface_scale); } void WaylandNoGUIPlatform::TopLevelClose(void* data, struct xdg_toplevel* xdg_toplevel) { Host::RunOnCPUThread([]() { Host::RequestExit(false); }); } void WaylandNoGUIPlatform::SeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities) { WaylandNoGUIPlatform* platform = static_cast(data); if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { static const wl_keyboard_listener keyboard_listener = { &WaylandNoGUIPlatform::KeyboardKeymap, &WaylandNoGUIPlatform::KeyboardEnter, &WaylandNoGUIPlatform::KeyboardLeave, &WaylandNoGUIPlatform::KeyboardKey, &WaylandNoGUIPlatform::KeyboardModifiers}; platform->m_wl_keyboard = wl_seat_get_keyboard(seat); wl_keyboard_add_listener(platform->m_wl_keyboard, &keyboard_listener, platform); } if (capabilities & WL_SEAT_CAPABILITY_POINTER) { static const wl_pointer_listener pointer_listener = { &WaylandNoGUIPlatform::PointerEnter, &WaylandNoGUIPlatform::PointerLeave, &WaylandNoGUIPlatform::PointerMotion, &WaylandNoGUIPlatform::PointerButton, &WaylandNoGUIPlatform::PointerAxis}; platform->m_wl_pointer = wl_seat_get_pointer(seat); wl_pointer_add_listener(platform->m_wl_pointer, &pointer_listener, platform); } } void WaylandNoGUIPlatform::KeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size) { WaylandNoGUIPlatform* platform = static_cast(data); char* keymap_string = static_cast(mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0)); if (platform->m_xkb_keymap) xkb_keymap_unref(platform->m_xkb_keymap); platform->m_xkb_keymap = xkb_keymap_new_from_string(platform->m_xkb_context, keymap_string, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); munmap(keymap_string, size); close(fd); if (platform->m_xkb_state) xkb_state_unref(platform->m_xkb_state); platform->m_xkb_state = xkb_state_new(platform->m_xkb_keymap); platform->InitializeKeyMap(); } void WaylandNoGUIPlatform::InitializeKeyMap() { m_key_map.clear(); Log_VerbosePrintf("Init keymap"); const xkb_keycode_t min_keycode = xkb_keymap_min_keycode(m_xkb_keymap); const xkb_keycode_t max_keycode = xkb_keymap_max_keycode(m_xkb_keymap); DebugAssert(max_keycode >= min_keycode); for (xkb_keycode_t keycode = min_keycode; keycode <= max_keycode; keycode++) { const xkb_layout_index_t num_layouts = xkb_keymap_num_layouts_for_key(m_xkb_keymap, keycode); if (num_layouts == 0) continue; // Take the first layout which we find a valid keysym for. bool found_keysym = false; for (xkb_layout_index_t layout = 0; layout < num_layouts && !found_keysym; layout++) { const xkb_level_index_t num_levels = xkb_keymap_num_levels_for_key(m_xkb_keymap, keycode, layout); if (num_levels == 0) continue; // Take the first level which we find a valid keysym for. for (xkb_level_index_t level = 0; level < num_levels; level++) { const xkb_keysym_t* keysyms; const int num_syms = xkb_keymap_key_get_syms_by_level(m_xkb_keymap, keycode, layout, level, &keysyms); if (num_syms == 0) continue; // Just take the first. Should only be one in most cases anyway. const xkb_keysym_t keysym = xkb_keysym_to_upper(keysyms[0]); char keysym_name_buf[64]; if (xkb_keysym_get_name(keysym, keysym_name_buf, sizeof(keysym_name_buf)) <= 0) continue; m_key_map.emplace(static_cast(keycode), keysym_name_buf); found_keysym = false; break; } } } } void WaylandNoGUIPlatform::KeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, wl_array* keys) { } void WaylandNoGUIPlatform::KeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface) {} void WaylandNoGUIPlatform::KeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { const xkb_keycode_t keycode = static_cast(key + 8); const bool pressed = (state == WL_KEYBOARD_KEY_STATE_PRESSED); NoGUIHost::ProcessPlatformKeyEvent(static_cast(keycode), pressed); } void WaylandNoGUIPlatform::KeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { WaylandNoGUIPlatform* platform = static_cast(data); xkb_state_update_mask(platform->m_xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); } void WaylandNoGUIPlatform::PointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { } void WaylandNoGUIPlatform::PointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface) {} void WaylandNoGUIPlatform::PointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y) { const float pos_x = static_cast(wl_fixed_to_double(x)); const float pos_y = static_cast(wl_fixed_to_double(y)); NoGUIHost::ProcessPlatformMouseMoveEvent(static_cast(pos_x), static_cast(pos_y)); } void WaylandNoGUIPlatform::PointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { if (button < BTN_MOUSE || (button - BTN_MOUSE) >= 32) return; const s32 button_index = (button - BTN_MOUSE); const bool button_pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED); NoGUIHost::ProcessPlatformMouseButtonEvent(button_index, button_pressed); } void WaylandNoGUIPlatform::PointerAxis(void* data, wl_pointer* pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { const float x = (axis == 1) ? std::clamp(static_cast(wl_fixed_to_double(value)), -1.0f, 1.0f) : 0.0f; const float y = (axis == 0) ? std::clamp(static_cast(-wl_fixed_to_double(value)), -1.0f, 1.0f) : 0.0f; NoGUIHost::ProcessPlatformMouseWheelEvent(x, y); } void WaylandNoGUIPlatform::RunMessageLoop() { while (m_message_loop_running.load(std::memory_order_acquire)) { wl_display_dispatch_pending(m_display); { std::unique_lock lock(m_callback_queue_mutex); while (!m_callback_queue.empty()) { std::function func = std::move(m_callback_queue.front()); m_callback_queue.pop_front(); lock.unlock(); func(); lock.lock(); } } // TODO: Make this suck less. std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } void WaylandNoGUIPlatform::ExecuteInMessageLoop(std::function func) { std::unique_lock lock(m_callback_queue_mutex); m_callback_queue.push_back(std::move(func)); } void WaylandNoGUIPlatform::QuitMessageLoop() { m_message_loop_running.store(false, std::memory_order_release); } void WaylandNoGUIPlatform::SetFullscreen(bool enabled) { // how the heck can we do this? } bool WaylandNoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) { return false; } bool WaylandNoGUIPlatform::OpenURL(const std::string_view& url) { Log_ErrorPrintf("WaylandNoGUIPlatform::OpenURL() not implemented: %.*s", static_cast(url.size()), url.data()); return false; } bool WaylandNoGUIPlatform::CopyTextToClipboard(const std::string_view& text) { Log_ErrorPrintf("WaylandNoGUIPlatform::CopyTextToClipboard() not implemented: %.*s", static_cast(text.size()), text.data()); return false; } std::unique_ptr NoGUIPlatform::CreateWaylandPlatform() { std::unique_ptr ret = std::unique_ptr(new WaylandNoGUIPlatform()); if (!ret->Initialize()) return {}; return ret; }