diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index c8c808c..0c89e2e 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -38,7 +38,7 @@ jobs: build-ubuntu: - name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} + name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.wayland.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} runs-on: ${{ matrix.os.label }} strategy: @@ -46,6 +46,7 @@ jobs: os: [ {label: ubuntu-latest, name: latest}, {label: ubuntu-20.04, name: 20.04} ] portal: [ {flag: OFF, dep: libgtk-3-dev, name: GTK}, {flag: ON, dep: libdbus-1-dev, name: Portal} ] # The NFD_PORTAL setting defaults to OFF (i.e. uses GTK) autoappend: [ {flag: OFF, name: NoAppendExtn} ] # By default the NFD_PORTAL mode does not append extensions, because it breaks some features of the portal + wayland: [ {flag: OFF, dep: , name: NoWayland} ] compiler: [ {c: gcc, cpp: g++, name: GCC}, {c: clang, cpp: clang++, name: Clang} ] # The default compiler is gcc/g++ cppstd: [20, 11] shared_lib: [ {flag: OFF, name: Static} ] @@ -53,35 +54,47 @@ jobs: - os: {label: ubuntu-latest, name: latest} portal: {flag: ON, dep: libdbus-1-dev, name: Portal} autoappend: {flag: ON, name: AutoAppendExtn} + wayland: {flag: OFF, dep: , name: NoWayland} compiler: {c: gcc, cpp: g++, name: GCC} cppstd: 11 shared_lib: {flag: OFF, name: Static} - os: {label: ubuntu-latest, name: latest} portal: {flag: ON, dep: libdbus-1-dev, name: Portal} autoappend: {flag: ON, name: AutoAppendExtn} + wayland: {flag: OFF, dep: , name: NoWayland} compiler: {c: clang, cpp: clang++, name: Clang} cppstd: 11 shared_lib: {flag: OFF, name: Static} - os: {label: ubuntu-latest, name: latest} portal: {flag: ON, dep: libdbus-1-dev, name: Portal} autoappend: {flag: OFF, name: NoAppendExtn} + wayland: {flag: OFF, dep: , name: NoWayland} compiler: {c: gcc, cpp: g++, name: GCC} cppstd: 11 shared_lib: {flag: ON, name: Shared} + - os: {label: ubuntu-latest, name: latest} + portal: {flag: ON, dep: libdbus-1-dev, name: Portal} + autoappend: {flag: OFF, name: NoAppendExtn} + wayland: {flag: ON, dep: libwayland-dev libwayland-bin, name: Wayland} + compiler: {c: gcc, cpp: g++, name: GCC} + cppstd: 11 + shared_lib: {flag: ON, name: Static} steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: true - name: Install Dependencies - run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} + run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} ${{ matrix.wayland.dep }} - name: Configure - run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_C_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_C_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Wshadow -Werror -pedantic" -DNFD_WAYLAND=OFF -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install - name: Upload test binaries uses: actions/upload-artifact@v4 with: - name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} + name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.wayland.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} path: | build/src/* build/test/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..efc44be --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "3ps/wayland-protocols"] + path = 3ps/wayland-protocols + url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git diff --git a/3ps/wayland-protocols b/3ps/wayland-protocols new file mode 160000 index 0000000..122a47a --- /dev/null +++ b/3ps/wayland-protocols @@ -0,0 +1 @@ +Subproject commit 122a47a1ff17a22c4b964a0bbe2b07f921eab7a5 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3dcd160..b98f158 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,13 +18,33 @@ if(nfd_PLATFORM STREQUAL PLATFORM_LINUX) option(NFD_PORTAL "Use xdg-desktop-portal instead of GTK" OFF) if(NOT NFD_PORTAL) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) - message("Using GTK version: ${GTK3_VERSION}") + message(STATUS "Using GTK version: ${GTK3_VERSION}") list(APPEND SOURCE_FILES nfd_gtk.cpp) else() pkg_check_modules(DBUS REQUIRED dbus-1) - message("Using DBUS version: ${DBUS_VERSION}") + message(STATUS "Using D-Bus version: ${DBUS_VERSION}") list(APPEND SOURCE_FILES nfd_portal.cpp) endif() + + # for Linux, we support X11, Wayland, or both + option(NFD_X11 "Support X11 on Linux" ON) + option(NFD_WAYLAND "Support Wayland on Linux" ON) + if(NFD_WAYLAND) + pkg_check_modules(WAYLAND REQUIRED wayland-client) + message(STATUS "Using Wayland version: ${WAYLAND_VERSION}") + set(NFD_WAYLAND_PROTOCOL_XDG_FOREIGN ${CMAKE_CURRENT_SOURCE_DIR}/../3ps/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.h + COMMAND wayland-scanner client-header < ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} > ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.h + MAIN_DEPENDENCY ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} + ) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.c + COMMAND wayland-scanner private-code < ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} > ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.c + MAIN_DEPENDENCY ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} + ) + list(APPEND SOURCE_FILES ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.h ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.c) + endif() endif() if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) @@ -94,6 +114,16 @@ if(nfd_PLATFORM STREQUAL PLATFORM_LINUX) if(NFD_APPEND_EXTENSION) target_compile_definitions(${TARGET_NAME} PRIVATE NFD_APPEND_EXTENSION) endif() + + if(NFD_X11) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_X11) + endif() + if(NFD_WAYLAND) + target_include_directories(${TARGET_NAME} PRIVATE ${WAYLAND_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} PRIVATE ${WAYLAND_LINK_LIBRARIES}) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_WAYLAND) + target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + endif() endif() if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) @@ -118,7 +148,7 @@ if(nfd_COMPILER STREQUAL COMPILER_CLANGCL) endif() if(nfd_COMPILER STREQUAL COMPILER_GNU) - target_compile_options(${TARGET_NAME} PRIVATE -nostdlib -fno-exceptions -fno-rtti) + target_compile_options(${TARGET_NAME} PRIVATE -nostdlib -fno-exceptions $<$:-fno-rtti>) endif() set_target_properties(${TARGET_NAME} PROPERTIES diff --git a/src/include/nfd.h b/src/include/nfd.h index adb1c81..40acbe4 100644 --- a/src/include/nfd.h +++ b/src/include/nfd.h @@ -105,7 +105,8 @@ enum { NFD_WINDOW_HANDLE_TYPE_COCOA = 2, // X11: handle is Window NFD_WINDOW_HANDLE_TYPE_X11 = 3, - // Wayland support will be implemented separately in the future + // Wayland: handle is wl_surface* + NFD_WINDOW_HANDLE_TYPE_WAYLAND = 4, }; // The native window handle. If using a platform abstraction framework (e.g. SDL2), this should be // obtained using the corresponding NFD glue header (e.g. nfd_sdl2.h). diff --git a/src/include/nfd_sdl2.h b/src/include/nfd_sdl2.h index 5703762..1bbc243 100644 --- a/src/include/nfd_sdl2.h +++ b/src/include/nfd_sdl2.h @@ -59,6 +59,12 @@ NFD_INLINE bool NFD_GetNativeWindowFromSDLWindow(SDL_Window* sdlWindow, nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_X11; nativeWindow->handle = (void*)info.info.x11.window; return true; +#endif +#if defined(SDL_VIDEO_DRIVER_WAYLAND) + case SDL_SYSWM_WAYLAND: + nativeWindow->type = NFD_WINDOW_HANDLE_TYPE_WAYLAND; + nativeWindow->handle = (void*)info.info.wl.surface; + return true; #endif default: // Silence the warning in case we are not using a supported backend. diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp index 684c502..0e9e28e 100644 --- a/src/nfd_portal.cpp +++ b/src/nfd_portal.cpp @@ -26,6 +26,15 @@ #define getrandom(buf, sz, flags) syscall(SYS_getrandom, buf, sz, flags) #endif +#ifdef NFD_WAYLAND +#include +#include "xdg-foreign-unstable-v1.h" +struct wl_display* wayland_display; +struct wl_registry* wayland_registry; +uint32_t wayland_xdg_exporter_v1_name; +struct zxdg_exporter_v1* wayland_xdg_exporter_v1; +#endif + #include "nfd.h" /* @@ -68,6 +77,15 @@ struct FreeCheck_Guard { } }; +void EmptyFn(void*) {} + +struct DestroyFunc { + DestroyFunc() : fn(&EmptyFn), context(nullptr) {} + ~DestroyFunc() { (*fn)(context); } + void (*fn)(void*); + void* context; +}; + struct DBusMessage_Guard { DBusMessage* data; DBusMessage_Guard(DBusMessage* freeable) noexcept : data(freeable) {} @@ -172,9 +190,46 @@ constexpr const char* DBUS_PATH = "/org/freedesktop/portal/desktop"; constexpr const char* DBUS_FILECHOOSER_IFACE = "org.freedesktop.portal.FileChooser"; constexpr const char* DBUS_REQUEST_IFACE = "org.freedesktop.portal.Request"; -void AppendOpenFileQueryParentWindow(DBusMessageIter& iter, const nfdwindowhandle_t& parentWindow) { +#ifdef NFD_WAYLAND +constexpr const char* XDG_EXPORTER_V1 = "zxdg_exporter_v1"; +constexpr const char* WAYLAND_PREFIX = "wayland:"; + +void DestroyXdgExported(void* context) { + zxdg_exported_v1_destroy(static_cast(context)); +} + +void zxdg_exported_v1_handle(void* context, + struct zxdg_exported_v1* zxdg_exported_v1, + const char* handle) { + if (!context) return; + DBusMessageIter& iter = *static_cast(context); + const size_t handle_len = strlen(handle); + const size_t prefix_len = strlen(WAYLAND_PREFIX); + char* const buf = NFDi_Malloc(prefix_len + handle_len + 1); + char* buf_end = copy(WAYLAND_PREFIX, WAYLAND_PREFIX + prefix_len, buf); + buf_end = copy(handle, handle + handle_len, buf_end); + *buf_end = '\0'; + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &buf); + NFDi_Free(buf); +} + +constexpr struct zxdg_exported_v1_listener wayland_xdg_exported_v1_listener { + &zxdg_exported_v1_handle +}; +#endif + +void AppendOpenFileQueryParentWindow(DBusMessageIter& iter, + const nfdwindowhandle_t& parentWindow, + void (*&destroyFn)(void*), + void*& destroyFnContext) { + (void)iter; + (void)parentWindow; + (void)destroyFn; + (void)destroyFnContext; switch (parentWindow.type) { +#ifdef NFD_X11 case NFD_WINDOW_HANDLE_TYPE_X11: { + fprintf(stderr, "X11\n"); constexpr size_t maxX11WindowStrLen = 4 + sizeof(uintptr_t) * 2 + 1; // "x11:" + "" + "\0" char serializedWindowBuf[maxX11WindowStrLen]; @@ -190,6 +245,28 @@ void AppendOpenFileQueryParentWindow(DBusMessageIter& iter, const nfdwindowhandl dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &serializedWindow); return; } +#endif +#ifdef NFD_WAYLAND + case NFD_WINDOW_HANDLE_TYPE_WAYLAND: { + fprintf(stderr, "Wayland\n"); + if (wayland_xdg_exporter_v1) { + struct zxdg_exported_v1* exported = zxdg_exporter_v1_export( + wayland_xdg_exporter_v1, static_cast(parentWindow.handle)); + if (!exported) { + // if we fail to export the wl_surface, act as if the window has no parent + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); + return; + } + zxdg_exported_v1_add_listener( + exported, &wayland_xdg_exported_v1_listener, static_cast(&iter)); + wl_display_roundtrip(wayland_display); + zxdg_exported_v1_set_user_data(exported, nullptr); + destroyFn = &DestroyXdgExported; + destroyFnContext = static_cast(exported); + } + return; + } +#endif default: { dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY); return; @@ -619,11 +696,13 @@ void AppendOpenFileQueryParams(DBusMessage* query, const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, - const nfdwindowhandle_t& parentWindow) { + const nfdwindowhandle_t& parentWindow, + void (*&destroyFn)(void*), + void*& destroyFnContext) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); - AppendOpenFileQueryParentWindow(iter, parentWindow); + AppendOpenFileQueryParentWindow(iter, parentWindow, destroyFn, destroyFnContext); AppendOpenFileQueryTitle(iter); DBusMessageIter sub_iter; @@ -643,11 +722,13 @@ void AppendSaveFileQueryParams(DBusMessage* query, nfdfiltersize_t filterCount, const nfdnchar_t* defaultPath, const nfdnchar_t* defaultName, - const nfdwindowhandle_t& parentWindow) { + const nfdwindowhandle_t& parentWindow, + void (*&destroyFn)(void*), + void*& destroyFnContext) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); - AppendOpenFileQueryParentWindow(iter, parentWindow); + AppendOpenFileQueryParentWindow(iter, parentWindow, destroyFn, destroyFnContext); AppendSaveFileQueryTitle(iter); DBusMessageIter sub_iter; @@ -1195,8 +1276,16 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, DBusMessage* query = dbus_message_new_method_call( DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "OpenFile"); DBusMessage_Guard query_guard(query); - AppendOpenFileQueryParams( - query, handle_token_ptr, filterList, filterCount, defaultPath, parentWindow); + + DestroyFunc destroy; + AppendOpenFileQueryParams(query, + handle_token_ptr, + filterList, + filterCount, + defaultPath, + parentWindow, + destroy.fn, + destroy.context); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1278,8 +1367,17 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, DBusMessage* query = dbus_message_new_method_call( DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "SaveFile"); DBusMessage_Guard query_guard(query); - AppendSaveFileQueryParams( - query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName, parentWindow); + + DestroyFunc destroy; + AppendSaveFileQueryParams(query, + handle_token_ptr, + filterList, + filterCount, + defaultPath, + defaultName, + parentWindow, + destroy.fn, + destroy.context); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1383,6 +1481,30 @@ nfdresult_t NFD_DBus_GetVersion(dbus_uint32_t& outVersion) { return NFD_OKAY; } +#ifdef NFD_WAYLAND +void registry_handle_global(void* context, + struct wl_registry* registry, + uint32_t name, + const char* interface, + uint32_t version) { + if (strcmp(interface, XDG_EXPORTER_V1) == 0) { + wayland_xdg_exporter_v1_name = name; + wayland_xdg_exporter_v1 = static_cast(wl_registry_bind( + registry, name, &zxdg_exporter_v1_interface, zxdg_exporter_v1_interface.version)); + } +} + +void registry_handle_global_remove(void* context, struct wl_registry* registry, uint32_t name) { + if (wayland_xdg_exporter_v1 && name == wayland_xdg_exporter_v1_name) { + zxdg_exporter_v1_destroy(wayland_xdg_exporter_v1); + wayland_xdg_exporter_v1 = nullptr; + } +} + +constexpr struct wl_registry_listener wayland_registry_listener = {®istry_handle_global, + ®istry_handle_global_remove}; +#endif + } // namespace /* public */ @@ -1408,11 +1530,30 @@ nfdresult_t NFD_Init(void) { dbus_unique_name = dbus_bus_get_unique_name(dbus_conn); if (!dbus_unique_name) { NFDi_SetError("Unable to get the unique name of our D-Bus connection."); + dbus_connection_unref(dbus_conn); return NFD_ERROR; } +#ifdef NFD_WAYLAND + // This might fail, but it is fine because the system might not actually have Wayland installed + wayland_display = wl_display_connect(nullptr); + if (wayland_display) { + wayland_registry = wl_display_get_registry(wayland_display); + wayland_xdg_exporter_v1 = nullptr; + // seems like registry can't be null + wl_registry_add_listener(wayland_registry, &wayland_registry_listener, nullptr); + wl_display_roundtrip(wayland_display); + } +#endif return NFD_OKAY; } void NFD_Quit(void) { +#ifdef NFD_WAYLAND + if (wayland_display) { + if (wayland_xdg_exporter_v1) zxdg_exporter_v1_destroy(wayland_xdg_exporter_v1); + wl_registry_destroy(wayland_registry); + wl_display_disconnect(wayland_display); + } +#endif dbus_connection_unref(dbus_conn); // Note: We do not free dbus_error since NFD_Init might set it. // To avoid leaking memory, the caller should explicitly call NFD_ClearError after reading the