From c096b82628fa3c4ebcc6d3d758b54e20b14196a1 Mon Sep 17 00:00:00 2001 From: Marius Gripsgard Date: Thu, 10 Aug 2023 22:35:06 +0100 Subject: [PATCH 1/1] Import wlcs_1.6.0.orig.tar.gz [dgit import orig wlcs_1.6.0.orig.tar.gz] --- .github/workflows/ppa-upload.yml | 57 + .github/workflows/spread.yml | 52 + .gitignore | 3 + CMakeLists.txt | 299 ++ COPYING.GPL2 | 339 ++ COPYING.GPL3 | 676 ++++ README.rst | 52 + cmake/FindGtestGmock.cmake | 75 + cmake/JoinPaths.cmake | 23 + debian/changelog | 106 + debian/compat | 1 + debian/control | 29 + debian/copyright | 73 + debian/rules | 41 + debian/source/format | 1 + debian/upstream/signing-key.asc | 366 +++ debian/watch | 6 + example/mir_integration.cpp | 909 ++++++ include/active_listeners.h | 54 + include/data_device.h | 195 ++ include/geometry/dimensions.h | 258 ++ include/geometry/displacement.h | 189 ++ include/geometry/forward.h | 88 + include/geometry/point.h | 98 + include/geometry/rectangle.h | 145 + include/geometry/size.h | 107 + include/gtest_helpers.h | 46 + include/gtk_primary_selection.h | 162 + include/helpers.h | 44 + include/in_process_server.h | 370 +++ include/input_method.h | 83 + include/layer_shell_v1.h | 94 + include/method_event_impl.h | 47 + include/mock_input_method_v2.h | 78 + include/mock_text_input_v3.h | 65 + include/mutex.h | 132 + include/pointer_constraints_unstable_v1.h | 104 + include/primary_selection.h | 162 + include/relative_pointer_unstable_v1.h | 75 + include/shared_library.h | 55 + include/surface_builder.h | 105 + include/version_specifier.h | 69 + include/wl_handle.h | 103 + include/wl_interface_descriptor.h | 61 + include/wlcs/display_server.h | 198 ++ include/wlcs/pointer.h | 72 + include/wlcs/touch.h | 48 + include/xdg_output_v1.h | 76 + include/xdg_shell_stable.h | 119 + include/xdg_shell_v6.h | 106 + spread.yaml | 31 + spread/build/alpine/task.yaml | 22 + spread/build/fedora/task.yaml | 23 + spread/build/ubuntu/task.yaml | 34 + src/data_device.cpp | 144 + src/gtk_primary_selection.cpp | 90 + src/helpers.cpp | 121 + src/in_process_server.cpp | 2198 +++++++++++++ src/input_method.cpp | 131 + src/layer_shell_v1.cpp | 67 + src/main.cpp | 100 + src/pointer_constraints_unstable_v1.cpp | 93 + src/primary_selection.cpp | 90 + src/protocol/gtk-primary-selection.xml | 225 ++ src/protocol/input-method-unstable-v2.xml | 490 +++ .../pointer-constraints-unstable-v1.xml | 339 ++ .../primary-selection-unstable-v1.xml | 225 ++ src/protocol/relative-pointer-unstable-v1.xml | 136 + src/protocol/text-input-unstable-v3.xml | 452 +++ src/protocol/wayland.xml | 2746 +++++++++++++++++ ...oreign-toplevel-management-unstable-v1.xml | 259 ++ src/protocol/wlr-layer-shell-unstable-v1.xml | 390 +++ .../wlr-virtual-pointer-unstable-v1.xml | 152 + src/protocol/xdg-output-unstable-v1.xml | 220 ++ src/protocol/xdg-shell-unstable-v6.xml | 1044 +++++++ src/protocol/xdg-shell.xml | 1351 ++++++++ src/relative_pointer_unstable_v1.cpp | 58 + src/shared_library.cpp | 55 + src/surface_builder.cpp | 150 + src/termcolor.hpp | 557 ++++ src/test_c_compile.c | 7 + src/thread_proxy.h | 465 +++ src/version_specifier.cpp | 89 + src/xdg_output_v1.cpp | 144 + src/xdg_shell_stable.cpp | 143 + src/xdg_shell_v6.cpp | 118 + src/xfail_supporting_test_listener.cpp | 212 ++ src/xfail_supporting_test_listener.h | 78 + tests/copy_cut_paste.cpp | 127 + tests/frame_submission.cpp | 146 + tests/gtk_primary_selection.cpp | 230 ++ tests/pointer_constraints.cpp | 301 ++ tests/primary_selection.cpp | 231 ++ tests/relative_pointer.cpp | 97 + tests/self_test.cpp | 215 ++ tests/subsurfaces.cpp | 936 ++++++ tests/surface_input_regions.cpp | 837 +++++ tests/test_bad_buffer.cpp | 182 ++ tests/test_surface_events.cpp | 576 ++++ tests/text_input_v3_with_input_method_v2.cpp | 298 ++ tests/touches.cpp | 170 + tests/wl_output.cpp | 57 + tests/wlr_foreign_toplevel_management_v1.cpp | 902 ++++++ tests/wlr_layer_shell_v1.cpp | 1125 +++++++ tests/wlr_virtual_pointer_v1.cpp | 361 +++ tests/xdg_output_v1.cpp | 43 + tests/xdg_popup.cpp | 1428 +++++++++ tests/xdg_surface_stable.cpp | 176 ++ tests/xdg_surface_v6.cpp | 60 + tests/xdg_toplevel_stable.cpp | 664 ++++ tests/xdg_toplevel_v6.cpp | 514 +++ tools/make_release_tarball | 64 + tools/ppa-upload.sh | 154 + wlcs.pc.in | 12 + 114 files changed, 29871 insertions(+) create mode 100644 .github/workflows/ppa-upload.yml create mode 100644 .github/workflows/spread.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 COPYING.GPL2 create mode 100644 COPYING.GPL3 create mode 100644 README.rst create mode 100644 cmake/FindGtestGmock.cmake create mode 100644 cmake/JoinPaths.cmake create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/upstream/signing-key.asc create mode 100644 debian/watch create mode 100644 example/mir_integration.cpp create mode 100644 include/active_listeners.h create mode 100644 include/data_device.h create mode 100644 include/geometry/dimensions.h create mode 100644 include/geometry/displacement.h create mode 100644 include/geometry/forward.h create mode 100644 include/geometry/point.h create mode 100644 include/geometry/rectangle.h create mode 100644 include/geometry/size.h create mode 100644 include/gtest_helpers.h create mode 100644 include/gtk_primary_selection.h create mode 100644 include/helpers.h create mode 100644 include/in_process_server.h create mode 100644 include/input_method.h create mode 100644 include/layer_shell_v1.h create mode 100644 include/method_event_impl.h create mode 100644 include/mock_input_method_v2.h create mode 100644 include/mock_text_input_v3.h create mode 100644 include/mutex.h create mode 100644 include/pointer_constraints_unstable_v1.h create mode 100644 include/primary_selection.h create mode 100644 include/relative_pointer_unstable_v1.h create mode 100644 include/shared_library.h create mode 100644 include/surface_builder.h create mode 100644 include/version_specifier.h create mode 100644 include/wl_handle.h create mode 100644 include/wl_interface_descriptor.h create mode 100644 include/wlcs/display_server.h create mode 100644 include/wlcs/pointer.h create mode 100644 include/wlcs/touch.h create mode 100644 include/xdg_output_v1.h create mode 100644 include/xdg_shell_stable.h create mode 100644 include/xdg_shell_v6.h create mode 100644 spread.yaml create mode 100644 spread/build/alpine/task.yaml create mode 100644 spread/build/fedora/task.yaml create mode 100644 spread/build/ubuntu/task.yaml create mode 100644 src/data_device.cpp create mode 100644 src/gtk_primary_selection.cpp create mode 100644 src/helpers.cpp create mode 100644 src/in_process_server.cpp create mode 100644 src/input_method.cpp create mode 100644 src/layer_shell_v1.cpp create mode 100644 src/main.cpp create mode 100644 src/pointer_constraints_unstable_v1.cpp create mode 100644 src/primary_selection.cpp create mode 100644 src/protocol/gtk-primary-selection.xml create mode 100644 src/protocol/input-method-unstable-v2.xml create mode 100644 src/protocol/pointer-constraints-unstable-v1.xml create mode 100644 src/protocol/primary-selection-unstable-v1.xml create mode 100644 src/protocol/relative-pointer-unstable-v1.xml create mode 100644 src/protocol/text-input-unstable-v3.xml create mode 100644 src/protocol/wayland.xml create mode 100644 src/protocol/wlr-foreign-toplevel-management-unstable-v1.xml create mode 100644 src/protocol/wlr-layer-shell-unstable-v1.xml create mode 100644 src/protocol/wlr-virtual-pointer-unstable-v1.xml create mode 100644 src/protocol/xdg-output-unstable-v1.xml create mode 100644 src/protocol/xdg-shell-unstable-v6.xml create mode 100644 src/protocol/xdg-shell.xml create mode 100644 src/relative_pointer_unstable_v1.cpp create mode 100644 src/shared_library.cpp create mode 100644 src/surface_builder.cpp create mode 100644 src/termcolor.hpp create mode 100644 src/test_c_compile.c create mode 100644 src/thread_proxy.h create mode 100644 src/version_specifier.cpp create mode 100644 src/xdg_output_v1.cpp create mode 100644 src/xdg_shell_stable.cpp create mode 100644 src/xdg_shell_v6.cpp create mode 100644 src/xfail_supporting_test_listener.cpp create mode 100644 src/xfail_supporting_test_listener.h create mode 100644 tests/copy_cut_paste.cpp create mode 100644 tests/frame_submission.cpp create mode 100644 tests/gtk_primary_selection.cpp create mode 100644 tests/pointer_constraints.cpp create mode 100644 tests/primary_selection.cpp create mode 100644 tests/relative_pointer.cpp create mode 100644 tests/self_test.cpp create mode 100644 tests/subsurfaces.cpp create mode 100644 tests/surface_input_regions.cpp create mode 100644 tests/test_bad_buffer.cpp create mode 100644 tests/test_surface_events.cpp create mode 100644 tests/text_input_v3_with_input_method_v2.cpp create mode 100644 tests/touches.cpp create mode 100644 tests/wl_output.cpp create mode 100644 tests/wlr_foreign_toplevel_management_v1.cpp create mode 100644 tests/wlr_layer_shell_v1.cpp create mode 100644 tests/wlr_virtual_pointer_v1.cpp create mode 100644 tests/xdg_output_v1.cpp create mode 100644 tests/xdg_popup.cpp create mode 100644 tests/xdg_surface_stable.cpp create mode 100644 tests/xdg_surface_v6.cpp create mode 100644 tests/xdg_toplevel_stable.cpp create mode 100644 tests/xdg_toplevel_v6.cpp create mode 100755 tools/make_release_tarball create mode 100755 tools/ppa-upload.sh create mode 100644 wlcs.pc.in diff --git a/.github/workflows/ppa-upload.yml b/.github/workflows/ppa-upload.yml new file mode 100644 index 0000000..c6fa169 --- /dev/null +++ b/.github/workflows/ppa-upload.yml @@ -0,0 +1,57 @@ +name: PPA Upload + +on: + push: + branches: + - main + - release/[0-9]+.[0-9]+ + tags: + - v[0-9]+.[0-9]+.[0-9]+ + +jobs: + PPAUpload: + strategy: + fail-fast: true + matrix: + release: + - "22.04" + - "22.10" + - "23.04" + - "devel" + + runs-on: ubuntu-latest + + env: + DEBFULLNAME: "Mir CI Bot" + DEBEMAIL: "mir-ci-bot@canonical.com" + + steps: + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v3 + with: + gpg-private-key: ${{ secrets.MIR_BOT_GPG_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@v3 + with: + # Need full history for version determination + fetch-depth: 0 + + - name: Install dependencies + run: | + sudo apt-get install --no-install-recommends --yes \ + debhelper \ + devscripts \ + dput \ + python3-launchpadlib + + - name: Set up Launchpad credentials + uses: DamianReeves/write-file-action@v1.0 + with: + path: lp_credentials + contents: ${{ secrets.LAUNCHPAD_CREDENTIALS }} + + - name: Upload to PPA + env: + RELEASE: ${{ matrix.release }} + run: tools/ppa-upload.sh lp_credentials diff --git a/.github/workflows/spread.yml b/.github/workflows/spread.yml new file mode 100644 index 0000000..b052681 --- /dev/null +++ b/.github/workflows/spread.yml @@ -0,0 +1,52 @@ +name: Spread + +on: + push: + branches: + - main + - release/[0-9]+.[0-9]+ + tags: + - v[0-9]+[0-9]+.[0-9]+ + merge_group: + types: [checks_requested] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + BuildAndTest: + strategy: + fail-fast: false + matrix: + spread-task: + - lxd:ubuntu-22.04:...:gcc + - lxd:ubuntu-22.10:...:gcc + - lxd:ubuntu-23.04:...:gcc + - lxd:ubuntu-23.04:...:clang + - lxd:fedora-37:...:gcc + - lxd:fedora-38:...:gcc + - lxd:alpine-3.18:...:gcc + - lxd:ubuntu-devel:...:gcc + - lxd:ubuntu-devel:...:clang + - lxd:alpine-edge:...:gcc + + runs-on: ubuntu-latest + + env: + DEBFULLNAME: "Mir CI Bot" + DEBEMAIL: "mir-ci-bot@canonical.com" + + steps: + - name: Set up LXD + uses: canonical/setup-lxd@main + + - name: Set up Spread + run: | + set -euo pipefail + sudo snap install spread-mir-ci + sudo snap run lxd init --auto + + - name: Check out code + uses: actions/checkout@v3 + + - name: Run Spread task + run: snap run spread-mir-ci.spread -v ${{ matrix.spread-task }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05cadb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +/build/ +/cmake-build-debug/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6668655 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,299 @@ +# Copyright © 2017 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Authored by: Thomas Voss , +# Alan Griffiths , +# Christopher James Halse Rogers + +cmake_minimum_required(VERSION 3.5) + +cmake_policy(SET CMP0015 NEW) +cmake_policy(SET CMP0022 NEW) + +project(wlcs VERSION 1.5.0) + +add_definitions(-D_GNU_SOURCE) +add_definitions(-D_FILE_OFFSET_BITS=64) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +include(GNUInstallDirs) +find_package(PkgConfig) +#include (Doxygen.cmake) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread -g -Werror -Wall -pedantic -Wextra -fPIC") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -g -Werror -Wall -fno-strict-aliasing -pedantic -Wnon-virtual-dtor -Wextra -fPIC") +if ("${CMAKE_SHARED_LINKER_FLAGS}" MATCHES ".*-fsanitize=.*") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS}") +else() + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--as-needed -Wl,--no-undefined") +endif() + +include(CheckCXXCompilerFlag) +check_cxx_compiler_flag(-Wgnu-zero-variadic-macro-arguments HAVE_W_GNU_VARIADIC_MACROS) +if (HAVE_W_GNU_VARIADIC_MACROS) + # GTest's parametrised test macro tweaks this warning + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=gnu-zero-variadic-macro-arguments") +endif () + +include(CheckCXXSymbolExists) +list(APPEND CMAKE_REQUIRED_HEADERS ${GTEST_INCLUDE_DIRECTORIES}) +check_cxx_symbol_exists(INSTANTIATE_TEST_SUITE_P "gtest/gtest.h" HAVE_INSTANTIATE_TEST_SUITE_P) +if (NOT HAVE_INSTANTIATE_TEST_SUITE_P) + #GTest conveniently renamed INSTANTIATE_TEST_CASE_P and then deprecated it. + add_definitions(-DINSTANTIATE_TEST_SUITE_P=INSTANTIATE_TEST_CASE_P) +endif() + +find_package(Boost) +find_package(GtestGmock) +pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client) +pkg_check_modules(WAYLAND_SERVER REQUIRED wayland-server) +pkg_check_modules(WAYLAND_SCANNER REQUIRED wayland-scanner) + +include_directories(include) +include_directories(${Boost_INCLUDE_DIRS}) +include_directories(${GMOCK_INCLUDE_DIR} ${GTEST_INCLUDE_DIR}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +set(PROTOCOL_SOURCES "") + +execute_process( + COMMAND wayland-scanner --version + OUTPUT_VARIABLE WAYLAND_SCANNER_VERSION_OUTPUT + ERROR_VARIABLE WAYLAND_SCANNER_VERSION_OUTPUT +) + +separate_arguments(WAYLAND_SCANNER_VERSION_OUTPUT) +list(LENGTH WAYLAND_SCANNER_VERSION_OUTPUT VERSION_STRING_COMPONENTS) +list(GET WAYLAND_SCANNER_VERSION_OUTPUT 1 VERSION_STRING) +string(STRIP ${VERSION_STRING} VERSION_STRING) + +if (NOT(VERSION_STRING_COMPONENTS EQUAL 2)) + message(AUTHOR_WARNING "Failed to parse wayland-scanner --version output") +endif() + +if (VERSION_STRING VERSION_GREATER 1.14.91) + message(STATUS "Found wayland-scanner version ${VERSION_STRING}, using private-code mode") + set(WAYLAND_SCANNER_CODE_GENERATION_TYPE "private-code") +else() + message(STATUS "Found wayland-scanner version ${VERSION_STRING}, using (old) code mode") + set(WAYLAND_SCANNER_CODE_GENERATION_TYPE "code") +endif() + +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/generated) + +macro(GENERATE_PROTOCOL PROTOCOL_NAME) + set(PROTOCOL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/protocol/${PROTOCOL_NAME}.xml") + set(OUTPUT_PATH_C "${CMAKE_CURRENT_BINARY_DIR}/generated/${PROTOCOL_NAME}.c") + set(OUTPUT_PATH_SERVER_H "${CMAKE_CURRENT_BINARY_DIR}/generated/${PROTOCOL_NAME}-server.h") + set(OUTPUT_PATH_CLIENT_H "${CMAKE_CURRENT_BINARY_DIR}/generated/${PROTOCOL_NAME}-client.h") + add_custom_command(OUTPUT "${OUTPUT_PATH_C}" + VERBATIM + COMMAND "wayland-scanner" "--include-core-only" ${WAYLAND_SCANNER_CODE_GENERATION_TYPE} "${PROTOCOL_PATH}" "${OUTPUT_PATH_C}" + DEPENDS "${PROTOCOL_PATH}" + ) + add_custom_command(OUTPUT "${OUTPUT_PATH_SERVER_H}" + VERBATIM + COMMAND "wayland-scanner" "--include-core-only" "server-header" "${PROTOCOL_PATH}" "${OUTPUT_PATH_SERVER_H}" + DEPENDS "${PROTOCOL_PATH}" + ) + add_custom_command(OUTPUT "${OUTPUT_PATH_CLIENT_H}" + VERBATIM + COMMAND "wayland-scanner" "--include-core-only" "client-header" "${PROTOCOL_PATH}" "${OUTPUT_PATH_CLIENT_H}" + DEPENDS "${PROTOCOL_PATH}" + ) + list(APPEND PROTOCOL_SOURCES ${OUTPUT_PATH_C} ${OUTPUT_PATH_CLIENT_H}) +endmacro() + +GENERATE_PROTOCOL(gtk-primary-selection) +GENERATE_PROTOCOL(primary-selection-unstable-v1) +GENERATE_PROTOCOL(wayland) +GENERATE_PROTOCOL(xdg-shell-unstable-v6) +GENERATE_PROTOCOL(xdg-shell) +GENERATE_PROTOCOL(wlr-layer-shell-unstable-v1) +GENERATE_PROTOCOL(xdg-output-unstable-v1) +GENERATE_PROTOCOL(wlr-foreign-toplevel-management-unstable-v1) +GENERATE_PROTOCOL(pointer-constraints-unstable-v1) +GENERATE_PROTOCOL(relative-pointer-unstable-v1) +GENERATE_PROTOCOL(text-input-unstable-v3) +GENERATE_PROTOCOL(input-method-unstable-v2) +GENERATE_PROTOCOL(wlr-virtual-pointer-unstable-v1) + +option(WLCS_BUILD_ASAN "Build a test runner with AddressSanitizer annotations" ON) +option(WLCS_BUILD_TSAN "Build a test runner with ThreadSanitizer annotations" ON) +option(WLCS_BUILD_UBSAN "Build a test runner with UndefinedBehaviourSanitizer annotations" ON) + +set(WLCS_TESTS + tests/test_bad_buffer.cpp + tests/pointer_constraints.cpp + tests/copy_cut_paste.cpp + tests/gtk_primary_selection.cpp + tests/test_surface_events.cpp + tests/touches.cpp + tests/wl_output.cpp + tests/surface_input_regions.cpp + tests/frame_submission.cpp + tests/primary_selection.cpp + tests/relative_pointer.cpp + tests/subsurfaces.cpp + tests/xdg_surface_v6.cpp + tests/xdg_toplevel_v6.cpp + tests/xdg_surface_stable.cpp + tests/xdg_toplevel_stable.cpp + tests/xdg_popup.cpp + tests/wlr_layer_shell_v1.cpp + tests/xdg_output_v1.cpp + tests/wlr_foreign_toplevel_management_v1.cpp + tests/self_test.cpp + tests/text_input_v3_with_input_method_v2.cpp + tests/wlr_virtual_pointer_v1.cpp +) + +set( + WLCS_SOURCES + + include/wlcs/display_server.h + include/wlcs/pointer.h + include/wlcs/touch.h + include/helpers.h + include/wl_handle.h + include/in_process_server.h + include/pointer_constraints_unstable_v1.h + include/primary_selection.h + include/relative_pointer_unstable_v1.h + include/xdg_shell_v6.h + include/xdg_shell_stable.h + include/xdg_output_v1.h + include/version_specifier.h + include/wl_interface_descriptor.h + include/layer_shell_v1.h + include/surface_builder.h + include/input_method.h + + src/data_device.cpp + src/gtk_primary_selection.cpp + src/helpers.cpp + src/in_process_server.cpp + src/xdg_shell_v6.cpp + src/xdg_shell_stable.cpp + src/layer_shell_v1.cpp + src/main.cpp + src/pointer_constraints_unstable_v1.cpp + src/primary_selection.cpp + src/shared_library.cpp + src/relative_pointer_unstable_v1.cpp + src/xfail_supporting_test_listener.h + src/xfail_supporting_test_listener.cpp + src/termcolor.hpp + src/thread_proxy.h + src/xdg_output_v1.cpp + src/version_specifier.cpp + src/surface_builder.cpp + src/input_method.cpp + + ${PROTOCOL_SOURCES} + ${WLCS_TESTS} +) + +# g++ 9.4 (on 20.04) hates the MOCK_METHOD macro +if (CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 10) + # Neither gnu-zero-variadic-macro-arguments nor variadic-macro help, + # we just have to drop pedantic... + set_property(SOURCE ${WLCS_TESTS} PROPERTY COMPILE_OPTIONS -Wno-pedantic) +endif() + +add_library(test_c_compile OBJECT src/test_c_compile.c) + +add_executable(wlcs ${WLCS_SOURCES}) +set(EXECUTABLE_TARGETS wlcs) +if (WLCS_BUILD_ASAN) + add_executable(wlcs.asan ${WLCS_SOURCES}) + target_compile_options(wlcs.asan PUBLIC -fsanitize=address -fno-omit-frame-pointer) + set_target_properties( + wlcs.asan + PROPERTIES + LINK_FLAGS -fsanitize=address) + + # Explicitly linking with the sanitiser is harmless + target_link_libraries(wlcs.asan asan) + list(APPEND EXECUTABLE_TARGETS wlcs.asan) +endif() + +if (WLCS_BUILD_TSAN) + add_executable(wlcs.tsan ${WLCS_SOURCES}) + target_compile_options(wlcs.tsan PUBLIC -fsanitize=thread -fno-omit-frame-pointer) + set_target_properties( + wlcs.tsan + PROPERTIES + LINK_FLAGS -fsanitize=thread) + # Explicitly linking with the sanitiser is harmless. + target_link_libraries(wlcs.tsan tsan) + list(APPEND EXECUTABLE_TARGETS wlcs.tsan) +endif() + +if (WLCS_BUILD_UBSAN) + add_executable(wlcs.ubsan ${WLCS_SOURCES}) + target_compile_options(wlcs.ubsan PUBLIC -fsanitize=undefined) + set_target_properties( + wlcs.ubsan + PROPERTIES + LINK_FLAGS -fsanitize=undefined) + + # Unsure quite why explicitly linking with ubsan is required, but here we are… + target_link_libraries(wlcs.ubsan ubsan) + list(APPEND EXECUTABLE_TARGETS wlcs.ubsan) +endif() + +foreach(TARGET IN LISTS EXECUTABLE_TARGETS) + target_link_libraries( + ${TARGET} + + ${WAYLAND_CLIENT_LDFLAGS} ${WAYLAND_CLIENT_LIBRARIES} + ${WAYLAND_SERVER_LDFLAGS} ${WAYLAND_CLIENT_LIBRARIES} + dl + + ${GMOCK_LIBRARY} + ${GTEST_LIBRARY} + ) +endforeach(TARGET) + +install( + TARGETS ${EXECUTABLE_TARGETS} + RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/wlcs +) + +install( + DIRECTORY include/wlcs + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +include(JoinPaths) +join_paths(PKGCONFIG_BINDIR "\${prefix}" "${CMAKE_INSTALL_BINDIR}") +join_paths(PKGCONFIG_LIBEXECDIR "\${prefix}" "${CMAKE_INSTALL_LIBEXECDIR}") +join_paths(PKGCONFIG_INCLUDEDIR "\${prefix}" "${CMAKE_INSTALL_INCLUDEDIR}") +configure_file( + wlcs.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/wlcs.pc + @ONLY +) + +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/wlcs.pc + DESTINATION + ${CMAKE_INSTALL_LIBDIR}/pkgconfig/ +) diff --git a/COPYING.GPL2 b/COPYING.GPL2 new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/COPYING.GPL2 @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/COPYING.GPL3 b/COPYING.GPL3 new file mode 100644 index 0000000..4432540 --- /dev/null +++ b/COPYING.GPL3 @@ -0,0 +1,676 @@ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7656dc3 --- /dev/null +++ b/README.rst @@ -0,0 +1,52 @@ +============================== +Wayland Conformance Test Suite +============================== + +``wlcs`` aspires to be a protocol-conformance-verifying test suite usable by +Wayland compositor implementors. + +It is growing out of porting the existing Weston test suite to be run +in `Mir's `_ test suite, +but it is designed to be usable by any compositor. + +There have been a number of previous attempts at a Wayland compositor test +suite - the `Wayland Functional Integration Test Suite `_, +and the `Weston test suite `_ +are a couple of examples. + +What sets ``wlcs`` apart from the graveyard of existing test suites is its +integration method. + +Previous test suites have used a Wayland protocol extension +to interrogate the compositor state. This obviously requires that compositors +implement that protocol, means that tests only have access to information +provided by that protocol, and the IPC means that there isn't a canonical +happens-before ordering available. + +Instead, ``wlcs`` relies on compositors providing an integration module, +providing ``wlcs`` with API hooks to start a compositor, connect a client, +move a window, and so on. This makes both writing and debugging tests easier - +the tests are (generally) in the same address space as the compositor, so there +is a consistent global clock available, it's easier to poke around in +compositor internals, and standard debugging tools can follow control flow from +the test client to the compositor and back again. + +Usage +----- + +In order for ``wlcs`` to test your compositor you need to provide an +integration module. This needs to implement the interfaces found in +``include/wlcs``. If your integration module is +``awesome_compositor_wlcs_integration.so``, then running ``wlcs +awesome_compositor_wlcs_integration.so`` will load and run all the tests. + +Development +----------- + +``wlcs`` requires a small number of hooks into your compositor to drive the +tests - it needs to know how to start and stop the mainloop, to get an fd to +connect a client to, and so on. + +To access these hooks, ``wlcs`` looks for a symbol ``wlcs_server_integration`` +of type ``struct WlcsServerIntegration const`` (defined in +``include/wlcs/display_server.h``) in your integration module. diff --git a/cmake/FindGtestGmock.cmake b/cmake/FindGtestGmock.cmake new file mode 100644 index 0000000..359f9ab --- /dev/null +++ b/cmake/FindGtestGmock.cmake @@ -0,0 +1,75 @@ +include(FindPackageHandleStandardArgs) + +find_package(GTest) + +if (NOT GTEST_FOUND) + include(ExternalProject) + + find_path(GTEST_ROOT + NAMES CMakeLists.txt + PATHS /usr/src/gtest /usr/src/googletest/googletest/ + DOC "Path to GTest CMake project") + + ExternalProject_Add(GTest PREFIX ./gtest + SOURCE_DIR ${GTEST_ROOT} + CMAKE_ARGS + -DCMAKE_CXX_COMPILER_WORKS=1 + -DCMAKE_CXX_FLAGS='${CMAKE_CXX_FLAGS}' + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + INSTALL_COMMAND true + BUILD_BYPRODUCTS + ${CMAKE_CURRENT_BINARY_DIR}/gtest/src/GTest-build/libgtest.a + ${CMAKE_CURRENT_BINARY_DIR}/gtest/src/GTest-build/libgtest_main.a + ${CMAKE_CURRENT_BINARY_DIR}/gtest/src/GMock-build/libgmock.a) + + ExternalProject_Get_Property(GTest binary_dir) + + add_library(gtest UNKNOWN IMPORTED) + set_target_properties(gtest PROPERTIES IMPORTED_LOCATION ${binary_dir}/libgtest.a) + add_dependencies(gtest GTest) + set(GTEST_LIBRARY "gtest") + + add_library(gtest_main UNKNOWN IMPORTED) + set_target_properties(gtest_main PROPERTIES IMPORTED_LOCATION ${binary_dir}/libgtest_main.a) + add_dependencies(gtest_main GTest) + set(GTEST_MAIN_LIBRARY "gtest_main") + + set(GTEST_BOTH_LIBRARIES ${GTEST_LIBRARY} ${GTEST_MAIN_LIBRARY}) + find_path(GTEST_INCLUDE_DIRS NAMES gtest/gtest.h) + find_package_handle_standard_args(GTest GTEST_LIBRARY GTEST_BOTH_LIBRARIES GTEST_INCLUDE_DIRS) +endif() + +# Upstream GTestConfig.cmake doesn't provide GTEST_LIBRARY but GTEST_LIBRARIES +# CMake 3.20+ uses the upstream gtest config if possible. +if (NOT DEFINED GTEST_LIBRARY) + set(GTEST_LIBRARY ${GTEST_LIBRARIES}) +endif() + +find_file(GMOCK_SOURCE + NAMES gmock-all.cc + DOC "GMock source" + PATHS /usr/src/googletest/googlemock/src/ /usr/src/gmock/ /usr/src/gmock/src) + +if (EXISTS ${GMOCK_SOURCE}) + find_path(GMOCK_INCLUDE_DIR gmock/gmock.h PATHS /usr/src/googletest/googlemock/include) + + add_library(GMock STATIC ${GMOCK_SOURCE}) + + if (EXISTS /usr/src/googletest/googlemock/src) + set_source_files_properties(${GMOCK_SOURCE} PROPERTIES COMPILE_FLAGS "-I/usr/src/googletest/googlemock") + endif() + + if (EXISTS /usr/src/gmock/src) + set_source_files_properties(${GMOCK_SOURCE} PROPERTIES COMPILE_FLAGS "-I/usr/src/gmock") + endif() + + find_package_handle_standard_args(GMock DEFAULT_MSG GMOCK_INCLUDE_DIR) + + set(GMOCK_LIBRARY GMock) +else() + # Assume gmock is no longer source, we'll find out soon enough if that's wrong + add_custom_target(GMock) + string(REPLACE gtest gmock GMOCK_LIBRARY ${GTEST_LIBRARY}) +endif() + +set(GMOCK_LIBRARIES ${GTEST_BOTH_LIBRARIES} ${GMOCK_LIBRARY}) diff --git a/cmake/JoinPaths.cmake b/cmake/JoinPaths.cmake new file mode 100644 index 0000000..c68d91b --- /dev/null +++ b/cmake/JoinPaths.cmake @@ -0,0 +1,23 @@ +# This module provides function for joining paths +# known from most languages +# +# SPDX-License-Identifier: (MIT OR CC0-1.0) +# Copyright 2020 Jan Tojnar +# https://github.com/jtojnar/cmake-snips +# +# Modelled after Python’s os.path.join +# https://docs.python.org/3.7/library/os.path.html#os.path.join +# Windows not supported +function(join_paths joined_path first_path_segment) + set(temp_path "${first_path_segment}") + foreach(current_segment IN LISTS ARGN) + if(NOT ("${current_segment}" STREQUAL "")) + if(IS_ABSOLUTE "${current_segment}") + set(temp_path "${current_segment}") + else() + set(temp_path "${temp_path}/${current_segment}") + endif() + endif() + endforeach() + set(${joined_path} "${temp_path}" PARENT_SCOPE) +endfunction() diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..e2eed91 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,106 @@ +wlcs (1.6.0-0ubuntu0) UNRELEASED; urgency=medium + + * New upstream release. Notable changes: + + Update wayland.xml to latest + + XDG stable: use mock methods instead of notification lists + + Test popup constraint_adjustment + + Tests for XDG shell (stable) version 5 + + Test that text input is entered after child window is closed + + Copy geometry headers from Mir + + Test popups are dismissed in the correct order + + Test buffer can be deleted after it's attached + + Less flaky synchronization for VirtualPointerV1Test (#297, #294) + + VirtualPointerV1Test: no-events-sent test fails if events are sent (#296) + + Fix various frame-event misunderstandings + + Change remaining std::experimental::optionals to std::optional + + -- Alan Griffiths Mon, 17 Jul 2023 13:12:00 +0100 + +wlcs (1.5.0-0ubuntu0) UNRELEASED; urgency=medium + + * New upstream release. Notable changes: + + Tests for zwlr_virtual_pointer_v1 + + Use maximum shared version when binding globals (Fixes #234) + + Make zxdg_shell_v6 ExpectedlyNotSupported if not in supported_extensions + (Fixes #237) + + Destroy xdg_toplevel before xdg_surface on cleanup + + Fixup CMakeLists.txt so tests can use MOCK_METHOD + + c++20 + + Fix BadBufferTest.test_truncated_shm_file protocol error + + Fix CMake install dir usage in pkgconfig, honour CMAKE_INSTALL_INCLUDEDIR + + -- Alan Griffiths Fri, 6 Jan 2023 11:40:13 +0000 + +wlcs (1.4.0-0ubuntu0) UNRELEASED; urgency=medium + + * New upstream release. Notable changes: + + Add tests for zwp_text_input_unstable_v3 + + Add tests for zwp_input_method_unstable_v2 + + Add tests for zwlr_layer_shell_v1 version 4 + + Drop requriement for compositors to implement wl_shell. Tests which + require an arbitrary "visible surface" should now only require one + of xdg_shell, xdg_shell_unstable_v6, or wl_shell. + + Fix expectations of keyboard focus for xdg_popup tests to match the + protocol. NOTE: These tests will fail on many compositors, including + Weston, as is is common to not follow the protocol correctly here. + + -- Christopher James Halse Rogers Fri, 25 Feb 2022 15:33:24 +1100 + +wlcs (1.3.0-0ubuntu0) groovy; urgency=medium + + * New upstream release. Notable changes: + + Check Cursor movement is not propagating to clients correctly when + zwp_relative_pointer_manager_v1 is enabled + + Support getting the latest serial sent from the compositor (useful for + popup grabs, and possibly other things in the future). Adding `wl_keyboard` + makes sure new surfaces have a serial once they're focused, and provides + access to keyboard focus + + Test that correct input event is used for interactive move + + Fix FindGtestGmock.cmake for new cmake (Fixes the build on Alpine Linux) + + Test that layer surfaces are correctly reconfigured + + Add tests for popup done event + + Test surfaces get enter/leave events + + Test version 2 and 3 features of zwlr_layer_shell_v1 + + Destroy subsurfaces + + Show surface type names in paramaterized touch tests + + -- Alan Griffiths Thu, 27 May 2021 13:13:13 +0100 + +wlcs (1.2.1-0ubuntu0) groovy; urgency=medium + + * New upstream release. Notable changes: + + Fix cut & paste test + + -- Alan Griffiths Fri, 19 Feb 2021 11:11:11 +0000 + +wlcs (1.2.0-0ubuntu0) groovy; urgency=medium + + * New upstream release. Notable changes: + + Add tests for wlr_layer_shell_unstable_v1 + + Build fixes for Musl libc; WLCS now builds on Alpine. Thanks, Luca Weiss + + More XDG Shell tests, particularly around protocol errors, + window-geometry, and input edge-cases. + + Add tests for wlr_foreign_toplevel_management_unstable_v1 + + Many improvements to wl_subsurface tests. Notably this fixes a + misinterpretation of the protocol which lead to testing incorrect + behaviour. + + -- Christopher James Halse Rogers Mon, 31 Aug 2020 16:39:21 +1000 + +wlcs (1.1.0-0ubuntu0) eoan; urgency=medium + + * New upstream release. Relevant upstream changes: + + Document the compositor-integration version macros + + Add tests for the wl_output protocol + + More tests for XDG Shell, particularly around popups and window movement + + Add tests for wp_primary_selection_unstable_v1 protocol + + Add tests for gdk_primary_selection protocol + + Lots of build fixes for !Ubuntu systems. Thanks, Neal Gompa! + + -- Christopher James Halse Rogers Tue, 23 Jul 2019 10:37:52 +1000 + +wlcs (1.0-0ubuntu0) disco; urgency=medium + + * Initial release + + -- Christopher James Halse Rogers Tue, 08 Jan 2019 11:32:06 +1100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..e65efd8 --- /dev/null +++ b/debian/control @@ -0,0 +1,29 @@ +Source: wlcs +Priority: optional +Maintainer: Christopher James Halse Rogers +Build-Depends: + debhelper (>= 9), + cmake, + libboost-dev, + libgtest-dev, + libwayland-dev, + pkg-config, + google-mock +Standards-Version: 4.1.3 +Section: devel +Homepage: https://github.com/MirServer/wlcs +Vcs-Browser: https://salsa.debian.org/mir-server-team/wlcs +Vcs-Git: https://salsa.debian.org/mir-server-team/wlcs.git + +Package: wlcs +Section: devel +Architecture: any +Multi-Arch: same +Depends: ${misc:Depends}, ${shlibs:Depends} +Description: Wayland Conformance Suite's + wlcs aspires to be a protocol-conformance-verifying test suite usable by + Wayland compositor implementations. + . + This package contains the headers necessary for a Wayland compositor to + provide the integration module needed to run wlcs tests, and the test + runner binary needed to run the tests against the compositor. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..fa4f094 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,73 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: wlcs +Source: https://github.com/MirServer/wlcs + +Files: * +Copyright: 2017-2019 Canonical, Ltd +License: GPL-3 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3 as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + On Debian systems, the full text of the GNU General Public + License version 3 can be found in the file + `/usr/share/common-licenses/GPL-3'. + +Files: tests/test_bad_buffer.cpp + tests/test_surface_events.cpp + xdg_popup_stable.cpp + xdg_popup_v6.cpp + xdg_surface_stable.cpp + xdg_surface_v6.cpp + xdg_toplevel_stable.cpp + xdg_toplevel_v6.cpp +Copyright: 2012 Intel Corporation + 2013 Collabora, Ltd. + 2017-2018 Canonical Ltd. +License: Expat + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + . + The above copyright notice and this permission notice (including the + next paragraph) shall be included in all copies or substantial + portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + +Files: debian/* +Copyright: 2019 Christopher James Halse Rogers +License: GPL-2+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..4944295 --- /dev/null +++ b/debian/rules @@ -0,0 +1,41 @@ +#!/usr/bin/make -f + +include /usr/share/dpkg/default.mk + +# see FEATURE AREAS in dpkg-buildflags(1) +export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +COMMON_CONFIGURE_OPTIONS =\ + -DWLCS_BUILD_ASAN=ON \ + -DWLCS_BUILD_UBSAN=ON \ + -DWLCS_BUILD_TSAN=ON + +ifneq ($(filter i386 armhf, $(DEB_HOST_ARCH)),) + # i386 and armhf do not have tsan + COMMON_CONFIGURE_OPTIONS += -DWLCS_BUILD_TSAN=OFF +endif + +ifeq ($(DEB_HOST_ARCH), riscv64) + # riscv64 does not have sanitizers + ifeq ($(DEB_DISTRIBUTION), focal) + COMMON_CONFIGURE_OPTIONS += -DWLCS_BUILD_ASAN=OFF + endif + COMMON_CONFIGURE_OPTIONS += -DWLCS_BUILD_UBSAN=OFF + COMMON_CONFIGURE_OPTIONS += -DWLCS_BUILD_TSAN=OFF +endif + +# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105329 +ifeq ($(DEB_HOST_ARCH), ppc64el) + ifneq ($(shell gcc --version | grep '12.[[:digit:]]\+.[[:digit:]]\+$$'),) + export DEB_CFLAGS_MAINT_APPEND = -O2 + export DEB_CXXFLAGS_MAINT_APPEND = -O2 + export DEB_FCFLAGS_MAINT_APPEND = -O2 + export DEB_FFLAGS_MAINT_APPEND = -O2 + endif +endif + +override_dh_auto_configure: + dh_auto_configure -- $(COMMON_CONFIGURE_OPTIONS) + +%: + dh $@ diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..d3827e7 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +1.0 diff --git a/debian/upstream/signing-key.asc b/debian/upstream/signing-key.asc new file mode 100644 index 0000000..f538fd7 --- /dev/null +++ b/debian/upstream/signing-key.asc @@ -0,0 +1,366 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFmTm/8BEACni7C38F6eITa90TlDHp1m5zMzF1ZMAjTJpiV43KRubeDnYmoo +HUkH0TtXlA5KY/LotWE8IupNtrf1c+17JvbhYYH+gKg3rLdHt2hinQxoNT4ncalt +BzTvq0YOrQGba6uSq4FMjbIhRMPjQLLWxnn3abtIh33y9d9HPMYRF1SG/VliLiwp +KC0zdiHssMFMaZwqSNowhffR4MZCcijxZbyJ1/2UbnK4u7Y6p0VkkmyNmlPXVPFn +cfbxJohaUwj/cphVDJQ0pDZm0FnNDJzfYU8E7Cx5uBEGOauAGj1vkHeVZ9SG512u +F6T1rFj5eaGWcsjIkIuTq5/i6FJf+tvod7/Ugpjf98KZ40Gf/m6iaZErzWg225II +9A7L/1C+Izy3orvxKeCuBCiGo2kzW+POcdp/h2Oc9cGAu88W8OcBSaxxxebsggV4 +3Zn9kxA5ZmTnc/2AOOHvN6GcvDASvAHUq8yZSmAdiCtkimlpJ9G9v54Y+vc8Ptuu +hqV/0uL21qFDxBeh7h6QTS9V+aZF98SbYra0dW1ufYMwHn0Vv33N3XuY0Tf+E9bi +Fc/TLDl1yQMWzl/mq9PNzyZMMUF4bmNyLX64GhJIXQ0ZAEh/S/y8TMpehjP/KK2e +WnXUQiWgNXwPxra6mk24K2k2LCHCVxan+nt5cyrETr8VxMJ0bcpMaG9CXwARAQAB +tDBDaHJpc3RvcGhlciBKYW1lcyBIYWxzZSBSb2dlcnMgPHJhb2ZAdWJ1bnR1LmNv +bT6JAhwEEAEKAAYFAlmo+VQACgkQGLP1D+WSPBiqlQ/+LitTooxk3zjg4xo3P7cf +enG1mF1fu/JonyLT+m5mJJ064ZiZ0ywldT5zwmHTFQAr2Z5S6aNg4o8MmeA9JjpL +LAX1kclD7a2/o33vyoRfE2EabYQkVI0qJgG0YxP0Uip070p6kBZHc9CsvBxiWn5c +k8bIW+LfEQwPT3HtvYFUBmFsrayHifvyV0nWfGKVzNHcpwdYqSHNKjM8EeY9lunv +/zNJMRUTPfhfD05FiyaUXCJyadXJ+IQxrG/wo7hKQcofopZQyMNZrT4LKq7KqFV9 +5tLaS74GzFX3L6m0cVlsGP0UrBWUPemex7Ui4gIbdUG/gRy4I5PS6D79ADoH0vIH +1IKd095LQcudIgCGZyUQabFx6vIC8FMbqK49jndFk6ZbmBZXRWxKK0M2B4Bnlp87 ++dJKXcvN5/lbt/7X3U3Q1IjbzCjPST7Rr+LvPpOK8RxrPwNBYa7ogrZWSoM5uPN4 +1jBDnVQiQ024LL9EMtr8IL1NjIFZHmRraokv8F+8Vk/MB8sxzQRl4Kuhj/Q/vVJY +UeijijhAyzEBoYN45BNVENq9NaMeeeArYeIrsX8QhbqMSB790ZuUgNaDemz6WDgL +wdybapwmOrWy/r8gHFox7ldkuSZS9vKGGrOJhS2vsnV0L95iEy4irm5iY/fqCW9n +Y8xtzwQ2wZLUKCnCN65BfTeJAjoEEwEKACQFAlmToAoCGwMFCQGLggAICwkIBw0M +CwoFFQoJCAsCHgECF4AACgkQoF7vcIbXMkKW6g/+ModoGKhi52wClyQnaMUzYVgB +xyO5iqJHLC+D90FN0fIpL0R6sTUwZ4ac/ymg9PfjZeoPtDcbFI0C93M+isCnoo9r +/IJsanSaoZhLJKlZ5XrLuQg/TRqMhMP+/xmjl9c0zkYFnTS5gdaTiSGhqn1OLoFM +z01fixlf3Q/Ej9Auw+YUE6bCMMqh3bbh4qInfd0o8GrvubtMeekhzasH2avQvrfV +zSx/WKf3Sz4Np4iq943fxDgswbeCZcAliFBEZ+wME4/+7LXW3DrZCMEY81USv1T0 +c+bIkI+A5Dupi1+iWqUfiQyVW6nocU+Hj6NjA0PfYJxeJX8+0xzB1m4FyJklUaJ/ +OEEs5NA5+8iQbSFDAnqb376IQJX/P2clEyR3oGkzZOFGYNJcNJpLxk/BBgkPYZ2v +LsC3eXykA81S7mbpiWIGUlEL9/LAIdEVYLiBp1+XzSwsPa3ME6y3OQHXLjrEPeYD +AKZWBZDkYF/F2IxFiCSJ6P+gzQD67xOPF7cxsG/iIeVuLvxDylkrPweYyTgMqx09 +ouirVW5VRlbVQVhviPTunwUofKZm18zyfpOEJ4e6+26Adsy45D1ziji8ZCbxchFd +quUPdu/VaxA957qAuveG0R8YQEG+MNvj6V+C3zzIvoiJo2t0V75KNZ033AKn16in +eGErY2ad84SLYxin3GSJAlEEEwEKADsCGwMICwkIBw0MCwoFFQoJCAsCHgECF4AW +IQQLDTVq0FnH2jOBvhGgXu9whtcyQgUCWx9paQUJA2ZpXQAKCRCgXu9whtcyQuiD +EACO0apBWPXR2AguczFY/pNpVEq7OyqP13uZbNcT1c48CBcLGy197MDBdfIwIPWF +dTrGUq7g9YTnhwAsjpi07ito57B/VY+lYEPOFqM0taY5k9sCubsJsnukpiHap+lX +YfZ4VrZUfcOIzAGb/rxevyhJq1S8ddmRy7dXUvRnYgOwqB71wbL3yd1tazO0sz1f +aCHqtXZfpHrCCwDxePjMLCrTDfe0CGUk7d25Vkf+wIFuxRdMZLEwCuhhiLeJg0Hw +Vn452MtiRLr+CqVzDLbA8jUDoygWheoIjzEOZCE13nBEPD59me/DKib1b6Q72ZXJ +HLEs7aOi7Rf6lCPIxivHK520BSjVjeejBlTikODgDmo7QiAv5GGa/uJEgBN/Bslv +RlRIQzmO1oKeD4EjruZtm1jomTQO9137raK5xeUJS1r4UyUhm6Eoz6gbVfEEhdrZ +gdL/E37Q6zDcRt+qXG/IfRblggLKiHTUE47HZNxuGxvNf4LlFQcmlX+gH4b8oDAw +4qJMPZlYzKY/3z7grLXukISQa2YdLEdDRuWiQUzWFsK3Baord4VezOnooEXqX9ut +uzksTYU7Uud+ZSZ5gW7apF+a5DLaY9LKsFxKhtc0wD4yRGW6ieCv68AN3+eJ/ehI +IdmsVOQD29UZYvcdpuUe+AkE69ttDZPy83jW9rgYdfho1IkCUQQTAQoAOwIbAwgL +CQgHDQwLCgUVCgkICwIeAQIXgBYhBAsNNWrQWcfaM4G+EaBe73CG1zJCBQJbYVZ7 +BQkCuwh2AAoJEKBe73CG1zJCxOsP/iEgsz2i4zTnn/Zr90WotWXSG8gNsaNq+F7U +aIWwTYBrRtIFnHPBePmVfNX1AQCTKdvHARXMLNaT49t5PeoLwPMhj1LN+80VYfVZ +ZLN2mRxHQRfd6gE/oisGGc5jnYIatwFIJ5RKq/sRCU9r2yp3rOceLwBdDw/BcPnE +dIBfN43rWmBNVI0HeUZyAhO+FABSQpnYiOuEpvYAoYLJroWvTXPT7nKpoAp6pTg+ +61MBzv4ZLkw3B0d7Dk3bKUPLnTeljv4CbtA8F8WZj9T69nk7az3gwnXITO+/taOM +UCYz10WJNVNJl2qdwdUTNOv/+PvVjZ5iUjy5zGEiMUgurFvodSKITVvrzmmAb0nr +QgzzQFk2B0sFyYoPo47edz/CN9tC2+WB+VTX+RBgNR+fuQYR1VZX0c1GVNTbjqii +JJC+t86PSzl5DD6GcjbuB/PswLwQ9im3zhBwtf0rg7SAAIl8FfKc6xAbbeQJsbQ5 +yW2ABIRCW0ZzgrP8+kQV9zsFK84mjLX1w8R0QVAgr+RvY6i0R1r7Ih91Vz3f7MIm +WMKR2TPaaBNzVkgZChilJUzQKmgB8SxNGMRvSQ3VQkzsef7xwoPiL/vsK5/EB2fC +c9amkFjQIDYDG8o2uIRLCmUP5bjtigLD7WDNsancD+qfrK2IIK2XBnbp0GPSiLii +i9MdSQiBiQJRBBMBCgA7AhsDCAsJCAcNDAsKBRUKCQgLAh4BAheAFiEECw01atBZ +x9ozgb4RoF7vcIbXMkIFAlwtcIIFCQOHInkACgkQoF7vcIbXMkJWog/+P8x/xr9Q +ZqV4uHQZf6OJs+PmxppmkiyOi5TU10VfKMB3WX+SsZa4S3NL8LAnfwaRN2IfdZul +NLh0LIE0HQAUoU+2iWtb0Obna9xXHyydkVAMKtMqqlbCKRjv//ElVoouFALnM6nA +MmlfMKrE21pkXin/umkPI7OnmHOzc/I201sV3Wp1Ql/G8RROeu0nJayFSGz9Basx +leby24k9N0y3B7Mk/ft9xUVGnHVrycxDFXVmxff2wF49bVXE98eUlyqQziXMUtJf +U6h7bWoSCmQkl4r2sR8Lz0Zmq2JiZhI7KYbPxfr4spGhIgijD+7ht6fUK4mdPnyq +WEO/mcQKMLo5CYCFvIXDCM2PJx/3EAucDUrlPR901zq10vY/wtEzKFOtpmfNts8/ +8JtNHTBsERvdXhXwBg0GJ4ZTB8dFPr4oQeX53kTw33FTgGy6RWSU8j6ruc+GG9bA +NbJSGvJPWXhC2fkxxTO8et79RmX8RIP21xhEtnEXWmYIfmcxRmZDjMquYIQBioUd +1Ec956GyjAmZIdoBqLwPX+NKpN3d7ZaqU+g+MPLCfzSc1pEpDNF26oJWmD9N2kSe +xzoDLa9PTEKTSMlMXuiIlEgVTCsrgGo/GilSzEyoS7vPLsGPySC6CZ548HnUz2N5 +ddRsrpJF2iHTiHFRItjcDYmA+FF3pd0MKLyJAlEEEwEKADsCGwMICwkIBw0MCwoF +FQoJCAsCHgECF4AWIQQLDTVq0FnH2jOBvhGgXu9whtcyQgUCXJRzQQUJA+4lJAAK +CRCgXu9whtcyQlaDD/9DlY/Zm/5uHsFjpDcrVdLMJizfRGKTm4mMPyK6inamF4h9 +0zFJhSRCKMJSKV5fvrJ54CdRc835wtMzG3DxU6YioHAuyPwcJv6rut3OPqDuH/0i +rSL8bwB+GjAtlTO2xT48pm1IFlzdiGOQNhFAzwxX7jFJbyYkbwT8w1h8EnBrLUwR +/7rlTXXTZtjga43gjPX1RSA9RuUGB4uUNVT+hmBt/W+hpCVcV4f6eV3E1UoWHAEH +6bEKyeo4Rv4DGoM5UEYzCL3QVKvEfuo0GQcBprsNCo+fQPHpNLf+ZAvqARZVzpsG +q9OiS0PMjDL6nLf1H8ehMQek2EUAS4nRJBH49AKJvz+MThI8peNqNpttnMXu2zO3 +kdV5VagHdyFsUmmz9QdptBU//VMIrasxw/drOVyTEY+XFeSW23KoQrIuSPoXuH/4 +J/M/AlZlZNa4KMOqdVddl19cp2DGPr9lYsnG2XwzNqlJ3WyiWJhRIeRUf8Sr1s01 +cigOHuh4eWWUMBpWG5i1ZgvCbXhLAMEjsk+NAykY1hmLH4XblEk+1TtFTKWxZ9B5 +MuA6ZoSfwnWyCmWKFZG7q6la2zIhxhBu8CrnretNZIpYgW/6dvYW19i+9qLI7xH7 +SYWoLavlbZ2QLWjbOBRFB5Hp4+xw1i3ehr2Q8NPvH8vaqXBq8Upcl4sHa0Vn87Q1 +Q2hyaXN0b3BoZXIgSmFtZXMgSGFsc2UgUm9nZXJzIDxjaHJpc0Bjb29wZXJ0ZWFt +Lm5ldD6JAhwEEAEKAAYFAlmo+VQACgkQGLP1D+WSPBjcSRAAxuqbAvHu3MhkRevx +aPfC4kEk2yY0i4JzEk2Wr8KKqvkdKTwcM1B7X6lzh4ZX9QakOgpzlRIgFj16jGyU +3eOFhRvdNp+lgB6ZgK4+kAGckM/UiCgQfmDL2yFgkUKlnrMoUgiSa8ZmqsF6waZ3 +NFqOlRMCGRDs8bPcOyv7mEcuqE/nVmQ8j8Q29f1DcnVe/nlBInO12TV0Ofbhoz/z +vV1ClYyMm+JDUrQE1ozDf6aoXhwJfJHNJCJxII9GQUxT/ke76rt8DgpLfK0xGw3I +qwCqiYyyp8N9F5n4odkvNED3X/uoJb6gs+YLIKOIBBvLEoGIlm76yOBpyNEYpdLZ +YFXYk+mEA069AdHsYoQGB3n9kFJMyDeyRct2TEurXiMWsRV5zOmpaMta1xNnyAg8 +XfN0FHO6jEj0HSmcZ+MUxeed8nMk3YasJyp9mhppog4WnomUYLlZ41rCaVbPgJ8j +0MR360nMHKZKyEVImUHtsLuCyGk+kHuxh3o7j9Wr2nVGYjrmKAEWKomlvFsEtUW9 +ZnvXAjhQesLefJAXKovmuk1lRFPWuOEVcs74372zSP0sQPQKg01CKiPDyALTQIhE +4zKHZPa6N7HK5DbDzAIXS1RFePLqbQNJ25bBxhBmvWq7Tu3frmJHNQA8GuzDC1Rc +XBQQUigpnoRN35RbDUj9u3jX7qWJAj0EEwEKACcCGwMFCQGLggAICwkIBw0MCwoF +FQoJCAsCHgECF4AFAlmTo9cCGQEACgkQoF7vcIbXMkL6YxAAhTUZMMa5nPKC2IYN +c9AS2jhkssKfUMslM/hn5R8Iw20mlfLuN186UzAR7zr6INWyQ44aXZehLdDU41o2 +xXZR0IZ08Ys0cG1xIvxwsyr2jKOCTwDb3K499tGZa7rTlvhYm5GwOWGs/onF1JZt +2asn5yYPOtmb66bwoPkFRyiMP3feI/o47eus2rVU9dMQ82YJ58CBCVvxq0BQAZdq +NzhmxYN0QK90zgn0IiAu4Och1iFEW1VnvfdHO79y7xi7TiEXilBr/I5nMsqdaNrT +8ErtGHTZ95yAClhWcwUEVCrMJaikfnImGdfQA0vHIc8ke9iRJfbWZKLZQBDtNCo7 +MiDd5tyyVrGfQBjN6Vy+CHIHDCYEpJP2WlUl5QO0LMyAJNqBOF4JRAHC9HH3rqDt +4mUy0OIm/XzBMMSFAVPb9uGhvN5im1AYxx7ilyatLO/NPFGGy/sNTn5+joZZIJmK +QjhIL8tOrZYrpn2jH0FSCG/ftM/phRSaHowBn8RqZs5n0JXfZahbj/wxjCFF2KH8 +0yOnYXn5mUZsvX6Y37T2DXokLcQVO3l6/sgO5qV4jOjJbsRm5t22O7KBIgIX4PTa +4Gsl0WeDz2ysujcLaIZvXWgpYDVMMCNAu8H5UhDFs1EUc2stNogP3TbwYgdvFnyg +drjvcIREs0IA+1H1SaXPtLeM2PSJAlQEEwEKAD4CGwMICwkIBw0MCwoFFQoJCAsC +HgECF4ACGQEWIQQLDTVq0FnH2jOBvhGgXu9whtcyQgUCWx9pXAUJA2ZpXQAKCRCg +Xu9whtcyQrAGD/4itne5M74b0J3FfwLuRdJuK+LkL90zFLJy3JkVw6Xcmk45/RwY +KsG3b2RlqzuX+ETgeF9Ov0Wk7L5I/LrRGR3kT1U6MalexVxrG8M5/34sVRqKNanO +AC6Pecduh+O7SH+FBDR68/qJTYAgjnJ7sYNENJwHTZxFa8FuRbFOum5p4kmAZrCF +BvjCQFUPv35rjSe9ZnUOaMY9lEIr/Q5cDUHm4AVIfOfeUjb09FXxmUi3K46smj2Y +IRTHya5zUr8cJDney9pafJrNi+RUkwG4qDTRpVKLD1k3BlItZ/gcN8wvP+/TM3Gx +g9B8XjxntTvFU4qRQfDclZtTRXHg2ceBTg0G2CvTF3FD3YYOtljhg2SeObjGxiF7 +6fTphzIPmSbHocXx4O5up1TVx63QFBvx5nQ09jCXYsAXAR2QmDUal5xRp5TOJgCn ++0KmmYzcmz3FEGJwWPq3D+23q3x6LN8JKWHQbpA04+K5QXRwN53vC+hYHru4tFhO +bG0bt5EsoYPTDI6rB53ZJbikyIcqyvMGnmA1A6OfdMiMxgPfvlf+4VP1GU+NYFKm +Seju2bfEM8L4xFLPUMUr/7BQhV2YCYsUd2FZ2yYjfzrgO6iAJ5eQtjv4suTg5WnD +olsc/TC+9BAJIDFfOgm8YqHB1c2N7eIDOH9KayMOx3B1l+67/kD4aOJzbYkCVAQT +AQoAPgIbAwgLCQgHDQwLCgUVCgkICwIeAQIXgAIZARYhBAsNNWrQWcfaM4G+EaBe +73CG1zJCBQJbYVZ1BQkCuwh2AAoJEKBe73CG1zJCbmMQAI9qmpmQ3F6on2X82Bej +VjixhCAr/gY7+MB9b9up4S8QdrJ1l8UE+BQdbFoHdQoE9iVxxKBwC888TK25clYs +111aw9YI3bWEcLbylqa+8aHSrj0g3BL84z2pwZzGO7bm9zhjMOW0311h71qYa5x8 +vi0Ukkkxb3vHo5EvT6gBu1yBGy1nHtHEEHfoxy3SdSxiCb4hJ02Fpn9AQksJ9hCN +yz7Yvj5/g76dFINOiCDaT3gQ7IHqa9Rquq7M7smeZ92W31oEhvcTuw5nmHEmx3sx +pLHPa3vpgDK1UHdco9bhBiO9YmhGfxa1tEKPb306e55WZZSdMT8e4b03hpk39mdJ +5aL80cKk3ZxFia6yEU8W664wprI4ysVgT6Y0CGv53P9iRo/7F4/foNVY+Lhk4mCt +/k2hyW7MrxH/JVT76Di1RUsDvEAgHB/NHRDkbVJFHhxrMdd8Yy8S1WLlqTjtPrdm +IHI5sIzkxmv4AesdA/2CRyBWWxLyBKRvhXxICholEkM/aj+HKmIKGwSVjqxbV7Ud +lpRH39+t1ZuBWvT4sW4pDkebfN6/pyeWRprcjCWl/oF72bLkLXH+yeHnYf7gai9m +o6+7XEiS4QYKlNad3X5pypuz14KdpdhzE9vnDqGjQbZ8b04ZMZYEX89T8AKDAdNV +geXKe5KXsw3e4+1amP+kgQICiQJUBBMBCgA+AhsDCAsJCAcNDAsKBRUKCQgLAh4B +AheAAhkBFiEECw01atBZx9ozgb4RoF7vcIbXMkIFAlwtcHgFCQOHInkACgkQoF7v +cIbXMkImvA/8CCW63v4QV/ZBDR2yWmrTM7EiN2eiBOl9THHWLQrzoDVnVaWQSjZf +w/irEyy5QkyqnlD3BxhVBCF5v+KvB3Ydk+iZ2z5zVR/sJAUy6b18AX8pErJZzCM7 +33AEVCTeEuQRIORZG0haGHsIyG5k/wmm6FHYapYS23koMFb9E3Fx5oHS+kQ7+4Lw +9MQfUfV3bC+4Aq99fU1XYIZFSbp1Veu+0e+tIlAKSWVQzx2dyiBoQTj96AEu1QUe +cXDZ5faO9O/HMhBVMDVJQDMwrPG5uYI9E4Vv3B3r3hThIFPpJi4YVqPfiNt3xpok +lV9K+Nkw+Ti1GvLNgwV9pcq49B3QB8BTXF2XDUsnD5l0xsKpg8DAmyHMGavQoMsF +o33RgMkVezhZsYrsh0hHXqqYUY8idKctsmtLHVvgPeYXIyDP2wMdDbaOF6zqk6hv +yM8JBfTArXxpI6deuQdNrPsCqW/uq/HmyPHQKuu05GnODpv+RlZvCcCf5o7bKc4W +nJxZlZeq7fYAbXqJEv7r6dV5wq4OrBlJsZILuCrdyxOmiiO1EDHCOJdqNkAAAvZu +t3fqHE5Frl5RBPWl/163AwP635MjbwNQpmSpuu6pVsjGTDeonXzWDlh+tdjhrz7O +vRhkcHgwsYov+wF2Fj8sZDQuniMTmvY5Hz71YlDcNuyvhVqc80l5tpyJAlQEEwEK +AD4CGwMICwkIBw0MCwoFFQoJCAsCHgECF4ACGQEWIQQLDTVq0FnH2jOBvhGgXu9w +htcyQgUCXJRzIwUJA+4lJAAKCRCgXu9whtcyQh+LD/0XbhrHnaRSC8ZVAG8fmn77 +Kb0o7fCPwW/CqMEkq26TI2Ze/qAV2J5Iu54BHl35Q8D92OmfqIoKOum5WnPeUKaZ +MA2gUiOJvnabo+ufFY59dWp2v6KxtfTP0cWtnkdIeAIq1UdhTv7VL7EZ3igunlBY +NdGxOqBtJDGvRcdiCpWK4PvHVcnk3m7XdZmt1+/S26kLNGMgJ9dwgB+ct4GdpQAI +aDGEpO9BKSjKlybRvJQBofDt3FKB5ozFseTHqCN36AcHznJDbImP5/pG01FlDi8i +MMqR2QAKcGBa966NKXEGMHG1KKI2VLN/HePcSw5ooGUV+CAdhYJunaLegfhG2rOJ +fn1tPsWchYHpUjV1sD92A6ePLLqKTXBm4Zx+65akn/7I5q7xJ14IBBDuy3q6PhYJ +81bQYysQz347CORFBXS6yBZhUI8UBism/IYqPfBjCE0Eximi6U8UknfWItdszLVC +P5u7d8j6yFNd60/MxTABkUQ8tZK/N2eePUBQBeLggl4cVr7z0TnfimAce+X14dnO +xmXfRzgenmG5mnQocAX8hne7KX7Dc0nCVDwZLTg0ZvrRQ2GfazmhWehxYyh4mWf6 +Ku8D39sW+eimrnvOdK8NRBTFsJAdSF+BaH8V5ouV1M+fy/ZNY4Z6Grc9/osJlhFd +QwRmGLakWbvUUo6v+vQo8rRHQ2hyaXN0b3BoZXIgSmFtZXMgSGFsc2UgUm9nZXJz +IDxjaHJpc3RvcGhlci5oYWxzZS5yb2dlcnNAY2Fub25pY2FsLmNvbT6JAhwEEAEK +AAYFAlmo+VQACgkQGLP1D+WSPBjdFBAAo3cJg0YIWU5DTZ65zpDSehSFo8ND2Paq +PAwUO4LAVk84uoHD6y9AjklyG999wVnjSs7cq07jvJr5A2zyq1lTr7cAcIXvZLVz +t7Jde7VukTORomvhVgfs2O5MQbsw/VS4k5EVu+Z95z9BBrsOfcTqhMpkplPcMZ5P +EvVBlWJ/XrxoJMroX0QVL5fADbQ25KgbARMvi/rFELtEFpjfsAh/gyR+Ki6cje+N +wisqulplwX9/j0g0ylm+W2py7pPMCKYWTtsSMzeToswwKJMc0BpK693+gelLbK4s +sB2DEAgLdeIi4UF+L8+gsIUodqdRH89J7vV4C1xkfFLV0r6aacuZiJFUyeCP3K89 +Pw5P+4rGTmwlDUlqzvQK6X/cEwYqiRnFKV1JiYEnrBBceWHJsFuErlvQ+qUX8vcR +VnTF5zu0MzYRS1WDVfrjjHTrfTdPUdSJjIPzL2AfIx4npFARHIj4R09ZJcVwcvou +Rg1ISqUySfPDbt7ol0EokxlTmWW9VF574P31WaABu+WL7c2aSljMS2AwtLOvr+KF +fFyoIZJZnz1XGrzSuwp1Fun48ZhDw9IqFvhyUEgWFSPFw+8DturB3NBu0Za0/lrr +NzbFIYxzyvwQ3KFDBTcSafgcFodw03pvJAK0VgPm3acHDSVHj51F3eMH1RlMAxyz +dSfE2Yh7OryJAjoEEwEKACQFAlmToCMCGwMFCQGLggAICwkIBw0MCwoFFQoJCAsC +HgECF4AACgkQoF7vcIbXMkLvgQ//W70n4+9dtmD2+Q0e0E3fqGVIndCSojVHQjJI +uytRCAxuS1UCGAkO6/kT+0iIMSfw+NuzkeWjf+mvezhQSG5OllFYsSONVOmN7mSB +2oUxNN/oZ6Cma/eWwOABtwIMWCcJaXQ6iBryhf1zRFrFpCVqheyq+dEKRJmCO5Ty +cb22LEeEBiVa2q+jgjW7vA3ds+oUXEcqxEclFofXbktbfHjjsDNg27s0ZGh9/Yj2 ++OsgF9R/oejnJgBUoXESW1OkRvG6mI+6WRJIjjMHhvOAAcvvMGADzIniVjialXaL +sWZiceJ5apuv19kKLdmBd5CRFRdxFdfDP0Dkc51Y+DkEXt+xiUHkDJQRoseuijiP +6c4CGmBwA0Tmz6xCXuFpoEfuxRYXFfu2m0DiQXunpjIXQ76uVqe3PMKtm2mQFaNl +NaNTvbC1UXIUCC420gBLOExUSjQ+SJ5L6bM2gwciwJhG+EfexgfVefkIqbtO8ZQJ +lfIQQJiAJwzAmQqwSrEkPz1f6uTFq7aogK7hCZD6WcamGezqeb0dp1jdL28d3BbS +Fze+jbNgrMD2LLrdoOitdEUFicaYfFc1K6IS6HYpviMTHVgt8KHllKsprKS63M3Y +rgFHgm8xxGVKfe9iWa6dtM9b3PZ3XJ77uFHje0xkpEcqzkj2s9O2xM1URKSqRV9F +OHGBUtiJAlEEEwEKADsCGwMICwkIBw0MCwoFFQoJCAsCHgECF4AWIQQLDTVq0FnH +2jOBvhGgXu9whtcyQgUCWx9paQUJA2ZpXQAKCRCgXu9whtcyQluIEACGndwTsQSD +YvTG+ibyToIOjDm1XbjiORc7vhqCImg0l1esaEKf1nPOch1KqK7rTTh+qcjsgzXc +AMhxzQCvbHwpQFycmZSjSlWSk3C19uAMXyEM66s4yBjpVc5MlGRgbbAYB0ooiOm9 +eRgjgrqEtrQMWrDkk1/8n6pyyCmkrWVh8Mp1v8NccPAoWsyAzJNhWqaxxMvRxgjH +GwdfbdRVWCtgv6oeZq0eypLL9rqir2KzKZxillgdeOlw69THa++/Ejpq1CEYdPt/ +VqTB1pLQm8u8eHg6wz44AN+Lr1WShX+TR3aVaiIhbbE0YwlDI9sGBgnanN/i9CaM +WYMjOUucj3czHd2jVBMksQ6Rkz3OSbrFroKg/jNUNqbWh+HY43UjDIdiq06+gFIq +Tu/xM1JRUwN1BXg3Xml/fY160JbruzvvN6hEzmBFa9Xhq0Airk/032A+Qsw04yh9 +YaIJuajrNk82ee/f6gSlzcCAmSZIdTlF85lg0/kpRJTbtiWNyP9HeHSyR7h20JVm +Xys2k8SrYI36Bzo92bbryRNmHB4t28VthqsH9l8OvjGY/jRogQsVW9Qx8YnCM/bZ +vgaAHNdmjO2N3TM5jY5+PkxiDbrCPr1/T/hr4znfPXgolLFkZrCqY1HCVHXlVZw9 +WaQeONDxc+rEmsx/EMZs5GHq/X4hKGBcb4kCUQQTAQoAOwIbAwgLCQgHDQwLCgUV +CgkICwIeAQIXgBYhBAsNNWrQWcfaM4G+EaBe73CG1zJCBQJbYVZ7BQkCuwh2AAoJ +EKBe73CG1zJCFXgQAJWZLYyQikx54xeU7OcHdUzcc6nmoaseOMTpd3I9Mt3d0CBn +8JQOEDCTQCz4qHRthsdXgz6pxjjl59K3tv6qkCd/un7RZIW1xfGKgHL7yWv1EKiP +flheszCD0uRBSoVR3qfBNruD9yy2+NrzDyzajo9N8jPnCdP45VY/V+3UhyXpUJhG +18ryv2XPqRlUIFw2QVIwc8ff3i+Tk7zF/Pf896sS66vf7JI4Q5NNgNygEUXsCj0q +DVKpv8/2XdTEc68FWTbdwDLNoXkBRkFhvLH1XVnq7JiUouWn0ELk/Da1IvbJwoZ1 +MrlzkSpspcwhmChMHAJcrS6dxMjiyo9M8fbfPfKTQOvr9P4M00i9EWHKgILJEDC2 +Gaa8IHihhgbGSdas71jRJWytr1EjOmT82pGzYOUE9OR8pjbj2o+14J8AQGAKHGla +yrSLi21RxrO3ozdOLAS8JFdveOTAYTfViSijbxRcufh0GMFPmA39CqsDNSM7SkkC +gHeiKztR2S5VxKCnAixy2kYxAp1gRMgYFvFECrEP+5x5LQtis36Xv9uh3km4ee/H +ZKEPisLku5c2XGNJsxscl68c9Um4Ike5zgY4D59rowg219RxrJvJkf7EgRuhhJQx +IPdrOLqK1/qu+RH7n/YOO3lyH918LbPzVWeNlYGnJzg2wrWgTg8YCwMwfJOriQJR +BBMBCgA7AhsDCAsJCAcNDAsKBRUKCQgLAh4BAheAFiEECw01atBZx9ozgb4RoF7v +cIbXMkIFAlwtcIIFCQOHInkACgkQoF7vcIbXMkKS6A//UjHAFAIqFi7C3/AlgA8z +GPpCoBeC8hTOGV0c7IGkkhjK0+4gfW9Qj6wMMew/yM4+wSbqtAhATEPLDg9k6LFr +Xn0jTq/mU+zFTLFq+OHk1cmteqgm+uNVS9F/u6gfPKw5KPEsjihuP0JFSQ4o14rO +qyvL4McLEeZW79pJKsUbqKgWGFKJKN7i739HM2MsvLlhhLw572XOdztH0KYy5Blg +qJECerbRd7NwzlcavckW5x7OS9TSwYivqh37mRA9DbMBcOHyDA+ptsntJKBSOD1E +mrSnM8/RSlIXaCinBpdd2D/MkK+44yu/l1XcWq5kt+oQ2pykYJIu3g50LlmCI+3s +DKXT6X+qy4i0SdVP0sggYe+Wij8X8pUqwnje9s8RykDAuQWpQNbm7xo547aGz+/1 +Q7ghZydw0qlEtLbageJ2BdK1DmDvJZIHHNkxgheEc7y5pjf9d1Xww1pZ1l8rCHNu +m0mgBZoL3Pd2yAqk5Ec1raG0VJiL/UcO456Qsftko3JhyH5IRYlM/blssnd8eIl0 +3509MUMHCJMG2GZDriEc8Qo2f4jqlW72DMeKX+cYBJQcFszu/HGP5ROi8/YIYXym +HYmiWiZc8sgQielHim93ZiIpt28qywBNnGUmYBvr6e2tNAIbIZqExFfGmC9JW6/+ +WsoaHfh20D3IGrCKZdIqobeJAlEEEwEKADsCGwMICwkIBw0MCwoFFQoJCAsCHgEC +F4AWIQQLDTVq0FnH2jOBvhGgXu9whtcyQgUCXJRzQQUJA+4lJAAKCRCgXu9whtcy +QpwLD/wMnETj7elIrz4dgsiymRXF09yEgegI4D8xqm3IbXhx3FaVcOJCjuFADWlw +HUAIQBb9n2oVHEK1WkjBlEpTP15DqQe04ZjX+f8PCaPxooNn3WZLXUR2ilEwTvbg +lYT/7y1BLFjW9K0ecaxFTRzja38v3OzNy5wGD0GePbuFDuzVhYRX55wDMgobNq8C +wa0b4F2skyHBzr9IAkB6RZuxN2Ab0dIobYsQZsVOSxAyXzEdt1J9V0rd+TnOMmeZ +VzwuiVTKZvEZuUj8wWlpAffHoSjuzHP2Pa/rlmGrFlZ2A5Bm33Phm822lLLfQwGl +Eli8N6bBGki8EZhe35PCy+yIJI4DWaXS6w7r95YcKoGWuLgzVSYuHEC+9LIs+qat +lojMWSBGl7NTyXHUJ4J2lh1gJSqEzvWvTdD8iCky4WLXmrvHC9Y1S8AgJcqN2jEL +OaUYCw59VhuHqKKirx8XOhqHCQMgkoy9n+0PtzgVkAxND6NtfjeqFTUJHT7YrrZu +XZyhWe7gUtrhzUXaIXh3gAyeaCMH7AnrDOv35hLjq4jv/8nTIV/q7ubSVfQIN2oV +MIGlbFaxRKKSvMu1c9QhMPzPcu9dKe9P40DxhZ/Mko2EsEgrMm2M0g6dajeufQy6 +RX7EFbVrKmq3wV3Z0o3WwP0BsoRl+8FWqxCDSMY/SlbZ/ymN6bkCDQRZk6PlARAA +0wwhX7Rq3xrEaZ2+LmgB8iwgBXOFGZ5/cTIC25gRB8MahJj2JmCuTHaV1Q8p3Dma ++hu46QziTm8IjN52ITJPF+Q7RkrGscIaAEZJ8yko7wT7nC7V7x2poDYQJZNoB3tY +kPfyrI/WT7w641LskUiQ1JkMK0e2iASwTZoiLHADN2e/jHusWafDBDqd3ZYEFXfo +zV6iw3ciF2u5ZiDQ8jU8iTrKZA9PvHcr4ifmqhE0ZT5u+ytXaRKH1l18U1dvSHCN ++ryt/sr3Ow1O12XxqjRU8WTOTaSrEoDpEmheuUGfupJXFE+G1pRc9RGtzChTobzo +tyReRb5ZjgnESdakQHsJXOTE4pJLfpeBawnJOk/jbvR9v8NOSK5MUirgDkG16EHa +o62MtND9fnVVZXDQ8xbASLLDvs7Svhu+AgvgIKjkb/DNq8PhJboAkfwfn+JiMv8i +ckDCwNQn/kZdpOEZsZ7/xjJsddud5LMy6FdYSA3GZuNNyQI035GJT3IDRhR94DyM +0qzq5mBqGrWYxrRPP5Yzvvc/0nTiNs8G4Tb+0PvodETFWcgGZEakKOdYOfd+nwA8 +8SmihaM0g+tReCUGm0fZCI6hZkirhJC35FQG4GOe1zvtV1WeinkpF66WNZzNr/D+ +2EUBEryHTLI4pqMH8o15V40R6U3YyAE0YCqey5iU770AEQEAAYkEWwQYAQoAJgIb +AhYhBAsNNWrQWcfaM4G+EaBe73CG1zJCBQJbYVeQBQkCuwGrAinBXSAEGQEKAAYF +AlmTo+UACgkQ1NtKn10ETxqZVg/+JcV6XanyDRdzZmNHkO7BS6DqL+hv+bKce794 +tzzNpJ9WIZPqa/t8YcL22z7UNT1FxCYVsgYCJ47n2LIlVBSc0gVc+yqPgNad9w/5 +PzHbC3vW5HtQzAqDdcVIDa595qDk4J9cuSFFu165bNs+8eEgHagcOVeZ/KufALjw +srKHcEGm8M+A8aQwtvDHCfmxDJ8OwgyfuvTQYKCn/xTLR3p98apOC/Jf91he11io +AHwrJ9LJsWw4IwCVFy+/vKNvpTj+vGcoro5JhjmV/HCRTYRMO9Jfdam/ZtsXK53M +ea28oUJiphFTRBlTEjt8g6BonLNdBuL/Z8jBtrfopaq4lfEQynOAZb8koDnOZWfR +9UiIL0lXM5CqpZYOiRG4sMa1P0kDH2s+oKbMs0DfuAl0DdpjUrUEKGuu0K82+u0p +q3v9J8B4rzIV3ZNLS6Z99+jCbIBrQiaHNffIWLP/9Hqd6R7fkfP9dj38pvfH4YvJ +yqz1oqtYB1dyQquxaOr0plO0DO+E8lVxDqCrEkztk0idSsAib/7iXQVFNuxZuVFi +bNBWS4ndq3mGO4iYP5QT0oP9oJD/cCGHmDSvS78GZ25M8Nsww2/sXl2Ph6a6+2kU +dQxQoRJF+J2M4eeoiE86L3ZrpQDjkexqaBC7PvuXplL3QhxaDTKr5REdyll/Ppjd +Ni3GLMcJEKBe73CG1zJCeLsP/iZBv60aJB+AKQEijm4AkA2yrGOcJr9/hVbSxS4/ +aW3jXI3Js3+44nwsMYTYHG2ZUhwikG0whnfy9ZvIzOPaKh6IZM+qE+okAMnlu50a +QU5BaH/lFVRiGmz0G/1+S/EWwU8jlqlQBVOVXRkxhu5EB7ZoKJ5U11XOLkB+fPxS +yVGq22PTLJ+FBcCXP9kY/X5bMSUq39txfxwqGkdSiAhK4Vp2nz/ZJyNNm/IyK/AL +Dci/zJdF4OXZ60d048dSamUN56/EtIM0BhaNwBove4gpsnIOjCHmQ5Y4l+5a38ge +WxL8sWmUP5Zm0HU8XLyKBiTKDLJc5tGDHSGQuKmimXJ+MMtzpmU160pf9lO1XqJO +riIAYmqRCuLap5b/mfC7cRLRtr4D6O21wkft8EI6B9T8WzgpDjwnwN1aufrWFvZV +4KXPG5splUwGKHhm27EWQ/F+jhARliSpZBxnEtcqz4jPLNs6hAM4/tsUR8NLDeOQ ++i2Q8iIz8EMb5tYfGeNFGOU/Bp98hMEvL7FPPBNiLgVWF+3XWnwPp1LrtrHSGrRc +Xg0MY/w+3OJtXfe9p+irFIFOnMyYKLYJ0N91Nk3t0a7qvzsyq/1opO+IAoi1Ioea +0WVRIvpcGh5beEV5aRtGFZuUDAie0rOcQHbZDMuFv9uTrFpyGGR0K7FA1+jN3aXq +GtRAiQRbBBgBCgAmAhsCFiEECw01atBZx9ozgb4RoF7vcIbXMkIFAlyUc04FCQPu +HWkCKcFdIAQZAQoABgUCWZOj5QAKCRDU20qfXQRPGplWD/4lxXpdqfINF3NmY0eQ +7sFLoOov6G/5spx7v3i3PM2kn1Yhk+pr+3xhwvbbPtQ1PUXEJhWyBgInjufYsiVU +FJzSBVz7Ko+A1p33D/k/MdsLe9bke1DMCoN1xUgNrn3moOTgn1y5IUW7Xrls2z7x +4SAdqBw5V5n8q58AuPCysodwQabwz4DxpDC28McJ+bEMnw7CDJ+69NBgoKf/FMtH +en3xqk4L8l/3WF7XWKgAfCsn0smxbDgjAJUXL7+8o2+lOP68ZyiujkmGOZX8cJFN +hEw70l91qb9m2xcrncx5rbyhQmKmEVNEGVMSO3yDoGics10G4v9nyMG2t+ilqriV +8RDKc4BlvySgOc5lZ9H1SIgvSVczkKqllg6JEbiwxrU/SQMfaz6gpsyzQN+4CXQN +2mNStQQoa67Qrzb67Smre/0nwHivMhXdk0tLpn336MJsgGtCJoc198hYs//0ep3p +Ht+R8/12Pfym98fhi8nKrPWiq1gHV3JCq7Fo6vSmU7QM74TyVXEOoKsSTO2TSJ1K +wCJv/uJdBUU27Fm5UWJs0FZLid2reYY7iJg/lBPSg/2gkP9wIYeYNK9LvwZnbkzw +2zDDb+xeXY+Hprr7aRR1DFChEkX4nYzh56iITzovdmulAOOR7GpoELs++5emUvdC +HFoNMqvlER3KWX8+mN02LcYsxwkQoF7vcIbXMkLCSQ/+P4offtEj5dxqb/fism9S +sqvW1OPR0xCmF5yEqFF7OPdJZGNeU3VDPl5uhOHYYqSDBo8B8iC9pmANTlQoZOJ4 +KyGiuFIXEK6MTFd1bks4k/87aDKqvFWL/BoRoVs1AGvuCbufkC3NperiXsO6m4TS +Nmgw7yydyKyqN927Hh0fjoo6OmrcWRwRTmrmt/gPyRJMQagvexsG6amsNN6ya3rL +8VNZxegQCF2SrpIQsjt57GzdGk7+hhWpTPEny9JdcDM4tF88YTEgS28kYOS3m7Jm +6x9UhQPIcL8egrr6h3vatlg3Mj7IapUsQIVgzWjnZnAloVVv7ZQOnv04spakNEDf +Ph/zJ2fujGxhVYrj9Q18YZXVz1zAOC6EPoRUAxL9eb4B3MFmmTMZq0Y45CYZ0S/1 +gtK+D1H1kCuQznGQ0z53YQkFJzTf28ITXtAKW/6A5uz1f197RNx5Y+KD53tWNBla +nZedNiIkiHybGf2KgBHMUujpVWZVVHgpM5x9wbPuuUrRQ/52iVyUErAtbsdUgQMU +kzMOiLgjTZAz70WI2qNbx2ttSFFrSTy9udy48blZrQ2ZpGdfnOhiVHCnlYxiKeqM +S9d3+mGJpqwSvBLwymHgtI0r2AJoO+6mSPNPSrO0U0u0hv3/lOrXg59Z0GR/YzUZ +UZ6AwcpvNrQOlok+WfYliFC5Ag0EWZOkSgEQAN7JyINQoAd4lKY3+26eiLKanA0u +BAsTu1XpnFa21verGnISXncrvSlvTHveGrPrHhu+mq1jZftfCpIBfRTg8xyz3OG0 +wnnCGFosvK3fZsfOr+ORCcQFxVVzF9cueXCIqatpFdvHA8FUuN03MJ1nrMGFz2d9 +XI5yUDzVNHNjaIYM3w0RPg4q1vg4xL+KKWW6Fs1cSvtN8nCjHgZTJ41E2udyHS29 +LL6cIaho49qIIdfwScXpMpghlIHxe55M8ARF92BsOUpBUr2J4nRh0U26Opy4CXmE +wAoylwj5MXixAfdSMUl8a3lDXx4/RHTo9PCSyCDygBe/dYS4EygdVvmeIit1anai +muS8iPfe1VpbdNQPcRQkfL2fYkd0NYNMFx998fBdVPSJH/wq6d6JTCv5VLmyHprU +ON9cVpMLFXMkxqhmL/KreczhiTv8oQuk+RiRLWOkN+sfAOhcLHymIA7gufBt8Iw+ +ggUAJOxWoA6QUAsRwor7nCi1EPENKYkEH0dosJZl3NDxjbhhH4tUqznQ+EOGg0dW +2EWtZBPlocjLiMhE7EwKoHY2u/Y7w9Hh/8Bnj/fWXUyhIaVWhkzYISvroGb2YIne +BwJn1cCacofCegBoXF2PeMgDNSPkD5OFzcxvgtNmOqtSZPtfo174/f/f3SMlfPJF +AGMnCcs1I0DqMvPfABEBAAGJAjwEGAEKACYCGwwWIQQLDTVq0FnH2jOBvhGgXu9w +htcyQgUCW2FXkAUJArsBRgAKCRCgXu9whtcyQjBED/48SjmzZKXzJkHUrgOKyIiJ +r3ELJ2DoHkUlGhWceoftZILEajoynDmJ/uM/d8x19H61i73VQmHVopi/x5mXNJOM +QccU5zvQdrXRW3Z106ct0tgaMW23zJP3YSQeXNJfu2ou+zQQwp7wt8I+62tLLlX7 +5BzrVVGyagRKu/JOJu0M1w6zNpGvXzYBXJ2WvuVEhDKzfqaWIt4wjAqGqjTmbA7S +2DWg8BxFxY+pz02V1bx7tjK/Ap11wBJiCH1VMG6+rOPurMXa4Jei879IDUXGGQqd +7jy68x2Tjsy7DLhPSdZTN4DO8Y4od2MQKFRSTQNuR89yHqM9RU5Vi023qxq5EX2I +rq0Tl/MM4NwYPLJ4snOWE5WjfJSyAoV81fJQJLYaeO2w1b8JkRC0orD7QUyZNB/T +9Q8MsNW1qDvwoSDNJw9DmyYcUhea0guLSlhGQjlDsvwVfuOoiAaf837UA8ed6o3v +KSRh63SUj9DXI8RRct3IajAvSrzmZK8/9JBgCVRVWNQRpJ2bQfremsSA3uLW3GFb +DFusS2QSx6hh8joVSNR1wV8tXeVKC+1g6zZwSRQWB5do0oN/uzZPxfX9HNh9JcsV +M0X4dYExSqRa35XkheqWVsmzdXHaiza6A1va3uobMbt/CI2+BxnRu+WIbCUx2na2 +VdS4pue6E5t3F4N0z5Tc64kCPAQYAQoAJgIbDBYhBAsNNWrQWcfaM4G+EaBe73CG +1zJCBQJclHNZBQkD7h0PAAoJEKBe73CG1zJC0noP/is42EQ7yl3aill/BBqdMr+z +54+W40afIxEr71k87zMSVjQGl5pg2+b4vq8stYbd4IcelOSatAWaq5XvXkc92yLE +OdBjO67bDxm2uZQN3JzB24PbLaQm6s45sk2sCX68wJt3ld+uIO2Xs6cO4YsQdjpl +vuo8HuTk8LHciNfHlQUaYjZJ8yL+mpAt4ER4QCHGpcpaMhFWPaP0UloMnU4LeCaQ +DRpUdkw6/OZ7el4XQ04HOZIVpflR4qZ8fzUcNKt3PcAfvhv5Fau6p2AHLLiSFJwx +p4iTzmdRw6F5fArQmSGqFsmpDrQ469ycRqSXg6PNfZkTcVgLLJi5JswqLFCdvJa/ +9F/06l5x6XF8KZDbaie/UkTN69UCvquyZFT0DsS1hxq02m53pMkhXEYzefvsP+/8 +IiZCN8Eu0Pq2zQJqd/LaWNHhyQ+zvKxnYR5ihytzrLpQLDmYTFM3SCeJVBAEbClt +PJBGFbICUaPmMKVAlAFA0RlWpJEWdtSzifGFGr7VwH3kntTTY5udWK6DQalcWkp/ +W49EfXcMGCBhaHPuWjZyeEkWbcHJyvoRwkFQVfZ32+RXxC+tp8el/aCoYpQYo0uW +Em70HEep28wJ7Qvv45ICRYXBrSST7IDynTggbRoutWowNQwTjSVTyWk3hKnZZDem +LwndVGKu0Gcmoj3BlqmguQINBFmTpMEBEAC4g2QIYGwCmfKYhtvM7ZeA9NMk725E +bocvUvZtbk2kgqT92iYulkOxvkY3Ish+kBYhBkQWNh+U+ilLc6kdd+zsxxctJAiT +9gSSMM9gz9cCG+ZXcYzqVFC/+7CuJGyHsd5KVPObSWelrRFN4EU80qS41yRADuR0 +2zxYoNkbK1UERYV+exsWNcaLHh4Jl9I7odNHuty0D6+qHSG7AIEZHd3Wo/dkHkt6 +NyuyTy7ZfxEGUmQIyvWAX5ESl6gjrYEaDn4sEcNGx6rbj1qs3kdUg20fZDzRL14v +F4rXBHTRYHwLBPtxW5RvaqZZY1K1DN4jIOAL5BpMWVAwMDK6+ku3s72eQRziAl4P +NljXxuBlI4UFtosZ1pVAS7S4aL3/rt/jwac65r3eoG5rc+/5jsyl5X6mKRLlH7Jt +QAu3QJdhy2XrhL/EpRoEC4XqqExBaDT64uPqRqGuv7UrFGyggTOTBRhu3ylpQflI +iN528yKorc+KQy8uS3ypBc6xLEI8vVWcb5PwEbplFeXTCaGld8G6q5AggYiTgg51 +T0S7XMY4Cu0nI3b5TIivBSoArVhtYrV9WTRMDRV1CtJfeI7p4F6O9d1uMu8JIeuy +/bqV2DrjSIEHaU7iBJl+YXnfczgYXBRJpbQaw6VB92vHnXLbenlimYQnB0Tt87mE +9V5VLsiI6bA6PwARAQABiQI8BBgBCgAmAhsgFiEECw01atBZx9ozgb4RoF7vcIbX +MkIFAlthV5AFCQK7AM8ACgkQoF7vcIbXMkIoRw/+P1GnVZmshOVDl8qsljv3mz95 +8QjeXZu28kcZEujzQR2eDjoYmTR0HIKX7EXjklQwKPtS9OKvLNeBXA5/qWpg6Eiz +XcZj++H6PX0B4AAzV6XhRGJfmpHtdVJr6POnBThfebGs7DK9rXPX8GQN15MLxu4W +wDW79CazJKIEHIPnsfPwpLUntJxIagcAsqHia/xMlGNJea8G0oGv9XlGXNW0oSBv +HBz5NGkc82nayh/8SnJa4FTHkk3mhGHyfXHOKoI+OoUT5d8Rex7YTNOZWokFYEdq +wWBLRXnJ2fYot5pRk6lt1wPDLObc1UyfHNcopgPFeEUiwAFS9hYp/CUNjsMtnZkR ++4QaGBXSymEKY1LaFDY20sJKKV1Jf1axH7bxVOXoaZcSTMAbA7tAr9ceTNyFXitV +yxa6EpdpKXcXswpNT4hBI8Ep8jkTu9CXmmIJN6/QpTkS0ggHcjClEhlSwQmNyjmC +Zm7PRLn3FSREyLpHvYhyT13Uk4MMHybb3lficDJSOzPyrpfj7JpTVrvAMKrXTD8z +yhpET3l19+1BCYfnzCiH1Ryo10UU0WFR96aQEvAk25+iGIABOwmSNx5+toUl6h6Z +muBtyTgkxTEWvgBkc6rYBHa510iwxnoZExhpFkIjy1aRt94Wozm0G8zBwhDVzTSi +1AUXB4rc1He6dmefjQiJAjwEGAEKACYCGyAWIQQLDTVq0FnH2jOBvhGgXu9whtcy +QgUCXJRzaAUJA+4cpwAKCRCgXu9whtcyQtEvD/9DbVWLPtFLmd1Kxhe5uZUaiO1O +hcWX8zrjtJ08JBI/fQbwakVqZeDCXvk+E3zkql8dIMnXXpz2lirIUolzCSiuXrDX +SUXpBy1PSC0/HLml+/C8g9/Xwh7WiFlGGZv8YuggEP5z3xdXQpR2B9gtKOv7oUwo +vt/TmXqY0btT1TGGghJ9a/Z5PmUOIub2m+IMNWoqvXggim1OajxnrremPKDvC+XI +ccbT5kuclAhHJkxMJdEbB3QVGVmGsd26VcC8JqoczAmbZgcz+aTeaLb0hI2hxDVQ +aYcUs0XM/5s+f7f5qU9Le9oyDm9OJ49tS3vnW/vy8h3YeWU6AidPNwU1q9amcrmd +5XNXv8TXK1zKj3jdReCjyV5vFGan0Db9fewddNa7lkX8UoFiyWru4FSyHhy0krf0 +W5D9D2M7Gpvu50VdBsjOBGqg1O1gHTNUIghRMSQKYlP7OF0k10AJngyk9+v7snXW +0B9Semadwpw02YOcVc1G/zj4+vsyeV12dv1Mxq5+pGCMjUAIHNCa+2GWY4Bgg5DI +Yy0Z6naCgl9lUaT9i0JIJuyZUkK+g9HFGOnEsM0bR/vYSB28HNE8N6W/+HYTbtaH +l5XY1qpzdwyC9JyQcLY0PFELkdHSvQ/HyUmGjfnNqNWhy7YMn9jlWTiMlf8f3rS7 +cRWQ6eS6adR9VqmKfg== +=Ot1v +-----END PGP PUBLIC KEY BLOCK----- diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..4c1712d --- /dev/null +++ b/debian/watch @@ -0,0 +1,6 @@ +# Compulsory line, this is a version 4 file +version=4 + +version=4 +opts="pgpsigurlmangle=s/$/.asc/" \ + https://github.com/MirServer/wlcs/releases/latest .*/wlcs-(\d\S+)\.tar\.xz diff --git a/example/mir_integration.cpp b/example/mir_integration.cpp new file mode 100644 index 0000000..b250b49 --- /dev/null +++ b/example/mir_integration.cpp @@ -0,0 +1,909 @@ +/* + * Copyright © 2017 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "display_server.h" +#include "pointer.h" +#include "touch.h" + +#include "mutex.h" + +#include "mir/fd.h" + +#include "mir/server.h" +#include "mir/options/option.h" +#include "mir_test_framework/async_server_runner.h" +#include "mir_test_framework/headless_display_buffer_compositor_factory.h" + +#include "mir_test_framework/executable_path.h" +#include "mir_test_framework/stub_server_platform_factory.h" +#include "mir_test_framework/fake_input_device.h" +#include "mir/input/device_capability.h" +#include "mir/input/input_device_info.h" +#include "mir/test/doubles/mock_gl.h" +#include "mir/frontend/session.h" +#include "mir/scene/session_listener.h" +#include "mir/scene/surface.h" +#include "mir/scene/session.h" +#include "mir/test/signal.h" + +namespace mtf = mir_test_framework; +namespace mi = mir::input; +namespace mf = mir::frontend; +using namespace std::chrono_literals; + +namespace std +{ +// std::chrono::nanoseconds doesn't have a standard hash<> implementation?! +template<> +struct hash<::std::chrono::nanoseconds> +{ + typedef std::chrono::nanoseconds argument_type; + typedef size_t result_type; + result_type operator()(argument_type const& arg) const noexcept + { + /* + * The underlying type of std::chrono::nanoseconds is guaranteed to be an + * integer of at least 50-something bits, which will already have a perfectly + * functional std::hash<> implementation… + */ + return std::hash{}(arg.count()); + } +}; +} + +namespace +{ +auto constexpr a_long_time = 5s; + +using ClientFd = int; + +class ResourceMapper : public mir::scene::SessionListener +{ +public: + ResourceMapper() + : listeners{&this->state} + { + } + + void starting(std::shared_ptr const&) override + { + } + + void stopping(std::shared_ptr const&) override + { + } + + void focused(std::shared_ptr const&) override + { + } + + void unfocused() override + { + } + + void surface_created( + mir::scene::Session&, + std::shared_ptr const& surface) override + { + auto state_accessor = state.lock(); + if (std::this_thread::get_id() == state_accessor->wayland_thread) + { + if (listeners.last_wl_window == nullptr) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{ + "Called Shell::create_surface() without first creating a wl_shell_surface?"})); + } + + auto stream = surface->primary_buffer_stream(); + auto wl_surface = state_accessor->stream_map.at(stream); + + state_accessor->surface_map[wl_surface] = surface; + } + } + + void destroying_surface( + mir::scene::Session&, + std::shared_ptr const&) override + { + // TODO: Maybe delete from map? + } + + void buffer_stream_created( + mir::scene::Session&, + std::shared_ptr const& stream) override + { + auto state_accessor = state.lock(); + if (std::this_thread::get_id() == state_accessor->wayland_thread) + { + if (listeners.last_wl_surface == nullptr) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{"BufferStream created without first constructing a wl_surface?"})); + } + + state_accessor->stream_map[stream] = listeners.last_wl_surface; + listeners.last_wl_surface = nullptr; + } + } + + void buffer_stream_destroyed( + mir::scene::Session&, + std::shared_ptr const& stream) override + { + state.lock()->stream_map.erase(stream); + } + + void init(wl_display* display) + { + state.lock()->wayland_thread = std::this_thread::get_id(); + + listeners.client_listener.notify = &client_created; + + wl_display_add_client_created_listener(display, &listeners.client_listener); + } + + std::weak_ptr surface_for_resource(wl_resource* surface) + { + if (strcmp(wl_resource_get_class(surface), "wl_surface") != 0) + { + BOOST_THROW_EXCEPTION(( + std::logic_error{ + std::string{"Expected a wl_surface, got: "} + + wl_resource_get_class(surface) + })); + } + + auto state_accessor = state.lock(); + return state_accessor->surface_map.at(surface); + } + + wl_client* client_for_fd(int client_socket) + { + return listeners.state->lock()->client_session_map.at(client_socket); + } + + void associate_client_socket(int client_socket) + { + auto state_accessor = state.wait_for( + [](State& state) { return static_cast(state.latest_client) ; }, + std::chrono::seconds{30}); + state_accessor->client_session_map[client_socket] = state_accessor->latest_client.value(); + state_accessor->latest_client = {}; + } + +private: + struct Listeners; + struct ResourceListener + { + ResourceListener(Listeners* const listeners) + : listeners{listeners} + { + } + + wl_listener resource_listener; + Listeners* const listeners; + }; + struct State + { + std::thread::id wayland_thread; + std::unordered_map> surface_map; + std::unordered_map, wl_resource*> stream_map; + + std::optional latest_client; + std::unordered_map client_session_map; + std::unordered_map resource_listener; + }; + wlcs::WaitableMutex state; + + struct Listeners + { + Listeners(wlcs::WaitableMutex* const state) + : state{state} + { + } + + wl_listener client_listener; + + wl_resource* last_wl_surface{nullptr}; + wl_resource* last_wl_window{nullptr}; + + wlcs::WaitableMutex* const state; + } listeners; + + static void resource_created(wl_listener* listener, void* ctx) + { + auto resource = static_cast(ctx); + ResourceListener* resource_listener; + resource_listener = + wl_container_of(listener, resource_listener, resource_listener); + + bool const is_surface = strcmp( + wl_resource_get_class(resource), + "wl_surface") == 0; + + bool const is_window = strcmp( + wl_resource_get_class(resource), + "wl_shell_surface") == 0 || + strcmp( + wl_resource_get_class(resource), + "zxdg_surface_v6") == 0; + + if (is_surface) + { + resource_listener->listeners->last_wl_surface = resource; + } + else if (is_window) + { + resource_listener->listeners->last_wl_window = resource; + } + } + + static void client_created(wl_listener* listener, void* ctx) + { + auto client = static_cast(ctx); + Listeners* listeners; + listeners = + wl_container_of(listener, listeners, client_listener); + + wl_listener* resource_listener; + { + auto state_accessor = listeners->state->lock(); + state_accessor->latest_client = client; + auto rl = state_accessor->resource_listener.emplace(client, listeners); + rl.first->second.resource_listener.notify = &resource_created; + resource_listener = &rl.first->second.resource_listener; + } + listeners->state->notify_all(); + + wl_client_add_resource_created_listener(client, resource_listener); + } +}; + +namespace +{ +class WaylandExecutor : public mir::Executor +{ +public: + void spawn (std::function&& work) override + { + { + std::lock_guard lock{mutex}; + workqueue.emplace_back(std::move(work)); + } + if (auto err = eventfd_write(notify_fd, 1)) + { + BOOST_THROW_EXCEPTION((std::system_error{err, std::system_category(), "eventfd_write failed to notify event loop"})); + } + } + + /** + * Get an Executor which dispatches onto a wl_event_loop + * + * \note The executor may outlive the wl_event_loop, but no tasks will be dispatched + * after the wl_event_loop is destroyed. + * + * \param [in] loop The event loop to dispatch on + * \return An Executor that queues onto the wl_event_loop + */ + static std::shared_ptr executor_for_event_loop(wl_event_loop* loop) + { + if (auto notifier = wl_event_loop_get_destroy_listener(loop, &on_display_destruction)) + { + DestructionShim* shim; + shim = wl_container_of(notifier, shim, destruction_listener); + + return shim->executor; + } + else + { + auto const executor = std::shared_ptr{new WaylandExecutor{loop}}; + auto shim = std::make_unique(executor); + + shim->destruction_listener.notify = &on_display_destruction; + wl_event_loop_add_destroy_listener(loop, &(shim.release())->destruction_listener); + + return executor; + } + } + +private: + WaylandExecutor(wl_event_loop* loop) + : notify_fd{eventfd(0, EFD_CLOEXEC | EFD_SEMAPHORE | EFD_NONBLOCK)}, + notify_source{wl_event_loop_add_fd(loop, notify_fd, WL_EVENT_READABLE, &on_notify, this)} + { + if (notify_fd == mir::Fd::invalid) + { + BOOST_THROW_EXCEPTION((std::system_error{ + errno, + std::system_category(), + "Failed to create IPC pause notification eventfd"})); + } + } + + std::function get_work() + { + std::lock_guard lock{mutex}; + if (!workqueue.empty()) + { + auto const work = std::move(workqueue.front()); + workqueue.pop_front(); + return work; + } + + return {}; + } + + static int on_notify(int fd, uint32_t, void* data) + { + auto executor = static_cast(data); + + eventfd_t unused; + if (auto err = eventfd_read(fd, &unused)) + { + mir::log( + mir::logging::Severity::error, + "wlcs-integration", + "eventfd_read failed to consume wakeup notification: %s (%i)", + strerror(err), + err); + } + + std::unique_lock lock{executor->mutex}; + while (auto work = executor->get_work()) + { + try + { + work(); + } + catch(...) + { + mir::log( + mir::logging::Severity::critical, + "wlcs-integration", + std::current_exception(), + "Exception processing Wayland event loop work item"); + } + } + + return 0; + } + + static void on_display_destruction(wl_listener* listener, void*) + { + DestructionShim* shim; + shim = wl_container_of(listener, shim, destruction_listener); + + { + std::lock_guard lock{shim->executor->mutex}; + wl_event_source_remove(shim->executor->notify_source); + } + delete shim; + } + + std::recursive_mutex mutex; + mir::Fd const notify_fd; + std::deque> workqueue; + + wl_event_source* const notify_source; + + struct DestructionShim + { + explicit DestructionShim(std::shared_ptr const& executor) + : executor{executor} + { + } + + std::shared_ptr const executor; + wl_listener destruction_listener; + }; + static_assert( + std::is_standard_layout::value, + "DestructionShim must be Standard Layout for wl_container_of to be defined behaviour"); +}; +} + +class InputEventListener; + +struct MirWlcsDisplayServer : mtf::AsyncServerRunner +{ + testing::NiceMock mockgl; + std::shared_ptr const resource_mapper{std::make_shared()}; + std::shared_ptr event_listener; + std::shared_ptr executor; + std::atomic cursor_x{0}, cursor_y{0}; + + mir::test::Signal started; +}; + +class InputEventListener : public mir::input::SeatObserver +{ +public: + InputEventListener(MirWlcsDisplayServer& runner) + : runner{runner} + { + } + + std::shared_ptr expect_event_with_time( + std::chrono::nanoseconds event_time) + { + auto done_signal = std::make_shared(); + expected_events.lock()->insert(std::make_pair(event_time, done_signal)); + return done_signal; + } + + void seat_add_device(uint64_t /*id*/) override + { + } + + void seat_remove_device(uint64_t /*id*/) override + { + } + + void seat_dispatch_event(std::shared_ptr const& event) override + { + auto iev = mir_event_get_input_event(event.get()); + auto event_time = std::chrono::nanoseconds{mir_input_event_get_event_time(iev)}; + + auto expected_events_accessor = expected_events.lock(); + if (expected_events_accessor->count(event_time)) + { + expected_events_accessor->at(event_time)->raise(); + expected_events_accessor->erase(event_time); + } + } + + void seat_set_key_state( + uint64_t /*id*/, + std::vector const& /*scan_codes*/) override + { + } + + void seat_set_pointer_state( + uint64_t /*id*/, + unsigned /*buttons*/) override + { + } + + void seat_set_cursor_position( + float cursor_x, + float cursor_y) override + { + runner.cursor_x = cursor_x; + runner.cursor_y = cursor_y; + } + + void seat_set_confinement_region_called( + mir::geometry::Rectangles const& /*regions*/) override + { + } + + void seat_reset_confinement_regions() override + { + } + +private: + wlcs::Mutex>> expected_events; + MirWlcsDisplayServer& runner; +}; + +template +void emit_mir_event(MirWlcsDisplayServer* runner, + mir::UniqueModulePtr& emitter, + T event) +{ + auto event_time = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()); + + auto event_sent = runner->event_listener->expect_event_with_time(event_time); + + emitter->emit_event(event.with_event_time(event_time)); + + EXPECT_THAT(event_sent->wait_for(a_long_time), testing::Eq(true)) << "fake event failed to go through"; +} +} + +void wlcs_server_start(WlcsDisplayServer* server) +{ + auto runner = reinterpret_cast(server); + + runner->start_server(); + + runner->started.wait_for(a_long_time); +} + +void wlcs_server_stop(WlcsDisplayServer* server) +{ + auto runner = reinterpret_cast(server); + + runner->stop_server(); +} + + +WlcsDisplayServer* wlcs_create_server(int argc, char const** argv) +{ + auto runner = new MirWlcsDisplayServer; + runner->add_to_environment("MIR_SERVER_PLATFORM_GRAPHICS_LIB", mtf::server_platform("graphics-dummy.so").c_str()); + runner->add_to_environment("MIR_SERVER_PLATFORM_INPUT_LIB", mtf::server_platform("input-stub.so").c_str()); + runner->add_to_environment("MIR_SERVER_ENABLE_KEY_REPEAT", "false"); + runner->add_to_environment("MIR_SERVER_NO_FILE", ""); + runner->add_to_environment("MIR_SERVER_WAYLAND_SOCKET_NAME", "wlcs-tests"); + runner->add_to_environment("WAYLAND_DISPLAY", "wlcs-tests"); + runner->server.override_the_display_buffer_compositor_factory([] + { + return std::make_shared(); + }); + + runner->server.override_the_session_listener( + [runner]() + { + return runner->resource_mapper; + }); + + runner->event_listener = std::make_shared(*runner); + + runner->server.set_command_line(argc, argv); + + runner->server.add_init_callback( + [runner]() + { + runner->server.run_on_wayland_display( + [runner](auto wayland_display) + { + runner->resource_mapper->init(wayland_display); + runner->executor = WaylandExecutor::executor_for_event_loop( + wl_display_get_event_loop(wayland_display)); + + // Execute all observations on the Wayland event loop… + runner->server.the_seat_observer_registrar()->register_interest( + runner->event_listener, + *runner->executor); + + runner->started.raise(); + }); + }); + + runner->server.wrap_cursor_listener( + [runner](auto const& wrappee) + { + class ListenerWrapper : public mir::input::CursorListener + { + public: + ListenerWrapper( + MirWlcsDisplayServer* runner, + std::shared_ptr const& wrapped) + : runner{runner}, + wrapped{wrapped} + { + } + + void cursor_moved_to(float abs_x, float abs_y) override + { + runner->cursor_x = abs_x; + runner->cursor_y = abs_y; + wrapped->cursor_moved_to(abs_x, abs_y); + } + + private: + MirWlcsDisplayServer* const runner; + std::shared_ptr const wrapped; + }; + return std::make_shared(runner, wrappee); + }); + + return reinterpret_cast(runner); +} + +void wlcs_destroy_server(WlcsDisplayServer* server) +{ + auto runner = reinterpret_cast(server); + delete runner; +} + +int wlcs_server_create_client_socket(WlcsDisplayServer* server) +{ + auto runner = reinterpret_cast(server); + + try + { + auto client_fd = fcntl( + runner->server.open_wayland_client_socket(), + F_DUPFD_CLOEXEC, + 3); + + runner->resource_mapper->associate_client_socket(client_fd); + + return client_fd; + } + catch (std::exception const&) + { + mir::log( + mir::logging::Severity::critical, + "wlcs-bindings", + std::current_exception(), + "Failed to create Wayland client socket"); + } + + return -1; +} + +struct FakePointer +{ + decltype(mtf::add_fake_input_device(mi::InputDeviceInfo())) pointer; + MirWlcsDisplayServer* runner; +}; + +WlcsPointer* wlcs_server_create_pointer(WlcsDisplayServer* server) +{ + auto runner = reinterpret_cast(server); + + auto constexpr uid = "mouse-uid"; + + class DeviceObserver : public mir::input::InputDeviceObserver + { + public: + DeviceObserver(std::shared_ptr const& done) + : done{done} + { + } + + void device_added(std::shared_ptr const& device) override + { + if (device->unique_id() == uid) + seen_device = true; + } + + void device_changed(std::shared_ptr const&) override + { + } + + void device_removed(std::shared_ptr const&) override + { + } + + void changes_complete() override + { + if (seen_device) + done->raise(); + } + + private: + std::shared_ptr const done; + bool seen_device{false}; + }; + + auto mouse_added = std::make_shared(); + auto observer = std::make_shared(mouse_added); + runner->server.the_input_device_hub()->add_observer(observer); + + + auto fake_mouse = mtf::add_fake_input_device( + mi::InputDeviceInfo{"mouse", uid, mi::DeviceCapability::pointer}); + + mouse_added->wait_for(a_long_time); + runner->executor->spawn([observer=std::move(observer), the_input_device_hub=runner->server.the_input_device_hub()] + { the_input_device_hub->remove_observer(observer); }); + + auto fake_pointer = new FakePointer; + fake_pointer->runner = runner; + fake_pointer->pointer = std::move(fake_mouse); + + return reinterpret_cast(fake_pointer); +} + +void wlcs_destroy_pointer(WlcsPointer* pointer) +{ + delete reinterpret_cast(pointer); +} + +void wlcs_pointer_move_relative(WlcsPointer* pointer, wl_fixed_t x, wl_fixed_t y) +{ + auto device = reinterpret_cast(pointer); + + auto event = mir::input::synthesis::a_pointer_event() + .with_movement(wl_fixed_to_int(x), wl_fixed_to_int(y)); + + emit_mir_event(device->runner, device->pointer, event); +} + +void wlcs_pointer_move_absolute(WlcsPointer* pointer, wl_fixed_t x, wl_fixed_t y) +{ + auto device = reinterpret_cast(pointer); + + auto rel_x = wl_fixed_to_double(x) - device->runner->cursor_x; + auto rel_y = wl_fixed_to_double(y) - device->runner->cursor_y; + + wlcs_pointer_move_relative(pointer, wl_fixed_from_double(rel_x), wl_fixed_from_double(rel_y)); +} + +void wlcs_pointer_button_down(WlcsPointer* pointer, int button) +{ + auto device = reinterpret_cast(pointer); + + auto event = mir::input::synthesis::a_button_down_event() + .of_button(button); + + emit_mir_event(device->runner, device->pointer, event); +} + +void wlcs_pointer_button_up(WlcsPointer* pointer, int button) +{ + auto device = reinterpret_cast(pointer); + + auto event = mir::input::synthesis::a_button_up_event() + .of_button(button); + + emit_mir_event(device->runner, device->pointer, event); +} + +struct FakeTouch +{ + decltype(mtf::add_fake_input_device(mi::InputDeviceInfo())) touch; + int last_x{0}, last_y{0}; + MirWlcsDisplayServer* runner; +}; + +WlcsTouch* wlcs_server_create_touch(WlcsDisplayServer* server) +{ + auto runner = reinterpret_cast(server); + + auto constexpr uid = "touch-uid"; + + class DeviceObserver : public mir::input::InputDeviceObserver + { + public: + DeviceObserver(std::shared_ptr const& done) + : done{done} + { + } + + void device_added(std::shared_ptr const& device) override + { + if (device->unique_id() == uid) + seen_device = true; + } + + void device_changed(std::shared_ptr const&) override + { + } + + void device_removed(std::shared_ptr const&) override + { + } + + void changes_complete() override + { + if (seen_device) + done->raise(); + } + + private: + std::shared_ptr const done; + bool seen_device{false}; + }; + + auto touch_added = std::make_shared(); + auto observer = std::make_shared(touch_added); + runner->server.the_input_device_hub()->add_observer(observer); + + auto fake_touch_dev = mtf::add_fake_input_device( + mi::InputDeviceInfo{"touch", uid, mi::DeviceCapability::multitouch}); + + touch_added->wait_for(a_long_time); + runner->executor->spawn([observer=std::move(observer), the_input_device_hub=runner->server.the_input_device_hub()] + { the_input_device_hub->remove_observer(observer); }); + + auto fake_touch = new FakeTouch; + fake_touch->runner = runner; + fake_touch->touch = std::move(fake_touch_dev); + + return reinterpret_cast(fake_touch); +} + +void wlcs_destroy_touch(WlcsTouch* touch) +{ + delete reinterpret_cast(touch); +} + +void wlcs_touch_down(WlcsTouch* touch, int x, int y) +{ + auto device = reinterpret_cast(touch); + + device->last_x = x; + device->last_y = y; + + auto event = mir::input::synthesis::a_touch_event() + .with_action(mir::input::synthesis::TouchParameters::Action::Tap) + .at_position({x, y}); + + emit_mir_event(device->runner, device->touch, event); +} + +void wlcs_touch_move(WlcsTouch* touch, int x, int y) +{ + auto device = reinterpret_cast(touch); + + device->last_x = x; + device->last_y = y; + + auto event = mir::input::synthesis::a_touch_event() + .with_action(mir::input::synthesis::TouchParameters::Action::Move) + .at_position({x, y}); + + emit_mir_event(device->runner, device->touch, event); +} + +void wlcs_touch_up(WlcsTouch* touch) +{ + auto device = reinterpret_cast(touch); + + auto event = mir::input::synthesis::a_touch_event() + .with_action(mir::input::synthesis::TouchParameters::Action::Release) + .at_position({device->last_x, device->last_y}); + + emit_mir_event(device->runner, device->touch, event); +} + +void wlcs_server_position_window_absolute(WlcsDisplayServer* server, wl_display* client, wl_surface* surface, int x, int y) +{ + auto runner = reinterpret_cast(server); + + try + { + auto const fd = wl_display_get_fd(client); + auto const client = runner->resource_mapper->client_for_fd(fd); + auto const id = wl_proxy_get_id(reinterpret_cast(surface)); + + auto resource = wl_client_get_object(client, id); + + auto mir_surface = runner->resource_mapper->surface_for_resource(resource); + + if (auto live_surface = mir_surface.lock()) + { + live_surface->move_to(mir::geometry::Point{x, y}); + } + else + { + abort(); + // TODO: log? Error handling? + } + } + catch(std::out_of_range const&) + { + abort(); + // TODO: Error handling. + } +} diff --git a/include/active_listeners.h b/include/active_listeners.h new file mode 100644 index 0000000..8f0de8b --- /dev/null +++ b/include/active_listeners.h @@ -0,0 +1,54 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef WLCS_ACTIVE_LISTENERS_H +#define WLCS_ACTIVE_LISTENERS_H + +#include +#include + +namespace wlcs +{ +class ActiveListeners +{ +public: + void add(void* listener) + { + std::lock_guard lock{mutex}; + listeners.insert(listener); + } + + void del(void* listener) + { + std::lock_guard lock{mutex}; + listeners.erase(listener); + } + + bool includes(void* listener) const + { + std::lock_guard lock{mutex}; + return listeners.find(listener) != end(listeners); + } + +private: + std::mutex mutable mutex; + std::set listeners; +}; +} + +#endif //WLCS_ACTIVE_LISTENERS_H diff --git a/include/data_device.h b/include/data_device.h new file mode 100644 index 0000000..35adce5 --- /dev/null +++ b/include/data_device.h @@ -0,0 +1,195 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef WLCS_DATA_DEVICE_H +#define WLCS_DATA_DEVICE_H + +#include "active_listeners.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include + +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(wl_data_device_manager) + +class DataSource +{ +public: + DataSource() = default; + + explicit DataSource(struct wl_data_source* ds) : self{ds, deleter} {} + + operator struct wl_data_source*() const { return self.get(); } + + void reset() { self.reset(); } + + void reset(struct wl_data_source* ds) { self.reset(ds, deleter); } + + friend void wl_data_source_destroy(DataSource const&) = delete; + +private: + static void deleter(struct wl_data_source* ds) { wl_data_source_destroy(ds); } + + std::shared_ptr self; +}; + +struct DataDeviceListener +{ + DataDeviceListener(struct wl_data_device* data_device) + { + active_listeners.add(this); + wl_data_device_add_listener(data_device, &thunks, this); + } + + virtual ~DataDeviceListener() { active_listeners.del(this); } + + DataDeviceListener(DataDeviceListener const&) = delete; + DataDeviceListener& operator=(DataDeviceListener const&) = delete; + + virtual void data_offer( + struct wl_data_device* wl_data_device, + struct wl_data_offer* id); + + virtual void enter( + struct wl_data_device* wl_data_device, + uint32_t serial, + struct wl_surface* surface, + wl_fixed_t x, + wl_fixed_t y, + struct wl_data_offer* id); + + virtual void leave(struct wl_data_device* wl_data_device); + + virtual void motion( + struct wl_data_device* wl_data_device, + uint32_t time, + wl_fixed_t x, + wl_fixed_t y); + + virtual void drop(struct wl_data_device* wl_data_device); + + virtual void selection( + struct wl_data_device* wl_data_device, + struct wl_data_offer* id); + +private: + static void data_offer( + void* data, + struct wl_data_device* wl_data_device, + struct wl_data_offer* id); + + static void enter( + void* data, + struct wl_data_device* wl_data_device, + uint32_t serial, + struct wl_surface* surface, + wl_fixed_t x, + wl_fixed_t y, + struct wl_data_offer* id); + + static void leave(void* data, struct wl_data_device* wl_data_device); + + static void motion( + void* data, + struct wl_data_device* wl_data_device, + uint32_t time, + wl_fixed_t x, + wl_fixed_t y); + + static void drop(void* data, struct wl_data_device* wl_data_device); + + static void selection( + void* data, + struct wl_data_device* wl_data_device, + struct wl_data_offer* id); + + + static ActiveListeners active_listeners; + constexpr static wl_data_device_listener thunks = + { + &data_offer, + &enter, + &leave, + &motion, + &drop, + &selection + }; +}; + +struct DataOfferListener +{ + DataOfferListener() { active_listeners.add(this); } + virtual ~DataOfferListener() { active_listeners.del(this); } + + DataOfferListener(DataOfferListener const&) = delete; + DataOfferListener& operator=(DataOfferListener const&) = delete; + + void listen_to(struct wl_data_offer* data_offer) + { + wl_data_offer_add_listener(data_offer, &thunks, this); + } + + virtual void offer(struct wl_data_offer* data_offer, char const* mime_type); + + virtual void source_actions(struct wl_data_offer* data_offer, uint32_t dnd_actions); + + virtual void action(struct wl_data_offer* data_offer, uint32_t dnd_action); + +private: + static void offer(void* data, struct wl_data_offer* data_offer, char const* mime_type); + + static void source_actions(void* data, struct wl_data_offer* data_offer, uint32_t dnd_actions); + + static void action(void* data, struct wl_data_offer* data_offer, uint32_t dnd_action); + + static ActiveListeners active_listeners; + constexpr static wl_data_offer_listener thunks = + { + &offer, + &source_actions, + &action + }; +}; + +class DataDevice +{ +public: + DataDevice() = default; + + explicit DataDevice(struct wl_data_device* dd) : self{dd, deleter} {} + + operator struct wl_data_device*() const { return self.get(); } + + void reset() { self.reset(); } + + void reset(struct wl_data_device* dd) { self.reset(dd, deleter); } + + friend void wl_data_device_destroy(DataDevice const&) = delete; + +private: + static void deleter(struct wl_data_device* dd) { wl_data_device_destroy(dd); } + + std::shared_ptr self; +}; +} + +#endif //WLCS_DATA_DEVICE_H diff --git a/include/geometry/dimensions.h b/include/geometry/dimensions.h new file mode 100644 index 0000000..fcc6259 --- /dev/null +++ b/include/geometry/dimensions.h @@ -0,0 +1,258 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WLCS_GEOMETRY_DIMENSIONS_H_ +#define WLCS_GEOMETRY_DIMENSIONS_H_ + +#include "forward.h" + +#include +#include +#include + +namespace wlcs +{ +namespace generic +{ +template +/// Wraps a geometry value and prevents it from being accidentally used for invalid operations (such as setting a +/// width to a height or adding two x positions together). Of course, explicit casts are possible to get around +/// these restrictions (see the as_*() functions). +struct Value +{ + using ValueType = T; + using TagType = Tag; + + template + constexpr typename std::enable_if::value, int>::type as_int() const + { + return this->value; + } + + template + constexpr typename std::enable_if::value, uint32_t>::type as_uint32_t() const + { + return this->value; + } + + constexpr T as_value() const noexcept + { + return value; + } + + constexpr Value() noexcept : value{} {} + + Value& operator=(Value const& that) noexcept + { + value = that.value; + return *this; + } + + constexpr Value(Value const& that) noexcept + : value{that.value} + { + } + + template + explicit constexpr Value(Value const& value) noexcept + : value{static_cast(value.as_value())} + { + } + + template::value, bool>::type = true> + explicit constexpr Value(U const& value) noexcept + : value{static_cast(value)} + { + } + + inline constexpr auto operator == (Value const& rhs) const -> bool + { + return value == rhs.as_value(); + } + + inline constexpr auto operator != (Value const& rhs) const -> bool + { + return value != rhs.as_value(); + } + + inline constexpr auto operator <= (Value const& rhs) const -> bool + { + return value <= rhs.as_value(); + } + + inline constexpr auto operator >= (Value const& rhs) const -> bool + { + return value >= rhs.as_value(); + } + + inline constexpr auto operator < (Value const& rhs) const -> bool + { + return value < rhs.as_value(); + } + + inline constexpr auto operator > (Value const& rhs) const -> bool + { + return value > rhs.as_value(); + } + +protected: + T value; +}; + +template +std::ostream& operator<<(std::ostream& out, Value const& value) +{ + out << value.as_value(); + return out; +} + +// Adding deltas is fine +template +inline constexpr DeltaX operator+(DeltaX lhs, DeltaX rhs){ return DeltaX(lhs.as_value() + rhs.as_value()); } +template +inline constexpr DeltaY operator+(DeltaY lhs, DeltaY rhs) { return DeltaY(lhs.as_value() + rhs.as_value()); } +template +inline constexpr DeltaX operator-(DeltaX lhs, DeltaX rhs) { return DeltaX(lhs.as_value() - rhs.as_value()); } +template +inline constexpr DeltaY operator-(DeltaY lhs, DeltaY rhs) { return DeltaY(lhs.as_value() - rhs.as_value()); } +template +inline constexpr DeltaX operator-(DeltaX rhs) { return DeltaX(-rhs.as_value()); } +template +inline constexpr DeltaY operator-(DeltaY rhs) { return DeltaY(-rhs.as_value()); } +template +inline DeltaX& operator+=(DeltaX& lhs, DeltaX rhs) { return lhs = lhs + rhs; } +template +inline DeltaY& operator+=(DeltaY& lhs, DeltaY rhs) { return lhs = lhs + rhs; } +template +inline DeltaX& operator-=(DeltaX& lhs, DeltaX rhs) { return lhs = lhs - rhs; } +template +inline DeltaY& operator-=(DeltaY& lhs, DeltaY rhs) { return lhs = lhs - rhs; } + +// Adding deltas to co-ordinates is fine +template +inline constexpr X operator+(X lhs, DeltaX rhs) { return X(lhs.as_value() + rhs.as_value()); } +template +inline constexpr Y operator+(Y lhs, DeltaY rhs) { return Y(lhs.as_value() + rhs.as_value()); } +template +inline constexpr X operator-(X lhs, DeltaX rhs) { return X(lhs.as_value() - rhs.as_value()); } +template +inline constexpr Y operator-(Y lhs, DeltaY rhs) { return Y(lhs.as_value() - rhs.as_value()); } +template +inline X& operator+=(X& lhs, DeltaX rhs) { return lhs = lhs + rhs; } +template +inline Y& operator+=(Y& lhs, DeltaY rhs) { return lhs = lhs + rhs; } +template +inline X& operator-=(X& lhs, DeltaX rhs) { return lhs = lhs - rhs; } +template +inline Y& operator-=(Y& lhs, DeltaY rhs) { return lhs = lhs - rhs; } + +// Adding deltas to generic::Width and generic::Height is fine +template +inline constexpr Width operator+(Width lhs, DeltaX rhs) { return Width(lhs.as_value() + rhs.as_value()); } +template +inline constexpr Height operator+(Height lhs, DeltaY rhs) { return Height(lhs.as_value() + rhs.as_value()); } +template +inline constexpr Width operator-(Width lhs, DeltaX rhs) { return Width(lhs.as_value() - rhs.as_value()); } +template +inline constexpr Height operator-(Height lhs, DeltaY rhs) { return Height(lhs.as_value() - rhs.as_value()); } +template +inline Width& operator+=(Width& lhs, DeltaX rhs) { return lhs = lhs + rhs; } +template +inline Height& operator+=(Height& lhs, DeltaY rhs) { return lhs = lhs + rhs; } +template +inline Width& operator-=(Width& lhs, DeltaX rhs) { return lhs = lhs - rhs; } +template +inline Height& operator-=(Height& lhs, DeltaY rhs) { return lhs = lhs - rhs; } + +// Adding Widths and Heights is fine +template +inline constexpr Width operator+(Width lhs, Width rhs) { return Width(lhs.as_value() + rhs.as_value()); } +template +inline constexpr Height operator+(Height lhs, Height rhs) { return Height(lhs.as_value() + rhs.as_value()); } +template +inline Width& operator+=(Width& lhs, Width rhs) { return lhs = lhs + rhs; } +template +inline Height& operator+=(Height& lhs, Height rhs) { return lhs = lhs + rhs; } + +// Subtracting coordinates is fine +template +inline constexpr DeltaX operator-(X lhs, X rhs) { return DeltaX(lhs.as_value() - rhs.as_value()); } +template +inline constexpr DeltaY operator-(Y lhs, Y rhs) { return DeltaY(lhs.as_value() - rhs.as_value()); } + +//Subtracting Width and Height is fine +template +inline constexpr DeltaX operator-(Width lhs, Width rhs) { return DeltaX(lhs.as_value() - rhs.as_value()); } +template +inline constexpr DeltaY operator-(Height lhs, Height rhs) { return DeltaY(lhs.as_value() - rhs.as_value()); } + +// Multiplying by a scalar value is fine +template +inline constexpr Width operator*(Scalar scale, Width const& w) { return Width{scale*w.as_value()}; } +template +inline constexpr Height operator*(Scalar scale, Height const& h) { return Height{scale*h.as_value()}; } +template +inline constexpr DeltaX operator*(Scalar scale, DeltaX const& dx) { return DeltaX{scale*dx.as_value()}; } +template +inline constexpr DeltaY operator*(Scalar scale, DeltaY const& dy) { return DeltaY{scale*dy.as_value()}; } +template +inline constexpr Width operator*(Width const& w, Scalar scale) { return scale*w; } +template +inline constexpr Height operator*(Height const& h, Scalar scale) { return scale*h; } +template +inline constexpr DeltaX operator*(DeltaX const& dx, Scalar scale) { return scale*dx; } +template +inline constexpr DeltaY operator*(DeltaY const& dy, Scalar scale) { return scale*dy; } + +// Dividing by a scaler value is fine +template +inline constexpr Width operator/(Width const& w, Scalar scale) { return Width{w.as_value() / scale}; } +template +inline constexpr Height operator/(Height const& h, Scalar scale) { return Height{h.as_value() / scale}; } +template +inline constexpr DeltaX operator/(DeltaX const& dx, Scalar scale) { return DeltaX{dx.as_value() / scale}; } +template +inline constexpr DeltaY operator/(DeltaY const& dy, Scalar scale) { return DeltaY{dy.as_value() / scale}; } +} // namespace + +// Converting between types is fine, as long as they are along the same axis +template +inline constexpr generic::Width as_width(generic::DeltaX const& dx) { return generic::Width{dx.as_value()}; } +template +inline constexpr generic::Height as_height(generic::DeltaY const& dy) { return generic::Height{dy.as_value()}; } +template +inline constexpr generic::X as_x(generic::DeltaX const& dx) { return generic::X{dx.as_value()}; } +template +inline constexpr generic::Y as_y(generic::DeltaY const& dy) { return generic::Y{dy.as_value()}; } +template +inline constexpr generic::DeltaX as_delta(generic::X const& x) { return generic::DeltaX{x.as_value()}; } +template +inline constexpr generic::DeltaY as_delta(generic::Y const& y) { return generic::DeltaY{y.as_value()}; } +template +inline constexpr generic::X as_x(generic::Width const& w) { return generic::X{w.as_value()}; } +template +inline constexpr generic::Y as_y(generic::Height const& h) { return generic::Y{h.as_value()}; } +template +inline constexpr generic::Width as_width(generic::X const& x) { return generic::Width{x.as_value()}; } +template +inline constexpr generic::Height as_height(generic::Y const& y) { return generic::Height{y.as_value()}; } +template +inline constexpr generic::DeltaX as_delta(generic::Width const& w) { return generic::DeltaX{w.as_value()}; } +template +inline constexpr generic::DeltaY as_delta(generic::Height const& h) { return generic::DeltaY{h.as_value()}; } +} // namespace wlcs + +#endif // WLCS_GEOMETRY_DIMENSIONS_H_ diff --git a/include/geometry/displacement.h b/include/geometry/displacement.h new file mode 100644 index 0000000..6942063 --- /dev/null +++ b/include/geometry/displacement.h @@ -0,0 +1,189 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WLCS_GEOMETRY_DISPLACEMENT_H_ +#define WLCS_GEOMETRY_DISPLACEMENT_H_ + +#include "forward.h" +#include "dimensions.h" +#include "point.h" +#include + +namespace wlcs +{ +namespace generic +{ +template +struct Point; + +template +struct Size; + +template +struct Displacement +{ + using ValueType = T; + + constexpr Displacement() {} + constexpr Displacement(Displacement const&) = default; + Displacement& operator=(Displacement const&) = default; + + template + explicit constexpr Displacement(Displacement const& other) noexcept + : dx{DeltaX{other.dx}}, + dy{DeltaY{other.dy}} + { + } + + template + constexpr Displacement(DeltaXType&& dx, DeltaYType&& dy) : dx{dx}, dy{dy} {} + + template + constexpr typename std::enable_if::value, long long>::type length_squared() const + { + long long x = dx.as_value(), y = dy.as_value(); + return x * x + y * y; + } + + template + constexpr typename std::enable_if::value, T>::type length_squared() const + { + T x = dx.as_value(), y = dy.as_value(); + return x * x + y * y; + } + + DeltaX dx; + DeltaY dy; +}; + +template +inline constexpr bool operator==(Displacement const& lhs, Displacement const& rhs) +{ + return lhs.dx == rhs.dx && lhs.dy == rhs.dy; +} + +template +inline constexpr bool operator!=(Displacement const& lhs, Displacement const& rhs) +{ + return lhs.dx != rhs.dx || lhs.dy != rhs.dy; +} + +template +std::ostream& operator<<(std::ostream& out, Displacement const& value) +{ + out << '(' << value.dx << ", " << value.dy << ')'; + return out; +} + +template +inline constexpr Displacement operator+(Displacement const& lhs, Displacement const& rhs) +{ + return Displacement{lhs.dx + rhs.dx, lhs.dy + rhs.dy}; +} + +template +inline constexpr Displacement operator-(Displacement const& lhs, Displacement const& rhs) +{ + return Displacement{lhs.dx - rhs.dx, lhs.dy - rhs.dy}; +} + +template +inline constexpr Displacement operator-(Displacement const& rhs) +{ + return Displacement{-rhs.dx, -rhs.dy}; +} + +template +inline constexpr Point operator+(Point const& lhs, Displacement const& rhs) +{ + return Point{lhs.x + rhs.dx, lhs.y + rhs.dy}; +} + +template +inline constexpr Point operator+(Displacement const& lhs, Point const& rhs) +{ + return Point{rhs.x + lhs.dx, rhs.y + lhs.dy}; +} + +template +inline constexpr Point operator-(Point const& lhs, Displacement const& rhs) +{ + return Point{lhs.x - rhs.dx, lhs.y - rhs.dy}; +} + +template +inline constexpr Displacement operator-(Point const& lhs, Point const& rhs) +{ + return Displacement{lhs.x - rhs.x, lhs.y - rhs.y}; +} + +template +inline constexpr Point& operator+=(Point& lhs, Displacement const& rhs) +{ + return lhs = lhs + rhs; +} + +template +inline constexpr Point& operator-=(Point& lhs, Displacement const& rhs) +{ + return lhs = lhs - rhs; +} + +template +inline bool operator<(Displacement const& lhs, Displacement const& rhs) +{ + return lhs.length_squared() < rhs.length_squared(); +} + +template +inline constexpr Displacement operator*(Scalar scale, Displacement const& disp) +{ + return Displacement{scale*disp.dx, scale*disp.dy}; +} + +template +inline constexpr Displacement operator*(Displacement const& disp, Scalar scale) +{ + return scale*disp; +} + +template +inline constexpr Displacement as_displacement(Size const& size) +{ + return Displacement{size.width.as_value(), size.height.as_value()}; +} + +template +inline constexpr Size as_size(Displacement const& disp) +{ + return Size{disp.dx.as_value(), disp.dy.as_value()}; +} + +template +inline constexpr Displacement as_displacement(Point const& point) +{ + return Displacement{point.x.as_value(), point.y.as_value()}; +} + +template +inline constexpr Point as_point(Displacement const& disp) +{ + return Point{disp.dx.as_value(), disp.dy.as_value()}; +} +} +} + +#endif // WLCS_GEOMETRY_DISPLACEMENT_H_ diff --git a/include/geometry/forward.h b/include/geometry/forward.h new file mode 100644 index 0000000..13c4049 --- /dev/null +++ b/include/geometry/forward.h @@ -0,0 +1,88 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WLCS_GEOMETRY_FORWARD_H_ +#define WLCS_GEOMETRY_FORWARD_H_ + +namespace wlcs +{ +/// These tag types determine what type of dimension a value holds and what operations are possible with it. They are +/// only used as template parameters, are never instantiated and should only require forward declarations, but some +/// compiler versions seem to fail if they aren't given real declarations. +/// @{ +struct WidthTag{}; +struct HeightTag{}; +struct XTag{}; +struct YTag{}; +struct DeltaXTag{}; +struct DeltaYTag{}; +struct StrideTag{}; +/// @} + +namespace generic +{ +template +struct Value; + +template +struct Point; + +template +struct Size; + +template +struct Displacement; + +template +struct Rectangle; + +template using Width = Value; +template using Height = Value; +template using X = Value; +template using Y = Value; +template using DeltaX = Value; +template using DeltaY = Value; +} + +using Width = generic::Width; +using Height = generic::Height; +using X = generic::X; +using Y = generic::Y; +using DeltaX = generic::DeltaX; +using DeltaY = generic::DeltaY; + +using WidthF = generic::Width; +using HeightF = generic::Height; +using XF = generic::X; +using YF = generic::Y; +using DeltaXF = generic::DeltaX; +using DeltaYF = generic::DeltaY; + +// Just to be clear, mir::geometry::Stride is the stride of the buffer in bytes +using Stride = generic::Value; + +using Point = generic::Point; +using Size = generic::Size; +using Displacement = generic::Displacement; +using Rectangle = generic::Rectangle; + +using PointF = generic::Point; +using SizeF = generic::Size; +using DisplacementF = generic::Displacement; +using RectangleF = generic::Rectangle; +} + +#endif // WLCS_GEOMETRY_FORWARD_H_ diff --git a/include/geometry/point.h b/include/geometry/point.h new file mode 100644 index 0000000..5c95437 --- /dev/null +++ b/include/geometry/point.h @@ -0,0 +1,98 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WLCS_GEOMETRY_POINT_H_ +#define WLCS_GEOMETRY_POINT_H_ + +#include "forward.h" +#include "dimensions.h" +#include + +namespace wlcs +{ +namespace generic +{ +template +struct Size; +template +struct Displacement; + +template +struct Point +{ + using ValueType = T; + + constexpr Point() = default; + constexpr Point(Point const&) = default; + Point& operator=(Point const&) = default; + + template + explicit constexpr Point(Point const& other) noexcept + : x{X{other.x}}, + y{Y{other.y}} + { + } + + template + constexpr Point(XType&& x, YType&& y) : x(x), y(y) {} + + X x; + Y y; +}; + +template +inline constexpr bool operator == (Point const& lhs, Point const& rhs) +{ + return lhs.x == rhs.x && lhs.y == rhs.y; +} + +template +inline constexpr bool operator != (Point const& lhs, Point const& rhs) +{ + return lhs.x != rhs.x || lhs.y != rhs.y; +} + +template +inline constexpr Point operator+(Point lhs, DeltaX rhs) { return{lhs.x + rhs, lhs.y}; } +template +inline constexpr Point operator+(Point lhs, DeltaY rhs) { return{lhs.x, lhs.y + rhs}; } + +template +inline constexpr Point operator-(Point lhs, DeltaX rhs) { return{lhs.x - rhs, lhs.y}; } +template +inline constexpr Point operator-(Point lhs, DeltaY rhs) { return{lhs.x, lhs.y - rhs}; } + +template +inline Point& operator+=(Point& lhs, DeltaX rhs) { lhs.x += rhs; return lhs; } +template +inline Point& operator+=(Point& lhs, DeltaY rhs) { lhs.y += rhs; return lhs; } + +template +inline Point& operator-=(Point& lhs, DeltaX rhs) { lhs.x -= rhs; return lhs; } +template +inline Point& operator-=(Point& lhs, DeltaY rhs) { lhs.y -= rhs; return lhs; } + +template +std::ostream& operator<<(std::ostream& out, Point const& value) +{ + out << value.x << ", " << value.y; + return out; +} + +} +} + +#endif // WLCS_GEOMETRY_POINT_H_ diff --git a/include/geometry/rectangle.h b/include/geometry/rectangle.h new file mode 100644 index 0000000..a414076 --- /dev/null +++ b/include/geometry/rectangle.h @@ -0,0 +1,145 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WLCS_GEOMETRY_RECTANGLE_H_ +#define WLCS_GEOMETRY_RECTANGLE_H_ + +#include "forward.h" +#include "point.h" +#include "size.h" +#include "displacement.h" + +#include + +namespace wlcs +{ +namespace generic +{ +template +struct Rectangle +{ + constexpr Rectangle() = default; + + constexpr Rectangle(Point const& top_left, Size const& size) + : top_left{top_left}, size{size} + { + } + + /** + * The bottom right boundary point of the rectangle. + * + * Note that the returned point is *not* included in the rectangle + * area, that is, the rectangle is represented as [top_left,bottom_right). + */ + Point bottom_right() const + { + return top_left + as_displacement(size); + } + + Point top_right() const + { + return top_left + as_delta(size.width); + } + + Point bottom_left() const + { + return top_left + as_delta(size.height); + } + + bool contains(Point const& p) const + { + if (size.width == decltype(size.width){} || size.height == decltype(size.height){}) + return false; + + auto br = bottom_right(); + return p.x >= left() && p.x < br.x && + p.y >= top() && p.y < br.y; + } + + /** + * Test if the rectangle contains another. + * + * Note that an empty rectangle can still contain other empty rectangles, + * which are treated as points or lines of thickness zero. + */ + bool contains(Rectangle const& r) const + { + return r.left() >= left() && + r.left() + as_delta(r.size.width) <= left() + as_delta(size.width) && + r.top() >= top() && + r.top() + as_delta(r.size.height) <= top() + as_delta(size.height); + } + + bool overlaps(Rectangle const& r) const + { + bool disjoint = r.left() >= right() + || r.right() <= left() + || r.top() >= bottom() + || r.bottom() <= top() + || size.width == decltype(size.width){} + || size.height == decltype(size.height){} + || r.size.width == decltype(r.size.width){} + || r.size.height == decltype(r.size.height){}; + return !disjoint; + } + + X left() const { return top_left.x; } + X right() const { return bottom_right().x; } + Y top() const { return top_left.y; } + Y bottom() const { return bottom_right().y; } + + Point top_left; + Size size; +}; + +template +Rectangle intersection_of(Rectangle const& a, Rectangle const& b) +{ + auto const max_left = std::max(a.left(), b.left()); + auto const min_right = std::min(a.right(), b.right()); + auto const max_top = std::max(a.top(), b.top()); + auto const min_bottom = std::min(a.bottom(), b.bottom()); + + if (max_left < min_right && max_top < min_bottom) + return {{max_left, max_top}, + {(min_right - max_left).as_value(), + (min_bottom - max_top).as_value()}}; + else + return {}; +} + +template +inline constexpr bool operator == (Rectangle const& lhs, Rectangle const& rhs) +{ + return lhs.top_left == rhs.top_left && lhs.size == rhs.size; +} + +template +inline constexpr bool operator != (Rectangle const& lhs, Rectangle const& rhs) +{ + return lhs.top_left != rhs.top_left || lhs.size != rhs.size; +} + +template +std::ostream& operator<<(std::ostream& out, Rectangle const& value) +{ + out << '(' << value.top_left << ", " << value.size << ')'; + return out; +} +} +} + +#endif // WLCS_GEOMETRY_RECTANGLE_H_ diff --git a/include/geometry/size.h b/include/geometry/size.h new file mode 100644 index 0000000..609ee49 --- /dev/null +++ b/include/geometry/size.h @@ -0,0 +1,107 @@ +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WLCS_GEOMETRY_SIZE_H_ +#define WLCS_GEOMETRY_SIZE_H_ + +#include "forward.h" +#include "dimensions.h" +#include + +namespace wlcs +{ +namespace generic +{ +template +struct Point; +template +struct Displacement; + +template +struct Size +{ + using ValueType = T; + + constexpr Size() noexcept {} + constexpr Size(Size const&) noexcept = default; + Size& operator=(Size const&) noexcept = default; + + template + explicit constexpr Size(Size const& other) noexcept + : width{Width{other.width}}, + height{Height{other.height}} + { + } + + template + constexpr Size(WidthType&& width, HeightType&& height) noexcept : width(width), height(height) {} + + Width width; + Height height; +}; + +template +inline constexpr bool operator == (Size const& lhs, Size const& rhs) +{ + return lhs.width == rhs.width && lhs.height == rhs.height; +} + +template +inline constexpr bool operator != (Size const& lhs, Size const& rhs) +{ + return lhs.width != rhs.width || lhs.height != rhs.height; +} + +template +std::ostream& operator<<(std::ostream& out, Size const& value) +{ + out << '(' << value.width << ", " << value.height << ')'; + return out; +} + +template +inline constexpr Size operator*(Scalar scale, Size const& size) +{ + return Size{scale*size.width, scale*size.height}; +} + +template +inline constexpr Size operator*(Size const& size, Scalar scale) +{ + return scale*size; +} + +template +inline constexpr Size operator/(Size const& size, Scalar scale) +{ + return Size{size.width / scale, size.height / scale}; +} + +template +inline constexpr Size as_size(Point const& point) +{ + return Size{point.x.as_value(), point.y.as_value()}; +} + +template +inline constexpr Point as_point(Size const& size) +{ + return Point{size.width.as_value(), size.height.as_value()}; +} +} +} + +#endif // WLCS_GEOMETRY_SIZE_H_ diff --git a/include/gtest_helpers.h b/include/gtest_helpers.h new file mode 100644 index 0000000..2ad1b92 --- /dev/null +++ b/include/gtest_helpers.h @@ -0,0 +1,46 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#include +#include +#include + +namespace std +{ +namespace chrono +{ +/// GTest helper to pretty-print time-points +template +void PrintTo(time_point const& time, ostream* os) +{ + auto remainder = time.time_since_epoch(); + auto const hours = duration_cast(remainder); + remainder -= hours; + auto const minutes = duration_cast(remainder); + remainder -= minutes; + auto const seconds = duration_cast(remainder); + remainder -= seconds; + auto const nsec = duration_cast(remainder); + (*os) + << hours.count() + << ":" << setw(2) << setfill('0') << minutes.count() + << ":" << setw(2) << setfill('0') << seconds.count() + << "." << setw(9) << setfill('0') << nsec.count(); +} +} +} diff --git a/include/gtk_primary_selection.h b/include/gtk_primary_selection.h new file mode 100644 index 0000000..4028dfe --- /dev/null +++ b/include/gtk_primary_selection.h @@ -0,0 +1,162 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef WLCS_PRIMARY_SELECTION_H +#define WLCS_PRIMARY_SELECTION_H + +#include "generated/gtk-primary-selection-client.h" + +#include "active_listeners.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include +#include +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(gtk_primary_selection_device_manager) + + class GtkPrimarySelectionSource + { + public: + using WrappedType = gtk_primary_selection_source; + + GtkPrimarySelectionSource() = default; + + explicit GtkPrimarySelectionSource(gtk_primary_selection_device_manager* manager) : + self{gtk_primary_selection_device_manager_create_source(manager), deleter} {} + + operator WrappedType*() const { return self.get(); } + + void reset() { self.reset(); } + + void reset(WrappedType* source) { self.reset(source, deleter); } + + friend void gtk_primary_selection_source_destroy(GtkPrimarySelectionSource const&) = delete; + + private: + static void deleter(WrappedType* source) { gtk_primary_selection_source_destroy(source); } + + std::shared_ptr self; + }; + + + class GtkPrimarySelectionDevice + { + public: + using WrappedType = gtk_primary_selection_device; + + GtkPrimarySelectionDevice() = default; + + GtkPrimarySelectionDevice(gtk_primary_selection_device_manager* manager, wl_seat* seat) : + self{gtk_primary_selection_device_manager_get_device(manager, seat), deleter} {} + + operator WrappedType*() const { return self.get(); } + + void reset() { self.reset(); } + + void reset(WrappedType* device) { self.reset(device, deleter); } + + friend void gtk_primary_selection_device_destroy(GtkPrimarySelectionDevice const&) = delete; + + private: + static void deleter(WrappedType* device) { gtk_primary_selection_device_destroy(device); } + + std::shared_ptr self; + }; + + struct GtkPrimarySelectionDeviceListener + { + GtkPrimarySelectionDeviceListener(gtk_primary_selection_device* device) + { + active_listeners.add(this); + gtk_primary_selection_device_add_listener(device, &thunks, this); + } + + virtual ~GtkPrimarySelectionDeviceListener() { active_listeners.del(this); } + + GtkPrimarySelectionDeviceListener(GtkPrimarySelectionDeviceListener const&) = delete; + GtkPrimarySelectionDeviceListener& operator=(GtkPrimarySelectionDeviceListener const&) = delete; + + virtual void data_offer(gtk_primary_selection_device* device, gtk_primary_selection_offer* offer); + + virtual void selection(gtk_primary_selection_device* device, gtk_primary_selection_offer* offer); + + private: + static void data_offer(void* data, gtk_primary_selection_device* device, gtk_primary_selection_offer* offer); + + static void selection(void* data, gtk_primary_selection_device* device, gtk_primary_selection_offer* offer); + + static ActiveListeners active_listeners; + constexpr static gtk_primary_selection_device_listener thunks = + { + &data_offer, + &selection + }; + }; + + struct GtkPrimarySelectionOfferListener + { + GtkPrimarySelectionOfferListener() { active_listeners.add(this); } + virtual ~GtkPrimarySelectionOfferListener() { active_listeners.del(this); } + + GtkPrimarySelectionOfferListener(GtkPrimarySelectionOfferListener const&) = delete; + GtkPrimarySelectionOfferListener& operator=(GtkPrimarySelectionOfferListener const&) = delete; + + void listen_to(gtk_primary_selection_offer* offer) + { + gtk_primary_selection_offer_add_listener(offer, &thunks, this); + } + + virtual void offer(gtk_primary_selection_offer* offer, const char* mime_type); + + private: + static void offer(void* data, gtk_primary_selection_offer* offer, const char* mime_type); + + static ActiveListeners active_listeners; + constexpr static gtk_primary_selection_offer_listener thunks = { &offer, }; + }; + + struct GtkPrimarySelectionSourceListener + { + explicit GtkPrimarySelectionSourceListener(GtkPrimarySelectionSource const& source) + { + active_listeners.add(this); + gtk_primary_selection_source_add_listener(source, &thunks, this); + } + + virtual ~GtkPrimarySelectionSourceListener() { active_listeners.del(this); } + + GtkPrimarySelectionSourceListener(GtkPrimarySelectionSourceListener const&) = delete; + GtkPrimarySelectionSourceListener& operator=(GtkPrimarySelectionSourceListener const&) = delete; + + virtual void send(gtk_primary_selection_source* source, const char* mime_type, int32_t fd); + virtual void cancelled(gtk_primary_selection_source* source); + + private: + static void send(void* data, gtk_primary_selection_source* source, const char* mime_type, int32_t fd); + static void cancelled(void* data, gtk_primary_selection_source* source); + + static ActiveListeners active_listeners; + constexpr static gtk_primary_selection_source_listener thunks = { &send, &cancelled }; + }; +} + +#endif //WLCS_PRIMARY_SELECTION_H diff --git a/include/helpers.h b/include/helpers.h new file mode 100644 index 0000000..3a22001 --- /dev/null +++ b/include/helpers.h @@ -0,0 +1,44 @@ +/* + * Copyright © 2017 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_HELPERS_H_ +#define WLCS_HELPERS_H_ + +#include +#include + +struct WlcsServerIntegration; + +namespace wlcs +{ +namespace helpers +{ +int create_anonymous_file(size_t size); + +void set_command_line(int argc, char const** argv); + +int get_argc(); +char const** get_argv(); + +void set_entry_point(std::shared_ptr const& entry_point); + +std::shared_ptr get_test_hooks(); +} +} + +#endif //WLCS_HELPERS_H_ diff --git a/include/in_process_server.h b/include/in_process_server.h new file mode 100644 index 0000000..5163684 --- /dev/null +++ b/include/in_process_server.h @@ -0,0 +1,370 @@ +/* + * Copyright © 2017-2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_IN_PROCESS_SERVER_H_ +#define WLCS_IN_PROCESS_SERVER_H_ + +#include "generated/wayland-client.h" +#include "generated/xdg-shell-unstable-v6-client.h" +#include "generated/xdg-shell-client.h" + +#include + +#include +#include +#include +#include +#include + +#include "shared_library.h" +#include "wl_handle.h" + +#include + +struct WlcsPointer; +struct WlcsTouch; +struct WlcsServerIntegration; + +struct zwlr_layer_shell_v1; + +namespace wlcs +{ +class VersionSpecifier; + +WLCS_CREATE_INTERFACE_DESCRIPTOR(wl_surface) +WLCS_CREATE_INTERFACE_DESCRIPTOR(wl_subsurface) + +/* We need a manually-specified descriptor for wl_output, + * as wl_output only has a destructor for version >= 3 + */ +namespace +{ +void send_release_if_supported(wl_output* to_destroy) +{ + if (wl_output_get_version(to_destroy) >= WL_OUTPUT_RELEASE_SINCE_VERSION) + { + wl_output_release(to_destroy); + } + else + { + wl_output_destroy(to_destroy); + } +} +} +template<> +struct WlInterfaceDescriptor +{ + static constexpr bool const has_specialisation = true; + static constexpr wl_interface const* const interface = &wl_output_interface; + static constexpr void (* const destructor)(wl_output*) = &send_release_if_supported; +}; + +class Pointer +{ +public: + ~Pointer(); + Pointer(Pointer&&); + + void move_to(int x, int y); + void move_by(int dx, int dy); + + void button_down(int button); + void button_up(int button); + void click(int button); + + void left_button_down(); + void left_button_up(); + void left_click(); + +private: + friend class Server; + template + Pointer( + WlcsPointer* raw_device, + std::shared_ptr const& proxy, + std::shared_ptr const& keep_dso_loaded); + + class Impl; + std::unique_ptr impl; +}; + +class Touch +{ +public: + ~Touch(); + Touch(Touch&&); + + void down_at(int x, int y); + void move_to(int x, int y); + void up(); + +private: + friend class Server; + template + Touch( + WlcsTouch* raw_device, + std::shared_ptr const& proxy, + std::shared_ptr const& keep_dso_loaded); + + class Impl; + std::unique_ptr impl; +}; + +class Surface; + +class Server +{ +public: + Server( + std::shared_ptr const& module, + int argc, + char const** argv); + ~Server(); + + int create_client_socket(); + + Pointer create_pointer(); + Touch create_touch(); + + void move_surface_to(Surface& surface, int x, int y); + + void start(); + void stop(); + + std::shared_ptr> supported_extensions(); +private: + class Impl; + std::unique_ptr const impl; +}; + +class Client; + +class Surface +{ +public: + explicit Surface(Client& client); + virtual ~Surface(); + + Surface(Surface&& other); + + operator ::wl_surface*() const; + auto wl_surface() const -> ::wl_surface* { return *this; }; + + void attach_buffer(int width, int height); + void add_frame_callback(std::function const& on_frame); + void attach_visible_buffer(int width, int height); + void run_on_destruction(std::function callback); + + Client& owner() const; + auto current_outputs() -> std::set const&; + +private: + class Impl; + std::unique_ptr impl; +}; + +class Subsurface: public Surface +{ +public: + static Subsurface create_visible(Surface& parent, int x, int y, int width, int height); + + Subsurface(Surface& parent); + Subsurface(Subsurface &&); + ~Subsurface(); + + operator wl_subsurface*() const; + + Surface& parent() const; + +private: + class Impl; + std::unique_ptr impl; +}; + +class ShmBuffer +{ +public: + ShmBuffer(Client& client, int width, int height); + ~ShmBuffer(); + + ShmBuffer(ShmBuffer&& other); + + operator wl_buffer*() const; + + void add_release_listener(std::function const &on_release); + +private: + class Impl; + std::unique_ptr impl; +}; + +struct OutputState +{ + OutputState(wl_output* output) + : output{output} + { + } + + wl_output* output; + std::optional> geometry_position; + std::optional> mode_size; + std::optional scale; +}; + +class Client +{ +public: + Client(Server& server); + ~Client(); + + // Accessors + + operator wl_display*() const; + + wl_compositor* compositor() const; + wl_subcompositor* subcompositor() const; + wl_shm* shm() const; + wl_seat* seat() const; + + void run_on_destruction(std::function callback); + + ShmBuffer const& create_buffer(int width, int height); + Surface create_wl_shell_surface(int width, int height); + Surface create_xdg_shell_v6_surface(int width, int height); + Surface create_xdg_shell_stable_surface(int width, int height); + Surface create_visible_surface(int width, int height); + + size_t output_count() const; + OutputState output_state(size_t index) const; + void add_output_done_notifier(size_t index, std::function const& notifier); + + wl_shell* shell() const; + wl_pointer* the_pointer() const; + zxdg_shell_v6* xdg_shell_v6() const; + xdg_wm_base* xdg_shell_stable() const; + wl_surface* keyboard_focused_window() const; + wl_surface* window_under_cursor() const; + wl_surface* touched_window() const; + std::pair pointer_position() const; + std::pair touch_position() const; + std::optional latest_serial() const; + + using PointerEnterNotifier = + std::function; + using PointerLeaveNotifier = + std::function; + using PointerMotionNotifier = + std::function; + using PointerButtonNotifier = + std::function; + void add_pointer_enter_notification(PointerEnterNotifier const& on_enter); + void add_pointer_leave_notification(PointerLeaveNotifier const& on_leave); + void add_pointer_motion_notification(PointerMotionNotifier const& on_motion); + void add_pointer_button_notification(PointerButtonNotifier const& on_button); + + void dispatch_until( + std::function const& predicate, + std::chrono::seconds timeout = std::chrono::seconds{10}); + + template + auto bind_if_supported(VersionSpecifier const& version) -> WlHandle + { + return wrap_wl_object( + static_cast(bind_if_supported(*WlInterfaceDescriptor::interface, version))); + } + + auto bind_if_supported(wl_interface const& interface, VersionSpecifier const& version) const -> void*; + + /** + * Perform a `wl_display_roundtrip()` + * + * This blocks until all previous client requests have been processed + * by the server, and the client has processed any server responses. + */ + void roundtrip(); + + /** + * Perform a `wl_display_flush()` + * + * This ensures all previous client requests have *actually* been + * sent to the server, but does not wait for any replies or process + * any incomming messages. + * + * \note It is possible that the server receive buffer will be too + * small to hold all the outgoing messages. This method does + * not provide a way to detect this case. + */ + void flush(); + +private: + class Impl; + std::unique_ptr const impl; +}; + +class ProtocolError : public std::system_error +{ +public: + ProtocolError(wl_interface const* interface, uint32_t code); + + char const* what() const noexcept override; + + uint32_t error_code() const; + wl_interface const* interface() const; +private: + wl_interface const* const interface_; + uint32_t const code_; + std::string const message; +}; + +class ExtensionExpectedlyNotSupported : public std::runtime_error +{ +public: + ExtensionExpectedlyNotSupported(char const* extension, VersionSpecifier const& version); +}; + +class Timeout : public std::runtime_error +{ +public: + explicit Timeout(char const* message); +}; + +class InProcessServer : public testing::Test +{ +public: + InProcessServer(); + + void SetUp() override; + void TearDown() override; + + Server& the_server(); +private: + Server server; +}; + +class StartedInProcessServer : public InProcessServer +{ +public: + StartedInProcessServer() { InProcessServer::SetUp(); } + ~StartedInProcessServer() { InProcessServer::TearDown(); } + + void SetUp() override {} + void TearDown() override {} +}; +} + +#endif //WLCS_IN_PROCESS_SERVER_H_ diff --git a/include/input_method.h b/include/input_method.h new file mode 100644 index 0000000..0a16f89 --- /dev/null +++ b/include/input_method.h @@ -0,0 +1,83 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef WLCS_INPUT_METHOD_H_ +#define WLCS_INPUT_METHOD_H_ + +#include +#include +#include "in_process_server.h" + +namespace wlcs +{ + +struct InputMethod +{ + InputMethod(std::string const& name) : name{name} {} + virtual ~InputMethod() = default; + + struct Device + { + virtual ~Device() = default; + /// Can only be called while decice is up + virtual void down_at(std::pair position) = 0; + /// Can only be called while device is down + virtual void move_to(std::pair position) = 0; + /// Can only be called while device is down + virtual void up() = 0; + }; + + virtual auto create_device(wlcs::Server& server) -> std::unique_ptr = 0; + virtual auto current_surface(wlcs::Client const& client) -> wl_surface* = 0; + virtual auto position_on_surface(wlcs::Client const& client) -> std::pair = 0; + + static auto all_input_methods() -> std::vector>; + + std::string const name; +}; + +struct PointerInputMethod : InputMethod +{ + PointerInputMethod() : InputMethod{"pointer"} {} + + struct Pointer; + + auto create_device(wlcs::Server& server) -> std::unique_ptr override; + auto current_surface(wlcs::Client const& client) -> wl_surface* override; + auto position_on_surface(wlcs::Client const& client) -> std::pair override; +}; + +struct TouchInputMethod : InputMethod +{ + TouchInputMethod() : InputMethod{"touch"} {} + + struct Touch; + + auto create_device(wlcs::Server& server) -> std::unique_ptr override; + auto current_surface(wlcs::Client const& client) -> wl_surface* override; + auto position_on_surface(wlcs::Client const& client) -> std::pair override; +}; + +} + +namespace std +{ +std::ostream& operator<<(std::ostream& out, std::shared_ptr const& param); +} + +#endif // WLCS_INPUT_METHOD_H_ diff --git a/include/layer_shell_v1.h b/include/layer_shell_v1.h new file mode 100644 index 0000000..93e79b7 --- /dev/null +++ b/include/layer_shell_v1.h @@ -0,0 +1,94 @@ +/* + * Copyright © 2018-2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef WLCS_LAYER_SHELL_V1_H +#define WLCS_LAYER_SHELL_V1_H + +#include "in_process_server.h" +#include "wl_handle.h" +#include "geometry/size.h" + +// Because _someone_ *cough*ddevault*cough* thought it would be a great idea to name an argument "namespace" +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wkeyword-macro" +#endif +#define namespace _namespace +#include "generated/wlr-layer-shell-unstable-v1-client.h" +#undef namespace +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +namespace wlcs +{ + +// We need to use a custom destructor as .destroyed() changed in v3 +namespace +{ +void send_destroy_if_supported(zwlr_layer_shell_v1* to_destroy) +{ + if (zwlr_layer_shell_v1_get_version(to_destroy) >= ZWLR_LAYER_SHELL_V1_DESTROY_SINCE_VERSION) + { + zwlr_layer_shell_v1_destroy(to_destroy); + } + else + { + wl_proxy_destroy(reinterpret_cast(to_destroy)); + } +} +} +template<> +struct WlInterfaceDescriptor +{ + static constexpr bool const has_specialisation = true; + static constexpr wl_interface const* const interface = &zwlr_layer_shell_v1_interface; + static constexpr void (* const destructor)(zwlr_layer_shell_v1*) = &send_destroy_if_supported; +}; + +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwlr_layer_surface_v1) + +class LayerSurfaceV1 +{ +public: + LayerSurfaceV1( + wlcs::Client& client, + wlcs::Surface& surface, + zwlr_layer_shell_v1_layer layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP, + wl_output* output = NULL, + const char *_namespace = "wlcs"); + LayerSurfaceV1(LayerSurfaceV1 const&) = delete; + LayerSurfaceV1& operator=(LayerSurfaceV1 const&) = delete; + + operator zwlr_layer_surface_v1*() const { return layer_surface; } + operator zwlr_layer_shell_v1*() const { return layer_shell; } + + void dispatch_until_configure(); + auto last_size() const -> wlcs::Size { return last_size_; } + +private: + wlcs::Client& client; + WlHandle layer_shell; + WlHandle layer_surface; + Size last_size_ = {-1, -1}; + int configure_count = 0; +}; + +} + +#endif // WLCS_LAYER_SHELL_V1_H diff --git a/include/method_event_impl.h b/include/method_event_impl.h new file mode 100644 index 0000000..c73920c --- /dev/null +++ b/include/method_event_impl.h @@ -0,0 +1,47 @@ +/* + * Copyright © 2021 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef WLCS_METHOD_EVENT_IMPL_H_ +#define WLCS_METHOD_EVENT_IMPL_H_ + +#include "wl_handle.h" + +namespace wlcs +{ +namespace detail +{ +template +struct MemberFunctionClass; + +template +struct MemberFunctionClass { + using type = T; +}; +} + +/// Can be used to easily implement Wayland listeners using methods, including gmock methods +template +void method_event_impl(void* data, WlType*, Args... args) +{ + auto self = static_cast::type*>(data); + (self->*member_fn)(args...); +} + +} + +#endif // WLCS_METHOD_EVENT_IMPL_H_ diff --git a/include/mock_input_method_v2.h b/include/mock_input_method_v2.h new file mode 100644 index 0000000..2cc2b0e --- /dev/null +++ b/include/mock_input_method_v2.h @@ -0,0 +1,78 @@ +/* + * Copyright © 2021 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef MOCK_INPUT_METHOD_V2 +#define MOCK_INPUT_METHOD_V2 + +#include "generated/wayland-client.h" +#include "generated/input-method-unstable-v2-client.h" + +#include "in_process_server.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" +#include "method_event_impl.h" + +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_input_method_manager_v2) +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_input_method_v2) + +class MockInputMethodV2 : public WlHandle +{ +private: + void done_wrapper() + { + done_count_++; + done(); + } + +public: + MockInputMethodV2(zwp_input_method_v2* proxy) + : WlHandle{proxy} + { + zwp_input_method_v2_add_listener(proxy, &listener, this); + } + + MOCK_METHOD0(activate, void()); + MOCK_METHOD0(deactivate, void()); + MOCK_METHOD3(surrounding_text, void(std::string const&, uint32_t, uint32_t)); + MOCK_METHOD1(text_change_cause, void(uint32_t)); + MOCK_METHOD2(content_type, void(uint32_t, uint32_t)); + MOCK_METHOD0(done, void()); + MOCK_METHOD0(unavailable, void()); + + auto done_count() -> uint32_t { return done_count_; } + + static zwp_input_method_v2_listener constexpr listener { + method_event_impl<&MockInputMethodV2::activate>, + method_event_impl<&MockInputMethodV2::deactivate>, + method_event_impl<&MockInputMethodV2::surrounding_text>, + method_event_impl<&MockInputMethodV2::text_change_cause>, + method_event_impl<&MockInputMethodV2::content_type>, + method_event_impl<&MockInputMethodV2::done_wrapper>, + method_event_impl<&MockInputMethodV2::unavailable>, + }; + +private: + uint32_t done_count_{0}; +}; +} + +#endif // MOCK_INPUT_METHOD_V2 diff --git a/include/mock_text_input_v3.h b/include/mock_text_input_v3.h new file mode 100644 index 0000000..03e99b2 --- /dev/null +++ b/include/mock_text_input_v3.h @@ -0,0 +1,65 @@ +/* + * Copyright © 2021 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef MOCK_TEXT_INPUT_V3_H +#define MOCK_TEXT_INPUT_V3_H + +#include "generated/wayland-client.h" +#include "generated/text-input-unstable-v3-client.h" + +#include "in_process_server.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" +#include "method_event_impl.h" + +#include +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_text_input_manager_v3) +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_text_input_v3) + +class MockTextInputV3 : public WlHandle +{ +public: + MockTextInputV3(zwp_text_input_v3* proxy) + : WlHandle{proxy} + { + zwp_text_input_v3_add_listener(proxy, &listener, this); + } + + MOCK_METHOD1(enter, void(wl_surface *)); + MOCK_METHOD1(leave, void(wl_surface *)); + MOCK_METHOD3(preedit_string, void(std::string const&, int32_t, int32_t)); + MOCK_METHOD1(commit_string, void(std::string const&)); + MOCK_METHOD2(delete_surrounding_text, void(int32_t, int32_t)); + MOCK_METHOD1(done, void(int32_t)); + + static zwp_text_input_v3_listener constexpr listener { + method_event_impl<&MockTextInputV3::enter>, + method_event_impl<&MockTextInputV3::leave>, + method_event_impl<&MockTextInputV3::preedit_string>, + method_event_impl<&MockTextInputV3::commit_string>, + method_event_impl<&MockTextInputV3::delete_surrounding_text>, + method_event_impl<&MockTextInputV3::done>, + }; +}; +} + +#endif // MOCK_TEXT_INPUT_V3_H diff --git a/include/mutex.h b/include/mutex.h new file mode 100644 index 0000000..76aeda9 --- /dev/null +++ b/include/mutex.h @@ -0,0 +1,132 @@ +/* + * Copyright © 2017 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_MUTEX_H_ +#define WLCS_MUTEX_H_ + +#include +#include +#include + +namespace wlcs +{ +/** + * Smart-pointer-esque accessor for Mutex<> protected data. + * + * Ensures exclusive access to the referenced data. + * + * \tparam Guarded Type of data guarded by the mutex. + */ +template +class MutexGuard +{ +public: + MutexGuard(std::unique_lock&& lock, Guarded& value) + : value{value}, + lock{std::move(lock)} + { + } + MutexGuard(MutexGuard&& from) = default; + ~MutexGuard() noexcept(false) + { + if (lock.owns_lock()) + { + lock.unlock(); + } + } + + Guarded& operator*() + { + return value; + } + Guarded* operator->() + { + return &value; + } +private: + Guarded& value; + std::unique_lock lock; +}; + +/** + * A data-locking mutex + * + * This is a mutex which owns the data it guards, and can give out a + * smart-pointer-esque lock to lock and access it. + * + * \tparam Guarded The type of data guarded by the mutex + */ +template +class Mutex +{ +public: + Mutex() = default; + Mutex(Guarded&& initial_value) + : value{std::move(initial_value)} + { + } + + Mutex(Mutex const&) = delete; + Mutex& operator=(Mutex const&) = delete; + + /** + * Lock the mutex and return an accessor for the protected data. + * + * \return A smart-pointer-esque accessor for the contained data. + * While code has access to the MutexGuard it is guaranteed to have exclusive + * access to the contained data. + */ + MutexGuard lock() + { + return MutexGuard{std::unique_lock{mutex}, value}; + } + +protected: + std::mutex mutex; + Guarded value; +}; + +template +class WaitableMutex : public Mutex +{ +public: + using Mutex::Mutex; + + template + MutexGuard wait_for(Predicate predicate, std::chrono::duration timeout) + { + std::unique_lock lock{this->mutex}; + if (!notifier.wait_for(lock, timeout, [this, &predicate]() { return predicate(this->value); })) + { + BOOST_THROW_EXCEPTION((std::runtime_error{"Notification timeout"})); + } + return MutexGuard{std::move(lock), this->value}; + } + + void notify_all() + { + notifier.notify_all(); + } +private: + std::condition_variable notifier; +}; + + +} + +#endif //WLCS_MUTEX_H_ diff --git a/include/pointer_constraints_unstable_v1.h b/include/pointer_constraints_unstable_v1.h new file mode 100644 index 0000000..5a1dfa8 --- /dev/null +++ b/include/pointer_constraints_unstable_v1.h @@ -0,0 +1,104 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef WLCS_POINTER_CONSTRAINTS_UNSTABLE_V1_H +#define WLCS_POINTER_CONSTRAINTS_UNSTABLE_V1_H + +#include "generated/pointer-constraints-unstable-v1-client.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include + +#include + +struct wl_pointer; + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_pointer_constraints_v1) + +class Client; + +class ZwpPointerConstraintsV1 +{ +public: + ZwpPointerConstraintsV1(Client& client); + ~ZwpPointerConstraintsV1(); + + operator zwp_pointer_constraints_v1*() const; + + friend void zwp_pointer_constraints_v1_destroy(ZwpPointerConstraintsV1 const&) = delete; + +private: + WlHandle const manager; +}; + +class ZwpConfinedPointerV1 +{ +public: + ZwpConfinedPointerV1( + ZwpPointerConstraintsV1& manager, + wl_surface* surface, + wl_pointer* pointer, + wl_region* region, + uint32_t lifetime); + + ~ZwpConfinedPointerV1(); + + MOCK_METHOD0(confined, void()); + MOCK_METHOD0(unconfined, void()); + + operator zwp_confined_pointer_v1*() const; + + friend void zwp_confined_pointer_v1_destroy(ZwpConfinedPointerV1 const&) = delete; + +private: + zwp_confined_pointer_v1* const relative_pointer; + uint32_t const version; + static zwp_confined_pointer_v1_listener const listener; +}; + +class ZwpLockedPointerV1 +{ +public: + ZwpLockedPointerV1( + ZwpPointerConstraintsV1& manager, + wl_surface* surface, + wl_pointer* pointer, + wl_region* region, + uint32_t lifetime); + + ~ZwpLockedPointerV1(); + + MOCK_METHOD0(locked, void()); + MOCK_METHOD0(unlocked, void()); + + operator zwp_locked_pointer_v1*() const; + + friend void zwp_locked_pointer_v1_destroy(ZwpLockedPointerV1 const&) = delete; + +private: + zwp_locked_pointer_v1* const locked_pointer; + uint32_t const version; + static zwp_locked_pointer_v1_listener const listener; +}; + +} + +#endif //WLCS_POINTER_CONSTRAINTS_UNSTABLE_V1_H diff --git a/include/primary_selection.h b/include/primary_selection.h new file mode 100644 index 0000000..91798af --- /dev/null +++ b/include/primary_selection.h @@ -0,0 +1,162 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef WLCS_PRIMARY_SELECTION_H +#define WLCS_PRIMARY_SELECTION_H + +#include "generated/primary-selection-unstable-v1-client.h" + +#include "active_listeners.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include +#include +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_primary_selection_device_manager_v1) + + class PrimarySelectionSource + { + public: + using WrappedType = zwp_primary_selection_source_v1; + + PrimarySelectionSource() = default; + + explicit PrimarySelectionSource(zwp_primary_selection_device_manager_v1* manager) : + self{zwp_primary_selection_device_manager_v1_create_source(manager), deleter} {} + + operator WrappedType*() const { return self.get(); } + + void reset() { self.reset(); } + + void reset(WrappedType* source) { self.reset(source, deleter); } + + friend void zwp_primary_selection_source_v1_destroy(PrimarySelectionSource const&) = delete; + + private: + static void deleter(WrappedType* source) { zwp_primary_selection_source_v1_destroy(source); } + + std::shared_ptr self; + }; + + + class PrimarySelectionDevice + { + public: + using WrappedType = zwp_primary_selection_device_v1; + + PrimarySelectionDevice() = default; + + PrimarySelectionDevice(zwp_primary_selection_device_manager_v1* manager, wl_seat* seat) : + self{zwp_primary_selection_device_manager_v1_get_device(manager, seat), deleter} {} + + operator WrappedType*() const { return self.get(); } + + void reset() { self.reset(); } + + void reset(WrappedType* device) { self.reset(device, deleter); } + + friend void zwp_primary_selection_device_v1_destroy(PrimarySelectionDevice const&) = delete; + + private: + static void deleter(WrappedType* device) { zwp_primary_selection_device_v1_destroy(device); } + + std::shared_ptr self; + }; + + struct PrimarySelectionDeviceListener + { + PrimarySelectionDeviceListener(zwp_primary_selection_device_v1* device) + { + active_listeners.add(this); + zwp_primary_selection_device_v1_add_listener(device, &thunks, this); + } + + virtual ~PrimarySelectionDeviceListener() { active_listeners.del(this); } + + PrimarySelectionDeviceListener(PrimarySelectionDeviceListener const&) = delete; + PrimarySelectionDeviceListener& operator=(PrimarySelectionDeviceListener const&) = delete; + + virtual void data_offer(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer); + + virtual void selection(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer); + + private: + static void data_offer(void* data, zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer); + + static void selection(void* data, zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer); + + static ActiveListeners active_listeners; + constexpr static zwp_primary_selection_device_v1_listener thunks = + { + &data_offer, + &selection + }; + }; + + struct PrimarySelectionOfferListener + { + PrimarySelectionOfferListener() { active_listeners.add(this); } + virtual ~PrimarySelectionOfferListener() { active_listeners.del(this); } + + PrimarySelectionOfferListener(PrimarySelectionOfferListener const&) = delete; + PrimarySelectionOfferListener& operator=(PrimarySelectionOfferListener const&) = delete; + + void listen_to(zwp_primary_selection_offer_v1* offer) + { + zwp_primary_selection_offer_v1_add_listener(offer, &thunks, this); + } + + virtual void offer(zwp_primary_selection_offer_v1* offer, const char* mime_type); + + private: + static void offer(void* data, zwp_primary_selection_offer_v1* offer, const char* mime_type); + + static ActiveListeners active_listeners; + constexpr static zwp_primary_selection_offer_v1_listener thunks = { &offer, }; + }; + + struct PrimarySelectionSourceListener + { + explicit PrimarySelectionSourceListener(PrimarySelectionSource const& source) + { + active_listeners.add(this); + zwp_primary_selection_source_v1_add_listener(source, &thunks, this); + } + + virtual ~PrimarySelectionSourceListener() { active_listeners.del(this); } + + PrimarySelectionSourceListener(PrimarySelectionSourceListener const&) = delete; + PrimarySelectionSourceListener& operator=(PrimarySelectionSourceListener const&) = delete; + + virtual void send(zwp_primary_selection_source_v1* source, const char* mime_type, int32_t fd); + virtual void cancelled(zwp_primary_selection_source_v1* source); + + private: + static void send(void* data, zwp_primary_selection_source_v1* source, const char* mime_type, int32_t fd); + static void cancelled(void* data, zwp_primary_selection_source_v1* source); + + static ActiveListeners active_listeners; + constexpr static zwp_primary_selection_source_v1_listener thunks = { &send, &cancelled }; + }; +} + +#endif //WLCS_PRIMARY_SELECTION_H diff --git a/include/relative_pointer_unstable_v1.h b/include/relative_pointer_unstable_v1.h new file mode 100644 index 0000000..7b68074 --- /dev/null +++ b/include/relative_pointer_unstable_v1.h @@ -0,0 +1,75 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef WLCS_RELATIVE_POINTER_UNSTABLE_V1_H +#define WLCS_RELATIVE_POINTER_UNSTABLE_V1_H + +#include "generated/relative-pointer-unstable-v1-client.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include + +#include + +struct wl_pointer; + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwp_relative_pointer_manager_v1) + +class Client; + +class ZwpRelativePointerManagerV1 +{ +public: + ZwpRelativePointerManagerV1(Client& client); + ~ZwpRelativePointerManagerV1(); + + operator zwp_relative_pointer_manager_v1*() const; + + friend void zwp_relative_pointer_manager_v1_destroy(ZwpRelativePointerManagerV1 const&) = delete; + +private: + WlHandle manager; +}; + +class ZwpRelativePointerV1 +{ +public: + ZwpRelativePointerV1(ZwpRelativePointerManagerV1& manager, wl_pointer* pointer); + ~ZwpRelativePointerV1(); + + MOCK_METHOD6(relative_motion, + void(uint32_t utime_hi, uint32_t utime_lo, + wl_fixed_t dx, wl_fixed_t dy, + wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel)); + + operator zwp_relative_pointer_v1*() const; + + friend void zwp_relative_pointer_v1_destroy(ZwpRelativePointerV1 const&) = delete; + +private: + zwp_relative_pointer_v1* const relative_pointer; + uint32_t const version; + static zwp_relative_pointer_v1_listener const listener; +}; + +} + +#endif //WLCS_RELATIVE_POINTER_UNSTABLE_V1_H diff --git a/include/shared_library.h b/include/shared_library.h new file mode 100644 index 0000000..ef275f7 --- /dev/null +++ b/include/shared_library.h @@ -0,0 +1,55 @@ +/* + * Copyright © 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#ifndef MIR_SHARED_LIBRARY_H_ +#define MIR_SHARED_LIBRARY_H_ + +#include + +namespace wlcs +{ +class SharedLibrary +{ +public: + explicit SharedLibrary(char const* library_name); + explicit SharedLibrary(std::string const& library_name); + ~SharedLibrary() noexcept; + + template + FunctionPtr load_function(char const* function_name) const + { + FunctionPtr result{}; + (void*&)result = load_symbol(function_name); + return result; + } + + template + FunctionPtr load_function(std::string const& function_name) const + { + return load_function(function_name.c_str()); + } +private: + void* const so; + void* load_symbol(char const* function_name) const; + SharedLibrary(SharedLibrary const&) = delete; + SharedLibrary& operator=(SharedLibrary const&) = delete; +}; +} + + +#endif /* MIR_SHARED_LIBRARY_H_ */ diff --git a/include/surface_builder.h b/include/surface_builder.h new file mode 100644 index 0000000..7d74455 --- /dev/null +++ b/include/surface_builder.h @@ -0,0 +1,105 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef WLCS_SURFACE_BUILDER_H_ +#define WLCS_SURFACE_BUILDER_H_ + +#include +#include +#include "in_process_server.h" + +namespace wlcs +{ + +struct SurfaceBuilder +{ + SurfaceBuilder(std::string const& name) : name{name} {} + virtual ~SurfaceBuilder() = default; + + virtual auto build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr = 0; + + std::string const name; + + static auto all_surface_types() -> std::vector>; + static auto toplevel_surface_types() -> std::vector>; + + static auto surface_builder_to_string( + testing::TestParamInfo> builder) -> std::string; +}; + +struct WlShellSurfaceBuilder : SurfaceBuilder +{ + WlShellSurfaceBuilder() : SurfaceBuilder{"wl_shell_surface"} {} + + auto build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr override; +}; + +struct XdgV6SurfaceBuilder : SurfaceBuilder +{ + XdgV6SurfaceBuilder() : SurfaceBuilder{"zxdg_surface_v6"} {} + + auto build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr override; +}; + +struct XdgStableSurfaceBuilder : SurfaceBuilder +{ + XdgStableSurfaceBuilder(int left_offset, int top_offset, int right_offset, int bottom_offset); + + auto build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr override; + + int left_offset, top_offset, right_offset, bottom_offset; +}; + +struct SubsurfaceBuilder : SurfaceBuilder +{ + SubsurfaceBuilder(std::pair offset); + + auto build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr override; + + std::pair offset; +}; + +// TODO: popup surfaces +} + +namespace std +{ +std::ostream& operator<<(std::ostream& out, std::shared_ptr const& param); +} + +#endif // WLCS_SURFACE_BUILDER_H_ diff --git a/include/version_specifier.h b/include/version_specifier.h new file mode 100644 index 0000000..c7dbb37 --- /dev/null +++ b/include/version_specifier.h @@ -0,0 +1,69 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_VERSION_SPECIFIER_H_ +#define WLCS_VERSION_SPECIFIER_H_ + +#include +#include +#include + +namespace wlcs +{ +class VersionSpecifier +{ +public: + VersionSpecifier() = default; + virtual ~VersionSpecifier() = default; + + virtual auto select_version( + uint32_t max_available_version, + uint32_t max_supported_version) const -> std::optional = 0; + virtual auto describe() const -> std::string = 0; +}; + +class ExactlyVersion : public VersionSpecifier +{ +public: + explicit ExactlyVersion(uint32_t version) noexcept; + + auto select_version( + uint32_t max_available_version, + uint32_t max_supported_version) const -> std::optional override; + auto describe() const -> std::string override; +private: + uint32_t const version; +}; + +class AtLeastVersion : public VersionSpecifier +{ +public: + explicit AtLeastVersion(uint32_t version) noexcept; + + auto select_version( + uint32_t max_available_version, + uint32_t max_supported_version) const -> std::optional override; + auto describe() const -> std::string override; +private: + uint32_t const version; +}; + +extern VersionSpecifier const& AnyVersion; +} + +#endif //WLCS_VERSION_SPECIFIER_H_ diff --git a/include/wl_handle.h b/include/wl_handle.h new file mode 100644 index 0000000..b7f4acc --- /dev/null +++ b/include/wl_handle.h @@ -0,0 +1,103 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + * William Wold + */ + +#ifndef WLCS_WL_PROXY_H_ +#define WLCS_WL_PROXY_H_ + +#include "wl_interface_descriptor.h" +#include "boost/throw_exception.hpp" +#include "boost/current_function.hpp" + +#include +#include + +struct wl_proxy; + +namespace wlcs +{ +template +class WlHandle +{ +public: + explicit WlHandle(T* const proxy) + : proxy{proxy}, + owns_wl_object{true} + { + static_assert( + WlInterfaceDescriptor::has_specialisation, + "Missing specialisation for WlInterfaceDescriptor"); + if (proxy == nullptr) + { + BOOST_THROW_EXCEPTION((std::logic_error{"Attempt to construct a WlHandle from null Wayland object"})); + } + } + + ~WlHandle() + { + if (owns_wl_object) + { + (*WlInterfaceDescriptor::destructor)(proxy); + } + } + + WlHandle(WlHandle&& from) + : WlHandle(from.proxy) + { + from.owns_wl_object = false; + } + + WlHandle(WlHandle const&) = delete; + + auto operator=(WlHandle&&) -> WlHandle& = delete; + auto operator=(WlHandle const&) -> bool = delete; + + operator T*() const + { + // This is a precondition failure, but as this is a test-suite let's be generous and make it fail hard and fast + if (!owns_wl_object) + { + std::abort(); + } + return proxy; + } + + auto wl_proxy() const -> struct wl_proxy* + { + // This is a precondition failure, but as this is a test-suite let's be generous and make it fail hard and fast + if (!owns_wl_object) + { + std::abort(); + } + return static_cast(proxy); + } + +private: + T* const proxy; + bool owns_wl_object; +}; + +template +auto wrap_wl_object(WlType* proxy) -> WlHandle +{ + return WlHandle{proxy}; +} + +} + +#endif // WLCS_WL_PROXY_H_ diff --git a/include/wl_interface_descriptor.h b/include/wl_interface_descriptor.h new file mode 100644 index 0000000..ae71a93 --- /dev/null +++ b/include/wl_interface_descriptor.h @@ -0,0 +1,61 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_WL_INTERFACE_DESCRIPTOR_H +#define WLCS_WL_INTERFACE_DESCRIPTOR_H + +struct wl_interface; + +namespace wlcs +{ +/*** + * A specialisable struct containing the constants and types associated with a Wayland protocol + * + * \tparam WlType The base Wayland object type (eg; wl_surface, xdg_wm_base, etc) + */ +template +struct WlInterfaceDescriptor +{ + // Needed because apparently GCC < 10 can't compare a constexpr wl_interface const* const to nullptr in constexpr context?! + static constexpr bool const has_specialisation = false; + static constexpr wl_interface const* const interface = nullptr; + static constexpr void (* const destructor)(WlType*) = nullptr; +}; +} + +/*** + * Declare a specialisation of WlInterfaceDescriptor for \param name + * + * This will use the standard Wayland conventions of + * name - name_interface - name_destroy + * (eg: wl_surface - wl_surface_interface - wl_surface_destroy) + * + * If an interface requires special handling, a manual specialisation can be + * provided (for example, see wl_output handling in in_process_server.h) + */ +#define WLCS_CREATE_INTERFACE_DESCRIPTOR(name) \ + template<> \ + struct WlInterfaceDescriptor \ + { \ + static constexpr bool const has_specialisation = true; \ + static constexpr wl_interface const* const interface = &name##_interface; \ + static constexpr void(* const destructor)(name*) = &name##_destroy; \ + }; + + +#endif //WLCS_WL_INTERFACE_DESCRIPTOR_H diff --git a/include/wlcs/display_server.h b/include/wlcs/display_server.h new file mode 100644 index 0000000..0bb79dd --- /dev/null +++ b/include/wlcs/display_server.h @@ -0,0 +1,198 @@ +/* + * Copyright © 2017 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ +#ifndef WLCS_SERVER_H_ +#define WLCS_SERVER_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct wl_surface wl_surface; +typedef struct wl_display wl_display; +typedef struct wl_event_loop wl_event_loop; + +typedef struct WlcsPointer WlcsPointer; +typedef struct WlcsTouch WlcsTouch; + +/** + * Maximum version of WlcsIntegrationDescriptor this header provides a definition for + */ +#define WLCS_INTEGRATION_DESCRIPTOR_VERSION 1 +typedef struct WlcsExtensionDescriptor WlcsExtensionDescriptor; +struct WlcsExtensionDescriptor +{ + /** + * Protocol name of extension (eg: wl_shell, xdg_shell, etc) + */ + char const* name; + /** + * Maximum version of extension supported + */ + uint32_t version; +}; + +typedef struct WlcsIntegrationDescriptor WlcsIntegrationDescriptor; +struct WlcsIntegrationDescriptor +{ + uint32_t version; /**< Version of the struct this instance provides */ + + size_t num_extensions; /**< Length of the supported_extensions array */ + /** + * Array of extension descriptions + * + * This must be an array of length num_extensions; that is, accesses from + * supported_extensions[0] to supported_extensions[num_extensions - 1] + * must be valid. + */ + WlcsExtensionDescriptor const* supported_extensions; +}; + +/** + * Maximum version of WlcsDisplayServer this header provides a definition for + */ +#define WLCS_DISPLAY_SERVER_VERSION 3 +typedef struct WlcsDisplayServer WlcsDisplayServer; +struct WlcsDisplayServer +{ + uint32_t version; /**< Version of the struct this instance provides */ + + /** + * Start the display server's mainloop. + * + * This should *not* block until the mainloop exits, which implies the mainloop + * will need to be run in a separate thread. + * + * This does not need to block until the display server is ready to process + * input, but the WlcsDisplayServer does need to be able to process other + * calls (notably create_client_socket) once this returns. + */ + void (*start)(WlcsDisplayServer* server); + + /** + * Stop the display server's mainloop. + * + * In contrast to the start hook, this *should* block until the server's mainloop + * has been torn down, so that it does not persist into later tests. + */ + void (*stop)(WlcsDisplayServer* server); + + /** + * Create a socket that can be connected to by wl_display_connect_fd + * + * \return A FD to a client Wayland socket. WLCS owns this fd, and will close + * it as necessary. + */ + int (*create_client_socket)(WlcsDisplayServer* server); + + /** + * Position a window in the compositor coördinate space + * + * \param client the (wayland-client-side) wl_client which owns the + * surface + * \param surface the (wayland-client-side) wl_surface* + * \param x x coördinate (in compositor-space pixels) to move the + * left of the window to + * \param y y coördinate (in compositor-space pixels) to move the + * top of the window to + */ + void (*position_window_absolute)(WlcsDisplayServer* server, wl_display* client, wl_surface* surface, int x, int y); + + /** + * Create a fake pointer device + */ + WlcsPointer* (*create_pointer)(WlcsDisplayServer* server); + + /** + * Create a mock touch object + */ + WlcsTouch* (*create_touch)(WlcsDisplayServer* server); + + /* Added in version 2 */ + /** + * Describe the capabilities of this WlcsDisplayServer + * + * WLCS will use this description to skip tests that the display server + * is known to not support. For example, if the set of extensions + * described by the WlcsIntegrationDescriptor does not include "xdg_shell" + * then all XDG Shell tests will be skipped. + * + * Different WlcsDisplayServer instances may report different + * capabilities (for example, if command line options should influence + * the set of extensions exposed). + */ + WlcsIntegrationDescriptor const* (*get_descriptor)(WlcsDisplayServer const* server); + + /* Added in version 3 */ + /** + * Start the display server's event loop, blocking the calling thread. + * + * When started in this way WLCS will proxy all requests to this mainloop. + * All calls to WLCS interfaces will be dispatched from the + * wlcs_event_dispatcher loop, so implementations are required to drive + * this loop from their own. + * + * \note This is an optional interface. An implementation must provide at + * least one of {start, start_on_this_thread}, but does not need to + * provide both. If both are provided, start is preferred. + + * \param wlcs_event_dispatcher + */ + void (*start_on_this_thread)(WlcsDisplayServer* server, wl_event_loop* wlcs_event_dispatcher); +}; + +/** + * Maximum version of WlcsServerIntegration this header provides a definition of + */ +#define WLCS_SERVER_INTEGRATION_VERSION 1 +typedef struct WlcsServerIntegration WlcsServerIntegration; +struct WlcsServerIntegration +{ + uint32_t version; /**< Version of the struct this instance provides */ + + /** + * Create a WlcsDisplayServer instance + * + * This can do any setup necessary, but should not start the compositor's + * mainloop. + * + * \param argc Command line argument count (after wlcs-specific options + * have been stripped) + * \param argv Command line arguments (after wlcs-specific options have + * been stripped) + * \return + */ + WlcsDisplayServer* (*create_server)(int argc, char const** argv); + void (*destroy_server)(WlcsDisplayServer* server); +}; + +/** + * Main WLCS entry point + * + * WLCS will load this symbol to discover the compositor integration + * hooks. + */ +extern WlcsServerIntegration const wlcs_server_integration; + +#ifdef __cplusplus +} +#endif + +#endif //WLCS_SERVER_H_ diff --git a/include/wlcs/pointer.h b/include/wlcs/pointer.h new file mode 100644 index 0000000..0ee14d3 --- /dev/null +++ b/include/wlcs/pointer.h @@ -0,0 +1,72 @@ +/* + * Copyright © 2017 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ +#ifndef WLCS_POINTER_H_ +#define WLCS_POINTER_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Maximum version of WlcsPointer this header provides a definition for + */ +#define WLCS_POINTER_VERSION 1 + +typedef struct WlcsPointer WlcsPointer; +/** + * An object to manipulate the server's pointer state + */ +struct WlcsPointer +{ + uint32_t version; /**< Version of the struct this instance provides */ + + /** + * Move the pointer to the specified location, in compositor coördinate space + */ + void (*move_absolute)(WlcsPointer* pointer, wl_fixed_t x, wl_fixed_t y); + /** + * Move the pointer by the specified amount, in compositor coördinates. + */ + void (*move_relative)(WlcsPointer* pointer, wl_fixed_t dx, wl_fixed_t dy); + + /** + * Generate a button-up event + * + * \param button Button code (as per wl_pointer. eg: BTN_LEFT) + */ + void (*button_up)(WlcsPointer* pointer, int button); + /** + * Generate a button-down event + * + * \param button Button code (as per wl_pointer. eg: BTN_LEFT) + */ + void (*button_down)(WlcsPointer* pointer, int button); + + /** + * Destroy this pointer, freeing any resources. + */ + void (*destroy)(WlcsPointer* pointer); +}; + +#ifdef __cplusplus +} +#endif + +#endif //WLCS_SERVER_H_ diff --git a/include/wlcs/touch.h b/include/wlcs/touch.h new file mode 100644 index 0000000..8dfe3e6 --- /dev/null +++ b/include/wlcs/touch.h @@ -0,0 +1,48 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ +#ifndef WLCS_TOUCH_H_ +#define WLCS_TOUCH_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Maximum version of WlcsTouch this header provides a definition for + */ +#define WLCS_TOUCH_VERSION 1 + +typedef struct WlcsTouch WlcsTouch; +struct WlcsTouch +{ + uint32_t version; /**< Version of the struct this instance provides */ + + void (*touch_down)(WlcsTouch* touch, wl_fixed_t x, wl_fixed_t y); + void (*touch_move)(WlcsTouch* touch, wl_fixed_t x, wl_fixed_t y); + void (*touch_up)(WlcsTouch* touch); + + void (*destroy)(WlcsTouch* touch); +}; + +#ifdef __cplusplus +} +#endif + +#endif //WLCS_TOUCH_H_ diff --git a/include/xdg_output_v1.h b/include/xdg_output_v1.h new file mode 100644 index 0000000..f899527 --- /dev/null +++ b/include/xdg_output_v1.h @@ -0,0 +1,76 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef XDG_OUTPUT_V1_H +#define XDG_OUTPUT_V1_H + +#include "generated/wayland-client.h" +#include "generated/xdg-output-unstable-v1-client.h" + +#include "in_process_server.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zxdg_output_manager_v1) + +class XdgOutputManagerV1 +{ +public: + XdgOutputManagerV1(Client& client); + ~XdgOutputManagerV1(); + + operator zxdg_output_manager_v1*() const; + auto client() const -> Client&; + +private: + struct Impl; + std::unique_ptr const impl; +}; + +class XdgOutputV1 +{ +public: + struct State + { + std::optional> logical_position; + std::optional> logical_size; + std::optional name; + std::optional description; + }; + + XdgOutputV1(XdgOutputManagerV1& manager, size_t output_index); + + XdgOutputV1(XdgOutputV1 const&) = delete; + XdgOutputV1 const& operator=(XdgOutputV1 const&) = delete; + + operator zxdg_output_v1*() const; + auto state() -> State const&; + +private: + struct Impl; + std::shared_ptr const impl; // shared instead of unique because interally a weak is needed +}; + +} + +#endif // XDG_OUTPUT_V1_H diff --git a/include/xdg_shell_stable.h b/include/xdg_shell_stable.h new file mode 100644 index 0000000..52fff6e --- /dev/null +++ b/include/xdg_shell_stable.h @@ -0,0 +1,119 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef WLCS_XDG_SHELL_STABLE_H +#define WLCS_XDG_SHELL_STABLE_H + +#include "in_process_server.h" +#include "generated/xdg-shell-client.h" +#include "wl_interface_descriptor.h" +#include "wl_handle.h" + +#include + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(xdg_wm_base) + +class XdgSurfaceStable +{ +public: + XdgSurfaceStable(wlcs::Client& client, wlcs::Surface& surface); + XdgSurfaceStable(XdgSurfaceStable const&) = delete; + XdgSurfaceStable& operator=(XdgSurfaceStable const&) = delete; + ~XdgSurfaceStable(); + + MOCK_METHOD(void, configure, (uint32_t serial)); + + operator xdg_surface*() const {return shell_surface;} + +private: + xdg_surface* shell_surface; +}; + +class XdgToplevelStable +{ +public: + struct State + { + State(int32_t width, int32_t height, struct wl_array *states); + + int width; + int height; + + bool maximized; + bool fullscreen; + bool resizing; + bool activated; + }; + + XdgToplevelStable(XdgSurfaceStable& shell_surface_); + XdgToplevelStable(XdgToplevelStable const&) = delete; + XdgToplevelStable& operator=(XdgToplevelStable const&) = delete; + ~XdgToplevelStable(); + + MOCK_METHOD(void, configure, (int32_t width, int32_t height, wl_array* states)); + MOCK_METHOD(void, close, ()); + MOCK_METHOD(void, configure_bounds, (int32_t width, int32_t height)); + MOCK_METHOD(void, wm_capabilities, (wl_array* capabilities)); + + operator xdg_toplevel*() const {return toplevel;} + + XdgSurfaceStable* const shell_surface; + xdg_toplevel* toplevel; +}; + +class XdgPositionerStable +{ +public: + XdgPositionerStable(wlcs::Client& client); + ~XdgPositionerStable(); + operator xdg_positioner*() const {return positioner;} + auto setup_default(std::pair size) -> XdgPositionerStable&; + +private: + xdg_positioner* const positioner; +}; + +class XdgPopupStable +{ +public: + XdgPopupStable( + XdgSurfaceStable& shell_surface_, + std::optional parent, + XdgPositionerStable& positioner); + XdgPopupStable(XdgPopupStable const&) = delete; + XdgPopupStable& operator=(XdgPopupStable const&) = delete; + ~XdgPopupStable(); + + MOCK_METHOD(void, configure, (int32_t x, int32_t y, int32_t width, int32_t height)); + MOCK_METHOD(void, done, ()); + MOCK_METHOD(void, repositioned, (uint32_t token)); + + operator xdg_popup*() const {return popup;} + + XdgSurfaceStable* const shell_surface; + xdg_popup* const popup; + + std::vector> configure_notifiers; + std::vector> popup_done_notifiers; +}; + +} + +#endif // WLCS_XDG_SHELL_STABLE_H diff --git a/include/xdg_shell_v6.h b/include/xdg_shell_v6.h new file mode 100644 index 0000000..3ec6f2b --- /dev/null +++ b/include/xdg_shell_v6.h @@ -0,0 +1,106 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#ifndef WLCS_XDG_SHELL_V6_ +#define WLCS_XDG_SHELL_V6_ + +#include "in_process_server.h" +#include "generated/xdg-shell-unstable-v6-client.h" + +#include + +namespace wlcs +{ + +class XdgSurfaceV6 +{ +public: + XdgSurfaceV6(wlcs::Client& client, wlcs::Surface& surface); + XdgSurfaceV6(XdgSurfaceV6 const&) = delete; + XdgSurfaceV6& operator=(XdgSurfaceV6 const&) = delete; + ~XdgSurfaceV6(); + + MOCK_METHOD(void, configure, (uint32_t serial)); + + operator zxdg_surface_v6*() const {return shell_surface;} + +private: + zxdg_surface_v6* shell_surface; +}; + +class XdgToplevelV6 +{ +public: + struct State + { + State(int32_t width, int32_t height, struct wl_array *states); + + int width; + int height; + + bool maximized; + bool fullscreen; + bool resizing; + bool activated; + }; + + XdgToplevelV6(XdgSurfaceV6& shell_surface_); + XdgToplevelV6(XdgToplevelV6 const&) = delete; + XdgToplevelV6& operator=(XdgToplevelV6 const&) = delete; + ~XdgToplevelV6(); + + MOCK_METHOD(void, configure, (int32_t width, int32_t height, wl_array* states)); + MOCK_METHOD(void, close, ()); + + operator zxdg_toplevel_v6*() const {return toplevel;} + + XdgSurfaceV6* const shell_surface; + zxdg_toplevel_v6* toplevel; +}; + +class XdgPositionerV6 +{ +public: + XdgPositionerV6(wlcs::Client& client); + ~XdgPositionerV6(); + operator zxdg_positioner_v6*() const {return positioner;} + +private: + zxdg_positioner_v6* const positioner; +}; + +class XdgPopupV6 +{ +public: + XdgPopupV6(XdgSurfaceV6& shell_surface_, XdgSurfaceV6& parent, XdgPositionerV6& positioner); + XdgPopupV6(XdgPopupV6 const&) = delete; + XdgPopupV6& operator=(XdgPopupV6 const&) = delete; + ~XdgPopupV6(); + + MOCK_METHOD(void, configure, (int32_t x, int32_t y, int32_t width, int32_t height)); + MOCK_METHOD(void, done, ()); + + operator zxdg_popup_v6*() const {return popup;} + + XdgSurfaceV6* const shell_surface; + zxdg_popup_v6* const popup; +}; + +} + +#endif // WLCS_XDG_SHELL_V6_ diff --git a/spread.yaml b/spread.yaml new file mode 100644 index 0000000..b169c5a --- /dev/null +++ b/spread.yaml @@ -0,0 +1,31 @@ +project: wlcs + +kill-timeout: 50m + +backends: + lxd: + systems: + - ubuntu-22.04 + - ubuntu-22.10 + - ubuntu-23.04 + - ubuntu-devel: + image: ubuntu-daily:devel/amd64 + - fedora-37 + - fedora-38 + - alpine-3.18 + - alpine-edge + +suites: + spread/build/: + summary: Build wlcs + environment: + CC/gcc: gcc + CXX/gcc: g++ + CC/clang: clang + CXX/clang: clang++ + +path: + /spread/wlcs + +exclude: + - .git diff --git a/spread/build/alpine/task.yaml b/spread/build/alpine/task.yaml new file mode 100644 index 0000000..d5e5561 --- /dev/null +++ b/spread/build/alpine/task.yaml @@ -0,0 +1,22 @@ +summary: Build (on Alpine Linux) +systems: [alpine-*] + +execute: | + apk add \ + coreutils \ + make \ + g++ \ + wayland-dev \ + cmake \ + boost-dev \ + gtest-dev + + cd $SPREAD_PATH + cd $(mktemp --directory) + # Alpine doesn't build gcc's libsanitizer + cmake $SPREAD_PATH \ + -DWLCS_BUILD_ASAN=False \ + -DWLCS_BUILD_TSAN=False \ + -DWLCS_BUILD_UBSAN=False + make -j$(nproc) + diff --git a/spread/build/fedora/task.yaml b/spread/build/fedora/task.yaml new file mode 100644 index 0000000..065c796 --- /dev/null +++ b/spread/build/fedora/task.yaml @@ -0,0 +1,23 @@ +summary: Build (on Fedora) +systems: [fedora-*] + +execute: | + dnf install --assumeyes \ + wayland-devel \ + cmake \ + make \ + clang \ + gcc-c++ \ + libasan.x86_64 \ + libtsan.x86_64 \ + libubsan.x86_64 \ + pkg-config \ + boost-devel \ + gtest-devel \ + gmock-devel + + cd $SPREAD_PATH + cd $(mktemp --directory) + cmake $SPREAD_PATH + make -j$(nproc) + diff --git a/spread/build/ubuntu/task.yaml b/spread/build/ubuntu/task.yaml new file mode 100644 index 0000000..ad45894 --- /dev/null +++ b/spread/build/ubuntu/task.yaml @@ -0,0 +1,34 @@ +summary: Build (on Ubuntu) +systems: [-fedora-*, -alpine-*] + +execute: | + # Grab builds of Mir git master + add-apt-repository ppa:mir-team/dev + + apt-get update + + apt install --yes \ + dpkg-dev \ + libwayland-dev \ + cmake \ + clang \ + g++ \ + pkg-config \ + libgtest-dev \ + google-mock \ + libboost-dev \ + mir-test-tools + + # Check that we build… + cd $SPREAD_PATH + cd $(mktemp --directory) + cmake -DCMAKE_C_FLAGS="-D_FORTIFY_SOURCE=2" -DCMAKE_CXX_FLAGS="-D_FORTIFY_SOURCE=2" $SPREAD_PATH + make -j$(nproc) + + # …and run the Mir tests, but don't fail on them, unless we fail with a signal + # TODO: Store the set of passing tests in the Travis cache, and fail if a test which + # previously passed now fails. + ./wlcs /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/mir/miral_wlcs_integration.so || { + test $? -lt 128 -o $? -gt 165 + } + diff --git a/src/data_device.cpp b/src/data_device.cpp new file mode 100644 index 0000000..966dc3d --- /dev/null +++ b/src/data_device.cpp @@ -0,0 +1,144 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "data_device.h" + +wlcs::ActiveListeners wlcs::DataDeviceListener::active_listeners; +constexpr wl_data_device_listener wlcs::DataDeviceListener::thunks; + + +void wlcs::DataDeviceListener::data_offer(void* data, struct wl_data_device* wl_data_device, struct wl_data_offer* id) +{ + if (active_listeners.includes(data)) + static_cast(data)->data_offer(wl_data_device, id); +} + +void wlcs::DataDeviceListener::enter( + void* data, + struct wl_data_device* wl_data_device, + uint32_t serial, + struct wl_surface* surface, + wl_fixed_t x, + wl_fixed_t y, + struct wl_data_offer* id) +{ + if (active_listeners.includes(data)) + static_cast(data)->enter(wl_data_device, serial, surface, x, y, id); +} + +void wlcs::DataDeviceListener::leave(void* data, struct wl_data_device* wl_data_device) +{ + if (active_listeners.includes(data)) + static_cast(data)->leave(wl_data_device); +} + +void wlcs::DataDeviceListener::motion( + void* data, + struct wl_data_device* wl_data_device, + uint32_t time, + wl_fixed_t x, + wl_fixed_t y) +{ + if (active_listeners.includes(data)) + static_cast(data)->motion(wl_data_device, time, x, y); +} + +void wlcs::DataDeviceListener::drop(void* data, struct wl_data_device* wl_data_device) +{ + if (active_listeners.includes(data)) + static_cast(data)->drop(wl_data_device); +} + +void wlcs::DataDeviceListener::selection( + void* data, + struct wl_data_device* wl_data_device, + struct wl_data_offer* id) +{ + if (active_listeners.includes(data)) + static_cast(data)->selection(wl_data_device, wl_data_device, id); +} + +void wlcs::DataDeviceListener::data_offer(struct wl_data_device* /*wl_data_device*/, struct wl_data_offer* /*id*/) +{ +} + +void wlcs::DataDeviceListener::enter( + struct wl_data_device* /*wl_data_device*/, + uint32_t /*serial*/, + struct wl_surface* /*surface*/, + wl_fixed_t /*x*/, + wl_fixed_t /*y*/, + struct wl_data_offer* /*id*/) +{ +} + +void wlcs::DataDeviceListener::leave(struct wl_data_device* /*wl_data_device*/) +{ +} + +void wlcs::DataDeviceListener::motion( + struct wl_data_device* /*wl_data_device*/, + uint32_t /*time*/, + wl_fixed_t /*x*/, + wl_fixed_t /*y*/) +{ +} + +void wlcs::DataDeviceListener::drop(struct wl_data_device* /*wl_data_device*/) +{ +} + +void wlcs::DataDeviceListener::selection( + struct wl_data_device* /*wl_data_device*/, + struct wl_data_offer* /*id*/) +{ +} + + +wlcs::ActiveListeners wlcs::DataOfferListener::active_listeners; +constexpr wl_data_offer_listener wlcs::DataOfferListener::thunks; + +void wlcs::DataOfferListener::offer(void* data, struct wl_data_offer* data_offer, char const* mime_type) +{ + if (active_listeners.includes(data)) + static_cast(data)->offer(data_offer, mime_type); +} + +void wlcs::DataOfferListener::source_actions(void* data, struct wl_data_offer* data_offer, uint32_t dnd_actions) +{ + if (active_listeners.includes(data)) + static_cast(data)->source_actions(data_offer, dnd_actions); +} + +void wlcs::DataOfferListener::action(void* data, struct wl_data_offer* data_offer, uint32_t dnd_action) +{ + if (active_listeners.includes(data)) + static_cast(data)->action(data_offer, dnd_action); +} + +void wlcs::DataOfferListener::offer(struct wl_data_offer* /*data_offer*/, char const* /*mime_type*/) +{ +} + +void wlcs::DataOfferListener::source_actions(struct wl_data_offer* /*data_offer*/, uint32_t /*dnd_actions*/) +{ +} + +void wlcs::DataOfferListener::action(struct wl_data_offer* /*data_offer*/, uint32_t /*dnd_action*/) +{ +} diff --git a/src/gtk_primary_selection.cpp b/src/gtk_primary_selection.cpp new file mode 100644 index 0000000..d39e9bf --- /dev/null +++ b/src/gtk_primary_selection.cpp @@ -0,0 +1,90 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "gtk_primary_selection.h" + +#include + +wlcs::ActiveListeners wlcs::GtkPrimarySelectionDeviceListener::active_listeners; +constexpr gtk_primary_selection_device_listener wlcs::GtkPrimarySelectionDeviceListener::thunks; + +wlcs::ActiveListeners wlcs::GtkPrimarySelectionOfferListener::active_listeners; +constexpr gtk_primary_selection_offer_listener wlcs::GtkPrimarySelectionOfferListener::thunks; + +wlcs::ActiveListeners wlcs::GtkPrimarySelectionSourceListener::active_listeners; +constexpr gtk_primary_selection_source_listener wlcs::GtkPrimarySelectionSourceListener::thunks; + + +void wlcs::GtkPrimarySelectionDeviceListener::data_offer( + void* data, + gtk_primary_selection_device* device, + gtk_primary_selection_offer* offer) +{ + if (active_listeners.includes(data)) + static_cast(data)->data_offer(device, offer); +} + +void wlcs::GtkPrimarySelectionDeviceListener::data_offer(gtk_primary_selection_device*, gtk_primary_selection_offer*) +{ +} + +void wlcs::GtkPrimarySelectionDeviceListener::selection(gtk_primary_selection_device*, gtk_primary_selection_offer*) +{ +} + +void wlcs::GtkPrimarySelectionDeviceListener::selection( + void* data, + gtk_primary_selection_device* device, + gtk_primary_selection_offer* offer) +{ + if (active_listeners.includes(data)) + static_cast(data)->selection(device, offer); +} + +void wlcs::GtkPrimarySelectionOfferListener::offer(gtk_primary_selection_offer*, const char*) +{ +} + +void wlcs::GtkPrimarySelectionOfferListener::offer( + void* data, gtk_primary_selection_offer* offer, const char* mime_type) +{ + if (active_listeners.includes(data)) + static_cast(data)->offer(offer, mime_type); +} + +void wlcs::GtkPrimarySelectionSourceListener::send(gtk_primary_selection_source*, const char*, int32_t fd) +{ + close(fd); +} + +void wlcs::GtkPrimarySelectionSourceListener::cancelled(gtk_primary_selection_source*) +{ +} + +void wlcs::GtkPrimarySelectionSourceListener::send( + void* data, gtk_primary_selection_source* source, const char* mime_type, int32_t fd) +{ + if (active_listeners.includes(data)) + static_cast(data)->send(source, mime_type, fd); +} + +void wlcs::GtkPrimarySelectionSourceListener::cancelled(void* data, gtk_primary_selection_source* source) +{ + if (active_listeners.includes(data)) + static_cast(data)->cancelled(source); +} diff --git a/src/helpers.cpp b/src/helpers.cpp new file mode 100644 index 0000000..7dc4f26 --- /dev/null +++ b/src/helpers.cpp @@ -0,0 +1,121 @@ +/* + * Copyright © 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authored by: + * Alexandros Frantzis + */ + +#include "helpers.h" +#include "shared_library.h" + +#include +#include + +#include +#include +#include + +namespace +{ + +bool error_indicates_tmpfile_not_supported(int error) +{ + return + error == EISDIR || // Directory exists, but no support for O_TMPFILE + error == ENOENT || // Directory doesn't exist, and no support for O_TMPFILE + error == EOPNOTSUPP || // Filesystem that directory resides on does not support O_TMPFILE + error == EINVAL; // There apparently exists at least one development board that has a kernel + // that incorrectly returns EINVAL. Yay. +} + +int memfd_create(char const* name, unsigned int flags) +{ + return static_cast(syscall(SYS_memfd_create, name, flags)); +} +} + +int wlcs::helpers::create_anonymous_file(size_t size) +{ + + int fd = memfd_create("wlcs-unnamed", MFD_CLOEXEC); + if (fd == -1 && errno == ENOSYS) + { + fd = open("/dev/shm", O_TMPFILE | O_RDWR | O_EXCL | O_CLOEXEC, S_IRWXU); + + // Workaround for filesystems that don't support O_TMPFILE + if (fd == -1 && error_indicates_tmpfile_not_supported(errno)) + { + char template_filename[] = "/dev/shm/wlcs-buffer-XXXXXX"; + fd = mkostemp(template_filename, O_CLOEXEC); + if (fd != -1) + { + if (unlink(template_filename) < 0) + { + close(fd); + fd = -1; + } + } + } + } + + if (fd == -1) + { + BOOST_THROW_EXCEPTION( + std::system_error(errno, std::system_category(), "Failed to open temporary file")); + } + + if (ftruncate(fd, size) == -1) + { + close(fd); + BOOST_THROW_EXCEPTION( + std::system_error(errno, std::system_category(), "Failed to resize temporary file")); + } + + return fd; +} + +namespace +{ +static int argc; +static char const** argv; + +std::shared_ptr entry_point; +} + +void wlcs::helpers::set_command_line(int argc, char const** argv) +{ + ::argc = argc; + ::argv = argv; +} + +int wlcs::helpers::get_argc() +{ + return ::argc; +} + +char const** wlcs::helpers::get_argv() +{ + return ::argv; +} + +void wlcs::helpers::set_entry_point(std::shared_ptr const& entry_point) +{ + ::entry_point = entry_point; +} + +std::shared_ptr wlcs::helpers::get_test_hooks() +{ + return ::entry_point; +} diff --git a/src/in_process_server.cpp b/src/in_process_server.cpp new file mode 100644 index 0000000..39d7791 --- /dev/null +++ b/src/in_process_server.cpp @@ -0,0 +1,2198 @@ +/* + * Copyright © 2017-2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#include "in_process_server.h" +#include "thread_proxy.h" +#include "version_specifier.h" +#include "wlcs/display_server.h" +#include "helpers.h" +#include "wlcs/pointer.h" +#include "wlcs/touch.h" +#include "xdg_shell_v6.h" +#include "xdg_shell_stable.h" +#include "generated/wayland-client.h" +#include "generated/xdg-shell-unstable-v6-client.h" +#include "generated/xdg-shell-client.h" + +#include "linux/input.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals::chrono_literals; + +class ShimNotImplemented : public std::logic_error +{ +public: + ShimNotImplemented() : std::logic_error("Function not implemented in display server shim") + { + } + + ShimNotImplemented(const std::string& name) + : std::logic_error("Function '" + name + "()' not implemented in display server shim") + { + } +}; + +namespace +{ +auto interface_description_if_valid(wl_interface const* interface) -> std::string +{ + if (interface) + { + using namespace std::literals::string_literals; + return interface->name + " v"s + std::to_string(interface->version); + } + return ""; +} +} +wlcs::ProtocolError::ProtocolError(wl_interface const* interface, uint32_t code) + : std::system_error(EPROTO, std::system_category(), "Wayland protocol error"), + interface_{interface}, + code_{code}, + message{ + std::string{"Wayland protocol error: "} + + std::to_string(code) + + " on interface " + interface_description_if_valid(interface)} + { + } + +char const* wlcs::ProtocolError::what() const noexcept +{ + return message.c_str(); +} + +uint32_t wlcs::ProtocolError::error_code() const +{ + return code_; +} + +wl_interface const* wlcs::ProtocolError::interface() const +{ + return interface_; +} + +wlcs::ExtensionExpectedlyNotSupported::ExtensionExpectedlyNotSupported(char const* extension, VersionSpecifier const& version) + : std::runtime_error{ + std::string{"Extension: "} + + extension + " version " + version.describe() + + " not supported by compositor under test."} +{ + auto const skip_reason = + std::string{"Missing extension: "} + extension + version.describe(); + ::testing::Test::RecordProperty("wlcs-skip-test", skip_reason); +} + +wlcs::Timeout::Timeout(char const* message) + : std::runtime_error(message) +{ +} + +class wlcs::Pointer::Impl +{ +public: + template + Impl( + WlcsPointer* raw_device, + std::shared_ptr const& proxy, + std::shared_ptr const& keep_dso_loaded) + : keep_dso_loaded{keep_dso_loaded}, + pointer{raw_device} + { + setup_thunks(proxy); + } + + ~Impl() + { + destroy_thunk(); + } + + void move_to(int x, int y) + { + move_absolute_thunk(wl_fixed_from_int(x), wl_fixed_from_int(y)); + } + + void move_by(int dx, int dy) + { + move_relative_thunk(wl_fixed_from_int(dx), wl_fixed_from_int(dy)); + } + + void button_down(int button) + { + button_down_thunk(button); + } + + void button_up(int button) + { + button_up_thunk(button); + } + +private: + template + void setup_thunks(std::shared_ptr const& proxy) + { + move_absolute_thunk = proxy->register_op( + [this](wl_fixed_t x, wl_fixed_t y) + { + pointer->move_absolute(pointer, x, y); + }); + move_relative_thunk = proxy->register_op( + [this](wl_fixed_t dx, wl_fixed_t dy) + { + pointer->move_relative(pointer, dx, dy); + }); + button_down_thunk = proxy->register_op( + [this](int button) + { + pointer->button_down(pointer, button); + }); + button_up_thunk = proxy->register_op( + [this](int button) + { + pointer->button_up(pointer, button); + }); + destroy_thunk = proxy->register_op( + [this]() + { + pointer->destroy(pointer); + }); + } + + std::shared_ptr const keep_dso_loaded; + WlcsPointer* const pointer; + + std::function move_absolute_thunk; + std::function move_relative_thunk; + std::function button_down_thunk; + std::function button_up_thunk; + std::function destroy_thunk; +}; + +wlcs::Pointer::~Pointer() = default; +wlcs::Pointer::Pointer(Pointer&&) = default; + +template +wlcs::Pointer::Pointer( + WlcsPointer* raw_device, + std::shared_ptr const& proxy, + std::shared_ptr const& keep_dso_loaded) + : impl{std::make_unique(raw_device, proxy, keep_dso_loaded)} +{ +} + +void wlcs::Pointer::move_to(int x, int y) +{ + impl->move_to(x, y); +} + +void wlcs::Pointer::move_by(int dx, int dy) +{ + impl->move_by(dx, dy); +} + +void wlcs::Pointer::button_down(int button) +{ + impl->button_down(button); +} + +void wlcs::Pointer::button_up(int button) +{ + impl->button_up(button); +} + +void wlcs::Pointer::click(int button) +{ + button_down(button); + button_up(button); +} + +void wlcs::Pointer::left_button_down() +{ + button_down(BTN_LEFT); +} + +void wlcs::Pointer::left_button_up() +{ + button_up(BTN_LEFT); +} + +void wlcs::Pointer::left_click() +{ + click(BTN_LEFT); +} + +class wlcs::Touch::Impl +{ +public: + template + Impl( + WlcsTouch* raw_device, + std::shared_ptr const& proxy, + std::shared_ptr const& keep_dso_loaded) + : keep_dso_loaded{keep_dso_loaded}, + touch{raw_device, proxy->register_op([](WlcsTouch* raw_device) { raw_device->destroy(raw_device); })} + { + if (touch->version != WLCS_TOUCH_VERSION) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{ + std::string{"Unexpected WlcsTouch version. Expected: "} + + std::to_string(WLCS_TOUCH_VERSION) + + " received: " + + std::to_string(touch->version)})); + } + set_up_thunks(proxy); + } + + void down_at(int x, int y) + { + touch->touch_down(touch.get(), x, y); + } + + void move_to(int x, int y) + { + touch->touch_move(touch.get(), x, y); + } + + void up() + { + touch->touch_up(touch.get()); + } + +private: + template + void set_up_thunks(std::shared_ptr const& proxy) + { + touch_down_thunk = proxy->register_op( + [this](int x, int y) + { + touch->touch_down(touch.get(), x, y); + }); + touch_move_thunk = proxy->register_op( + [this](int x, int y) + { + touch->touch_move(touch.get(), x, y); + }); + touch_up_thunk = proxy->register_op( + [this]() + { + touch->touch_up(touch.get()); + }); + } + + std::shared_ptr const keep_dso_loaded; + std::unique_ptr> const touch; + + std::function touch_down_thunk; + std::function touch_move_thunk; + std::function touch_up_thunk; +}; + +wlcs::Touch::~Touch() = default; +wlcs::Touch::Touch(Touch&&) = default; + +template +wlcs::Touch::Touch( + WlcsTouch* raw_device, + std::shared_ptr const& proxy, + std::shared_ptr const& keep_dso_loaded) + : impl{std::make_unique(raw_device, proxy, keep_dso_loaded)} +{ +} + +void wlcs::Touch::down_at(int x, int y) +{ + impl->down_at(x, y); +} + +void wlcs::Touch::move_to(int x, int y) +{ + impl->move_to(x, y); +} + +void wlcs::Touch::up() +{ + impl->up(); +} + +namespace +{ +std::shared_ptr const> extract_supported_extensions(WlcsDisplayServer* server) +{ + if (server->version < 2) + { + return {}; + } + + auto const descriptor = server->get_descriptor(server); + auto extensions = std::make_shared>(); + + for (auto i = 0u; i < descriptor->num_extensions; ++i) + { + (*extensions)[descriptor->supported_extensions[i].name] = descriptor->supported_extensions[i].version; + } + return extensions; +} + +class NullProxy +{ +public: + template + auto register_op(Callable handler) + { + return handler; + } +}; +} + +class wlcs::Server::Impl +{ +public: + Impl(std::shared_ptr const& hooks, int argc, char const** argv) + : server{hooks->create_server(argc, argv), hooks->destroy_server}, + thread_context{make_context_if_needed(*server)}, + hooks{hooks}, + supported_extensions_{extract_supported_extensions(server.get())} + { + if (hooks->version < 1) + { + BOOST_THROW_EXCEPTION((std::runtime_error{"Server integration too old"})); + } + if (server->version < 1) + { + BOOST_THROW_EXCEPTION((std::runtime_error{"Server integration too old"})); + } + if (!server->stop) + { + BOOST_THROW_EXCEPTION((std::logic_error{"Missing required WlcsDisplayServer.stop definition"})); + } + if (thread_context) + { + initialise_thunks(thread_context->proxy); + } + else + { + initialise_thunks(std::make_shared()); + } + } + + void start() + { + if (thread_context) + { + thread_context->server_thread = std::thread{ + [this]() + { + server->start_on_this_thread(server.get(), thread_context->event_loop.get()); + }}; + } + else + { + server->start(server.get()); + } + } + + void stop() + { + stop_thunk(); + } + + int create_client_socket() + { + auto fd = create_client_socket_thunk(); + if (fd < 0) + { + BOOST_THROW_EXCEPTION((std::system_error{ + errno, + std::system_category(), + "Failed to get client socket from server"})); + } + return fd; + } + + Pointer create_pointer() + { + if (thread_context) + { + return Pointer{create_pointer_thunk(), thread_context->proxy, hooks}; + } + else + { + return Pointer{create_pointer_thunk(), std::make_shared(), hooks}; + } + } + + Touch create_touch() + { + if (thread_context) + { + return Touch{create_touch_thunk(), thread_context->proxy, hooks}; + } + else + { + return Touch{create_touch_thunk(), std::make_shared(), hooks}; + } + } + + void move_surface_to(Surface& surface, int x, int y) + { + // Ensure the server knows about the IDs we're about to send... + surface.owner().roundtrip(); + + position_window_absolute_thunk(surface.owner(), surface, x, y); + } + + WlcsDisplayServer* wlcs_server() const + { + return server.get(); + } + + std::shared_ptr> supported_extensions() const + { + return supported_extensions_; + } + +private: + struct ThreadContext + { + explicit ThreadContext(std::unique_ptr loop) + : event_loop{std::move(loop)}, + proxy{std::make_shared(event_loop.get())} + { + } + ThreadContext(ThreadContext&&) = default; + + ~ThreadContext() + { + if (server_thread.joinable()) + { + server_thread.join(); + } + } + + std::unique_ptr event_loop; + std::thread server_thread; + std::shared_ptr proxy; + }; + + static std::optional make_context_if_needed(WlcsDisplayServer const& server) + { + if (server.version >= 3) + { + if (!server.start) + { + if (!server.start_on_this_thread) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{"Server integration missing both start() and start_on_this_thread()"})); + } + auto loop = std::unique_ptr{ + wl_event_loop_create(), + &wl_event_loop_destroy + }; + if (!loop) + { + BOOST_THROW_EXCEPTION(( + std::runtime_error{"Failed to create eventloop for WLCS events"})); + } + + return {ThreadContext{std::move(loop)}}; + } + } + return {}; + }; + + std::unique_ptr const server; + std::optional thread_context; + std::shared_ptr const hooks; + std::shared_ptr const> const supported_extensions_; + + template + void initialise_thunks(std::shared_ptr proxy) + { + stop_thunk = proxy->register_op( + [this]() + { + server->stop(server.get()); + }); + create_client_socket_thunk = proxy->register_op( + [this]() + { + return server->create_client_socket(server.get()); + }); + create_pointer_thunk = proxy->register_op( + [this]() + { + return server->create_pointer(server.get()); + }); + create_touch_thunk = proxy->register_op( + [this]() + { + return server->create_touch(server.get()); + }); + position_window_absolute_thunk = proxy->register_op( + [this]( + struct wl_display* client, + struct wl_surface* surface, + int x, + int y) + { + server->position_window_absolute( + server.get(), + client, + surface, + x, + y); + }); + } + std::function stop_thunk; + std::function create_client_socket_thunk; + std::function create_pointer_thunk; + std::function create_touch_thunk; + std::function position_window_absolute_thunk; +}; + +wlcs::Server::Server( + std::shared_ptr const& hooks, + int argc, + char const** argv) + : impl{std::make_unique(hooks, argc, argv)} +{ +} + +wlcs::Server::~Server() = default; + +void wlcs::Server::start() +{ + impl->start(); +} + +void wlcs::Server::stop() +{ + impl->stop(); +} + +std::shared_ptr> wlcs::Server::supported_extensions() +{ + return impl->supported_extensions(); +} + +int wlcs::Server::create_client_socket() +{ + return impl->create_client_socket(); +} + +void wlcs::Server::move_surface_to(Surface& surface, int x, int y) +{ + impl->move_surface_to(surface, x, y); +} + +wlcs::Pointer wlcs::Server::create_pointer() +{ + return impl->create_pointer(); +} + +wlcs::Touch wlcs::Server::create_touch() +{ + return impl->create_touch(); +} + +wlcs::InProcessServer::InProcessServer() + : server{helpers::get_test_hooks(), helpers::get_argc(), helpers::get_argv()} +{ +} + +void wlcs::InProcessServer::SetUp() +{ + server.start(); +} + +void wlcs::InProcessServer::TearDown() +{ + server.stop(); +} + +wlcs::Server& wlcs::InProcessServer::the_server() +{ + return server; +} + +void throw_wayland_error(wl_display* display) +{ + auto err = wl_display_get_error(display); + if (err != EPROTO) + { + BOOST_THROW_EXCEPTION((std::system_error{ + err, + std::system_category(), + "Error while dispatching Wayland events" + })); + } + else + { + uint32_t object_id; + uint32_t protocol_error; + wl_interface const* interface; + protocol_error = wl_display_get_protocol_error(display, &interface, &object_id); + BOOST_THROW_EXCEPTION((wlcs::ProtocolError{interface, protocol_error})); + } +} + +class wlcs::Client::Impl +{ +public: + Impl(Server& server) + : supported_extensions{server.supported_extensions()} + { + try + { + display = wl_display_connect_to_fd(server.create_client_socket()); + } + catch (ShimNotImplemented const&) + { + // TODO: Warn about connecting to who-knows-what + display = wl_display_connect(NULL); + } + + if (!display) + { + BOOST_THROW_EXCEPTION((std::runtime_error{"Failed to connect to Wayland socket"})); + } + + registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, this); + + server_roundtrip(); + } + + ~Impl() + { + if (shm) wl_shm_destroy(shm); + if (shell) wl_shell_destroy(shell); + if (compositor) wl_compositor_destroy(compositor); + if (subcompositor) wl_subcompositor_destroy(subcompositor); + if (registry) wl_registry_destroy(registry); + if (seat) wl_seat_destroy(seat); + if (keyboard) wl_keyboard_destroy(keyboard); + if (pointer) wl_pointer_destroy(pointer); + if (touch) wl_touch_destroy(touch); + if (xdg_shell_v6) zxdg_shell_v6_destroy(xdg_shell_v6); + if (xdg_shell_stable) xdg_wm_base_destroy(xdg_shell_stable); + for (auto const& output: outputs) + wl_output_destroy(output->current.output); + for (auto const& callback: destruction_callbacks) + callback(); + destruction_callbacks.clear(); + wl_display_disconnect(display); + } + + struct wl_display* wl_display() const + { + return display; + } + + struct wl_compositor* wl_compositor() const + { + return compositor; + } + + struct wl_subcompositor* wl_subcompositor() const + { + return subcompositor; + } + + struct wl_shm* wl_shm() const + { + return shm; + } + + struct wl_seat* wl_seat() const + { + return seat; + } + + void run_on_destruction(std::function callback) + { + destruction_callbacks.push_back(callback); + } + + ShmBuffer const& create_buffer(Client& client, int width, int height) + { + auto buffer = std::make_shared(client, width, height); + run_on_destruction([buffer]() mutable + { + buffer.reset(); + }); + return *buffer; + } + + Surface create_wl_shell_surface(Client& client, int width, int height) + { + Surface surface{client}; + + wl_shell_surface * shell_surface = wl_shell_get_shell_surface(the_shell(), surface); + surface.run_on_destruction([shell_surface]() + { + wl_shell_surface_destroy(shell_surface); + }); + wl_shell_surface_set_toplevel(shell_surface); + + wl_surface_commit(surface); + + surface.attach_visible_buffer(width, height); + + return surface; + } + + Surface create_xdg_shell_v6_surface(Client& client, int width, int height) + { + Surface surface{client}; + + auto xdg_surface = std::make_shared(client, surface); + auto xdg_toplevel = std::make_shared(*xdg_surface); + + surface.run_on_destruction([xdg_surface, xdg_toplevel]() mutable + { + xdg_toplevel.reset(); + xdg_surface.reset(); + }); + + wl_surface_commit(surface); + + surface.attach_visible_buffer(width, height); + + return surface; + } + + Surface create_xdg_shell_stable_surface(Client& client, int width, int height) + { + Surface surface{client}; + + auto xdg_surface = std::make_shared(client, surface); + auto xdg_toplevel = std::make_shared(*xdg_surface); + + surface.run_on_destruction([xdg_surface, xdg_toplevel]() mutable + { + xdg_toplevel.reset(); + xdg_surface.reset(); + }); + + wl_surface_commit(surface); + + surface.attach_visible_buffer(width, height); + + return surface; + } + + Surface create_visible_surface(Client& client, int width, int height) + { + if (shell) + { + return create_wl_shell_surface(client, width, height); + } + else if (xdg_shell_stable) + { + return create_xdg_shell_stable_surface(client, width, height); + } + else if (xdg_shell_v6) + { + return create_xdg_shell_v6_surface(client, width, height); + } + else + { + throw std::runtime_error("compositor does not support any known shell protocols"); + } + } + + wl_shell* the_shell() const + { + if (shell) + { + return shell; + } + else + { + if (!supported_extensions || !supported_extensions->count("wl_shell")) + { + BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{"wl_shell", AnyVersion})); + } + else + { + throw std::runtime_error("Failed to bind to wl_shell"); + } + } + } + + zxdg_shell_v6* the_xdg_shell_v6() const + { + if (xdg_shell_v6) + { + return xdg_shell_v6; + } + else + { + if (!supported_extensions || !supported_extensions->count("zxdg_shell_v6")) + { + BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{"zxdg_shell_v6", AnyVersion})); + } + else + { + throw std::runtime_error("Failed to bind to zxdg_shell_v6"); + } + } + } + + xdg_wm_base* the_xdg_shell_stable() const + { + return xdg_shell_stable; + } + + wl_pointer* the_pointer() const { return pointer; } + + wl_surface* keyboard_focused_window() const + { + return keyboard_focused_surface; + } + + wl_surface* window_under_cursor() const + { + if (pointer_events_pending()) + BOOST_THROW_EXCEPTION(std::runtime_error("Pointer events pending")); + if (current_pointer_location) + { + return current_pointer_location->surface; + } + return nullptr; + } + + wl_surface* touched_window() const + { + if (touch_events_pending()) + BOOST_THROW_EXCEPTION(std::runtime_error("Touch events pending")); + wl_surface* surface = nullptr; + for (auto const& touch : current_touches) + { + if (surface && touch.second.surface != surface) + BOOST_THROW_EXCEPTION(std::runtime_error("Multiple surfaces have active touches")); + else + surface = touch.second.surface; + } + return surface; + } + + std::pair pointer_position() const + { + if (pointer_events_pending()) + BOOST_THROW_EXCEPTION(std::runtime_error("Pointer events pending")); + return current_pointer_location.value().coordinates; + }; + + std::pair touch_position() const + { + if (touch_events_pending()) + BOOST_THROW_EXCEPTION(std::runtime_error("Touch events pending")); + if (current_touches.empty()) + BOOST_THROW_EXCEPTION(std::runtime_error("No touches")); + else if (current_touches.size() == 1) + return current_touches.begin()->second.coordinates; + else + BOOST_THROW_EXCEPTION(std::runtime_error("More than one touches")); + }; + + std::optional latest_serial() const + { + return latest_serial_; + } + + bool pointer_events_pending() const + { + return !pending_buttons.empty() || pending_pointer_location; + } + + bool touch_events_pending() const + { + return !pending_touches.empty() || !pending_up_touches.empty(); + } + + void add_pointer_enter_notification(PointerEnterNotifier const& on_enter) + { + enter_notifiers.push_back(on_enter); + } + void add_pointer_leave_notification(PointerLeaveNotifier const& on_leave) + { + leave_notifiers.push_back(on_leave); + } + void add_pointer_motion_notification(PointerMotionNotifier const& on_motion) + { + motion_notifiers.push_back(on_motion); + } + void add_pointer_button_notification(PointerButtonNotifier const& on_button) + { + button_notifiers.push_back(on_button); + } + + void* bind_if_supported(wl_interface const& to_bind, VersionSpecifier const& version) const + { + std::optional expected_to_be_supported{}; + + if (supported_extensions) + { + expected_to_be_supported = + supported_extensions->count(to_bind.name) && + version.select_version(supported_extensions->at(to_bind.name), to_bind.version); + } + + /* TODO: Mark tests using globals which exist, but are listed as unsupported as + * may-fail. + * This should help incrementally implementing a protocol, while getting test + * feedback. + */ + auto const global = globals.find(to_bind.name); + if (global != globals.end()) + { + auto const version_to_bind = version.select_version(global->second.version, to_bind.version); + if (version_to_bind) + { + auto global_proxy = wl_registry_bind(registry, global->second.id, &to_bind, version_to_bind.value()); + if (!global_proxy) + { + throw_wayland_error(display); + } + return global_proxy; + } + else if (!expected_to_be_supported.value_or(true)) + { + // We didn't expect to find the needed version of this extension anyway… + BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{to_bind.name, version})); + } + + BOOST_THROW_EXCEPTION((std::runtime_error{ + "Failed to bind to " + std::string{to_bind.name} + " version " + version.describe()})); + } + else if (!expected_to_be_supported.value_or(true)) + { + // We didn't expect to find this extension anyway… + BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{to_bind.name, version})); + } + BOOST_THROW_EXCEPTION((std::runtime_error{ + "Failed to bind to " + std::string{to_bind.name} + " version " + version.describe()})); + } + + void dispatch_until( + std::function const& predicate, std::chrono::seconds timeout) + { + using namespace std::chrono; + + auto const end_time = steady_clock::now() + timeout; + while (!predicate()) + { + while (wl_display_prepare_read(display) != 0) + { + if (wl_display_dispatch_pending(display) < 0) + throw_wayland_error(display); + } + wl_display_flush(display); + + auto const time_left = end_time - steady_clock::now(); + if (time_left.count() < 0) + { + BOOST_THROW_EXCEPTION((Timeout{"Timeout waiting for condition"})); + } + + /* + * TODO: We really want std::chrono::duration::ceil() here, but that's C++17 + */ + /* + * We want to wait *at least* as long as time_left. duration_cast + * will perform integer division, so any fractional milliseconds will get dropped. + * + * Adding 1ms will ensure we wait until *after* we're meant to timeout. + */ + auto const maximum_wait_ms = duration_cast(time_left) + 1ms; + pollfd fd{ + wl_display_get_fd(display), + POLLIN | POLLERR, + 0 + }; + + auto const poll_result = poll(&fd, 1, maximum_wait_ms.count()); + if (poll_result < 0) + { + wl_display_cancel_read(display); + BOOST_THROW_EXCEPTION((std::system_error{ + errno, + std::system_category(), + "Failed to wait for Wayland event"})); + } + + if (poll_result == 0) + { + wl_display_cancel_read(display); + BOOST_THROW_EXCEPTION((Timeout{"Timeout waiting for condition"})); + } + + if (wl_display_read_events(display) < 0) + { + throw_wayland_error(display); + } + + if (wl_display_dispatch_pending(display) < 0) + { + throw_wayland_error(display); + } + } + } + + void server_roundtrip() + { + if (wl_display_roundtrip(display) < 0) + { + throw_wayland_error(display); + } + } + + void client_flush() + { + if (wl_display_flush(display) == -1) + { + /* flush will return a (non-fatal) EAGAIN if the send buffer is + * full. + * + * We don't particularly want to care about the EAGAIN case + * at the moment, so just ignore it. + */ + if (errno != EAGAIN) + { + throw_wayland_error(display); + } + } + } + + struct Output + { + OutputState current; + OutputState pending; + std::vector> done_notifiers; + + Output(struct wl_output* output) + : current{output}, + pending{output} + { + } + + static void geometry_thunk( + void* ctx, + struct wl_output */*wl_output*/, + int32_t x, + int32_t y, + int32_t /*physical_width*/, + int32_t /*physical_height*/, + int32_t /*subpixel*/, + const char */*make*/, + const char */*model*/, + int32_t /*transform*/) + { + auto me = static_cast(ctx); + me->pending.geometry_position = std::make_pair(x, y); + } + + static void mode_thunk( + void* ctx, + struct wl_output */*wl_output*/, + uint32_t /*flags*/, + int32_t width, + int32_t height, + int32_t /*refresh*/) + { + auto me = static_cast(ctx); + me->pending.mode_size = std::make_pair(width, height); + } + + static void done_thunk(void* ctx, struct wl_output */*wl_output*/) + { + auto me = static_cast(ctx); + + if (me->pending.geometry_position) + me->current.geometry_position = me->pending.geometry_position; + if (me->pending.mode_size) + me->current.mode_size = me->pending.mode_size; + if (me->pending.scale) + me->current.scale = me->pending.scale; + + me->pending = OutputState{me->current.output}; + + for (auto const& notifier : me->done_notifiers) + notifier(); + } + + static void scale_thunk(void* ctx, struct wl_output */*wl_output*/, int32_t factor) + { + auto me = static_cast(ctx); + me->pending.scale = factor; + } + + static constexpr wl_output_listener listener = { + &Impl::Output::geometry_thunk, + &Impl::Output::mode_thunk, + &Impl::Output::done_thunk, + &Impl::Output::scale_thunk, + }; + }; + + std::vector> outputs; + +private: + static void keyboard_keymap(void*, wl_keyboard*, uint32_t, int32_t fd, uint32_t) + { + close(fd); + } + + static void keyboard_enter( + void* ctx, + wl_keyboard*, + uint32_t serial, + wl_surface *surface, + wl_array* /*keys*/) + { + auto me = static_cast(ctx); + me->keyboard_focused_surface = surface; + me->latest_serial_ = serial; + } + + static void keyboard_leave( + void *ctx, + wl_keyboard*, + uint32_t serial, + wl_surface* surface) + { + auto me = static_cast(ctx); + if (me->keyboard_focused_surface == surface) + { + me->keyboard_focused_surface = nullptr; + } + me->latest_serial_ = serial; + } + + static void keyboard_key( + void* ctx, + wl_keyboard*, + uint32_t serial, + uint32_t /*time*/, + uint32_t /*key*/, + uint32_t /*state*/) + { + auto me = static_cast(ctx); + me->latest_serial_ = serial; + } + + static void keyboard_modifiers(void*, wl_keyboard*, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) + { + } + + static void keyboard_repeat_info(void*, wl_keyboard*, int32_t, int32_t) + { + } + + static constexpr wl_keyboard_listener keyboard_listener = { + keyboard_keymap, + keyboard_enter, + keyboard_leave, + keyboard_key, + keyboard_modifiers, + keyboard_repeat_info, + }; + + static void pointer_enter( + void* ctx, + wl_pointer* /*pointer*/, + uint32_t serial, + wl_surface* surface, + wl_fixed_t x, + wl_fixed_t y) + { + auto me = static_cast(ctx); + me->latest_serial_ = serial; + + if (me->current_pointer_location && !me->pending_pointer_leave) + FAIL() + << "Pointer tried to enter surface " << surface + << " without first leaving surface " << me->current_pointer_location.value().surface; + + me->pending_pointer_location = SurfaceLocation{ + surface, + std::make_pair(x, y) + }; + } + + static void pointer_leave( + void* ctx, + wl_pointer* /*pointer*/, + uint32_t serial, + wl_surface* surface) + { + auto me = static_cast(ctx); + me->latest_serial_ = serial; + + if (!me->current_pointer_location) + FAIL() << "Got wl_pointer.leave when the pointer was not on a surface"; + + // the surface should never be null along the wire, but may come out as null if it's been destroyed + if (surface != nullptr && surface != me->current_pointer_location.value().surface) + FAIL() + << "Got wl_pointer.leave with surface " << surface + << " instead of " << me->current_pointer_location.value().surface; + + me->pending_pointer_location = std::nullopt; + me->pending_pointer_leave = true; + } + + static void pointer_motion( + void* ctx, + wl_pointer* /*pointer*/, + uint32_t /*time*/, + wl_fixed_t x, + wl_fixed_t y) + { + auto me = static_cast(ctx); + + if (!me->current_pointer_location && !me->pending_pointer_location) + FAIL() << "Got wl_pointer.motion when the pointer was not on a surface"; + + if (!me->pending_pointer_location) + me->pending_pointer_location = me->current_pointer_location; + me->pending_pointer_location.value().coordinates = std::make_pair(x, y); + } + + static void pointer_button( + void *ctx, + wl_pointer* /*wl_pointer*/, + uint32_t serial, + uint32_t /*time*/, + uint32_t button, + uint32_t state) + { + auto me = static_cast(ctx); + me->latest_serial_ = serial; + + me->pending_buttons[button] = std::make_pair(serial, state == WL_POINTER_BUTTON_STATE_PRESSED); + } + + static void pointer_frame(void* ctx, wl_pointer* /*pointer*/) + { + auto me = static_cast(ctx); + + if (me->pending_pointer_leave) + { + if (!me->current_pointer_location) + FAIL() << "Pointer tried to leave when it was not on a surface"; + + wl_surface* old_surface = me->current_pointer_location.value().surface; + me->current_pointer_location = std::nullopt; + me->pending_pointer_leave = false; + + me->notify_of_pointer_leave(old_surface); + } + + if (me->pending_pointer_location) + { + auto const old_pointer_location = me->current_pointer_location; + me->current_pointer_location = me->pending_pointer_location; + me->pending_pointer_location = std::nullopt; + + if (!old_pointer_location) + { + me->notify_of_pointer_enter( + me->current_pointer_location.value().surface, + me->current_pointer_location.value().coordinates); + } + else + { + me->notify_of_pointer_motion(me->current_pointer_location.value().coordinates); + } + } + + if (!me->pending_buttons.empty()) + { + me->notify_of_pointer_buttons(me->pending_buttons); + me->pending_buttons.clear(); + } + } + + void notify_of_pointer_enter(wl_surface* surface, std::pair position) + { + std::vector to_remove; + for (auto notifier = enter_notifiers.begin(); notifier != enter_notifiers.end(); ++notifier) + { + if (!(*notifier)(surface, position.first, position.second)) + { + to_remove.push_back(notifier); + } + } + for (auto removed : to_remove) + { + enter_notifiers.erase(removed); + } + } + + void notify_of_pointer_leave(wl_surface* surface) + { + std::vector to_remove; + for (auto notifier = leave_notifiers.begin(); notifier != leave_notifiers.end(); ++notifier) + { + if (!(*notifier)(surface)) + { + to_remove.push_back(notifier); + } + } + for (auto removed : to_remove) + { + leave_notifiers.erase(removed); + } + } + + void notify_of_pointer_motion(std::pair position) + { + + std::vector to_remove; + for (auto notifier = motion_notifiers.begin(); notifier != motion_notifiers.end(); ++notifier) + { + if (!(*notifier)(position.first, position.second)) + { + to_remove.push_back(notifier); + } + } + for (auto removed : to_remove) + { + motion_notifiers.erase(removed); + } + } + + void notify_of_pointer_buttons(std::map> const& buttons) + { + for (auto const& button : buttons) + { + std::vector to_remove; + for (auto notifier = button_notifiers.begin(); notifier != button_notifiers.end(); ++notifier) + { + if (!(*notifier)(button.second.first, button.first, button.second.second)) + { + to_remove.push_back(notifier); + } + } + for (auto removed : to_remove) + { + button_notifiers.erase(removed); + } + } + } + + static constexpr wl_pointer_listener pointer_listener = { + &Impl::pointer_enter, + &Impl::pointer_leave, + &Impl::pointer_motion, + &Impl::pointer_button, + [](auto...){}, // axis + &Impl::pointer_frame, // frame + [](auto...){}, // axis_source + [](auto...){}, // axis_stop + [](auto...){}, // axis_discrete + }; + + static void touch_down( + void* ctx, + wl_touch* /*wl_touch*/, + uint32_t serial, + uint32_t /*time*/, + wl_surface* surface, + int32_t id, + wl_fixed_t x, + wl_fixed_t y) + { + auto me = static_cast(ctx); + me->latest_serial_ = serial; + + auto touch = me->current_touches.find(id); + if (touch != me->current_touches.end()) + FAIL() << "Got wl_touch.down with ID " << id << " which is already down"; + + me->pending_touches[id] = SurfaceLocation { + surface, + std::make_pair(x,y) + }; + } + + static void touch_up( + void* ctx, + wl_touch* /*wl_touch*/, + uint32_t serial, + uint32_t /*time*/, + int32_t id) + { + auto me = static_cast(ctx); + me->latest_serial_ = serial; + + auto touch = me->current_touches.find(id); + if (touch == me->current_touches.end()) + FAIL() << "Got wl_touch.up with unknown ID " << id; + + me->pending_up_touches.insert(id); + } + + static void touch_motion( + void* ctx, + wl_touch* /*wl_touch*/, + uint32_t /*time*/, + int32_t id, + wl_fixed_t x, + wl_fixed_t y) + { + auto me = static_cast(ctx); + + auto touch = me->current_touches.find(id); + if (touch == me->current_touches.end()) + FAIL() << "Got wl_touch.up with unknown ID " << id; + + me->pending_touches[id] = SurfaceLocation { + touch->second.surface, + std::make_pair(x,y) + }; + } + + static void touch_frame(void* ctx, wl_touch* /*touch*/) + { + auto me = static_cast(ctx); + + for (auto const& id : me->pending_up_touches) + { + me->current_touches.erase(id); + } + + for (auto const& touch : me->pending_touches) + { + me->current_touches[touch.first] = touch.second; + } + + me->pending_up_touches.clear(); + me->pending_touches.clear(); + } + + static constexpr wl_touch_listener touch_listener = { + &Impl::touch_down, + &Impl::touch_up, + &Impl::touch_motion, + &Impl::touch_frame, + nullptr, // cancel + nullptr, // shape + nullptr, // orientation + }; + + static void seat_capabilities( + void* ctx, + struct wl_seat* seat, + uint32_t capabilities) + { + auto me = static_cast(ctx); + + if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) + { + me->keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(me->keyboard, &keyboard_listener, me); + } + + if (capabilities & WL_SEAT_CAPABILITY_POINTER) + { + me->pointer = wl_seat_get_pointer(seat); + wl_pointer_add_listener(me->pointer, &pointer_listener, me); + } + + if (capabilities & WL_SEAT_CAPABILITY_TOUCH) + { + me->touch = wl_seat_get_touch(seat); + wl_touch_add_listener(me->touch, &touch_listener, me); + } + } + + static void seat_name( + void*, + struct wl_seat*, + char const*) + { + } + + static constexpr wl_seat_listener seat_listener = { + &Impl::seat_capabilities, + &Impl::seat_name + }; + + static void global_handler( + void* ctx, + wl_registry* registry, + uint32_t id, + char const* interface, + uint32_t version) + { + using namespace std::literals::string_literals; + + static auto const safe_bind = [] + (wl_registry* registry, uint32_t name, const wl_interface* iface, uint32_t version) + { + return wl_registry_bind(registry, name, iface, std::min(version, static_cast(iface->version))); + }; + + auto me = static_cast(ctx); + me->global_type_names[id] = interface; + me->globals[interface] = Global{id, version}; + + if ("wl_shm"s == interface) + { + me->shm = static_cast(safe_bind(registry, id, &wl_shm_interface, version)); + } + else if ("wl_compositor"s == interface) + { + me->compositor = static_cast( + safe_bind(registry, id, &wl_compositor_interface, version)); + } + else if ("wl_subcompositor"s == interface) + { + me->subcompositor = static_cast( + safe_bind(registry, id, &wl_subcompositor_interface, version)); + } + else if ("wl_shell"s == interface) + { + me->shell = static_cast(safe_bind(registry, id, &wl_shell_interface, version)); + } + else if ("wl_seat"s == interface) + { + me->seat = static_cast(safe_bind(registry, id, &wl_seat_interface, version)); + + wl_seat_add_listener(me->seat, &seat_listener, me); + // Ensure we receive the initial seat events. + me->server_roundtrip(); + } + else if ("wl_output"s == interface) + { + auto wl_output = static_cast(safe_bind(registry, id, &wl_output_interface, version)); + auto output = std::make_unique(wl_output); + wl_output_add_listener(wl_output, &Output::listener, output.get()); + me->outputs.push_back(std::move(output)); + + // Ensure we receive the initial output events. + me->server_roundtrip(); + } + else if ("zxdg_shell_v6"s == interface) + { + me->xdg_shell_v6 = static_cast(safe_bind(registry, id, &zxdg_shell_v6_interface, version)); + } + else if ("xdg_wm_base"s == interface) + { + me->xdg_shell_stable = static_cast(safe_bind(registry, id, &xdg_wm_base_interface, version)); + } + } + + static void global_removed(void* ctx, wl_registry*, uint32_t id) + { + auto me = static_cast(ctx); + auto const name = me->global_type_names.find(id); + if (name != me->global_type_names.end()) + { + me->globals.erase(name->second); + me->global_type_names.erase(name); + } + } + + constexpr static wl_registry_listener registry_listener = { + &global_handler, + &global_removed + }; + + std::shared_ptr const> const supported_extensions; + + struct wl_display* display; + struct wl_registry* registry = nullptr; + struct wl_compositor* compositor = nullptr; + struct wl_subcompositor* subcompositor = nullptr; + struct wl_shm* shm = nullptr; + struct wl_shell* shell = nullptr; + struct wl_seat* seat = nullptr; + struct wl_keyboard* keyboard = nullptr; + struct wl_pointer* pointer = nullptr; + struct wl_touch* touch = nullptr; + struct zxdg_shell_v6* xdg_shell_v6 = nullptr; + std::vector> destruction_callbacks; + struct xdg_wm_base* xdg_shell_stable = nullptr; + + struct Global + { + uint32_t id; + uint32_t version; + }; + std::map globals; + std::map global_type_names; + + struct SurfaceLocation + { + wl_surface* surface; + std::pair coordinates; + }; + wl_surface* keyboard_focused_surface = nullptr; + std::optional current_pointer_location; + std::optional pending_pointer_location; + bool pending_pointer_leave{false}; + std::map> pending_buttons; ///< Maps button id to the serial and if it's down + std::map current_touches; ///< Touches that have gotten a frame event + std::map pending_touches; ///< Touches that have gotten down or motion events without a frame + std::set pending_up_touches; ///< Touches that have gotten up events without a frame + std::optional latest_serial_; + + std::vector enter_notifiers; + std::vector leave_notifiers; + std::vector motion_notifiers; + std::vector button_notifiers; +}; + +constexpr wl_keyboard_listener wlcs::Client::Impl::keyboard_listener; +constexpr wl_pointer_listener wlcs::Client::Impl::pointer_listener; +constexpr wl_touch_listener wlcs::Client::Impl::touch_listener; +constexpr wl_seat_listener wlcs::Client::Impl::seat_listener; +constexpr wl_output_listener wlcs::Client::Impl::Output::listener; +constexpr wl_registry_listener wlcs::Client::Impl::registry_listener; + +wlcs::Client::Client(Server& server) + : impl{std::make_unique(server)} +{ +} + +wlcs::Client::~Client() = default; + +wlcs::Client::operator wl_display*() const +{ + return impl->wl_display(); +} + +wl_compositor* wlcs::Client::compositor() const +{ + return impl->wl_compositor(); +} + +wl_subcompositor* wlcs::Client::subcompositor() const +{ + return impl->wl_subcompositor(); +} + +wl_shm* wlcs::Client::shm() const +{ + return impl->wl_shm(); +} + +wl_seat* wlcs::Client::seat() const +{ + return impl->wl_seat(); +} + +void wlcs::Client::run_on_destruction(std::function callback) +{ + impl->run_on_destruction(callback); +} + +wlcs::ShmBuffer const& wlcs::Client::create_buffer(int width, int height) +{ + return impl->create_buffer(*this, width, height); +} + +wlcs::Surface wlcs::Client::create_wl_shell_surface(int width, int height) +{ + return impl->create_wl_shell_surface(*this, width, height); +} + +wlcs::Surface wlcs::Client::create_xdg_shell_v6_surface(int width, int height) +{ + return impl->create_xdg_shell_v6_surface(*this, width, height); +} + +wlcs::Surface wlcs::Client::create_xdg_shell_stable_surface(int width, int height) +{ + return impl->create_xdg_shell_stable_surface(*this, width, height); +} + +wlcs::Surface wlcs::Client::create_visible_surface(int width, int height) +{ + return impl->create_visible_surface(*this, width, height); +} + +size_t wlcs::Client::output_count() const +{ + return impl->outputs.size(); +} + +wlcs::OutputState wlcs::Client::output_state(size_t index) const +{ + if (index > output_count()) + throw std::out_of_range("Invalid output index"); + + return impl->outputs[index]->current; +} + +void wlcs::Client::add_output_done_notifier(size_t index, std::function const& notifier) +{ + if (index > output_count()) + throw std::runtime_error("Invalid output index"); + + impl->outputs[index]->done_notifiers.push_back(notifier); +} + +wl_shell* wlcs::Client::shell() const +{ + return impl->the_shell(); +} + +zxdg_shell_v6* wlcs::Client::xdg_shell_v6() const +{ + return impl->the_xdg_shell_v6(); +} + +xdg_wm_base* wlcs::Client::xdg_shell_stable() const +{ + return impl->the_xdg_shell_stable(); +} + +wl_surface* wlcs::Client::keyboard_focused_window() const +{ + return impl->keyboard_focused_window(); +} + +wl_surface* wlcs::Client::window_under_cursor() const +{ + return impl->window_under_cursor(); +} + +wl_surface* wlcs::Client::touched_window() const +{ + return impl->touched_window(); +} + +std::pair wlcs::Client::pointer_position() const +{ + return impl->pointer_position(); +} + +std::pair wlcs::Client::touch_position() const +{ + return impl->touch_position(); +} + +std::optional wlcs::Client::latest_serial() const +{ + return impl->latest_serial(); +} + +void wlcs::Client::add_pointer_enter_notification(PointerEnterNotifier const& on_enter) +{ + impl->add_pointer_enter_notification(on_enter); +} + +void wlcs::Client::add_pointer_leave_notification(PointerLeaveNotifier const& on_leave) +{ + impl->add_pointer_leave_notification(on_leave); +} + +void wlcs::Client::add_pointer_motion_notification(PointerMotionNotifier const& on_motion) +{ + impl->add_pointer_motion_notification(on_motion); +} + +void wlcs::Client::add_pointer_button_notification(PointerButtonNotifier const& on_button) +{ + impl->add_pointer_button_notification(on_button); +} + +void wlcs::Client::dispatch_until(std::function const& predicate, std::chrono::seconds timeout) +{ + impl->dispatch_until(predicate, timeout); +} + +void wlcs::Client::roundtrip() +{ + impl->server_roundtrip(); +} + +void wlcs::Client::flush() +{ + impl->client_flush(); +} + +void* wlcs::Client::bind_if_supported(wl_interface const& interface, VersionSpecifier const& version) const +{ + return impl->bind_if_supported(interface, version); +} + +wl_pointer* wlcs::Client::the_pointer() const +{ + return impl->the_pointer(); +} + +class wlcs::Surface::Impl +{ +public: + Impl(Client& client) + : surface_{wl_compositor_create_surface(client.compositor())}, + owner_{client} + { + wl_surface_add_listener(surface_, &surface_listener, this); + } + + ~Impl() + { + for (auto i = 0u; i < pending_callbacks.size(); ) + { + if (pending_callbacks[i].first == this) + { + auto pending_callback = pending_callbacks[i].second; + delete static_cast*>(wl_callback_get_user_data(pending_callback)); + + wl_callback_destroy(pending_callback); + pending_callbacks.erase(pending_callbacks.begin() + i); + } + else + { + ++i; + } + } + + for (auto const& callback: destruction_callbacks) + callback(); + destruction_callbacks.clear(); + + wl_surface_destroy(surface_); + } + + ::wl_surface* surface() const + { + return surface_; + } + + void attach_buffer(int width, int height) + { + auto& buffer = owner_.create_buffer(width, height); + wl_surface_attach(surface_, buffer, 0, 0); + } + + void add_frame_callback(std::function const& on_frame) + { + std::unique_ptr> holder{ + new std::function(on_frame)}; + + auto callback = wl_surface_frame(surface_); + + pending_callbacks.push_back(std::make_pair(this, callback)); + + wl_callback_add_listener(callback, &frame_listener, holder.release()); + } + + void attach_visible_buffer(int width, int height) + { + attach_buffer(width, height); + auto surface_rendered = std::make_shared(false); + add_frame_callback([surface_rendered](auto) { *surface_rendered = true; }); + wl_surface_commit(surface_); + owner_.dispatch_until([surface_rendered]() { return *surface_rendered; }); + } + + void run_on_destruction(std::function callback) + { + destruction_callbacks.push_back(callback); + } + + Client& owner() const + { + return owner_; + } + + auto current_outputs() -> std::set const& + { + return outputs; + } + +private: + + static std::vector> pending_callbacks; + std::set outputs; + + static void frame_callback(void* ctx, wl_callback* callback, uint32_t frame_time) + { + auto us = std::find_if( + pending_callbacks.begin(), + pending_callbacks.end(), + [callback](auto const& elem) + { + return elem.second == callback; + }); + pending_callbacks.erase(us); + + auto frame_callback = static_cast*>(ctx); + + (*frame_callback)(frame_time); + + wl_callback_destroy(callback); + delete frame_callback; + } + + static constexpr wl_callback_listener frame_listener = { + &frame_callback + }; + + static void on_enter(void* data, ::wl_surface* /*wl_surface*/, wl_output* output) + { + auto const self = static_cast(data); + + auto const inserted = self->outputs.insert(output); + + if (!inserted.second) + { + BOOST_THROW_EXCEPTION(std::runtime_error( + "Got wl_surface.enter(wl_output@" + + std::to_string(wl_proxy_get_id(reinterpret_cast(output))) + + ") for an output the surface is already on")); + } + } + + static void on_leave(void* data, ::wl_surface* /*wl_surface*/, wl_output* output) + { + auto const self = static_cast(data); + + auto const erased = self->outputs.erase(output); + + if (!erased) + { + BOOST_THROW_EXCEPTION(std::runtime_error( + "Got wl_surface.leave(wl_output@" + + std::to_string(wl_proxy_get_id(reinterpret_cast(output))) + + ") for an output the surface is not on")); + } + } + + static constexpr wl_surface_listener surface_listener{ + &on_enter, + &on_leave, + }; + + struct wl_surface* const surface_; + Client& owner_; + std::vector> destruction_callbacks; +}; + +std::vector> wlcs::Surface::Impl::pending_callbacks; + +constexpr wl_callback_listener wlcs::Surface::Impl::frame_listener; +constexpr wl_surface_listener wlcs::Surface::Impl::surface_listener; + +wlcs::Surface::Surface(Client& client) + : impl{std::make_unique(client)} +{ +} + +wlcs::Surface::~Surface() = default; + +wlcs::Surface::Surface(Surface&&) = default; + +wlcs::Surface::operator ::wl_surface*() const +{ + return impl->surface(); +} + +void wlcs::Surface::attach_buffer(int width, int height) +{ + impl->attach_buffer(width, height); +} + +void wlcs::Surface::add_frame_callback(std::function const& on_frame) +{ + impl->add_frame_callback(on_frame); +} + +void wlcs::Surface::attach_visible_buffer(int width, int height) +{ + impl->attach_visible_buffer(width, height); +} + +void wlcs::Surface::run_on_destruction(std::function callback) +{ + impl->run_on_destruction(callback); +} + +wlcs::Client& wlcs::Surface::owner() const +{ + return impl->owner(); +} + +auto wlcs::Surface:: current_outputs() -> std::set const& +{ + return impl->current_outputs(); +} + +class wlcs::Subsurface::Impl +{ +public: + Impl(Client& client, Surface& surface, Surface& parent) + : subsurface_{wl_subcompositor_get_subsurface(client.subcompositor(), surface, parent)}, + parent_{parent} + { + } + + ~Impl() + { + wl_subsurface_destroy(subsurface_); + } + + struct wl_subsurface* const subsurface_; + Surface& parent_; +}; + +wlcs::Subsurface wlcs::Subsurface::create_visible(Surface& parent, int x, int y, int width, int height) +{ + wlcs::Subsurface subsurface{parent}; + wl_subsurface_set_position(subsurface, x, y); + + subsurface.attach_buffer(width, height); + + bool surface_rendered{false}; + subsurface.add_frame_callback([&surface_rendered](auto) { surface_rendered = true; }); + wlcs::Surface* surface_ptr = &subsurface; + while (surface_ptr) + { + wl_surface_commit(*surface_ptr); + auto subsurface_ptr = dynamic_cast(surface_ptr); + if (subsurface_ptr) + surface_ptr = &subsurface_ptr->parent(); + else + surface_ptr = nullptr; + } + parent.owner().dispatch_until([&surface_rendered]() { return surface_rendered; }); + + return subsurface; +} + +wlcs::Subsurface::Subsurface(wlcs::Surface& parent) + : Surface{parent.owner()}, + impl{std::make_unique(parent.owner(), *this, parent)} +{ +} + +wlcs::Subsurface::Subsurface(Subsurface &&) = default; + +wlcs::Subsurface::~Subsurface() = default; + +wlcs::Subsurface::operator wl_subsurface*() const +{ + return impl->subsurface_; +} + +wlcs::Surface& wlcs::Subsurface::parent() const +{ + return impl->parent_; +} + +class wlcs::ShmBuffer::Impl +{ +public: + Impl(Client& client, int width, int height) + { + auto stride = width * 4; + auto size = stride * height; + auto fd = wlcs::helpers::create_anonymous_file(size); + + auto pool = wl_shm_create_pool(client.shm(), fd, size); + buffer_ = wl_shm_pool_create_buffer( + pool, + 0, + width, + height, + stride, + WL_SHM_FORMAT_ARGB8888); + wl_shm_pool_destroy(pool); + close(fd); + + wl_buffer_add_listener(buffer_, &listener, this); + } + + ~Impl() + { + wl_buffer_destroy(buffer_); + } + + wl_buffer* buffer() const + { + return buffer_; + } + + void add_release_listener(std::function const& on_release) + { + release_notifiers.push_back(on_release); + } + +private: + static void on_release(void* ctx, wl_buffer* /*buffer*/) + { + auto me = static_cast(ctx); + + std::vectorrelease_notifiers.begin())> expired_notifiers; + + for (auto notifier = me->release_notifiers.begin(); notifier != me->release_notifiers.end(); ++notifier) + { + if (!(*notifier)()) + { + expired_notifiers.push_back(notifier); + } + } + for (auto const& expired : expired_notifiers) + me->release_notifiers.erase(expired); + } + + static constexpr wl_buffer_listener listener { + &on_release + }; + + wl_buffer* buffer_; + std::vector> release_notifiers; +}; + +constexpr wl_buffer_listener wlcs::ShmBuffer::Impl::listener; + +wlcs::ShmBuffer::ShmBuffer(Client &client, int width, int height) + : impl{std::make_unique(client, width, height)} +{ +} + +wlcs::ShmBuffer::ShmBuffer(ShmBuffer&&) = default; +wlcs::ShmBuffer::~ShmBuffer() = default; + +wlcs::ShmBuffer::operator wl_buffer*() const +{ + return impl->buffer(); +} + +void wlcs::ShmBuffer::add_release_listener(std::function const &on_release) +{ + impl->add_release_listener(on_release); +} diff --git a/src/input_method.cpp b/src/input_method.cpp new file mode 100644 index 0000000..19a8ef1 --- /dev/null +++ b/src/input_method.cpp @@ -0,0 +1,131 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "input_method.h" +#include + +auto wlcs::InputMethod::all_input_methods() -> std::vector> +{ + return { + std::make_shared(), + std::make_shared()}; +} + +struct wlcs::PointerInputMethod::Pointer : Device +{ + Pointer(wlcs::Server& server) + : pointer{server.create_pointer()} + { + } + + void down_at(std::pair position) override + { + ASSERT_THAT(button_down, testing::Eq(false)) << "Called down_at() with pointer already down"; + pointer.move_to(position.first, position.second); + pointer.left_button_down(); + button_down = true; + } + + void move_to(std::pair position) override + { + ASSERT_THAT(button_down, testing::Eq(true)) << "Called move_to() with pointer up"; + pointer.move_to(position.first, position.second); + } + + void up() override + { + ASSERT_THAT(button_down, testing::Eq(true)) << "Called up() with pointer already up"; + pointer.left_button_up(); + button_down = false; + } + + wlcs::Pointer pointer; + bool button_down = false; +}; + +auto wlcs::PointerInputMethod::create_device(wlcs::Server& server) -> std::unique_ptr +{ + return std::make_unique(server); +} + +auto wlcs::PointerInputMethod::current_surface(wlcs::Client const& client) -> wl_surface* +{ + return client.window_under_cursor(); +} + +auto wlcs::PointerInputMethod::position_on_surface(wlcs::Client const& client) -> std::pair +{ + auto const wl_fixed_position = client.pointer_position(); + return { + wl_fixed_to_int(wl_fixed_position.first), + wl_fixed_to_int(wl_fixed_position.second)}; +} + +struct wlcs::TouchInputMethod::Touch : Device +{ + Touch(wlcs::Server& server) + : touch{server.create_touch()} + { + } + + void down_at(std::pair position) override + { + ASSERT_THAT(is_down, testing::Eq(false)) << "Called down_at() with touch already down"; + touch.down_at(position.first, position.second); + is_down = true; + } + + void move_to(std::pair position) override + { + ASSERT_THAT(is_down, testing::Eq(true)) << "Called move_to() with touch up"; + touch.move_to(position.first, position.second); + } + + void up() override + { + ASSERT_THAT(is_down, testing::Eq(true)) << "Called up() with touch already up"; + touch.up(); + is_down = false; + } + + wlcs::Touch touch; + bool is_down = false; +}; + +auto wlcs::TouchInputMethod::create_device(wlcs::Server& server) -> std::unique_ptr +{ + return std::make_unique(server); +} + +auto wlcs::TouchInputMethod::current_surface(wlcs::Client const& client) -> wl_surface* +{ + return client.touched_window(); +} + +auto wlcs::TouchInputMethod::position_on_surface(wlcs::Client const& client) -> std::pair +{ + auto const wl_fixed_position = client.touch_position(); + return { + wl_fixed_to_int(wl_fixed_position.first), + wl_fixed_to_int(wl_fixed_position.second)}; +} + +std::ostream& std::operator<<(std::ostream& out, std::shared_ptr const& param) +{ + return out << param->name; +} diff --git a/src/layer_shell_v1.cpp b/src/layer_shell_v1.cpp new file mode 100644 index 0000000..e59a0ec --- /dev/null +++ b/src/layer_shell_v1.cpp @@ -0,0 +1,67 @@ +/* + * Copyright © 2018-2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "layer_shell_v1.h" +#include "in_process_server.h" +#include "version_specifier.h" + +wlcs::LayerSurfaceV1::LayerSurfaceV1( + wlcs::Client& client, + wlcs::Surface& surface, + zwlr_layer_shell_v1_layer layer, + wl_output* output, + const char *_namespace) + : client{client}, + layer_shell{client.bind_if_supported(AnyVersion)}, + layer_surface{wrap_wl_object( + zwlr_layer_shell_v1_get_layer_surface( + layer_shell, + surface, + output, + layer, + _namespace))} +{ + static struct zwlr_layer_surface_v1_listener const listener { + [](void* data, + struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, + uint32_t serial, + uint32_t width, + uint32_t height) + { + auto self = static_cast(data); + self->last_size_ = {width, height}; + self->configure_count++; + (void)zwlr_layer_surface_v1; + (void)serial; + zwlr_layer_surface_v1_ack_configure(zwlr_layer_surface_v1, serial); + }, + [](void* /*data*/, + struct zwlr_layer_surface_v1 */*zwlr_layer_surface_v1*/) + { + } + }; + zwlr_layer_surface_v1_add_listener(layer_surface, &listener, this); +} + +void wlcs::LayerSurfaceV1::dispatch_until_configure() +{ + client.dispatch_until([prev_config_count = configure_count, ¤t_config_count = configure_count] + { + return current_config_count > prev_config_count; + }); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..b77c79b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,100 @@ +/* + * Copyright © 2017 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#include +#include +#include + +#include + +#include "xfail_supporting_test_listener.h" +#include "shared_library.h" +#include "wlcs/display_server.h" + +#include "helpers.h" + + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + if (argc < 2 || !argv[1] || std::string{"--help"} == argv[1]) + { + std::cerr + << "WayLand Conformance Suite test runner" << std::endl + << "Usage: " << argv[0] << " COMPOSITOR_INTEGRATION_MODULE [GTEST OPTIONS]... [COMPOSITOR_OPTIONS]..." << std::endl; + return 1; + } + + auto const integration_filename = argv[1]; + + // Shuffle the integration module argument out of argv + for (auto i = 1 ; i < (argc - 1) ; ++i) + { + argv[i] = argv[i + 1]; + } + wlcs::helpers::set_command_line(argc - 1, const_cast(argv)); + + std::shared_ptr dso; + try + { + dso = std::make_shared(integration_filename); + } + catch (std::exception const& err) + { + std::cerr + << "Failed to load compositor integration module " << integration_filename << ": " << err.what() << std::endl; + return 1; + } + + std::shared_ptr entry_point; + try + { + entry_point = std::shared_ptr{ + dso, + dso->load_function("wlcs_server_integration") + }; + } + catch (std::exception const& err) + { + std::cerr + << "Failed to load compositor entry point: " << err.what() << std::endl; + return 1; + } + + wlcs::helpers::set_entry_point(entry_point); + + auto& listeners = ::testing::UnitTest::GetInstance()->listeners(); + auto wrapping_listener = new testing::XFailSupportingTestListenerWrapper{ + std::unique_ptr<::testing::TestEventListener>{ + listeners.Release(listeners.default_result_printer())}}; + listeners.Append(wrapping_listener); + + /* (void)! is apparently the magical incantation required to get GCC to + * *actually* silently ignore the return value of a function declared with + * __attribute__(("warn_unused_result")) + * cf: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66425 + */ + (void)!RUN_ALL_TESTS(); + if (wrapping_listener->failed()) + { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + diff --git a/src/pointer_constraints_unstable_v1.cpp b/src/pointer_constraints_unstable_v1.cpp new file mode 100644 index 0000000..e9336c2 --- /dev/null +++ b/src/pointer_constraints_unstable_v1.cpp @@ -0,0 +1,93 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "pointer_constraints_unstable_v1.h" + +#include "in_process_server.h" +#include + +wlcs::ZwpPointerConstraintsV1::ZwpPointerConstraintsV1(Client& client) : + manager{client.bind_if_supported(AnyVersion)} +{ +} + +wlcs::ZwpPointerConstraintsV1::~ZwpPointerConstraintsV1() = default; + +wlcs::ZwpPointerConstraintsV1::operator zwp_pointer_constraints_v1*() const +{ + return manager; +} + +wlcs::ZwpConfinedPointerV1::ZwpConfinedPointerV1( + ZwpPointerConstraintsV1& manager, + wl_surface* surface, + wl_pointer* pointer, + wl_region* region, + uint32_t lifetime) : + relative_pointer{zwp_pointer_constraints_v1_confine_pointer(manager, surface, pointer, region, lifetime)}, + version{zwp_confined_pointer_v1_get_version(relative_pointer)} +{ + zwp_confined_pointer_v1_set_user_data(relative_pointer, this); + zwp_confined_pointer_v1_add_listener(relative_pointer, &listener, this); +} + +wlcs::ZwpConfinedPointerV1::~ZwpConfinedPointerV1() +{ + zwp_confined_pointer_v1_destroy(relative_pointer); +} + +wlcs::ZwpConfinedPointerV1::operator zwp_confined_pointer_v1*() const +{ + return relative_pointer; +} + +zwp_confined_pointer_v1_listener const wlcs::ZwpConfinedPointerV1::listener = + { + [](void* self, auto*) { static_cast(self)->confined(); }, + [](void* self, auto*) { static_cast(self)->unconfined(); }, + }; + + +wlcs::ZwpLockedPointerV1::ZwpLockedPointerV1( + ZwpPointerConstraintsV1& manager, + wl_surface* surface, + wl_pointer* pointer, + wl_region* region, + uint32_t lifetime) : + locked_pointer{zwp_pointer_constraints_v1_lock_pointer(manager, surface, pointer, region, lifetime)}, + version{zwp_locked_pointer_v1_get_version(locked_pointer)} +{ + zwp_locked_pointer_v1_set_user_data(locked_pointer, this); + zwp_locked_pointer_v1_add_listener(locked_pointer, &listener, this); +} + +wlcs::ZwpLockedPointerV1::~ZwpLockedPointerV1() +{ + zwp_locked_pointer_v1_destroy(locked_pointer); +} + +wlcs::ZwpLockedPointerV1::operator zwp_locked_pointer_v1*() const +{ + return locked_pointer; +} + +zwp_locked_pointer_v1_listener const wlcs::ZwpLockedPointerV1::listener = + { + [](void* self, auto*) { static_cast(self)->locked(); }, + [](void* self, auto*) { static_cast(self)->unlocked(); }, + }; diff --git a/src/primary_selection.cpp b/src/primary_selection.cpp new file mode 100644 index 0000000..85bb0c1 --- /dev/null +++ b/src/primary_selection.cpp @@ -0,0 +1,90 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "primary_selection.h" + +#include + +wlcs::ActiveListeners wlcs::PrimarySelectionDeviceListener::active_listeners; +constexpr zwp_primary_selection_device_v1_listener wlcs::PrimarySelectionDeviceListener::thunks; + +wlcs::ActiveListeners wlcs::PrimarySelectionOfferListener::active_listeners; +constexpr zwp_primary_selection_offer_v1_listener wlcs::PrimarySelectionOfferListener::thunks; + +wlcs::ActiveListeners wlcs::PrimarySelectionSourceListener::active_listeners; +constexpr zwp_primary_selection_source_v1_listener wlcs::PrimarySelectionSourceListener::thunks; + + +void wlcs::PrimarySelectionDeviceListener::data_offer( + void* data, + zwp_primary_selection_device_v1* device, + zwp_primary_selection_offer_v1* offer) +{ + if (active_listeners.includes(data)) + static_cast(data)->data_offer(device, offer); +} + +void wlcs::PrimarySelectionDeviceListener::data_offer(zwp_primary_selection_device_v1*, zwp_primary_selection_offer_v1*) +{ +} + +void wlcs::PrimarySelectionDeviceListener::selection(zwp_primary_selection_device_v1*, zwp_primary_selection_offer_v1*) +{ +} + +void wlcs::PrimarySelectionDeviceListener::selection( + void* data, + zwp_primary_selection_device_v1* device, + zwp_primary_selection_offer_v1* offer) +{ + if (active_listeners.includes(data)) + static_cast(data)->selection(device, offer); +} + +void wlcs::PrimarySelectionOfferListener::offer(zwp_primary_selection_offer_v1*, const char*) +{ +} + +void wlcs::PrimarySelectionOfferListener::offer( + void* data, zwp_primary_selection_offer_v1* offer, const char* mime_type) +{ + if (active_listeners.includes(data)) + static_cast(data)->offer(offer, mime_type); +} + +void wlcs::PrimarySelectionSourceListener::send(zwp_primary_selection_source_v1*, const char*, int32_t fd) +{ + close(fd); +} + +void wlcs::PrimarySelectionSourceListener::cancelled(zwp_primary_selection_source_v1*) +{ +} + +void wlcs::PrimarySelectionSourceListener::send( + void* data, zwp_primary_selection_source_v1* source, const char* mime_type, int32_t fd) +{ + if (active_listeners.includes(data)) + static_cast(data)->send(source, mime_type, fd); +} + +void wlcs::PrimarySelectionSourceListener::cancelled(void* data, zwp_primary_selection_source_v1* source) +{ + if (active_listeners.includes(data)) + static_cast(data)->cancelled(source); +} diff --git a/src/protocol/gtk-primary-selection.xml b/src/protocol/gtk-primary-selection.xml new file mode 100644 index 0000000..aee85c2 --- /dev/null +++ b/src/protocol/gtk-primary-selection.xml @@ -0,0 +1,225 @@ + + + + Copyright © 2015, 2016 Red Hat + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol provides the ability to have a primary selection device to + match that of the X server. This primary selection is a shortcut to the + common clipboard selection, where text just needs to be selected in order + to allow copying it elsewhere. The de facto way to perform this action + is the middle mouse button, although it is not limited to this one. + + Clients wishing to honor primary selection should create a primary + selection source and set it as the selection through + wp_primary_selection_device.set_selection whenever the text selection + changes. In order to minimize calls in pointer-driven text selection, + it should happen only once after the operation finished. Similarly, + a NULL source should be set when text is unselected. + + wp_primary_selection_offer objects are first announced through the + wp_primary_selection_device.data_offer event. Immediately after this event, + the primary data offer will emit wp_primary_selection_offer.offer events + to let know of the mime types being offered. + + When the primary selection changes, the client with the keyboard focus + will receive wp_primary_selection_device.selection events. Only the client + with the keyboard focus will receive such events with a non-NULL + wp_primary_selection_offer. Across keyboard focus changes, previously + focused clients will receive wp_primary_selection_device.events with a + NULL wp_primary_selection_offer. + + In order to request the primary selection data, the client must pass + a recent serial pertaining to the press event that is triggering the + operation, if the compositor deems the serial valid and recent, the + wp_primary_selection_source.send event will happen in the other end + to let the transfer begin. The client owning the primary selection + should write the requested data, and close the file descriptor + immediately. + + If the primary selection owner client disappeared during the transfer, + the client reading the data will receive a + wp_primary_selection_device.selection event with a NULL + wp_primary_selection_offer, the client should take this as a hint + to finish the reads related to the no longer existing offer. + + The primary selection owner should be checking for errors during + writes, merely cancelling the ongoing transfer if any happened. + + + + + The primary selection device manager is a singleton global object that + provides access to the primary selection. It allows to create + wp_primary_selection_source objects, as well as retrieving the per-seat + wp_primary_selection_device objects. + + + + + Create a new primary selection source. + + + + + + + Create a new data device for a given seat. + + + + + + + + Destroy the primary selection device manager. + + + + + + + + Replaces the current selection. The previous owner of the primary selection + will receive a wp_primary_selection_source.cancelled event. + + To unset the selection, set the source to NULL. + + + + + + + + Introduces a new wp_primary_selection_offer object that may be used + to receive the current primary selection. Immediately following this + event, the new wp_primary_selection_offer object will send + wp_primary_selection_offer.offer events to describe the offered mime + types. + + + + + + + The wp_primary_selection_device.selection event is sent to notify the + client of a new primary selection. This event is sent after the + wp_primary_selection.data_offer event introducing this object, and after + the offer has announced its mimetypes through + wp_primary_selection_offer.offer. + + The data_offer is valid until a new offer or NULL is received + or until the client loses keyboard focus. The client must destroy the + previous selection data_offer, if any, upon receiving this event. + + + + + + + Destroy the primary selection device. + + + + + + + A wp_primary_selection_offer represents an offer to transfer the contents + of the primary selection clipboard to the client. Similar to + wl_data_offer, the offer also describes the mime types that the source + will transferthat the + data can be converted to and provides the mechanisms for transferring the + data directly to the client. + + + + + To transfer the contents of the primary selection clipboard, the client + issues this request and indicates the mime type that it wants to + receive. The transfer happens through the passed file descriptor + (typically created with the pipe system call). The source client writes + the data in the mime type representation requested and then closes the + file descriptor. + + The receiving client reads from the read end of the pipe until EOF and + closes its end, at which point the transfer is complete. + + + + + + + + Destroy the primary selection offer. + + + + + + Sent immediately after creating announcing the wp_primary_selection_offer + through wp_primary_selection_device.data_offer. One event is sent per + offered mime type. + + + + + + + + The source side of a wp_primary_selection_offer, it provides a way to + describe the offered data and respond to requests to transfer the + requested contents of the primary selection clipboard. + + + + + This request adds a mime type to the set of mime types advertised to + targets. Can be called several times to offer multiple types. + + + + + + + Destroy the primary selection source. + + + + + + Request for the current primary selection contents from the client. + Send the specified mime type over the passed file descriptor, then + close it. + + + + + + + + This primary selection source is no longer valid. The client should + clean up and destroy this primary selection source. + + + + \ No newline at end of file diff --git a/src/protocol/input-method-unstable-v2.xml b/src/protocol/input-method-unstable-v2.xml new file mode 100644 index 0000000..62be9d9 --- /dev/null +++ b/src/protocol/input-method-unstable-v2.xml @@ -0,0 +1,490 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows applications to act as input methods for compositors. + + An input method context is used to manage the state of the input method. + + Text strings are UTF-8 encoded, their indices and lengths are in bytes. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + An input method object allows for clients to compose text. + + The objects connects the client to a text input in an application, and + lets the client to serve as an input method for a seat. + + The zwp_input_method_v2 object can occupy two distinct states: active and + inactive. In the active state, the object is associated to and + communicates with a text input. In the inactive state, there is no + associated text input, and the only communication is with the compositor. + Initially, the input method is in the inactive state. + + Requests issued in the inactive state must be accepted by the compositor. + Because of the serial mechanism, and the state reset on activate event, + they will not have any effect on the state of the next text input. + + There must be no more than one input method object per seat. + + + + + Notification that a text input focused on this seat requested the input + method to be activated. + + This event serves the purpose of providing the compositor with an + active input method. + + This event resets all state associated with previous enable, disable, + surrounding_text, text_change_cause, and content_type events, as well + as the state associated with set_preedit_string, commit_string, and + delete_surrounding_text requests. In addition, it marks the + zwp_input_method_v2 object as active, and makes any existing + zwp_input_popup_surface_v2 objects visible. + + The surrounding_text, and content_type events must follow before the + next done event if the text input supports the respective + functionality. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Notification that no focused text input currently needs an active + input method on this seat. + + This event marks the zwp_input_method_v2 object as inactive. The + compositor must make all existing zwp_input_popup_surface_v2 objects + invisible until the next activate event. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Updates the surrounding plain text around the cursor, excluding the + preedit text. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. + + The argument text is a buffer containing the preedit string, and must + include the cursor position, and the complete selection. It should + contain additional characters before and after these. There is a + maximum length of wayland messages, so text can not be longer than 4000 + bytes. + + cursor is the byte offset of the cursor within the text buffer. + + anchor is the byte offset of the selection anchor within the text + buffer. If there is no selected text, anchor must be the same as + cursor. + + If this event does not arrive before the first done event, the input + method may assume that the text input does not support this + functionality and ignore following surrounding_text events. + + Values set with this event are double-buffered. They will get applied + and set to initial values on the next zwp_input_method_v2.done + event. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Tells the input method why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor position, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this event is double-buffered. It will get applied + and set to its initial value on the next zwp_input_method_v2.done + event. + + The initial value of cause is input_method. + + + + + + + Indicates the content type and hint for the current + zwp_input_method_v2 instance. + + Values set with this event are double-buffered. They will get applied + on the next zwp_input_method_v2.done event. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Atomically applies state changes recently sent to the client. + + The done event establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (content purpose, content hint, surrounding text, and + change cause) is conceptually double-buffered within an input method + context. + + Events modify the pending state, as opposed to the current state in use + by the input method. A done event atomically applies all pending state, + replacing the current state. After done, the new pending state is as + documented for each related request. + + Events must be applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + + + + + Send the commit string text for insertion to the application. + + Inserts a string at current cursor position (see commit event + sequence). The string to commit could be either just a single character + after a key press or the result of some composing. + + The argument text is a buffer containing the string to insert. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.commit request. + + The initial value of text is an empty string. + + + + + + + Send the pre-edit string text to the application text input. + + Place a new composing text (pre-edit) at the current cursor position. + Any previously set composing text must be removed. Any previously + existing selected text must be removed. The cursor is moved to a new + position within the preedit string. + + The argument text is a buffer containing the preedit string. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + The arguments cursor_begin and cursor_end are counted in bytes relative + to the beginning of the submitted string buffer. Cursor should be + hidden by the text input when both are equal to -1. + + cursor_begin indicates the beginning of the cursor. cursor_end + indicates the end of the cursor. It may be equal or different than + cursor_begin. + + Values set with this event are double-buffered. They must be applied on + the next zwp_input_method_v2.commit event. + + The initial value of text is an empty string. The initial value of + cursor_begin, and cursor_end are both 0. + + + + + + + + + Remove the surrounding text. + + before_length and after_length are the number of bytes before and after + the current cursor index (excluding the preedit text) to delete. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. In effect before_length is counted from the + beginning of preedit text, and after_length from its end (see commit + event sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_input_method_v2.commit request. + + The initial values of both before_length and after_length are 0. + + + + + + + + Apply state changes from commit_string, set_preedit_string and + delete_surrounding_text requests. + + The state relating to these events is double-buffered, and each one + modifies the pending state. This request replaces the current state + with the pending state. + + The connected text input is expected to proceed by evaluating the + changes in the following order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_input_method_v2 + object known to the client. The value of the serial argument must be + equal to the number of done events already issued by that object. When + the compositor receives a commit request with a serial different than + the number of past done events, it must proceed as normal, except it + should not change the current state of the zwp_input_method_v2 object. + + + + + + + Creates a new zwp_input_popup_surface_v2 object wrapping a given + surface. + + The surface gets assigned the "input_popup" role. If the surface + already has an assigned role, the compositor must issue a protocol + error. + + + + + + + + Allow an input method to receive hardware keyboard input and process + key events to generate text events (with pre-edit) over the wire. This + allows input methods which compose multiple key events for inputting + text like it is done for CJK languages. + + The compositor should send all keyboard events on the seat to the grab + holder via the returned wl_keyboard object. Nevertheless, the + compositor may decide not to forward any particular event. The + compositor must not further process any event after it has been + forwarded to the grab holder. + + Releasing the resulting wl_keyboard object releases the grab. + + + + + + + The input method ceased to be available. + + The compositor must issue this event as the only event on the object if + there was another input_method object associated with the same seat at + the time of its creation. + + The compositor must issue this request when the object is no longer + useable, e.g. due to seat removal. + + The input method context becomes inert and should be destroyed after + deactivation is handled. Any further requests and events except for the + destroy request must be ignored. + + + + + + Destroys the zwp_text_input_v2 object and any associated child + objects, i.e. zwp_input_popup_surface_v2 and + zwp_input_method_keyboard_grab_v2. + + + + + + + This interface marks a surface as a popup for interacting with an input + method. + + The compositor should place it near the active text input area. It must + be visible if and only if the input method is in the active state. + + The client must not destroy the underlying wl_surface while the + zwp_input_popup_surface_v2 object exists. + + + + + Notify about the position of the area of the text input expressed as a + rectangle in surface local coordinates. + + This is a hint to the input method telling it the relative position of + the text being entered. + + + + + + + + + + + + + + The zwp_input_method_keyboard_grab_v2 interface represents an exclusive + grab of the wl_keyboard interface associated with the seat. + + + + + This event provides a file descriptor to the client which can be + memory-mapped to provide a keyboard mapping description. + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. + + + + + + + + + + Notifies clients that the modifier and/or group state has changed, and + it should update its local state. + + + + + + + + + + + + + + + Informs the client about the keyboard's repeat rate and delay. + + This event is sent as soon as the zwp_input_method_keyboard_grab_v2 + object has been created, and is guaranteed to be received by the + client before any key press event. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + This event can be sent later on as well with a new value if necessary, + so clients should continue listening for the event past the creation + of zwp_input_method_keyboard_grab_v2. + + + + + + + + + The input method manager allows the client to become the input method on + a chosen seat. + + No more than one input method must be associated with any seat at any + given time. + + + + + Request a new input zwp_input_method_v2 object associated with a given + seat. + + + + + + + + Destroys the zwp_input_method_manager_v2 object. + + The zwp_input_method_v2 objects originating from it remain valid. + + + + diff --git a/src/protocol/pointer-constraints-unstable-v1.xml b/src/protocol/pointer-constraints-unstable-v1.xml new file mode 100644 index 0000000..4e67a13 --- /dev/null +++ b/src/protocol/pointer-constraints-unstable-v1.xml @@ -0,0 +1,339 @@ + + + + + Copyright © 2014 Jonas Ådahl + Copyright © 2015 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol specifies a set of interfaces used for adding constraints to + the motion of a pointer. Possible constraints include confining pointer + motions to a given region, or locking it to its current position. + + In order to constrain the pointer, a client must first bind the global + interface "wp_pointer_constraints" which, if a compositor supports pointer + constraints, is exposed by the registry. Using the bound global object, the + client uses the request that corresponds to the type of constraint it wants + to make. See wp_pointer_constraints for more details. + + Warning! The protocol described in this file is experimental and backward + incompatible changes may be made. Backward compatible changes may be added + together with the corresponding interface version bump. Backward + incompatible changes are done by bumping the version number in the protocol + and interface names and resetting the interface version. Once the protocol + is to be declared stable, the 'z' prefix and the version number in the + protocol and interface names are removed and the interface version number is + reset. + + + + + The global interface exposing pointer constraining functionality. It + exposes two requests: lock_pointer for locking the pointer to its + position, and confine_pointer for locking the pointer to a region. + + The lock_pointer and confine_pointer requests create the objects + wp_locked_pointer and wp_confined_pointer respectively, and the client can + use these objects to interact with the lock. + + For any surface, only one lock or confinement may be active across all + wl_pointer objects of the same seat. If a lock or confinement is requested + when another lock or confinement is active or requested on the same surface + and with any of the wl_pointer objects of the same seat, an + 'already_constrained' error will be raised. + + + + + These errors can be emitted in response to wp_pointer_constraints + requests. + + + + + + + These values represent different lifetime semantics. They are passed + as arguments to the factory requests to specify how the constraint + lifetimes should be managed. + + + + A oneshot pointer constraint will never reactivate once it has been + deactivated. See the corresponding deactivation event + (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) for + details. + + + + + A persistent pointer constraint may again reactivate once it has + been deactivated. See the corresponding deactivation event + (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) for + details. + + + + + + + Used by the client to notify the server that it will no longer use this + pointer constraints object. + + + + + + The lock_pointer request lets the client request to disable movements of + the virtual pointer (i.e. the cursor), effectively locking the pointer + to a position. This request may not take effect immediately; in the + future, when the compositor deems implementation-specific constraints + are satisfied, the pointer lock will be activated and the compositor + sends a locked event. + + The protocol provides no guarantee that the constraints are ever + satisfied, and does not require the compositor to send an error if the + constraints cannot ever be satisfied. It is thus possible to request a + lock that will never activate. + + There may not be another pointer constraint of any kind requested or + active on the surface for any of the wl_pointer objects of the seat of + the passed pointer when requesting a lock. If there is, an error will be + raised. See general pointer lock documentation for more details. + + The intersection of the region passed with this request and the input + region of the surface is used to determine where the pointer must be + in order for the lock to activate. It is up to the compositor whether to + warp the pointer or require some kind of user interaction for the lock + to activate. If the region is null the surface input region is used. + + A surface may receive pointer focus without the lock being activated. + + The request creates a new object wp_locked_pointer which is used to + interact with the lock as well as receive updates about its state. See + the the description of wp_locked_pointer for further information. + + Note that while a pointer is locked, the wl_pointer objects of the + corresponding seat will not emit any wl_pointer.motion events, but + relative motion events will still be emitted via wp_relative_pointer + objects of the same seat. wl_pointer.axis and wl_pointer.button events + are unaffected. + + + + + + + + + + + The confine_pointer request lets the client request to confine the + pointer cursor to a given region. This request may not take effect + immediately; in the future, when the compositor deems implementation- + specific constraints are satisfied, the pointer confinement will be + activated and the compositor sends a confined event. + + The intersection of the region passed with this request and the input + region of the surface is used to determine where the pointer must be + in order for the confinement to activate. It is up to the compositor + whether to warp the pointer or require some kind of user interaction for + the confinement to activate. If the region is null the surface input + region is used. + + The request will create a new object wp_confined_pointer which is used + to interact with the confinement as well as receive updates about its + state. See the the description of wp_confined_pointer for further + information. + + + + + + + + + + + + The wp_locked_pointer interface represents a locked pointer state. + + While the lock of this object is active, the wl_pointer objects of the + associated seat will not emit any wl_pointer.motion events. + + This object will send the event 'locked' when the lock is activated. + Whenever the lock is activated, it is guaranteed that the locked surface + will already have received pointer focus and that the pointer will be + within the region passed to the request creating this object. + + To unlock the pointer, send the destroy request. This will also destroy + the wp_locked_pointer object. + + If the compositor decides to unlock the pointer the unlocked event is + sent. See wp_locked_pointer.unlock for details. + + When unlocking, the compositor may warp the cursor position to the set + cursor position hint. If it does, it will not result in any relative + motion events emitted via wp_relative_pointer. + + If the surface the lock was requested on is destroyed and the lock is not + yet activated, the wp_locked_pointer object is now defunct and must be + destroyed. + + + + + Destroy the locked pointer object. If applicable, the compositor will + unlock the pointer. + + + + + + Set the cursor position hint relative to the top left corner of the + surface. + + If the client is drawing its own cursor, it should update the position + hint to the position of its own cursor. A compositor may use this + information to warp the pointer upon unlock in order to avoid pointer + jumps. + + The cursor position hint is double buffered. The new hint will only take + effect when the associated surface gets it pending state applied. See + wl_surface.commit for details. + + + + + + + + Set a new region used to lock the pointer. + + The new lock region is double-buffered. The new lock region will + only take effect when the associated surface gets its pending state + applied. See wl_surface.commit for details. + + For details about the lock region, see wp_locked_pointer. + + + + + + + Notification that the pointer lock of the seat's pointer is activated. + + + + + + Notification that the pointer lock of the seat's pointer is no longer + active. If this is a oneshot pointer lock (see + wp_pointer_constraints.lifetime) this object is now defunct and should + be destroyed. If this is a persistent pointer lock (see + wp_pointer_constraints.lifetime) this pointer lock may again + reactivate in the future. + + + + + + + The wp_confined_pointer interface represents a confined pointer state. + + This object will send the event 'confined' when the confinement is + activated. Whenever the confinement is activated, it is guaranteed that + the surface the pointer is confined to will already have received pointer + focus and that the pointer will be within the region passed to the request + creating this object. It is up to the compositor to decide whether this + requires some user interaction and if the pointer will warp to within the + passed region if outside. + + To unconfine the pointer, send the destroy request. This will also destroy + the wp_confined_pointer object. + + If the compositor decides to unconfine the pointer the unconfined event is + sent. The wp_confined_pointer object is at this point defunct and should + be destroyed. + + + + + Destroy the confined pointer object. If applicable, the compositor will + unconfine the pointer. + + + + + + Set a new region used to confine the pointer. + + The new confine region is double-buffered. The new confine region will + only take effect when the associated surface gets its pending state + applied. See wl_surface.commit for details. + + If the confinement is active when the new confinement region is applied + and the pointer ends up outside of newly applied region, the pointer may + warped to a position within the new confinement region. If warped, a + wl_pointer.motion event will be emitted, but no + wp_relative_pointer.relative_motion event. + + The compositor may also, instead of using the new region, unconfine the + pointer. + + For details about the confine region, see wp_confined_pointer. + + + + + + + Notification that the pointer confinement of the seat's pointer is + activated. + + + + + + Notification that the pointer confinement of the seat's pointer is no + longer active. If this is a oneshot pointer confinement (see + wp_pointer_constraints.lifetime) this object is now defunct and should + be destroyed. If this is a persistent pointer confinement (see + wp_pointer_constraints.lifetime) this pointer confinement may again + reactivate in the future. + + + + + diff --git a/src/protocol/primary-selection-unstable-v1.xml b/src/protocol/primary-selection-unstable-v1.xml new file mode 100644 index 0000000..e5a39e3 --- /dev/null +++ b/src/protocol/primary-selection-unstable-v1.xml @@ -0,0 +1,225 @@ + + + + Copyright © 2015, 2016 Red Hat + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol provides the ability to have a primary selection device to + match that of the X server. This primary selection is a shortcut to the + common clipboard selection, where text just needs to be selected in order + to allow copying it elsewhere. The de facto way to perform this action + is the middle mouse button, although it is not limited to this one. + + Clients wishing to honor primary selection should create a primary + selection source and set it as the selection through + wp_primary_selection_device.set_selection whenever the text selection + changes. In order to minimize calls in pointer-driven text selection, + it should happen only once after the operation finished. Similarly, + a NULL source should be set when text is unselected. + + wp_primary_selection_offer objects are first announced through the + wp_primary_selection_device.data_offer event. Immediately after this event, + the primary data offer will emit wp_primary_selection_offer.offer events + to let know of the mime types being offered. + + When the primary selection changes, the client with the keyboard focus + will receive wp_primary_selection_device.selection events. Only the client + with the keyboard focus will receive such events with a non-NULL + wp_primary_selection_offer. Across keyboard focus changes, previously + focused clients will receive wp_primary_selection_device.events with a + NULL wp_primary_selection_offer. + + In order to request the primary selection data, the client must pass + a recent serial pertaining to the press event that is triggering the + operation, if the compositor deems the serial valid and recent, the + wp_primary_selection_source.send event will happen in the other end + to let the transfer begin. The client owning the primary selection + should write the requested data, and close the file descriptor + immediately. + + If the primary selection owner client disappeared during the transfer, + the client reading the data will receive a + wp_primary_selection_device.selection event with a NULL + wp_primary_selection_offer, the client should take this as a hint + to finish the reads related to the no longer existing offer. + + The primary selection owner should be checking for errors during + writes, merely cancelling the ongoing transfer if any happened. + + + + + The primary selection device manager is a singleton global object that + provides access to the primary selection. It allows to create + wp_primary_selection_source objects, as well as retrieving the per-seat + wp_primary_selection_device objects. + + + + + Create a new primary selection source. + + + + + + + Create a new data device for a given seat. + + + + + + + + Destroy the primary selection device manager. + + + + + + + + Replaces the current selection. The previous owner of the primary + selection will receive a wp_primary_selection_source.cancelled event. + + To unset the selection, set the source to NULL. + + + + + + + + Introduces a new wp_primary_selection_offer object that may be used + to receive the current primary selection. Immediately following this + event, the new wp_primary_selection_offer object will send + wp_primary_selection_offer.offer events to describe the offered mime + types. + + + + + + + The wp_primary_selection_device.selection event is sent to notify the + client of a new primary selection. This event is sent after the + wp_primary_selection.data_offer event introducing this object, and after + the offer has announced its mimetypes through + wp_primary_selection_offer.offer. + + The data_offer is valid until a new offer or NULL is received + or until the client loses keyboard focus. The client must destroy the + previous selection data_offer, if any, upon receiving this event. + + + + + + + Destroy the primary selection device. + + + + + + + A wp_primary_selection_offer represents an offer to transfer the contents + of the primary selection clipboard to the client. Similar to + wl_data_offer, the offer also describes the mime types that the data can + be converted to and provides the mechanisms for transferring the data + directly to the client. + + + + + To transfer the contents of the primary selection clipboard, the client + issues this request and indicates the mime type that it wants to + receive. The transfer happens through the passed file descriptor + (typically created with the pipe system call). The source client writes + the data in the mime type representation requested and then closes the + file descriptor. + + The receiving client reads from the read end of the pipe until EOF and + closes its end, at which point the transfer is complete. + + + + + + + + Destroy the primary selection offer. + + + + + + Sent immediately after creating announcing the + wp_primary_selection_offer through + wp_primary_selection_device.data_offer. One event is sent per offered + mime type. + + + + + + + + The source side of a wp_primary_selection_offer, it provides a way to + describe the offered data and respond to requests to transfer the + requested contents of the primary selection clipboard. + + + + + This request adds a mime type to the set of mime types advertised to + targets. Can be called several times to offer multiple types. + + + + + + + Destroy the primary selection source. + + + + + + Request for the current primary selection contents from the client. + Send the specified mime type over the passed file descriptor, then + close it. + + + + + + + + This primary selection source is no longer valid. The client should + clean up and destroy this primary selection source. + + + + diff --git a/src/protocol/relative-pointer-unstable-v1.xml b/src/protocol/relative-pointer-unstable-v1.xml new file mode 100644 index 0000000..ca6f81d --- /dev/null +++ b/src/protocol/relative-pointer-unstable-v1.xml @@ -0,0 +1,136 @@ + + + + + Copyright © 2014 Jonas Ådahl + Copyright © 2015 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol specifies a set of interfaces used for making clients able to + receive relative pointer events not obstructed by barriers (such as the + monitor edge or other pointer barriers). + + To start receiving relative pointer events, a client must first bind the + global interface "wp_relative_pointer_manager" which, if a compositor + supports relative pointer motion events, is exposed by the registry. After + having created the relative pointer manager proxy object, the client uses + it to create the actual relative pointer object using the + "get_relative_pointer" request given a wl_pointer. The relative pointer + motion events will then, when applicable, be transmitted via the proxy of + the newly created relative pointer object. See the documentation of the + relative pointer interface for more details. + + Warning! The protocol described in this file is experimental and backward + incompatible changes may be made. Backward compatible changes may be added + together with the corresponding interface version bump. Backward + incompatible changes are done by bumping the version number in the protocol + and interface names and resetting the interface version. Once the protocol + is to be declared stable, the 'z' prefix and the version number in the + protocol and interface names are removed and the interface version number is + reset. + + + + + A global interface used for getting the relative pointer object for a + given pointer. + + + + + Used by the client to notify the server that it will no longer use this + relative pointer manager object. + + + + + + Create a relative pointer interface given a wl_pointer object. See the + wp_relative_pointer interface for more details. + + + + + + + + + A wp_relative_pointer object is an extension to the wl_pointer interface + used for emitting relative pointer events. It shares the same focus as + wl_pointer objects of the same seat and will only emit events when it has + focus. + + + + + + + + + Relative x/y pointer motion from the pointer of the seat associated with + this object. + + A relative motion is in the same dimension as regular wl_pointer motion + events, except they do not represent an absolute position. For example, + moving a pointer from (x, y) to (x', y') would have the equivalent + relative motion (x' - x, y' - y). If a pointer motion caused the + absolute pointer position to be clipped by for example the edge of the + monitor, the relative motion is unaffected by the clipping and will + represent the unclipped motion. + + This event also contains non-accelerated motion deltas. The + non-accelerated delta is, when applicable, the regular pointer motion + delta as it was before having applied motion acceleration and other + transformations such as normalization. + + Note that the non-accelerated delta does not represent 'raw' events as + they were read from some device. Pointer motion acceleration is device- + and configuration-specific and non-accelerated deltas and accelerated + deltas may have the same value on some devices. + + Relative motions are not coupled to wl_pointer.motion events, and can be + sent in combination with such events, but also independently. There may + also be scenarios where wl_pointer.motion is sent, but there is no + relative motion. The order of an absolute and relative motion event + originating from the same physical motion is not guaranteed. + + If the client needs button events or focus state, it can receive them + from a wl_pointer object of the same seat that the wp_relative_pointer + object is associated with. + + + + + + + + + + + diff --git a/src/protocol/text-input-unstable-v3.xml b/src/protocol/text-input-unstable-v3.xml new file mode 100644 index 0000000..d5f6322 --- /dev/null +++ b/src/protocol/text-input-unstable-v3.xml @@ -0,0 +1,452 @@ + + + + + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows compositors to act as input methods and to send text + to applications. A text input object is used to manage state of what are + typically text entry fields in the application. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + The zwp_text_input_v3 interface represents text input and input methods + associated with a seat. It provides enter/leave events to follow the + text input focus for a seat. + + Requests are used to enable/disable the text-input object and set + state information like surrounding and selected text or the content type. + The information about the entered text is sent to the text-input object + via the preedit_string and commit_string events. + + Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + must not point to middle bytes inside a code point: they must either + point to the first byte of a code point or to the end of the buffer. + Lengths must be measured between two valid indices. + + Focus moving throughout surfaces will result in the emission of + zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + surface must commit zwp_text_input_v3.enable and + zwp_text_input_v3.disable requests as the keyboard focus moves across + editable and non-editable elements of the UI. Those two requests are not + expected to be paired with each other, the compositor must be able to + handle consecutive series of the same request. + + State is sent by the state requests (set_surrounding_text, + set_content_type and set_cursor_rectangle) and a commit request. After an + enter event or disable request all state information is invalidated and + needs to be resent by the client. + + + + + Destroy the wp_text_input object. Also disables all surfaces enabled + through this wp_text_input object. + + + + + + Requests text input on the surface previously obtained from the enter + event. + + This request must be issued every time the active text input changes + to a new one, including within the current surface. Use + zwp_text_input_v3.disable when there is no longer any input focus on + the current surface. + + Clients must not enable more than one text input on the single seat + and should disable the current text input before enabling the new one. + At most one instance of text input may be in enabled state per instance, + Requests to enable the another text input when some text input is active + must be ignored by compositor. + + This request resets all state associated with previous enable, disable, + set_surrounding_text, set_text_change_cause, set_content_type, and + set_cursor_rectangle requests, as well as the state associated with + preedit_string, commit_string, and delete_surrounding_text events. + + The set_surrounding_text, set_content_type and set_cursor_rectangle + requests must follow if the text input supports the necessary + functionality. + + State set with this request is double-buffered. It will get applied on + the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The changes must be applied by the compositor after issuing a + zwp_text_input_v3.commit request. + + + + + + Explicitly disable text input on the current surface (typically when + there is no focus on any text entry inside the surface). + + State set with this request is double-buffered. It will get applied on + the next zwp_text_input_v3.commit request. + + + + + + Sets the surrounding plain text around the input, excluding the preedit + text. + + The client should notify the compositor of any changes in any of the + values carried with this request, including changes caused by handling + incoming text-input events as well as changes caused by other + mechanisms like keyboard typing. + + If the client is unaware of the text around the cursor, it should not + issue this request, to signify lack of support to the compositor. + + Text is UTF-8 encoded, and should include the cursor position, the + complete selection and additional characters before and after them. + There is a maximum length of wayland messages, so text can not be + longer than 4000 bytes. + + Cursor is the byte offset of the cursor within text buffer. + + Anchor is the byte offset of the selection anchor within text buffer. + If there is no selected text, anchor is the same as cursor. + + If any preedit text is present, it is replaced with a cursor for the + purpose of this event. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Reason for the change of surrounding text or cursor posision. + + + + + + + + Tells the compositor why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor posision, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this request is double-buffered. It must be applied + and reset to initial at the next zwp_text_input_v3.commit request. + + The initial value of cause is input_method. + + + + + + + Content hint is a bitmask to allow to modify the behavior of the text + input. + + + + + + + + + + + + + + + + + The content purpose allows to specify the primary purpose of a text + input. + + This allows an input method to show special purpose input panels with + extra characters or to disallow some characters. + + + + + + + + + + + + + + + + + + + + Sets the content purpose and content hint. While the purpose is the + basic purpose of an input field, the hint flags allow to modify some of + the behavior. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request. + Subsequent attempts to update them may have no effect. The values + remain valid until the next committed enable or disable request. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Marks an area around the cursor as a x, y, width, height rectangle in + surface local coordinates. + + Allows the compositor to put a window with word suggestions near the + cursor, without obstructing the text being input. + + If the client is unaware of the position of edited text, it should not + issue this request, to signify lack of support to the compositor. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The initial values describing a cursor rectangle are empty. That means + the text input does not support describing the cursor area. If the + empty values get applied, subsequent attempts to change them may have + no effect. + + + + + + + + + + Atomically applies state changes recently sent to the compositor. + + The commit request establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (enabled status, content purpose, content hint, + surrounding text and change cause, cursor rectangle) is conceptually + double-buffered within the context of a text input, i.e. between a + committed enable request and the following committed enable or disable + request. + + Protocol requests modify the pending state, as opposed to the current + state in use by the input method. A commit request atomically applies + all pending state, replacing the current state. After commit, the new + pending state is as documented for each related request. + + Requests are applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + The compositor must count the number of commit requests coming from + each zwp_text_input_v3 object and use the count as the serial in done + events. + + + + + + Notification that this seat's text-input focus is on a certain surface. + + If client has created multiple text input objects, compositor must send + this event to all of them. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. This event sets the current surface for the + text-input object. + + + + + + + Notification that this seat's text-input focus is no longer on a + certain surface. The client should reset any preedit string previously + set. + + The leave notification clears the current surface. It is sent before + the enter notification for the new focus. After leave event, compositor + must ignore requests from any text input instances until next enter + event. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. + + + + + + + Notify when a new composing text (pre-edit) should be set at the + current cursor position. Any previously set composing text must be + removed. Any previously existing selected text must be removed. + + The argument text contains the pre-edit string buffer. + + The parameters cursor_begin and cursor_end are counted in bytes + relative to the beginning of the submitted text buffer. Cursor should + be hidden when both are equal to -1. + + They could be represented by the client as a line if both values are + the same, or as a text highlight otherwise. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial value of text is an empty string, and cursor_begin, + cursor_end and cursor_hidden are all 0. + + + + + + + + + Notify when text should be inserted into the editor widget. The text to + commit could be either just a single character after a key press or the + result of some composing (pre-edit). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial value of text is an empty string. + + + + + + + Notify when the text around the current cursor position should be + deleted. + + Before_length and after_length are the number of bytes before and after + the current cursor index (excluding the selection) to delete. + + If a preedit text is present, in effect before_length is counted from + the beginning of it, and after_length from its end (see done event + sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial values of both before_length and after_length are 0. + + + + + + + + Instruct the application to apply changes to state requested by the + preedit_string, commit_string and delete_surrounding_text events. The + state relating to these events is double-buffered, and each one + modifies the pending state. This event replaces the current state with + the pending state. + + The application must proceed by evaluating the changes in the following + order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_text_input_v3 + object known to the compositor. The value of the serial argument must + be equal to the number of commit requests already issued on that object. + When the client receives a done event with a serial different than the + number of past commit requests, it must proceed as normal, except it + should not change the current state of the zwp_text_input_v3 object. + + + + + + + + A factory for text-input objects. This object is a global singleton. + + + + + Destroy the wp_text_input_manager object. + + + + + + Creates a new text-input object for a given seat. + + + + + + diff --git a/src/protocol/wayland.xml b/src/protocol/wayland.xml new file mode 100644 index 0000000..29b63be --- /dev/null +++ b/src/protocol/wayland.xml @@ -0,0 +1,2746 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice (including the + next paragraph) shall be included in all copies or substantial + portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + + + + The core global object. This is a special singleton object. It + is used for internal Wayland protocol features. + + + + + The sync request asks the server to emit the 'done' event + on the returned wl_callback object. Since requests are + handled in-order and events are delivered in-order, this can + be used as a barrier to ensure all previous requests and the + resulting events have been handled. + + The object returned by this request will be destroyed by the + compositor after the callback is fired and as such the client must not + attempt to use it after that point. + + The callback_data passed in the callback is the event serial. + + + + + + + This request creates a registry object that allows the client + to list and bind the global objects available from the + compositor. + + + + + + + The error event is sent out when a fatal (non-recoverable) + error has occurred. The object_id argument is the object + where the error occurred, most often in response to a request + to that object. The code identifies the error and is defined + by the object interface. As such, each interface defines its + own set of error codes. The message is a brief description + of the error, for (debugging) convenience. + + + + + + + + + These errors are global and can be emitted in response to any + server request. + + + + + + + + + This event is used internally by the object ID management + logic. When a client deletes an object, the server will send + this event to acknowledge that it has seen the delete request. + When the client receives this event, it will know that it can + safely reuse the object ID. + + + + + + + + The singleton global registry object. The server has a number of + global objects that are available to all clients. These objects + typically represent an actual object in the server (for example, + an input device) or they are singleton objects that provide + extension functionality. + + When a client creates a registry object, the registry object + will emit a global event for each global currently in the + registry. Globals come and go as a result of device or + monitor hotplugs, reconfiguration or other events, and the + registry will send out global and global_remove events to + keep the client up to date with the changes. To mark the end + of the initial burst of events, the client can use the + wl_display.sync request immediately after calling + wl_display.get_registry. + + A client can bind to a global object by using the bind + request. This creates a client-side handle that lets the object + emit events to the client and lets the client invoke requests on + the object. + + + + + Binds a new, client-created object to the server using the + specified name as the identifier. + + + + + + + + Notify the client of global objects. + + The event notifies the client that a global object with + the given name is now available, and it implements the + given version of the given interface. + + + + + + + + + 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. + + The object remains valid and requests to the object will be + ignored until the client destroys it, to avoid races between + the global going away and a client sending a request to it. + + + + + + + + Clients can handle the 'done' event to get notified when + the related request is done. + + + + + Notify the client when the related request is done. + + + + + + + + A compositor. This object is a singleton global. The + compositor is in charge of combining the contents of multiple + surfaces into one displayable output. + + + + + Ask the compositor to create a new surface. + + + + + + + Ask the compositor to create a new region. + + + + + + + + The wl_shm_pool object encapsulates a piece of memory shared + between the compositor and client. Through the wl_shm_pool + object, the client can allocate shared memory wl_buffer objects. + All objects created through the same pool share the same + underlying mapped memory. Reusing the mapped memory avoids the + setup/teardown overhead and is useful when interactively resizing + a surface or for many small buffers. + + + + + Create a wl_buffer object from the pool. + + The buffer is created offset bytes into the pool and has + width and height as specified. The stride argument specifies + the number of bytes from the beginning of one row to the beginning + of the next. The format is the pixel format of the buffer and + must be one of those advertised through the wl_shm.format event. + + A buffer will keep a reference to the pool it was created from + so it is valid to destroy the pool immediately after creating + a buffer from it. + + + + + + + + + + + + Destroy the shared memory pool. + + The mmapped memory will be released when all + buffers that have been created from this pool + are gone. + + + + + + This request will cause the server to remap the backing memory + for the pool from the file descriptor passed when the pool was + created, but using the new size. This request can only be + used to make the pool bigger. + + + + + + + + A singleton global object that provides support for shared + memory. + + Clients can create wl_shm_pool objects using the create_pool + request. + + At connection setup time, the wl_shm object emits one or more + format events to inform clients about the valid pixel formats + that can be used for buffers. + + + + + These errors can be emitted in response to wl_shm requests. + + + + + + + + + This describes the memory layout of an individual pixel. + + All renderers should support argb8888 and xrgb8888 but any other + formats are optional and may not be supported by the particular + renderer in use. + + The drm format codes match the macros defined in drm_fourcc.h. + The formats actually supported by the compositor will be + reported by the format event. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a new wl_shm_pool object. + + The pool can be used to create shared memory based buffer + objects. The server will mmap size bytes of the passed file + descriptor, to use as backing memory for the pool. + + + + + + + + + Informs the client about a valid pixel format that + can be used for buffers. Known formats include + argb8888 and xrgb8888. + + + + + + + + A buffer provides the content for a wl_surface. Buffers are + created through factory interfaces such as wl_drm, wl_shm or + similar. It has a width and a height and can be attached to a + wl_surface, but the mechanism by which a client provides and + updates the contents is defined by the buffer factory interface. + + + + + Destroy a buffer. If and how you need to release the backing + storage is defined by the buffer factory interface. + + For possible side-effects to a surface, see wl_surface.attach. + + + + + + Sent when this wl_buffer is no longer used by the compositor. + The client is now free to reuse or destroy this buffer and its + backing storage. + + If a client receives a release event before the frame callback + requested in the same wl_surface.commit that attaches this + wl_buffer to a surface, then the client is immediately free to + reuse the buffer and its backing storage, and does not need a + second buffer for the next surface content update. Typically + this is possible, when the compositor maintains a copy of the + wl_surface contents, e.g. as a GL texture. This is an important + optimization for GL(ES) compositors with wl_shm clients. + + + + + + + A wl_data_offer represents a piece of data offered for transfer + by another client (the source client). It is used by the + copy-and-paste and drag-and-drop mechanisms. The offer + describes the different mime types that the data can be + converted to and provides the mechanism for transferring the + data directly from the source client. + + + + + + + + + + + + Indicate that the client can accept the given mime type, or + NULL for not accepted. + + For objects of version 2 or older, this request is used by the + client to give feedback whether the client can receive the given + mime type, or NULL if none is accepted; the feedback does not + determine whether the drag-and-drop operation succeeds or not. + + For objects of version 3 or newer, this request determines the + final result of the drag-and-drop operation. If the end result + is that no mime types were accepted, the drag-and-drop operation + will be cancelled and the corresponding drag source will receive + wl_data_source.cancelled. Clients may still use this event in + conjunction with wl_data_source.action for feedback. + + + + + + + + To transfer the offered data, the client issues this request + and indicates the mime type it wants to receive. The transfer + happens through the passed file descriptor (typically created + with the pipe system call). The source client writes the data + in the mime type representation requested and then closes the + file descriptor. + + The receiving client reads from the read end of the pipe until + EOF and then closes its end, at which point the transfer is + complete. + + This request may happen multiple times for different mime types, + both before and after wl_data_device.drop. Drag-and-drop destination + clients may preemptively fetch data or examine it more closely to + determine acceptance. + + + + + + + + Destroy the data offer. + + + + + + Sent immediately after creating the wl_data_offer object. One + event per offered mime type. + + + + + + + + + Notifies the compositor that the drag destination successfully + finished the drag-and-drop operation. + + Upon receiving this request, the compositor will emit + wl_data_source.dnd_finished on the drag source client. + + It is a client error to perform other requests than + wl_data_offer.destroy after this one. It is also an error to perform + this request after a NULL mime type has been set in + wl_data_offer.accept or no action was received through + wl_data_offer.action. + + + + + + Sets the actions that the destination side client supports for + this operation. This request may trigger the emission of + wl_data_source.action and wl_data_offer.action events if the compositor + needs to change the selected action. + + This request can be called multiple times throughout the + drag-and-drop operation, typically in response to wl_data_device.enter + or wl_data_device.motion events. + + This request determines the final result of the drag-and-drop + operation. If the end result is that no action is accepted, + the drag source will receive wl_drag_source.cancelled. + + The dnd_actions argument must contain only values expressed in the + wl_data_device_manager.dnd_actions enum, and the preferred_action + argument must only contain one of those values set, otherwise it + will result in a protocol error. + + While managing an "ask" action, the destination drag-and-drop client + may perform further wl_data_offer.receive requests, and is expected + to perform one last wl_data_offer.set_actions request with a preferred + action other than "ask" (and optionally wl_data_offer.accept) before + requesting wl_data_offer.finish, in order to convey the action selected + by the user. If the preferred action is not in the + wl_data_offer.source_actions mask, an error will be raised. + + If the "ask" action is dismissed (e.g. user cancellation), the client + is expected to perform wl_data_offer.destroy right away. + + This request can only be made on drag-and-drop offers, a protocol error + will be raised otherwise. + + + + + + + + This event indicates the actions offered by the data source. It + will be sent right after wl_data_device.enter, or anytime the source + side changes its offered actions through wl_data_source.set_actions. + + + + + + + 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. + + This event can be emitted multiple times during the drag-and-drop + operation in response to destination side action changes through + wl_data_offer.set_actions. + + This event will no longer be emitted after wl_data_device.drop + happened on the drag-and-drop destination, the client must + honor the last action received, or the last preferred one set + through wl_data_offer.set_actions when handling an "ask" action. + + Compositors may also change the selected action on the fly, mainly + in response to keyboard modifier changes during the drag-and-drop + operation. + + The most recent action received is always the valid one. Prior to + receiving wl_data_device.drop, the chosen action may change (e.g. + due to keyboard modifiers being pressed). At the time of receiving + wl_data_device.drop the drag-and-drop destination must honor the + last action received. + + Action changes may still happen after wl_data_device.drop, + especially on "ask" actions, where the drag-and-drop destination + may choose another action afterwards. Action changes happening + at this stage are always the result of inter-client negotiation, the + compositor shall no longer be able to induce a different action. + + Upon "ask" actions, it is expected that the drag-and-drop destination + may potentially choose a different action and/or mime type, + based on wl_data_offer.source_actions and finally chosen by the + user (e.g. popping up a menu with the available options). The + final wl_data_offer.set_actions and wl_data_offer.accept requests + must happen before the call to wl_data_offer.finish. + + + + + + + + The wl_data_source object is the source side of a wl_data_offer. + It is created by the source client in a data transfer and + provides a way to describe the offered data and a way to respond + to requests to transfer the data. + + + + + + + + + + This request adds a mime type to the set of mime types + advertised to targets. Can be called several times to offer + multiple types. + + + + + + + Destroy the data source. + + + + + + Sent when a target accepts pointer_focus or motion events. If + a target does not accept any of the offered types, type is NULL. + + Used for feedback during drag-and-drop. + + + + + + + Request for data from the client. Send the data as the + specified mime type over the passed file descriptor, then + close it. + + + + + + + + This data source is no longer valid. There are several reasons why + this could happen: + + - The data source has been replaced by another data source. + - The drag-and-drop operation was performed, but the drop destination + did not accept any of the mime types offered through + wl_data_source.target. + - The drag-and-drop operation was performed, but the drop destination + did not select any of the actions present in the mask offered through + wl_data_source.action. + - The drag-and-drop operation was performed but didn't happen over a + surface. + - The compositor cancelled the drag-and-drop operation (e.g. compositor + dependent timeouts to avoid stale drag-and-drop transfers). + + The client should clean up and destroy this data source. + + For objects of version 2 or older, wl_data_source.cancelled will + only be emitted if the data source was replaced by another data + source. + + + + + + + + Sets the actions that the source side client supports for this + operation. This request may trigger wl_data_source.action and + wl_data_offer.action events if the compositor needs to change the + selected action. + + The dnd_actions argument must contain only values expressed in the + wl_data_device_manager.dnd_actions enum, otherwise it will result + in a protocol error. + + This request must be made once only, and can only be made on sources + used in drag-and-drop, so it must be performed before + wl_data_device.start_drag. Attempting to use the source other than + for drag-and-drop will raise a protocol error. + + + + + + + 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. + + However, this event might however not be received if the compositor + cancelled the drag-and-drop operation before this event could happen. + + Note that the data_source may still be used in the future and should + not be destroyed here. + + + + + + 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. + + If the action used to perform the operation was "move", the + source can now delete the transferred data. + + + + + + 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. + + This event can be emitted multiple times during the drag-and-drop + operation, mainly in response to destination side changes through + wl_data_offer.set_actions, and as the data device enters/leaves + surfaces. + + It is only possible to receive this event after + wl_data_source.dnd_drop_performed if the drag-and-drop operation + ended in an "ask" action, in which case the final wl_data_source.action + event will happen immediately before wl_data_source.dnd_finished. + + Compositors may also change the selected action on the fly, mainly + in response to keyboard modifier changes during the drag-and-drop + operation. + + The most recent action received is always the valid one. The chosen + action may change alongside negotiation (e.g. an "ask" action can turn + into a "move" operation), so the effects of the final action must + always be applied in wl_data_offer.dnd_finished. + + Clients can trigger cursor surface changes from this point, so + they reflect the current action. + + + + + + + + There is one wl_data_device per seat which can be obtained + from the global wl_data_device_manager singleton. + + A wl_data_device provides access to inter-client data transfer + mechanisms such as copy-and-paste and drag-and-drop. + + + + + + + + + This request asks the compositor to start a drag-and-drop + operation on behalf of the client. + + The source argument is the data source that provides the data + for the eventual data transfer. If source is NULL, enter, leave + and motion events are sent only to the client that initiated the + drag and the client is expected to handle the data passing + internally. + + The origin surface is the surface where the drag originates and + the client must have an active implicit grab that matches the + serial. + + The icon surface is an optional (can be NULL) surface that + provides an icon to be moved around with the cursor. Initially, + the top-left corner of the icon surface is placed at the cursor + hotspot, but subsequent wl_surface.attach request can move the + relative position. Attach requests must be confirmed with + wl_surface.commit as usual. The icon surface is given the role of + a drag-and-drop icon. If the icon surface already has another role, + it raises a protocol error. + + The current and pending input regions of the icon wl_surface are + cleared, and wl_surface.set_input_region is ignored until the + wl_surface is no longer used as the icon surface. When the use + as an icon ends, the current and pending input regions become + undefined, and the wl_surface is unmapped. + + + + + + + + + + This request asks the compositor to set the selection + to the data from the source on behalf of the client. + + To unset the selection, set the source to NULL. + + + + + + + + The data_offer event introduces a new wl_data_offer object, + which will subsequently be used in either the + data_device.enter event (for drag-and-drop) or the + data_device.selection event (for selections). Immediately + following the data_device_data_offer event, the new data_offer + object will send out data_offer.offer events to describe the + mime types it offers. + + + + + + + This event is sent when an active drag-and-drop pointer enters + a surface owned by the client. The position of the pointer at + enter time is provided by the x and y arguments, in surface-local + coordinates. + + + + + + + + + + + This event is sent when the drag-and-drop pointer leaves the + surface and the session ends. The client must destroy the + wl_data_offer introduced at enter time at this point. + + + + + + This event is sent when the drag-and-drop pointer moves within + the currently focused surface. The new position of the pointer + is provided by the x and y arguments, in surface-local + coordinates. + + + + + + + + + The event is sent when a drag-and-drop operation is ended + because the implicit grab is removed. + + The drag-and-drop destination is expected to honor the last action + received through wl_data_offer.action, if the resulting action is + "copy" or "move", the destination can still perform + wl_data_offer.receive requests, and is expected to end all + transfers with a wl_data_offer.finish request. + + If the resulting action is "ask", the action will not be considered + final. The drag-and-drop destination is expected to perform one last + wl_data_offer.set_actions request, or wl_data_offer.destroy in order + to cancel the operation. + + + + + + The selection event is sent out to notify the client of a new + wl_data_offer for the selection for this device. The + data_device.data_offer and the data_offer.offer events are + sent out immediately before this event to introduce the data + offer object. The selection event is sent to a client + immediately before receiving keyboard focus and when a new + selection is set while the client has keyboard focus. The + data_offer is valid until a new data_offer or NULL is received + or until the client loses keyboard focus. The client must + destroy the previous selection data_offer, if any, upon receiving + this event. + + + + + + + + + This request destroys the data device. + + + + + + + The wl_data_device_manager is a singleton global object that + provides access to inter-client data transfer mechanisms such as + copy-and-paste and drag-and-drop. These mechanisms are tied to + a wl_seat and this interface lets a client get a wl_data_device + corresponding to a wl_seat. + + Depending on the version bound, the objects created from the bound + wl_data_device_manager object will have different requirements for + functioning properly. See wl_data_source.set_actions, + wl_data_offer.accept and wl_data_offer.finish for details. + + + + + Create a new data source. + + + + + + + Create a new data device for a given seat. + + + + + + + + + + This is a bitmask of the available/preferred actions in a + drag-and-drop operation. + + In the compositor, the selected action is a result of matching the + actions offered by the source and destination sides. "action" events + with a "none" action will be sent to both source and destination if + there is no match. All further checks will effectively happen on + (source actions ∩ destination actions). + + In addition, compositors may also pick different actions in + reaction to key modifiers being pressed. One common design that + is used in major toolkits (and the behavior recommended for + compositors) is: + + - If no modifiers are pressed, the first match (in bit order) + will be used. + - Pressing Shift selects "move", if enabled in the mask. + - Pressing Control selects "copy", if enabled in the mask. + + Behavior beyond that is considered implementation-dependent. + Compositors may for example bind other modifiers (like Alt/Meta) + or drags initiated with other buttons than BTN_LEFT to specific + actions (e.g. "ask"). + + + + + + + + + + + This interface is implemented by servers that provide + desktop-style user interfaces. + + It allows clients to associate a wl_shell_surface with + a basic surface. + + + + + + + + + Create a shell surface for an existing surface. This gives + the wl_surface the role of a shell surface. If the wl_surface + already has another role, it raises a protocol error. + + Only one shell surface can be associated with a given surface. + + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides requests to treat surfaces like toplevel, fullscreen + or popup windows, move, resize or maximize them, associate + metadata like title and class, etc. + + On the server side the object is automatically destroyed when + the related wl_surface is destroyed. On the client side, + wl_shell_surface_destroy() must be called before destroying + the wl_surface object. + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. + + + + + + + Start a pointer-driven move of the surface. + + This request must be used in response to a button press event. + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized). + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. The server may + use this information to adapt its behavior, e.g. choose + an appropriate cursor image. + + + + + + + + + + + + + + + Start a pointer-driven resizing of the surface. + + This request must be used in response to a button press event. + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + + + + + + + + Map the surface as a toplevel surface. + + A toplevel surface is not fullscreen, maximized or transient. + + + + + + These flags specify details of the expected behaviour + of transient surfaces. Used in the set_transient request. + + + + + + + Map the surface relative to an existing surface. + + The x and y arguments specify the location of the upper left + corner of the surface relative to the upper left corner of the + parent surface, in surface-local coordinates. + + The flags argument controls details of the transient behaviour. + + + + + + + + + + Hints to indicate to the compositor how to deal with a conflict + between the dimensions of the surface and the dimensions of the + output. The compositor is free to ignore this parameter. + + + + + + + + + + Map the surface as a fullscreen surface. + + If an output parameter is given then the surface will be made + fullscreen on that output. If the client does not specify the + output then the compositor will apply its policy - usually + choosing the output on which the surface has the biggest surface + area. + + The client may specify a method to resolve a size conflict + between the output size and the surface size - this is provided + through the method parameter. + + The framerate parameter is used only when the method is set + to "driver", to indicate the preferred framerate. A value of 0 + indicates that the client does not care about framerate. The + framerate is specified in mHz, that is framerate of 60000 is 60Hz. + + A method of "scale" or "driver" implies a scaling operation of + the surface, either via a direct scaling operation or a change of + the output mode. This will override any kind of output scaling, so + that mapping a surface with a buffer size equal to the mode can + fill the screen independent of buffer_scale. + + A method of "fill" means we don't scale up the buffer, however + any output scale is applied. This means that you may run into + an edge case where the application maps a buffer with the same + size of the output mode but buffer_scale 1 (thus making a + surface larger than the output). In this case it is allowed to + downscale the results to fit the screen. + + The compositor must reply to this request with a configure event + with the dimensions for the output on which the surface will + be made fullscreen. + + + + + + + + + Map the surface as a popup. + + A popup surface is a transient surface with an added pointer + grab. + + An existing implicit grab will be changed to owner-events mode, + and the popup grab will continue after the implicit grab ends + (i.e. releasing the mouse button does not cause the popup to + be unmapped). + + The popup grab continues until the window is destroyed or a + mouse button is pressed in any other client's window. A click + in any of the client's surfaces is reported as normal, however, + clicks in other clients' surfaces will be discarded and trigger + the callback. + + The x and y arguments specify the location of the upper left + corner of the surface relative to the upper left corner of the + parent surface, in surface-local coordinates. + + + + + + + + + + + + Map the surface as a maximized surface. + + If an output parameter is given then the surface will be + maximized on that output. If the client does not specify the + output then the compositor will apply its policy - usually + choosing the output on which the surface has the biggest surface + area. + + The compositor will reply with a configure event telling + the expected new surface size. The operation is completed + on the next buffer attach to this surface. + + A maximized surface typically fills the entire output it is + bound to, except for desktop elements such as panels. This is + the main difference between a maximized shell surface and a + fullscreen shell surface. + + The details depend on the compositor implementation. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set a class for the surface. + + The surface class identifies the general class of applications + to which the surface belongs. A common convention is to use the + file name (or the full path if it is a non-standard location) of + the application's .desktop file as the class. + + + + + + + Ping a client to check if it is receiving events and sending + requests. A client is expected to reply with a pong request. + + + + + + + The configure event asks the client to resize its surface. + + The size is a hint, in the sense that the client is free to + ignore it if it doesn't resize, pick a smaller size (to + satisfy aspect ratio or resize in steps of NxM pixels). + + The edges parameter provides a hint about how the surface + was resized. The client may use this information to decide + how to adjust its content to the new size (e.g. a scrolling + area might adjust its content position to leave the viewable + content unmoved). + + The client is free to dismiss all but the last configure + event it received. + + The width and height arguments specify the size of the window + in surface-local coordinates. + + + + + + + + + The popup_done event is sent out when a popup grab is broken, + that is, when the user clicks a surface that doesn't belong + to the client owning the popup surface. + + + + + + + A surface is a rectangular area that is displayed on the screen. + It has a location, size and pixel contents. + + The size of a surface (and relative positions on it) is described + in surface-local coordinates, which may differ from the buffer + coordinates of the pixel content, in case a buffer_transform + or a buffer_scale is used. + + A surface without a "role" is fairly useless: a compositor does + not know where, when or how to present it. The role is the + purpose of a wl_surface. Examples of roles are a cursor for a + pointer (as set by wl_pointer.set_cursor), a drag icon + (wl_data_device.start_drag), a sub-surface + (wl_subcompositor.get_subsurface), and a window as defined by a + shell protocol (e.g. wl_shell.get_shell_surface). + + A surface can have only one role at a time. Initially a + wl_surface does not have a role. Once a wl_surface is given a + role, it is set permanently for the whole lifetime of the + wl_surface object. Giving the current role again is allowed, + unless explicitly forbidden by the relevant interface + specification. + + Surface roles are given by requests in other interfaces such as + wl_pointer.set_cursor. The request should explicitly mention + that this request gives a role to a wl_surface. Often, this + request also creates a new protocol object that represents the + role and adds additional functionality to wl_surface. When a + client wants to destroy a wl_surface, they must destroy this 'role + object' before the wl_surface. + + Destroying the role object does not remove the role from the + wl_surface, but it may stop the wl_surface from "playing the role". + For instance, if a wl_subsurface object is destroyed, the wl_surface + it was created for will be unmapped and forget its position and + z-order. It is allowed to create a wl_subsurface for the same + wl_surface again, but it is not allowed to use the wl_surface as + a cursor (cursor is a different role than sub-surface, and role + switching is not allowed). + + + + + These errors can be emitted in response to wl_surface requests. + + + + + + + + Deletes the surface and invalidates its object ID. + + + + + + Set a buffer as the content of this surface. + + The new size of the surface is calculated based on the buffer + size transformed by the inverse buffer_transform and the + inverse buffer_scale. This means that the supplied buffer + must be an integer multiple of the buffer_scale. + + The x and y arguments specify the location of the new pending + buffer's upper left corner, relative to the current buffer's upper + left corner, in surface-local coordinates. In other words, the + x and y, combined with the new surface size define in which + directions the surface's size changes. + + Surface contents are double-buffered state, see wl_surface.commit. + + The initial surface contents are void; there is no content. + wl_surface.attach assigns the given wl_buffer as the pending + wl_buffer. wl_surface.commit makes the pending wl_buffer the new + surface contents, and the size of the surface becomes the size + calculated from the wl_buffer, as described above. After commit, + there is no pending buffer until the next attach. + + Committing a pending wl_buffer allows the compositor to read the + pixels in the wl_buffer. The compositor may access the pixels at + any time after the wl_surface.commit request. When the compositor + will not access the pixels anymore, it will send the + wl_buffer.release event. Only after receiving wl_buffer.release, + the client may reuse the wl_buffer. A wl_buffer that has been + attached and then replaced by another attach instead of committed + will not receive a release event, and is not used by the + compositor. + + Destroying the wl_buffer after wl_buffer.release does not change + the surface contents. However, if the client destroys the + wl_buffer before receiving the wl_buffer.release event, the surface + contents become undefined immediately. + + If wl_surface.attach is sent with a NULL wl_buffer, the + following wl_surface.commit will remove the surface content. + + + + + + + + + This request is used to describe the regions where the pending + buffer is different from the current surface contents, and where + the surface therefore needs to be repainted. The compositor + ignores the parts of the damage that fall outside of the surface. + + Damage is double-buffered state, see wl_surface.commit. + + The damage rectangle is specified in surface-local coordinates, + where x and y specify the upper left corner of the damage rectangle. + + The initial value for pending damage is empty: no damage. + wl_surface.damage adds pending damage: the new pending damage + is the union of old pending damage and the given rectangle. + + wl_surface.commit assigns pending damage as the current damage, + and clears pending damage. The server will clear the current + damage as it repaints the surface. + + Alternatively, damage can be posted with wl_surface.damage_buffer + which uses buffer coordinates instead of surface coordinates, + and is probably the preferred and intuitive way of doing this. + + + + + + + + + + Request a notification when it is a good time to start drawing a new + frame, by creating a frame callback. This is useful for throttling + redrawing operations, and driving animations. + + When a client is animating on a wl_surface, it can use the 'frame' + request to get notified when it is a good time to draw and commit the + next frame of animation. If the client commits an update earlier than + that, it is likely that some updates will not make it to the display, + and the client is wasting resources by drawing too often. + + The frame request will take effect on the next wl_surface.commit. + The notification will only be posted for one frame unless + requested again. For a wl_surface, the notifications are posted in + the order the frame requests were committed. + + The server must send the notifications so that a client + will not send excessive updates, while still allowing + the highest possible update rate for clients that wait for the reply + before drawing again. The server should give some time for the client + to draw and commit after sending the frame callback events to let it + hit the next output refresh. + + A server should avoid signaling the frame callbacks if the + surface is not visible in any way, e.g. the surface is off-screen, + or completely obscured by other opaque surfaces. + + The object returned by this request will be destroyed by the + compositor after the callback is fired and as such the client must not + attempt to use it after that point. + + The callback_data passed in the callback is the current time, in + milliseconds, with an undefined base. + + + + + + + This request sets the region of the surface that contains + opaque content. + + The opaque region is an optimization hint for the compositor + that lets it optimize the redrawing of content behind opaque + regions. Setting an opaque region is not required for correct + behaviour, but marking transparent content as opaque will result + in repaint artifacts. + + The opaque region is specified in surface-local coordinates. + + The compositor ignores the parts of the opaque region that fall + outside of the surface. + + Opaque region is double-buffered state, see wl_surface.commit. + + wl_surface.set_opaque_region changes the pending opaque region. + wl_surface.commit copies the pending region to the current region. + Otherwise, the pending and current regions are never changed. + + The initial value for an opaque region is empty. Setting the pending + opaque region has copy semantics, and the wl_region object can be + destroyed immediately. A NULL wl_region causes the pending opaque + region to be set to empty. + + + + + + + This request sets the region of the surface that can receive + pointer and touch events. + + Input events happening outside of this region will try the next + surface in the server surface stack. The compositor ignores the + parts of the input region that fall outside of the surface. + + The input region is specified in surface-local coordinates. + + Input region is double-buffered state, see wl_surface.commit. + + wl_surface.set_input_region changes the pending input region. + wl_surface.commit copies the pending region to the current region. + Otherwise the pending and current regions are never changed, + except cursor and icon surfaces are special cases, see + wl_pointer.set_cursor and wl_data_device.start_drag. + + The initial value for an input region is infinite. That means the + whole surface will accept input. Setting the pending input region + has copy semantics, and the wl_region object can be destroyed + immediately. A NULL wl_region causes the input region to be set + to infinite. + + + + + + + Surface state (input, opaque, and damage regions, attached buffers, + etc.) is double-buffered. Protocol requests modify the pending state, + as opposed to the current state in use by the compositor. A commit + request atomically applies all pending state, replacing the current + state. After commit, the new pending state is as documented for each + related request. + + On commit, a pending wl_buffer is applied first, and all other state + second. This means that all coordinates in double-buffered state are + relative to the new wl_buffer coming into use, except for + wl_surface.attach itself. If there is no pending wl_buffer, the + coordinates are relative to the current surface contents. + + All requests that need a commit to become effective are documented + to affect double-buffered state. + + Other interfaces may add further double-buffered surface state. + + + + + + This is emitted whenever a surface's creation, movement, or resizing + results in some part of it being within the scanout region of an + output. + + Note that a surface may be overlapping with zero or more outputs. + + + + + + + This is emitted whenever a surface's creation, movement, or resizing + results in it no longer having any part of it within the scanout region + of an output. + + + + + + + + + This request sets an optional transformation on how the compositor + interprets the contents of the buffer attached to the surface. The + accepted values for the transform parameter are the values for + wl_output.transform. + + Buffer transform is double-buffered state, see wl_surface.commit. + + A newly created surface has its buffer transformation set to normal. + + wl_surface.set_buffer_transform changes the pending buffer + transformation. wl_surface.commit copies the pending buffer + transformation to the current one. Otherwise, the pending and current + values are never changed. + + The purpose of this request is to allow clients to render content + according to the output transform, thus permitting the compositor to + use certain optimizations even if the display is rotated. Using + hardware overlays and scanning out a client buffer for fullscreen + surfaces are examples of such optimizations. Those optimizations are + highly dependent on the compositor implementation, so the use of this + request should be considered on a case-by-case basis. + + Note that if the transform value includes 90 or 270 degree rotation, + the width of the buffer will become the surface height and the height + of the buffer will become the surface width. + + If transform is not one of the values from the + wl_output.transform enum the invalid_transform protocol error + is raised. + + + + + + + + + This request sets an optional scaling factor on how the compositor + interprets the contents of the buffer attached to the window. + + Buffer scale is double-buffered state, see wl_surface.commit. + + A newly created surface has its buffer scale set to 1. + + wl_surface.set_buffer_scale changes the pending buffer scale. + wl_surface.commit copies the pending buffer scale to the current one. + Otherwise, the pending and current values are never changed. + + The purpose of this request is to allow clients to supply higher + resolution buffer data for use on high resolution outputs. It is + intended that you pick the same buffer scale as the scale of the + output that the surface is displayed on. This means the compositor + can avoid scaling when rendering the surface on that output. + + Note that if the scale is larger than 1, then you have to attach + a buffer that is larger (by a factor of scale in each dimension) + than the desired surface size. + + If scale is not positive the invalid_scale protocol error is + raised. + + + + + + + + This request is used to describe the regions where the pending + buffer is different from the current surface contents, and where + the surface therefore needs to be repainted. The compositor + ignores the parts of the damage that fall outside of the surface. + + Damage is double-buffered state, see wl_surface.commit. + + The damage rectangle is specified in buffer coordinates, + where x and y specify the upper left corner of the damage rectangle. + + The initial value for pending damage is empty: no damage. + wl_surface.damage_buffer adds pending damage: the new pending + damage is the union of old pending damage and the given rectangle. + + wl_surface.commit assigns pending damage as the current damage, + and clears pending damage. The server will clear the current + damage as it repaints the surface. + + This request differs from wl_surface.damage in only one way - it + takes damage in buffer coordinates instead of surface-local + coordinates. While this generally is more intuitive than surface + coordinates, it is especially desirable when using wp_viewport + or when a drawing library (like EGL) is unaware of buffer scale + and buffer transform. + + Note: Because buffer transformation changes and damage requests may + be interleaved in the protocol stream, it is impossible to determine + the actual mapping between surface and buffer damage until + wl_surface.commit time. Therefore, compositors wishing to take both + kinds of damage into account will have to accumulate damage from the + two requests separately and only transform from one to the other + after receiving the wl_surface.commit. + + + + + + + + + + + A seat is a group of keyboards, pointer and touch devices. This + object is published as a global during start up, or when such a + device is hot plugged. A seat typically has a pointer and + maintains a keyboard focus and a pointer focus. + + + + + This is a bitmask of capabilities this seat has; if a member is + set, then it is present on the seat. + + + + + + + + + This is emitted whenever a seat gains or loses the pointer, + keyboard or touch capabilities. The argument is a capability + enum containing the complete set of capabilities this seat has. + + When the pointer capability is added, a client may create a + wl_pointer object using the wl_seat.get_pointer request. This object + will receive pointer events until the capability is removed in the + future. + + When the pointer capability is removed, a client should destroy the + wl_pointer objects associated with the seat where the capability was + removed, using the wl_pointer.release request. No further pointer + events will be received on these objects. + + In some compositors, if a seat regains the pointer capability and a + client has a previously obtained wl_pointer object of version 4 or + less, that object may start sending pointer events again. This + behavior is considered a misinterpretation of the intended behavior + and must not be relied upon by the client. wl_pointer objects of + version 5 or later must not send events if created before the most + recent event notifying the client of an added pointer capability. + + The above behavior also applies to wl_keyboard and wl_touch with the + keyboard and touch capabilities, respectively. + + + + + + + The ID provided will be initialized to the wl_pointer interface + for this seat. + + This request only takes effect if the seat has the pointer + capability, or has had the pointer capability in the past. + It is a protocol violation to issue this request on a seat that has + never had the pointer capability. + + + + + + + The ID provided will be initialized to the wl_keyboard interface + for this seat. + + This request only takes effect if the seat has the keyboard + capability, or has had the keyboard capability in the past. + It is a protocol violation to issue this request on a seat that has + never had the keyboard capability. + + + + + + + The ID provided will be initialized to the wl_touch interface + for this seat. + + This request only takes effect if the seat has the touch + capability, or has had the touch capability in the past. + It is a protocol violation to issue this request on a seat that has + never had the touch capability. + + + + + + + + + In a multiseat configuration this can be used by the client to help + identify which physical devices the seat represents. Based on + the seat configuration used by the compositor. + + + + + + + + + Using this request a client can tell the server that it is not going to + use the seat object anymore. + + + + + + + + The wl_pointer interface represents one or more input devices, + such as mice, which control the pointer location and pointer_focus + of a seat. + + The wl_pointer interface generates motion, enter and leave + events for the surfaces that the pointer is located over, + and button and axis events for button presses, button releases + and scrolling. + + + + + + + + + Set the pointer surface, i.e., the surface that contains the + pointer image (cursor). This request gives the surface the role + of a cursor. If the surface already has another role, it raises + a protocol error. + + The cursor actually changes only if the pointer + focus for this device is one of the requesting client's surfaces + or the surface parameter is the current pointer surface. If + there was a previous surface set with this request it is + replaced. If surface is NULL, the pointer image is hidden. + + The parameters hotspot_x and hotspot_y define the position of + the pointer surface relative to the pointer location. Its + top-left corner is always at (x, y) - (hotspot_x, hotspot_y), + where (x, y) are the coordinates of the pointer location, in + surface-local coordinates. + + On surface.attach requests to the pointer surface, hotspot_x + and hotspot_y are decremented by the x and y parameters + passed to the request. Attach must be confirmed by + wl_surface.commit as usual. + + The hotspot can also be updated by passing the currently set + pointer surface to this request with new values for hotspot_x + and hotspot_y. + + The current and pending input regions of the wl_surface are + cleared, and wl_surface.set_input_region is ignored until the + wl_surface is no longer used as the cursor. When the use as a + cursor ends, the current and pending input regions become + undefined, and the wl_surface is unmapped. + + + + + + + + + + Notification that this seat's pointer is focused on a certain + surface. + + When a seat's focus enters a surface, the pointer image + is undefined and a client should respond to this event by setting + an appropriate pointer image with the set_cursor request. + + + + + + + + + + Notification that this seat's pointer is no longer focused on + a certain surface. + + The leave notification is sent before the enter notification + for the new focus. + + + + + + + + Notification of pointer location change. The arguments + surface_x and surface_y are the location relative to the + focused surface. + + + + + + + + + Describes the physical state of a button that produced the button + event. + + + + + + + + Mouse button click and release notifications. + + The location of the click is given by the last motion or + enter event. + The time argument is a timestamp with millisecond + granularity, with an undefined base. + + The button is a button code as defined in the Linux kernel's + linux/input-event-codes.h header file, e.g. BTN_LEFT. + + Any 16-bit button code value is reserved for future additions to the + kernel's event code list. All other button codes above 0xFFFF are + currently undefined but may be used in future versions of this + protocol. + + + + + + + + + + Describes the axis types of scroll events. + + + + + + + + Scroll and other axis notifications. + + For scroll events (vertical and horizontal scroll axes), the + value parameter is the length of a vector along the specified + axis in a coordinate space identical to those of motion events, + representing a relative movement along the specified axis. + + For devices that support movements non-parallel to axes multiple + axis events will be emitted. + + When applicable, for example for touch pads, the server can + choose to emit scroll events where the motion vector is + equivalent to a motion event vector. + + When applicable, a client can transform its content relative to the + scroll distance. + + + + + + + + + + + Using this request a client can tell the server that it is not going to + use the pointer object anymore. + + This request destroys the pointer proxy object, so clients must not call + wl_pointer_destroy() after using this request. + + + + + + + + Indicates the end of a set of events that logically belong together. + A client is expected to accumulate the data in all events within the + frame before proceeding. + + All wl_pointer events before a wl_pointer.frame event belong + logically together. For example, in a diagonal scroll motion the + compositor will send an optional wl_pointer.axis_source event, two + wl_pointer.axis events (horizontal and vertical) and finally a + wl_pointer.frame event. The client may use this information to + calculate a diagonal vector for scrolling. + + When multiple wl_pointer.axis events occur within the same frame, + the motion vector is the combined motion of all events. + When a wl_pointer.axis and a wl_pointer.axis_stop event occur within + the same frame, this indicates that axis movement in one axis has + stopped but continues in the other axis. + When multiple wl_pointer.axis_stop events occur within the same + frame, this indicates that these axes stopped in the same instance. + + A wl_pointer.frame event is sent for every logical event group, + even if the group only contains a single wl_pointer event. + Specifically, a client may get a sequence: motion, frame, button, + frame, axis, frame, axis_stop, frame. + + The wl_pointer.enter and wl_pointer.leave events are logical events + generated by the compositor and not the hardware. These events are + also grouped by a wl_pointer.frame. When a pointer moves from one + surface to another, a compositor should group the + wl_pointer.leave event within the same wl_pointer.frame. + However, a client must not rely on wl_pointer.leave and + wl_pointer.enter being in the same wl_pointer.frame. + Compositor-specific policies may require the wl_pointer.leave and + wl_pointer.enter event being split across multiple wl_pointer.frame + groups. + + + + + + Describes the source types for axis events. This indicates to the + client how an axis event was physically generated; a client may + adjust the user interface accordingly. For example, scroll events + from a "finger" source may be in a smooth coordinate space with + kinetic scrolling whereas a "wheel" source may be in discrete steps + of a number of lines. + + The "continuous" axis source is a device generating events in a + continuous coordinate space, but using something other than a + finger. One example for this source is button-based scrolling where + the vertical motion of a device is converted to scroll events while + a button is held down. + + The "wheel tilt" axis source indicates that the actual device is a + wheel but the scroll event is not caused by a rotation but a + (usually sideways) tilt of the wheel. + + + + + + + + + + Source information for scroll and other axes. + + This event does not occur on its own. It is sent before a + wl_pointer.frame event and carries the source information for + all events within that frame. + + The source specifies how this event was generated. If the source is + wl_pointer.axis_source.finger, a wl_pointer.axis_stop event will be + sent when the user lifts the finger off the device. + + If the source is wl_pointer.axis_source.wheel, + wl_pointer.axis_source.wheel_tilt or + wl_pointer.axis_source.continuous, a wl_pointer.axis_stop event may + or may not be sent. Whether a compositor sends an axis_stop event + for these sources is hardware-specific and implementation-dependent; + clients must not rely on receiving an axis_stop event for these + scroll sources and should treat scroll sequences from these scroll + sources as unterminated by default. + + This event is optional. If the source is unknown for a particular + axis event sequence, no event is sent. + Only one wl_pointer.axis_source event is permitted per frame. + + The order of wl_pointer.axis_discrete and wl_pointer.axis_source is + not guaranteed. + + + + + + + Stop notification for scroll and other axes. + + For some wl_pointer.axis_source types, a wl_pointer.axis_stop event + is sent to notify a client that the axis sequence has terminated. + This enables the client to implement kinetic scrolling. + See the wl_pointer.axis_source documentation for information on when + this event may be generated. + + Any wl_pointer.axis events with the same axis_source after this + event should be considered as the start of a new axis motion. + + The timestamp is to be interpreted identical to the timestamp in the + wl_pointer.axis event. The timestamp value may be the same as a + preceding wl_pointer.axis event. + + + + + + + + Discrete step information for scroll and other axes. + + This event carries the axis value of the wl_pointer.axis event in + discrete steps (e.g. mouse wheel clicks). + + This event does not occur on its own, it is coupled with a + wl_pointer.axis event that represents this axis value on a + continuous scale. The protocol guarantees that each axis_discrete + event is always followed by exactly one axis event with the same + axis number within the same wl_pointer.frame. Note that the protocol + allows for other events to occur between the axis_discrete and + its coupled axis event, including other axis_discrete or axis + events. + + This event is optional; continuous scrolling devices + like two-finger scrolling on touchpads do not have discrete + steps and do not generate this event. + + The discrete value carries the directional information. e.g. a value + of -2 is two steps towards the negative direction of this axis. + + The axis number is identical to the axis number in the associated + axis event. + + The order of wl_pointer.axis_discrete and wl_pointer.axis_source is + not guaranteed. + + + + + + + + + The wl_keyboard interface represents one or more keyboards + associated with a seat. + + + + + This specifies the format of the keymap provided to the + client with the wl_keyboard.keymap event. + + + + + + + + This event provides a file descriptor to the client which can be + memory-mapped to provide a keyboard mapping description. + + + + + + + + + Notification that this seat's keyboard focus is on a certain + surface. + + + + + + + + + Notification that this seat's keyboard focus is no longer on + a certain surface. + + The leave notification is sent before the enter notification + for the new focus. + + + + + + + + Describes the physical state of a key that produced the key event. + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond + granularity, with an undefined base. + + + + + + + + + + Notifies clients that the modifier and/or group state has + changed, and it should update its local state. + + + + + + + + + + + + + + + + + + + Informs the client about the keyboard's repeat rate and delay. + + This event is sent as soon as the wl_keyboard object has been created, + and is guaranteed to be received by the client before any key press + event. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + This event can be sent later on as well with a new value if necessary, + so clients should continue listening for the event past the creation + of wl_keyboard. + + + + + + + + + The wl_touch interface represents a touchscreen + associated with a seat. + + Touch interactions can consist of one or more contacts. + For each contact, a series of events is generated, starting + with a down event, followed by zero or more motion events, + and ending with an up event. Events relating to the same + contact point can be identified by the ID of the sequence. + + + + + A new touch point has appeared on the surface. This touch point is + assigned a unique ID. Future events from this touch point reference + this ID. The ID ceases to be valid after a touch up event and may be + reused in the future. + + + + + + + + + + + + The touch point has disappeared. No further events will be sent for + this touch point and the touch point's ID is released and may be + reused in a future touch down event. + + + + + + + + + A touch point has changed coordinates. + + + + + + + + + + Indicates the end of a set of events that logically belong together. + A client is expected to accumulate the data in all events within the + frame before proceeding. + + A wl_touch.frame terminates at least one event but otherwise no + guarantee is provided about the set of events within a frame. A client + must assume that any state not updated in a frame is unchanged from the + previously known state. + + + + + + Sent if the compositor decides the touch stream is a global + gesture. No further events are sent to the clients from that + particular gesture. Touch cancellation applies to all touch points + currently active on this client's surface. The client is + responsible for finalizing the touch points, future touch points on + this surface may reuse the touch point ID. + + + + + + + + + + + + + + Sent when a touchpoint has changed its shape. + + This event does not occur on its own. It is sent before a + wl_touch.frame event and carries the new shape information for + any previously reported, or new touch points of that frame. + + Other events describing the touch point such as wl_touch.down, + wl_touch.motion or wl_touch.orientation may be sent within the + same wl_touch.frame. A client should treat these events as a single + logical touch point update. The order of wl_touch.shape, + wl_touch.orientation and wl_touch.motion is not guaranteed. + A wl_touch.down event is guaranteed to occur before the first + wl_touch.shape event for this touch ID but both events may occur within + the same wl_touch.frame. + + A touchpoint shape is approximated by an ellipse through the major and + minor axis length. The major axis length describes the longer diameter + of the ellipse, while the minor axis length describes the shorter + diameter. Major and minor are orthogonal and both are specified in + surface-local coordinates. The center of the ellipse is always at the + touchpoint location as reported by wl_touch.down or wl_touch.move. + + This event is only sent by the compositor if the touch device supports + shape reports. The client has to make reasonable assumptions about the + shape if it did not receive this event. + + + + + + + + + Sent when a touchpoint has changed its orientation. + + This event does not occur on its own. It is sent before a + wl_touch.frame event and carries the new shape information for + any previously reported, or new touch points of that frame. + + Other events describing the touch point such as wl_touch.down, + wl_touch.motion or wl_touch.shape may be sent within the + same wl_touch.frame. A client should treat these events as a single + logical touch point update. The order of wl_touch.shape, + wl_touch.orientation and wl_touch.motion is not guaranteed. + A wl_touch.down event is guaranteed to occur before the first + wl_touch.orientation event for this touch ID but both events may occur + within the same wl_touch.frame. + + The orientation describes the clockwise angle of a touchpoint's major + axis to the positive surface y-axis and is normalized to the -180 to + +180 degree range. The granularity of orientation depends on the touch + device, some devices only support binary rotation values between 0 and + 90 degrees. + + This event is only sent by the compositor if the touch device supports + orientation reports. + + + + + + + + + An output describes part of the compositor geometry. The + compositor works in the 'compositor coordinate system' and an + output corresponds to a rectangular area in that space that is + actually visible. This typically corresponds to a monitor that + displays part of the compositor space. This object is published + as global during start up, or when a monitor is hotplugged. + + + + + This enumeration describes how the physical + pixels on an output are laid out. + + + + + + + + + + + + This describes the transform that a compositor will apply to a + surface to compensate for the rotation or mirroring of an + output device. + + The flipped values correspond to an initial flip around a + vertical axis followed by rotation. + + The purpose is mainly to allow clients to render accordingly and + tell the compositor, so that for fullscreen surfaces, the + compositor will still be able to scan out directly from client + surfaces. + + + + + + + + + + + + + + The geometry event describes geometric properties of the output. + The event is sent when binding to the output object and whenever + any of the properties change. + + + + + + + + + + + + + + These flags describe properties of an output mode. + They are used in the flags bitfield of the mode event. + + + + + + + + The mode event describes an available mode for the output. + + The event is sent when binding to the output object and there + will always be one mode, the current mode. The event is sent + again if an output changes mode, for the mode that is now + current. In other words, the current mode is always the last + mode that was received with the current flag set. + + The size of a mode is given in physical hardware units of + the output device. This is not necessarily the same as + the output size in the global compositor space. For instance, + the output may be scaled, as described in wl_output.scale, + or transformed, as described in wl_output.transform. + + + + + + + + + + + + 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. + + + + + + This event contains scaling geometry information + that is not in the geometry event. It may be sent after + binding the output object or if the output scale changes + later. If it is not sent, the client should assume a + scale of 1. + + A scale larger than 1 means that the compositor will + automatically scale surface buffers by this amount + when rendering. This is used for very high resolution + displays where applications rendering at the native + resolution would be too small to be legible. + + It is intended that scaling aware clients track the + current output of a surface, and if it is on a scaled + output it should use wl_surface.set_buffer_scale with + the scale of the output. That way the compositor can + avoid scaling the surface, and the client can supply + a higher detail image. + + + + + + + + + Using this request a client can tell the server that it is not going to + use the output object anymore. + + + + + + + A region object describes an area. + + Region objects are used to describe the opaque and input + regions of a surface. + + + + + Destroy the region. This will invalidate the object ID. + + + + + + Add the specified rectangle to the region. + + + + + + + + + + Subtract the specified rectangle from the region. + + + + + + + + + + + The global interface exposing sub-surface compositing capabilities. + A wl_surface, that has sub-surfaces associated, is called the + parent surface. Sub-surfaces can be arbitrarily nested and create + a tree of sub-surfaces. + + The root surface in a tree of sub-surfaces is the main + surface. The main surface cannot be a sub-surface, because + sub-surfaces must always have a parent. + + A main surface with its sub-surfaces forms a (compound) window. + For window management purposes, this set of wl_surface objects is + to be considered as a single window, and it should also behave as + such. + + The aim of sub-surfaces is to offload some of the compositing work + within a window from clients to the compositor. A prime example is + a video player with decorations and video in separate wl_surface + objects. This should allow the compositor to pass YUV video buffer + processing to dedicated overlay hardware when possible. + + + + + Informs the server that the client will not be using this + protocol object anymore. This does not affect any other + objects, wl_subsurface objects included. + + + + + + + + + + Create a sub-surface interface for the given surface, and + associate it with the given parent surface. This turns a + plain wl_surface into a sub-surface. + + The to-be sub-surface must not already have another role, and it + must not have an existing wl_subsurface object. Otherwise a protocol + error is raised. + + + + + + + + + + An additional interface to a wl_surface object, which has been + made a sub-surface. A sub-surface has one parent surface. A + sub-surface's size and position are not limited to that of the parent. + Particularly, a sub-surface is not automatically clipped to its + parent's area. + + A sub-surface becomes mapped, when a non-NULL wl_buffer is applied + and the parent surface is mapped. The order of which one happens + first is irrelevant. A sub-surface is hidden if the parent becomes + hidden, or if a NULL wl_buffer is applied. These rules apply + recursively through the tree of surfaces. + + The behaviour of a wl_surface.commit request on a sub-surface + depends on the sub-surface's mode. The possible modes are + synchronized and desynchronized, see methods + wl_subsurface.set_sync and wl_subsurface.set_desync. Synchronized + mode caches the wl_surface state to be applied when the parent's + state gets applied, and desynchronized mode applies the pending + wl_surface state directly. A sub-surface is initially in the + synchronized mode. + + Sub-surfaces have also other kind of state, which is managed by + wl_subsurface requests, as opposed to wl_surface requests. This + state includes the sub-surface position relative to the parent + surface (wl_subsurface.set_position), and the stacking order of + the parent and its sub-surfaces (wl_subsurface.place_above and + .place_below). This state is applied when the parent surface's + wl_surface state is applied, regardless of the sub-surface's mode. + As the exception, set_sync and set_desync are effective immediately. + + The main surface can be thought to be always in desynchronized mode, + since it does not have a parent in the sub-surfaces sense. + + Even if a sub-surface is in desynchronized mode, it will behave as + in synchronized mode, if its parent surface behaves as in + synchronized mode. This rule is applied recursively throughout the + tree of surfaces. This means, that one can set a sub-surface into + synchronized mode, and then assume that all its child and grand-child + sub-surfaces are synchronized, too, without explicitly setting them. + + If the wl_surface associated with the wl_subsurface is destroyed, the + wl_subsurface object becomes inert. Note, that destroying either object + takes effect immediately. If you need to synchronize the removal + of a sub-surface to the parent surface update, unmap the sub-surface + first by attaching a NULL wl_buffer, update parent, and then destroy + the sub-surface. + + If the parent wl_surface object is destroyed, the sub-surface is + unmapped. + + + + + The sub-surface interface is removed from the wl_surface object + that was turned into a sub-surface with a + wl_subcompositor.get_subsurface request. The wl_surface's association + to the parent is deleted, and the wl_surface loses its role as + a sub-surface. The wl_surface is unmapped. + + + + + + + + + + This schedules a sub-surface position change. + The sub-surface will be moved so that its origin (top left + corner pixel) will be at the location x, y of the parent surface + coordinate system. The coordinates are not restricted to the parent + surface area. Negative values are allowed. + + The scheduled coordinates will take effect whenever the state of the + parent surface is applied. When this happens depends on whether the + parent surface is in synchronized mode or not. See + wl_subsurface.set_sync and wl_subsurface.set_desync for details. + + If more than one set_position request is invoked by the client before + the commit of the parent surface, the position of a new request always + replaces the scheduled position from any previous request. + + The initial position is 0, 0. + + + + + + + + This sub-surface is taken from the stack, and put back just + above the reference surface, changing the z-order of the sub-surfaces. + The reference surface must be one of the sibling surfaces, or the + parent surface. Using any other surface, including this sub-surface, + will cause a protocol error. + + The z-order is double-buffered. Requests are handled in order and + applied immediately to a pending state. The final pending state is + copied to the active state the next time the state of the parent + surface is applied. When this happens depends on whether the parent + surface is in synchronized mode or not. See wl_subsurface.set_sync and + wl_subsurface.set_desync for details. + + A new sub-surface is initially added as the top-most in the stack + of its siblings and parent. + + + + + + + The sub-surface is placed just below the reference surface. + See wl_subsurface.place_above. + + + + + + + Change the commit behaviour of the sub-surface to synchronized + mode, also described as the parent dependent mode. + + In synchronized mode, wl_surface.commit on a sub-surface will + accumulate the committed state in a cache, but the state will + not be applied and hence will not change the compositor output. + The cached state is applied to the sub-surface immediately after + the parent surface's state is applied. This ensures atomic + updates of the parent and all its synchronized sub-surfaces. + Applying the cached state will invalidate the cache, so further + parent surface commits do not (re-)apply old state. + + See wl_subsurface for the recursive effect of this mode. + + + + + + Change the commit behaviour of the sub-surface to desynchronized + mode, also described as independent or freely running mode. + + In desynchronized mode, wl_surface.commit on a sub-surface will + apply the pending state directly, without caching, as happens + normally with a wl_surface. Calling wl_surface.commit on the + parent surface has no effect on the sub-surface's wl_surface + state. This mode allows a sub-surface to be updated on its own. + + If cached state exists when wl_surface.commit is called in + desynchronized mode, the pending state is added to the cached + state, and applied as a whole. This invalidates the cache. + + Note: even if a sub-surface is set to desynchronized, a parent + sub-surface may override it to behave as synchronized. For details, + see wl_subsurface. + + If a surface's parent surface behaves as desynchronized, then + the cached state is applied on set_desync. + + + + + diff --git a/src/protocol/wlr-foreign-toplevel-management-unstable-v1.xml b/src/protocol/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 0000000..a97738f --- /dev/null +++ b/src/protocol/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,259 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + diff --git a/src/protocol/wlr-layer-shell-unstable-v1.xml b/src/protocol/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..d62fd51 --- /dev/null +++ b/src/protocol/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,390 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + diff --git a/src/protocol/wlr-virtual-pointer-unstable-v1.xml b/src/protocol/wlr-virtual-pointer-unstable-v1.xml new file mode 100644 index 0000000..ea243e7 --- /dev/null +++ b/src/protocol/wlr-virtual-pointer-unstable-v1.xml @@ -0,0 +1,152 @@ + + + + Copyright © 2019 Josef Gajdusek + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This protocol allows clients to emulate a physical pointer device. The + requests are mostly mirror opposites of those specified in wl_pointer. + + + + + + + + + + The pointer has moved by a relative amount to the previous request. + + Values are in the global compositor space. + + + + + + + + + The pointer has moved in an absolute coordinate frame. + + Value of x can range from 0 to x_extent, value of y can range from 0 + to y_extent. + + + + + + + + + + + A button was pressed or released. + + + + + + + + + Scroll and other axis requests. + + + + + + + + + Indicates the set of events that logically belong together. + + + + + + Source information for scroll and other axis. + + + + + + + Stop notification for scroll and other axes. + + + + + + + + Discrete step information for scroll and other axes. + + This event allows the client to extend data normally sent using the axis + event with discrete value. + + + + + + + + + + + + + + + This object allows clients to create individual virtual pointer objects. + + + + + Creates a new virtual pointer. The optional seat is a suggestion to the + compositor. + + + + + + + + + + + + + Creates a new virtual pointer. The seat and the output arguments are + optional. If the seat argument is set, the compositor should assign the + input device to the requested seat. If the output argument is set, the + compositor should map the input device to the requested output. + + + + + + + diff --git a/src/protocol/xdg-output-unstable-v1.xml b/src/protocol/xdg-output-unstable-v1.xml new file mode 100644 index 0000000..fe3a70a --- /dev/null +++ b/src/protocol/xdg-output-unstable-v1.xml @@ -0,0 +1,220 @@ + + + + + Copyright © 2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol aims at describing outputs in a way which is more in line + with the concept of an output on desktop oriented systems. + + Some information are more specific to the concept of an output for + a desktop oriented system and may not make sense in other applications, + such as IVI systems for example. + + Typically, the global compositor space on a desktop system is made of + a contiguous or overlapping set of rectangular regions. + + Some of the information provided in this protocol might be identical + to their counterparts already available from wl_output, in which case + the information provided by this protocol should be preferred to their + equivalent in wl_output. The goal is to move the desktop specific + concepts (such as output location within the global compositor space, + the connector name and types, etc.) out of the core wl_output protocol. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + + + + + A global factory interface for xdg_output objects. + + + + + Using this request a client can tell the server that it is not + going to use the xdg_output_manager object anymore. + + Any objects already created through this instance are not affected. + + + + + + This creates a new xdg_output object for the given wl_output. + + + + + + + + + An xdg_output describes part of the compositor geometry. + + This typically corresponds to a monitor that displays part of the + compositor space. + + For objects version 3 onwards, after all xdg_output properties have been + sent (when the object is created and when properties are updated), a + wl_output.done event is sent. This allows changes to the output + properties to be seen as atomic, even if they happen via multiple events. + + + + + Using this request a client can tell the server that it is not + going to use the xdg_output object anymore. + + + + + + The position event describes the location of the wl_output within + the global compositor space. + + The logical_position event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the location + of the output changes within the global compositor space. + + + + + + + + The logical_size event describes the size of the output in the + global compositor space. + + For example, a surface without any buffer scale, transformation + nor rotation set, with the size matching the logical_size will + have the same size as the corresponding output when displayed. + + Most regular Wayland clients should not pay attention to the + logical size and would rather rely on xdg_shell interfaces. + + Some clients such as Xwayland, however, need this to configure + their surfaces in the global compositor space as the compositor + may apply a different scale from what is advertised by the output + scaling property (to achieve fractional scaling, for example). + + For example, for a wl_output mode 3840×2160 and a scale factor 2: + + - A compositor not scaling the surface buffers will advertise a + logical size of 3840×2160, + + - A compositor automatically scaling the surface buffers will + advertise a logical size of 1920×1080, + + - A compositor using a fractional scale of 1.5 will advertise a + logical size to 2560×1620. + + For example, for a wl_output mode 1920×1080 and a 90 degree rotation, + the compositor will advertise a logical size of 1080x1920. + + The logical_size event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the logical + size of the output changes, either as a result of a change in the + applied scale or because of a change in the corresponding output + mode(see wl_output.mode) or transform (see wl_output.transform). + + + + + + + + This event is sent after all other properties of an xdg_output + have been sent. + + This allows changes to the xdg_output properties to be seen as + atomic, even if they happen via multiple events. + + For objects version 3 onwards, this event is deprecated. Compositors + are not required to send it anymore and must send wl_output.done + instead. + + + + + + + + Many compositors will assign names to their outputs, show them to the + user, allow them to be configured by name, etc. The client may wish to + know this name as well to offer the user similar behaviors. + + The naming convention is compositor defined, but limited to + alphanumeric characters and dashes (-). Each name is unique among all + wl_output globals, but if a wl_output global is destroyed the same name + may be reused later. The names will also remain consistent across + sessions with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + The name event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output). This event is only sent once per + xdg_output, and the name does not change over the lifetime of the + wl_output global. + + + + + + + Many compositors can produce human-readable descriptions of their + outputs. The client may wish to know this description as well, to + communicate the user for various purposes. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. + + The description event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output) and whenever the description + changes. The description is optional, and may not be sent at all. + + For objects of version 2 and lower, this event is only sent once per + xdg_output, and the description does not change over the lifetime of + the wl_output global. + + + + + + diff --git a/src/protocol/xdg-shell-unstable-v6.xml b/src/protocol/xdg-shell-unstable-v6.xml new file mode 100644 index 0000000..1c0f924 --- /dev/null +++ b/src/protocol/xdg-shell-unstable-v6.xml @@ -0,0 +1,1044 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + xdg_shell allows clients to turn a wl_surface into a "real window" + which can be dragged, resized, stacked, and moved around by the + user. Everything about this interface is suited towards traditional + desktop environments. + + + + + + + + + + + + + + Destroy this xdg_shell object. + + Destroying a bound xdg_shell object while there are surfaces + still alive created by this xdg_shell object instance is illegal + and will result in a protocol error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. While xdg_surface + itself is not a role, the corresponding surface may only be assigned + a role extending xdg_surface, such as xdg_toplevel or xdg_popup. + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_shell.ping. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_shell.ping. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_shell object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. The rectangle must be at least 1x1 large. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + Defines a set of edges for the anchor rectangle. These are used to + derive an anchor point that the child surface will be positioned + relative to. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left position of the rectangle); otherwise, the derived + anchor point will be centered on the specified edge, or in the center of + the anchor rectangle if no edge is specified. + + If two parallel anchor edges are specified (e.g. 'left' and 'right'), + the invalid_input error is raised. + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If two orthogonal gravities are + specified (e.g. 'bottom' and 'right'), then the child surface will be + placed in the specified direction; otherwise, the child surface will be + centered over the anchor point on any axis that had no gravity + specified. + + If two parallel gravities are specified (e.g. 'left' and 'right'), the + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of a monitor. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + For a surface to be mapped by the compositor, the following conditions + must be met: (1) the client has assigned a xdg_surface based role to the + surface, (2) the client has set and committed the xdg_surface state and + the role dependent state to the surface and (3) the client has committed a + buffer to the surface. + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives the + associated wl_surface the xdg_popup role. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. + + The window geometry is double buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface. + + The width and height must be greater than zero. Setting an invalid size + will raise an error. When applied, the effective window geometry will be + the set window geometry clamped to the bounding rectangle of the + combined geometry of the surface of the xdg_surface and the associated + subsurfaces. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + + + + Unmap and destroy the window. The window will be effectively + hidden from the user's point of view, and all state like + maximization, fullscreen, and so on, will be lost. + + + + + + Set the "parent" of this surface. This window should be stacked + above a parent. The parent surface must be mapped as long as this + surface is mapped. + + Parent windows should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] http://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, + and is one of the values of the resize_edge enum. The compositor + may use this information to update the surface position for + example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an + appropriate cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered. They will get applied on + the next commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client. + + + + + The surface is fullscreen. The window geometry specified in the configure + event must be obeyed by the client. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered. They will get applied + on the next commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in a protocol error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in a + protocol error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered. They will get applied + on the next commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in a protocol error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in a + protocol error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event with the "maximized" state + and the required window geometry. The client should then update its + content, drawing it in a maximized state, i.e. without shadow or other + decoration outside of the window geometry. The client must also + acknowledge the configure when committing the new content (see + ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event without the "maximized" + state. If available, the compositor will include the window geometry + dimensions the window had prior to being maximized in the configure + request. The client must then update its content, drawing it in a + regular state, i.e. potentially with shadow, etc. The client must also + acknowledge the configure when committing the new content (see + ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + + + + + Make the surface fullscreen. + + You can specify an output that you would prefer to be fullscreen. + If this value is NULL, it's up to the compositor to choose which + display will be used to map this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + black borders filling the rest of the output. + + + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + The parent surface must have either the xdg_toplevel or xdg_popup surface + role. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The x and y arguments passed when creating the popup object specify + where the top left of the popup should be placed, relative to the + local surface coordinates of the parent surface. See + xdg_surface.get_popup. An xdg_popup must intersect with or be at least + partially adjacent to its parent surface. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, a protocol error + will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + The parent of a grabbing popup must either be another xdg_popup with an + active explicit grab, or an xdg_popup or xdg_toplevel, if there are no + explicit grabs already taken. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + diff --git a/src/protocol/xdg-shell.xml b/src/protocol/xdg-shell.xml new file mode 100644 index 0000000..6eb0a67 --- /dev/null +++ b/src/protocol/xdg-shell.xml @@ -0,0 +1,1351 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + Copyright © 2015-2017 Samsung Electronics Co., Ltd + Copyright © 2015-2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The xdg_wm_base interface is exposed as a global object enabling clients + to turn their wl_surfaces into windows in a desktop environment. It + defines the basic functionality needed for clients and the compositor to + create windows that can be dragged, resized, maximized, etc, as well as + creating transient windows such as popup menus. + + + + + + + + + + + + + + + Destroy this xdg_wm_base object. + + Destroying a bound xdg_wm_base object while there are surfaces + still alive created by this xdg_wm_base object instance is illegal + and will result in a defunct_surfaces error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. While xdg_surface + itself is not a role, the corresponding surface may only be assigned + a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + illegal to create an xdg_surface for a wl_surface which already has an + assigned role and this will result in a role error. + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_wm_base.ping + and xdg_wm_base.error.unresponsive. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_wm_base.pong. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. The “unresponsive” + error is provided for compositors that wish to disconnect unresponsive + clients. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_wm_base object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an invalid_positioner error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + + + + + Defines the anchor point for the anchor rectangle. The specified anchor + is used derive an anchor point that the child surface will be + positioned relative to. If a corner anchor is set (e.g. 'top_left' or + 'bottom_right'), the anchor point will be at the specified corner; + otherwise, the derived anchor point will be centered on the specified + edge, or in the center of the anchor rectangle if no edge is specified. + + + + + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If a corner gravity is + specified (e.g. 'bottom_right' or 'top_left'), then the child surface + will be placed towards the specified gravity; otherwise, the child + surface will be centered over the anchor point on any axis that had no + gravity specified. If the gravity is not in the ‘gravity’ enum, an + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of an output. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + The adjusted position is calculated given the original anchor + rectangle and offset, but with the new flipped anchor and gravity + values. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + + When set reactive, the surface is reconstrained if the conditions used + for constraining changed, e.g. the parent window moved. + + If the conditions changed and the popup was reconstrained, an + xdg_popup.configure event is sent with updated geometry, followed by an + xdg_surface.configure event. + + + + + + Set the parent window geometry the compositor should use when + positioning the popup. The compositor may use this information to + determine the future state the popup should be constrained using. If + this doesn't match the dimension of the parent the popup is eventually + positioned against, the behavior is undefined. + + The arguments are given in the surface-local coordinate space. + + + + + + + + Set the serial of an xdg_surface.configure event this positioner will be + used in response to. The compositor may use this information together + with set_parent_size to determine what future state the popup should be + constrained using. + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + After creating a role-specific object and setting it up, the client must + perform an initial commit without any buffer attached. The compositor + will reply with an xdg_surface.configure event. The client must + acknowledge it and is then allowed to attach a buffer to map the surface. + + Mapping an xdg_surface-based role surface is defined as making it + possible for the surface to be shown by the compositor. Note that + a mapped surface is not guaranteed to be visible once it is mapped. + + For an xdg_surface to be mapped by the compositor, the following + conditions must be met: + (1) the client has assigned an xdg_surface-based role to the surface + (2) the client has set and committed the xdg_surface state and the + role-dependent state to the surface + (3) the client has committed a buffer to the surface + + A newly-unmapped surface is considered to have met condition (1) out + of the 3 required conditions for mapping a surface if its role surface + has not been destroyed, i.e. the client must perform the initial commit + again before attaching a buffer. + + + + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed, otherwise + a defunct_role_object error is raised. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives + the associated wl_surface the xdg_popup role. + + If null is passed as a parent, a parent surface must be specified using + some other protocol, before committing the initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. + + The window geometry is double buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + When maintaining a position, the compositor should treat the (x, y) + coordinate of the window geometry as the top left corner of the window. + A client changing the (x, y) window geometry coordinate should in + general not alter the position of the window. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface. + + The width and height must be greater than zero. Setting an invalid size + will raise an invalid_size error. When applied, the effective window + geometry will be the set window geometry clamped to the bounding + rectangle of the combined geometry of the surface of the xdg_surface and + the associated subsurfaces. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + Acking a configure event that was never sent raises an invalid_serial + error. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + Sending an ack_configure request consumes the serial number sent with + the request, as well as serial numbers sent by all configure events + sent on this xdg_surface prior to the configure event referenced by + the committed serial. + + It is an error to issue multiple ack_configure requests referencing a + serial from the same configure event, or to issue an ack_configure + request referencing a serial from a configure event issued before the + event identified by the last ack_configure request for the same + xdg_surface. Doing so will raise an invalid_serial error. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + Unmapping an xdg_toplevel means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + the state it had right after xdg_surface.get_toplevel. The client + can re-map the toplevel by perfoming a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a toplevel unmaps the surface. + + + + + This request destroys the role surface and unmaps the surface; + see "Unmapping" behavior in interface section for details. + + + + + + + + + + + + Set the "parent" of this surface. This surface should be stacked + above the parent surface and all other ancestor surfaces. + + Parent surfaces should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + Setting a null parent for a child surface unsets its parent. Setting + a null parent for a surface which currently has no parent is a no-op. + + Only mapped surfaces can have child surfaces. Setting a parent which + is not mapped is equivalent to setting a null parent. If a surface + becomes unmapped, its children's parent is set to the parent of + the now-unmapped surface. If the now-unmapped surface has no parent, + its children's parent is unset. If the now-unmapped surface becomes + mapped again, its parent-child relationship is not restored. + + The parent toplevel must not be one of the child toplevel's + descendants, and the parent must be different from the child toplevel, + otherwise the invalid_parent protocol error is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xdg_toplevel has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] https://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains, or even if a window menu will be drawn + at all. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, and + is one of the values of the resize_edge enum. Values not matching + a variant of the enum will cause a protocol error. The compositor + may use this information to update the surface position for example + when dragging the top left corner. The compositor may also use + this information to adapt its behavior, e.g. choose an appropriate + cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered. They will get applied on + the next commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client. + + The client should draw without shadow or other + decoration outside of the window geometry. + + + + + The surface is fullscreen. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. For + a surface to cover the whole fullscreened area, the geometry + dimensions must be obeyed by the client. For more details, see + xdg_toplevel.set_fullscreen. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + The window is currently in a tiled layout and the left edge is + considered to be adjacent to another part of the tiling grid. + + + + + The window is currently in a tiled layout and the right edge is + considered to be adjacent to another part of the tiling grid. + + + + + The window is currently in a tiled layout and the top edge is + considered to be adjacent to another part of the tiling grid. + + + + + The window is currently in a tiled layout and the bottom edge is + considered to be adjacent to another part of the tiling grid. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered. They will get applied + on the next commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width or height will result in a + invalid_size error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered. They will get applied + on the next commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in a + invalid_size error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event. Whether this configure + actually sets the window maximized is subject to compositor policies. + The client must then update its content, drawing in the configured + state. The client must also acknowledge the configure when committing + the new content (see ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event. Whether this actually + un-maximizes the window is subject to compositor policies. + If available and applicable, the compositor will include the window + geometry dimensions the window had prior to being maximized in the + configure event. The client must then update its content, drawing it in + the configured state. The client must also acknowledge the configure + when committing the new content (see ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Make the surface fullscreen. + + After requesting that the surface should be fullscreened, the + compositor will respond by emitting a configure event. Whether the + client is actually put into a fullscreen state is subject to compositor + policies. The client must also acknowledge the configure when + committing the new content (see ack_configure). + + The output passed by the request indicates the client's preference as + to which display it should be set fullscreen on. If this value is NULL, + it's up to the compositor to choose which display will be used to map + this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + with border fill covering the rest of the output. The content of the + border fill is undefined, but should be assumed to be in some way that + attempts to blend into the surrounding area (e.g. solid black). + + If the fullscreened surface is not opaque, the compositor must make + sure that other screen content not part of the same surface tree (made + up of subsurfaces, popups or similarly coupled surfaces) are not + visible below the fullscreened surface. + + + + + + + Make the surface no longer fullscreen. + + After requesting that the surface should be unfullscreened, the + compositor will respond by emitting a configure event. + Whether this actually removes the fullscreen state of the client is + subject to compositor policies. + + Making a surface unfullscreen sets states for the surface based on the following: + * the state(s) it may have had before becoming fullscreen + * any state(s) decided by the compositor + * any state(s) requested by the client while the surface was fullscreen + + The compositor may include the previous window geometry dimensions in + the configure event, if applicable. + + The client must also acknowledge the configure when committing the new + content (see ack_configure). + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + + The configure_bounds event may be sent prior to a xdg_toplevel.configure + event to communicate the bounds a window geometry size is recommended + to constrain to. + + The passed width and height are in surface coordinate space. If width + and height are 0, it means bounds is unknown and equivalent to as if no + configure_bounds event was ever sent for this surface. + + The bounds can for example correspond to the size of a monitor excluding + any panels or other shell components, so that a surface isn't created in + a way that it cannot fit. + + The bounds may change at any point, and in such a case, a new + xdg_toplevel.configure_bounds will be sent, followed by + xdg_toplevel.configure and xdg_surface.configure. + + + + + + + + + + + + + + + + + This event advertises the capabilities supported by the compositor. If + a capability isn't supported, clients should hide or disable the UI + elements that expose this functionality. For instance, if the + compositor doesn't advertise support for minimized toplevels, a button + triggering the set_minimized request should not be displayed. + + The compositor will ignore requests it doesn't support. For instance, + a compositor which doesn't advertise support for minimized will ignore + set_minimized requests. + + Compositors must send this event once before the first + xdg_surface.configure event. When the capabilities change, compositors + must send this event again and then send an xdg_surface.configure + event. + + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The capabilities are sent as an array of 32-bit unsigned integers in + native endianness. + + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, a protocol error + will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + For version 2 or older, the configure event for an xdg_popup is only + ever sent once for the initial configuration. Starting with version 3, + it may be sent again if the popup is setup with an xdg_positioner with + set_reactive requested, or in response to xdg_popup.reposition requests. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + + + + Reposition an already-mapped popup. The popup will be placed given the + details in the passed xdg_positioner object, and a + xdg_popup.repositioned followed by xdg_popup.configure and + xdg_surface.configure will be emitted in response. Any parameters set + by the previous positioner will be discarded. + + The passed token will be sent in the corresponding + xdg_popup.repositioned event. The new popup position will not take + effect until the corresponding configure event is acknowledged by the + client. See xdg_popup.repositioned for details. The token itself is + opaque, and has no other special meaning. + + If multiple reposition requests are sent, the compositor may skip all + but the last one. + + If the popup is repositioned in response to a configure event for its + parent, the client should send an xdg_positioner.set_parent_configure + and possibly an xdg_positioner.set_parent_size request to allow the + compositor to properly constrain the popup. + + If the popup is repositioned together with a parent that is being + resized, but not in response to a configure event, the client should + send an xdg_positioner.set_parent_size request. + + + + + + + + The repositioned event is sent as part of a popup configuration + sequence, together with xdg_popup.configure and lastly + xdg_surface.configure to notify the completion of a reposition request. + + The repositioned event is to notify about the completion of a + xdg_popup.reposition request. The token argument is the token passed + in the xdg_popup.reposition request. + + Immediately after this event is emitted, xdg_popup.configure and + xdg_surface.configure will be sent with the updated size and position, + as well as a new configure serial. + + The client should optionally update the content of the popup, but must + acknowledge the new popup configuration for the new position to take + effect. See xdg_surface.ack_configure for details. + + + + + + diff --git a/src/relative_pointer_unstable_v1.cpp b/src/relative_pointer_unstable_v1.cpp new file mode 100644 index 0000000..deb438c --- /dev/null +++ b/src/relative_pointer_unstable_v1.cpp @@ -0,0 +1,58 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "relative_pointer_unstable_v1.h" + +#include "in_process_server.h" +#include + +wlcs::ZwpRelativePointerManagerV1::ZwpRelativePointerManagerV1(Client& client) : + manager{client.bind_if_supported(AnyVersion)} +{ + +} + +wlcs::ZwpRelativePointerManagerV1::~ZwpRelativePointerManagerV1() = default; + +wlcs::ZwpRelativePointerManagerV1::operator zwp_relative_pointer_manager_v1*() const +{ + return manager; +} + +wlcs::ZwpRelativePointerV1::ZwpRelativePointerV1(wlcs::ZwpRelativePointerManagerV1& manager, wl_pointer* pointer) : + relative_pointer{zwp_relative_pointer_manager_v1_get_relative_pointer(manager, pointer)}, + version{zwp_relative_pointer_v1_get_version(relative_pointer)} +{ + zwp_relative_pointer_v1_set_user_data(relative_pointer, this); + zwp_relative_pointer_v1_add_listener(relative_pointer, &listener, this); +} + +wlcs::ZwpRelativePointerV1::~ZwpRelativePointerV1() +{ + zwp_relative_pointer_v1_destroy(relative_pointer); +} + +wlcs::ZwpRelativePointerV1::operator zwp_relative_pointer_v1*() const +{ + return relative_pointer; +} + +zwp_relative_pointer_v1_listener const wlcs::ZwpRelativePointerV1::listener = + { + [](void* self, auto*, auto... args) { static_cast(self)->relative_motion(args...); } + }; diff --git a/src/shared_library.cpp b/src/shared_library.cpp new file mode 100644 index 0000000..8fa7e26 --- /dev/null +++ b/src/shared_library.cpp @@ -0,0 +1,55 @@ +/* + * Copyright © 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 2 or 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "shared_library.h" + +#include +#include + +#include + +#include + +wlcs::SharedLibrary::SharedLibrary(char const* library_name) : + so(dlopen(library_name, RTLD_NOW | RTLD_LOCAL)) +{ + if (!so) + { + BOOST_THROW_EXCEPTION(std::runtime_error(dlerror())); + } +} + +wlcs::SharedLibrary::SharedLibrary(std::string const& library_name) : + SharedLibrary(library_name.c_str()) {} + +wlcs::SharedLibrary::~SharedLibrary() noexcept +{ + dlclose(so); +} + +void* wlcs::SharedLibrary::load_symbol(char const* function_name) const +{ + if (void* result = dlsym(so, function_name)) + { + return result; + } + else + { + BOOST_THROW_EXCEPTION(std::runtime_error(dlerror())); + } +} diff --git a/src/surface_builder.cpp b/src/surface_builder.cpp new file mode 100644 index 0000000..9d2ef84 --- /dev/null +++ b/src/surface_builder.cpp @@ -0,0 +1,150 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "surface_builder.h" +#include "xdg_shell_stable.h" + +auto wlcs::SurfaceBuilder::all_surface_types() -> std::vector> +{ + return { + std::make_shared(), + std::make_shared(), + std::make_shared(0, 0, 0, 0), + std::make_shared(12, 5, 20, 6), + std::make_shared(std::make_pair(0, 0)), + std::make_shared(std::make_pair(7, 12))}; +} + +auto wlcs::SurfaceBuilder::toplevel_surface_types() -> std::vector> +{ + return { + std::make_shared(), + std::make_shared(), + std::make_shared(0, 0, 0, 0)}; +} + +auto wlcs::SurfaceBuilder::surface_builder_to_string( + testing::TestParamInfo> builder) -> std::string +{ + return builder.param->name; +} + +auto wlcs::WlShellSurfaceBuilder::build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr +{ + auto surface = std::make_unique( + client.create_wl_shell_surface(size.first, size.second)); + server.move_surface_to(*surface, position.first, position.second); + return surface; +} + +auto wlcs::XdgV6SurfaceBuilder::build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr +{ + auto surface = std::make_unique( + client.create_xdg_shell_v6_surface(size.first, size.second)); + server.move_surface_to(*surface, position.first, position.second); + return surface; +} + +wlcs::XdgStableSurfaceBuilder::XdgStableSurfaceBuilder(int left_offset, int top_offset, int right_offset, int bottom_offset) + : SurfaceBuilder{"xdg_surface_stable" + + ((left_offset || top_offset || right_offset || bottom_offset) ? ("_" + + std::to_string(left_offset) + "_" + + std::to_string(top_offset) + "_" + + std::to_string(right_offset) + "_" + + std::to_string(bottom_offset)) : "")}, + left_offset{left_offset}, + top_offset{top_offset}, + right_offset{right_offset}, + bottom_offset{bottom_offset} +{ +} + +auto wlcs::XdgStableSurfaceBuilder::build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr +{ + auto surface = std::make_unique(client); + auto xdg_surface = std::make_shared(client, *surface); + auto xdg_toplevel = std::make_shared(*xdg_surface); + // The logical window will be shrunk and moved based on the offset, but the underlying surface will not + xdg_surface_set_window_geometry( + *xdg_surface, + left_offset, top_offset, + size.first - left_offset - right_offset, size.second -bottom_offset - top_offset); + surface->attach_visible_buffer(size.first, size.second); + surface->run_on_destruction( + [xdg_surface, xdg_toplevel]() mutable + { + xdg_toplevel.reset(); + xdg_surface.reset(); + }); + server.move_surface_to(*surface, position.first + left_offset, position.second + top_offset); + return surface; +} + +wlcs::SubsurfaceBuilder::SubsurfaceBuilder(std::pair offset) + : SurfaceBuilder{ + "subsurface_at_x" + + std::to_string(offset.first) + + "_y" + + std::to_string(offset.second)}, + offset{offset} +{ +} + +auto wlcs::SubsurfaceBuilder::build( + wlcs::Server& server, + wlcs::Client& client, + std::pair position, + std::pair size) const -> std::unique_ptr +{ + auto main_surface = std::make_shared( + client.create_visible_surface(80, 50)); + server.move_surface_to( + *main_surface, + position.first - offset.first, + position.second - offset.second); + client.run_on_destruction( + [main_surface]() mutable + { + main_surface.reset(); + }); + auto subsurface = std::make_unique(wlcs::Subsurface::create_visible( + *main_surface, + offset.first, offset.second, + size.first, size.second)); + // if subsurface is sync, tests would have to commit the parent to modify it + // this is inconvenient to do in a generic way, so we make it desync + wl_subsurface_set_desync(*subsurface); + return subsurface; +} + +std::ostream& std::operator<<(std::ostream& out, std::shared_ptr const& param) +{ + return out << param->name; +} diff --git a/src/termcolor.hpp b/src/termcolor.hpp new file mode 100644 index 0000000..f8a8629 --- /dev/null +++ b/src/termcolor.hpp @@ -0,0 +1,557 @@ +//! +//! termcolor +//! ~~~~~~~~~ +//! +//! termcolor is a header-only c++ library for printing colored messages +//! to the terminal. Written just for fun with a help of the Force. +//! +//! :copyright: (c) 2013 by Ihor Kalnytskyi +//! :license: BSD, see LICENSE for details +//! + +#ifndef TERMCOLOR_HPP_ +#define TERMCOLOR_HPP_ + +// the following snippet of code detects the current OS and +// defines the appropriate macro that is used to wrap some +// platform specific things +#if defined(_WIN32) || defined(_WIN64) +# define TERMCOLOR_OS_WINDOWS +#elif defined(__APPLE__) +# define TERMCOLOR_OS_MACOS +#elif defined(__unix__) || defined(__unix) +# define TERMCOLOR_OS_LINUX +#else +# error unsupported platform +#endif + + +// This headers provides the `isatty()`/`fileno()` functions, +// which are used for testing whether a standart stream refers +// to the terminal. As for Windows, we also need WinApi funcs +// for changing colors attributes of the terminal. +#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) +# include +#elif defined(TERMCOLOR_OS_WINDOWS) +# include +# include +#endif + + +#include +#include + + + +namespace termcolor +{ + // Forward declaration of the `_internal` namespace. + // All comments are below. + namespace _internal + { + // An index to be used to access a private storage of I/O streams. See + // colorize / nocolorize I/O manipulators for details. + static int colorize_index = std::ios_base::xalloc(); + + inline FILE* get_standard_stream(const std::ostream& stream); + inline bool is_colorized(std::ostream& stream); + inline bool is_atty(const std::ostream& stream); + + #if defined(TERMCOLOR_OS_WINDOWS) + inline void win_change_attributes(std::ostream& stream, int foreground, int background=-1); + #endif + } + + inline + std::ostream& colorize(std::ostream& stream) + { + stream.iword(_internal::colorize_index) = 1L; + return stream; + } + + inline + std::ostream& nocolorize(std::ostream& stream) + { + stream.iword(_internal::colorize_index) = 0L; + return stream; + } + + inline + std::ostream& reset(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[00m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, -1); + #endif + } + return stream; + } + + + inline + std::ostream& bold(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[1m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + #endif + } + return stream; + } + + + inline + std::ostream& dark(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[2m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + #endif + } + return stream; + } + + + inline + std::ostream& underline(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[4m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + #endif + } + return stream; + } + + + inline + std::ostream& blink(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[5m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + #endif + } + return stream; + } + + + inline + std::ostream& reverse(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[7m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + #endif + } + return stream; + } + + + inline + std::ostream& concealed(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[8m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + #endif + } + return stream; + } + + + inline + std::ostream& grey(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[30m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + 0 // grey (black) + ); + #endif + } + return stream; + } + + inline + std::ostream& red(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[31m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_RED + ); + #endif + } + return stream; + } + + inline + std::ostream& green(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[32m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_GREEN + ); + #endif + } + return stream; + } + + inline + std::ostream& yellow(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[33m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_GREEN | FOREGROUND_RED + ); + #endif + } + return stream; + } + + inline + std::ostream& blue(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[34m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_BLUE + ); + #endif + } + return stream; + } + + inline + std::ostream& magenta(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[35m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_BLUE | FOREGROUND_RED + ); + #endif + } + return stream; + } + + inline + std::ostream& cyan(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[36m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_BLUE | FOREGROUND_GREEN + ); + #endif + } + return stream; + } + + inline + std::ostream& white(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[37m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + ); + #endif + } + return stream; + } + + + + inline + std::ostream& on_grey(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[40m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + 0 // grey (black) + ); + #endif + } + return stream; + } + + inline + std::ostream& on_red(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[41m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_RED + ); + #endif + } + return stream; + } + + inline + std::ostream& on_green(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[42m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_GREEN + ); + #endif + } + return stream; + } + + inline + std::ostream& on_yellow(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[43m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_GREEN | BACKGROUND_RED + ); + #endif + } + return stream; + } + + inline + std::ostream& on_blue(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[44m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_BLUE + ); + #endif + } + return stream; + } + + inline + std::ostream& on_magenta(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[45m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_BLUE | BACKGROUND_RED + ); + #endif + } + return stream; + } + + inline + std::ostream& on_cyan(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[46m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_GREEN | BACKGROUND_BLUE + ); + #endif + } + return stream; + } + + inline + std::ostream& on_white(std::ostream& stream) + { + if (_internal::is_colorized(stream)) + { + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + stream << "\033[47m"; + #elif defined(TERMCOLOR_OS_WINDOWS) + _internal::win_change_attributes(stream, -1, + BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_RED + ); + #endif + } + + return stream; + } + + + + //! Since C++ hasn't a way to hide something in the header from + //! the outer access, I have to introduce this namespace which + //! is used for internal purpose and should't be access from + //! the user code. + namespace _internal + { + //! Since C++ hasn't a true way to extract stream handler + //! from the a given `std::ostream` object, I have to write + //! this kind of hack. + inline + FILE* get_standard_stream(const std::ostream& stream) + { + if (&stream == &std::cout) + return stdout; + else if ((&stream == &std::cerr) || (&stream == &std::clog)) + return stderr; + + return 0; + } + + // Say whether a given stream should be colorized or not. It's always + // true for ATTY streams and may be true for streams marked with + // colorize flag. + inline + bool is_colorized(std::ostream& stream) + { + return is_atty(stream) || static_cast(stream.iword(colorize_index)); + } + + //! Test whether a given `std::ostream` object refers to + //! a terminal. + inline + bool is_atty(const std::ostream& stream) + { + FILE* std_stream = get_standard_stream(stream); + + // Unfortunately, fileno() ends with segmentation fault + // if invalid file descriptor is passed. So we need to + // handle this case gracefully and assume it's not a tty + // if standard stream is not detected, and 0 is returned. + if (!std_stream) + return false; + + #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) + return ::isatty(fileno(std_stream)); + #elif defined(TERMCOLOR_OS_WINDOWS) + return ::_isatty(_fileno(std_stream)); + #endif + } + + #if defined(TERMCOLOR_OS_WINDOWS) + //! Change Windows Terminal colors attribute. If some + //! parameter is `-1` then attribute won't changed. + inline void win_change_attributes(std::ostream& stream, int foreground, int background) + { + // yeah, i know.. it's ugly, it's windows. + static WORD defaultAttributes = 0; + + // Windows doesn't have ANSI escape sequences and so we use special + // API to change Terminal output color. That means we can't + // manipulate colors by means of "std::stringstream" and hence + // should do nothing in this case. + if (!_internal::is_atty(stream)) + return; + + // get terminal handle + HANDLE hTerminal = INVALID_HANDLE_VALUE; + if (&stream == &std::cout) + hTerminal = GetStdHandle(STD_OUTPUT_HANDLE); + else if (&stream == &std::cerr) + hTerminal = GetStdHandle(STD_ERROR_HANDLE); + + // save default terminal attributes if it unsaved + if (!defaultAttributes) + { + CONSOLE_SCREEN_BUFFER_INFO info; + if (!GetConsoleScreenBufferInfo(hTerminal, &info)) + return; + defaultAttributes = info.wAttributes; + } + + // restore all default settings + if (foreground == -1 && background == -1) + { + SetConsoleTextAttribute(hTerminal, defaultAttributes); + return; + } + + // get current settings + CONSOLE_SCREEN_BUFFER_INFO info; + if (!GetConsoleScreenBufferInfo(hTerminal, &info)) + return; + + if (foreground != -1) + { + info.wAttributes &= ~(info.wAttributes & 0x0F); + info.wAttributes |= static_cast(foreground); + } + + if (background != -1) + { + info.wAttributes &= ~(info.wAttributes & 0xF0); + info.wAttributes |= static_cast(background); + } + + SetConsoleTextAttribute(hTerminal, info.wAttributes); + } + #endif // TERMCOLOR_OS_WINDOWS + + } // namespace _internal + +} // namespace termcolor + + +#undef TERMCOLOR_OS_WINDOWS +#undef TERMCOLOR_OS_MACOS +#undef TERMCOLOR_OS_LINUX + +#endif // TERMCOLOR_HPP_ diff --git a/src/test_c_compile.c b/src/test_c_compile.c new file mode 100644 index 0000000..a0bab36 --- /dev/null +++ b/src/test_c_compile.c @@ -0,0 +1,7 @@ +#include "wlcs/display_server.h" +#include "wlcs/pointer.h" +#include "wlcs/touch.h" + +WlcsServerIntegration server_integration; +WlcsPointer pointer; +WlcsTouch touch; diff --git a/src/thread_proxy.h b/src/thread_proxy.h new file mode 100644 index 0000000..93e678c --- /dev/null +++ b/src/thread_proxy.h @@ -0,0 +1,465 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_THREAD_PROXY_H_ +#define WLCS_THREAD_PROXY_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * ThreadProxy: Take a Callable and run it on the Wayland event loop. + * + * There are two main parts here: some template metaprogramming, and + * the ThreadProxy class. + * + * ThreadProxy maintains the infrastructure for taking Callables + * and invoking them on a Wayland event loop. The meat here is + * in register_op(), which takes a Callable and returns a + * new Callable that, when invoked, will result in the original + * Callable being called on the Wayland event loop. + * + * The mechanism used for invoking on the Wayland event loop is a + * socket: arguments are serialised to the socket, and then deserialised + * when processed by the event loop. + * + * The template metaprogramming is mostly for manipulating parameter + * packs: serialising them to and from linear buffers (so that they + * can be passed across a socket), and invoking a Callable with a + * tuple of args, and deducing the arguments and return value of + * a Callable + * + */ + +namespace +{ +/* + * Deduce the argument types and return values of a Callable. + * + * Notably this only works for Callables with exactly one implementation + * of operator(), otherwise template deduction will fail as it is unable + * to select which operator() to operate on. + * + * This isn't an issue for what we're using this with, which is primarly + * lambdas. + */ +template +struct callable_traits : public callable_traits {}; + +template +struct callable_traits +{ + using return_type = Returns; + using args = typename std::tuple; +}; + +/* + * call_helper and call are downmarket versions of C++17's std::invoke() + * + * We don't yet require C++17, so downmarket it is! + */ +template +Returns call_helper(Callable const& func, std::tuple const& args, std::index_sequence) +{ + return func(std::get(args)...); +} + +template +Returns call(Callable const& func, std::tuple const& args) +{ + return call_helper(func, args, std::index_sequence_for{}); +} + +/* + * tuple_from_buffer base case: To unpack a buffer containing no values, + * return a std::tuple<>. + */ +template< + typename... Args, + typename std::enable_if::type = 0> +std::tuple tuple_from_buffer(char*, std::tuple const*) +{ + return std::tuple{}; +} + +/* + * Unpack a linear buffer into a std::tuple of its component types. + */ +template +std::tuple tuple_from_buffer(char* buffer, std::tuple const*) +{ + // We need to memcpy out of the buffer as there's no guarantee data in the buffer has correct + // alignment for this type. + Head aligned_arg; + ::memcpy(&aligned_arg, buffer, sizeof(Head)); + + std::tuple const* type_deducer = nullptr; + return std::tuple_cat(std::make_tuple(aligned_arg), tuple_from_buffer(buffer + sizeof(Head), type_deducer)); +} + +std::array setup_socketpair() +{ + std::array socket_fds; + + if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, socket_fds.data()) < 0) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + errno, + std::system_category(), + "Failed to create Wayland thread communication socket"})); + } + + return socket_fds; +} + +constexpr ssize_t arguments_size() +{ + return 0; +} + +template +constexpr ssize_t arguments_size(T const& head, Remainder const&... tail) +{ + return sizeof(head) + arguments_size(tail...); +} + +/* + * pack_buffer base case: To pack no arguments into a buffer, do nothing. + */ +void pack_buffer(void*) +{ +} + +/* + * Pack a any number of (TriviallyCopyable) arguments into a linear buffer. + */ +template +void pack_buffer(char* buffer, T&& arg, Remainder&& ...args) +{ + memcpy(buffer, &arg, sizeof(arg)); + pack_buffer(buffer + sizeof(arg), std::forward(args)...); +} + +/* + * A bit more C++17 backporting... + * + * This bit of metaprogramming is in support of statically-asserting that + * all the types passed through the proxy are TriviallyCopyable. + */ +template +struct conjunction : std::true_type {}; + +template +struct conjunction : Cond {}; + +template +struct conjunction : + std::conditional_t, Head> {}; + +template +constexpr bool all_args_are_trivially_copyable(std::tuple const*) +{ + return conjunction...>::value; +} + +/* + * ThreadProxy: a mechanism to take a functor from any thread, and turn it into + * a functor that runs on the Wayland event loop. + * + * Theory of operation: + * The inter-thread channel used is a SOCK_SEQPACKET socket. + * + * register_op() takes an (almost) arbitrary Callable, wraps it in a + * std::function<> that recieves a pointer to a buffer of arguments, + * unpacks the buffer into a std::tuple, then invokes the original + * Callable, and adds this function to the handler array. + * + * register_op() then *returns* a function with the same signature that + * copies the array index of the relevant handler into a falt buffer, + * marshalls the arguments passed in into the buffer and then pushes that buffer + * into the socket. + * + * On the Wayland event loop side, we add a fd event_source to the event_loop + * which reads from the Wayland end of the socket, extracts the index into the + * handler array, then calls the handler with the rest of the message buffer. + */ +class ThreadProxy : public std::enable_shared_from_this +{ +private: + + template + void send_message(uint32_t opcode, Args&& ...args) const + { + char buffer[max_message_size]; + ssize_t const message_size = sizeof(uint32_t) + arguments_size(args...); + + *reinterpret_cast(buffer) = opcode; + pack_buffer(buffer + sizeof(uint32_t), std::forward(args)...); + + auto const written = send(fds[Fd::Client], buffer, message_size, MSG_DONTWAIT); + + if (written < message_size) + { + if (written < 0) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + errno, + std::system_category(), + "Failed to send message to Wayland thread"})); + } + BOOST_THROW_EXCEPTION(( + std::runtime_error{ + "Failed to send whole message to Wayland thread"})); + } + } + + template + T wait_for_reply() const + { + T buffer; + // Wait for it to be processed + auto const read = recv(fds[Fd::Client], &buffer, sizeof(buffer), 0); + if (read < static_cast(sizeof(buffer))) + { + if (read < 0) + { + BOOST_THROW_EXCEPTION(( + std::system_error{ + errno, + std::system_category(), + "Failed to receive reply from Wayland thread"})); + } + BOOST_THROW_EXCEPTION(( + std::runtime_error{ + "Received short reply from Wayland thread"})); + } + return buffer; + } + + template + auto make_send_functor(uint32_t opcode, std::tuple const*) const + { + return + [me = this->shared_from_this(), opcode](Args&& ...args) + { + // We technically don't need to serialise the send/receive, but + // it's a bit easier if only one request is in flight at once. + std::lock_guard lock{me->message_serialiser}; + me->send_message(opcode, std::forward(args)...); + me->wait_for_reply(); + }; + } + + template + auto make_send_functor(uint32_t opcode, std::tuple const*) const + { + return + [me = this->shared_from_this(), opcode](Args&& ...args) + { + // We technically don't need to serialise the send/receive, but + // it's a bit easier if only one request is in flight at once. + std::lock_guard lock{me->message_serialiser}; + me->send_message(opcode, std::forward(args)...); + return me->wait_for_reply(); + }; + } + + template< + typename Callable, + typename... Args, + typename + std::enable_if_t< + std::is_same< + typename callable_traits::type>::return_type, + void>::value, + int> = 0> + auto make_recv_functor(Callable handler, std::tuple const*) + { + return + [this, handler = std::move(handler)](void* data) + { + std::tuple const* type_resolver = nullptr; + auto const args = tuple_from_buffer(static_cast(data), type_resolver); + + call(handler, args); + + // void functions send a dummy char, to notify the other end that + // the call has completed. + char const dummy{0}; + send(fds[Fd::Wayland], &dummy, sizeof(dummy), 0); + }; + } + + template< + typename Callable, + typename... Args, + typename + std::enable_if_t< + !std::is_same< + typename callable_traits::type>::return_type, + void>::value, int> = 0> + auto make_recv_functor(Callable handler, std::tuple const*) + { + return + [this, handler = std::move(handler)](void* data) + { + using traits = callable_traits::type>; + + std::tuple const* type_resolver = nullptr; + auto const args = tuple_from_buffer(static_cast(data), type_resolver); + + auto const val = call(handler, args); + + send(this->fds[Fd::Wayland], &val, sizeof(val), 0); + }; + } +public: + template< + typename Callable, + typename + std::enable_if_t< + std::is_same< + typename callable_traits::type>::return_type, + void>::value, int> = 0> + auto register_op(Callable handler) + { + using traits = callable_traits::type>; + // This is technically too restrictive; std::tuple might have a size greater + // than the sum of its parts. + static_assert( + sizeof(typename traits::args) < max_arguments_size, + "Attempt to call function with too many arguments; bump max_message_size"); + + + // Get template deduction to work by passing an (unused) argument of type + // std::tuple*; we can't access Args... ourselves here. + constexpr typename traits::args const* type_resolver = nullptr; + + static_assert( + all_args_are_trivially_copyable(type_resolver), + "All arguments of register_op must be TriviallyCopyable as they must support memcpy"); + + auto const next_opcode = handlers.size(); + handlers.emplace_back(make_recv_functor(std::move(handler), type_resolver)); + + return make_send_functor(next_opcode, type_resolver); + } + + template< + typename Callable, + typename + std::enable_if_t< + !std::is_same< + typename callable_traits::type>::return_type, + void>::value, int> = 0> + auto register_op(Callable handler) + { + using traits = callable_traits::type>; + static_assert( + sizeof(typename traits::args) < max_arguments_size, + "Attempt to call function with too many arguments; bump max_message_size"); + static_assert( + sizeof(typename traits::return_type) < max_message_size, + "Attempt to call function with too large return value; bump max_message_size"); + + // Get template deduction to work by passing an (unused) argument of type + // std::tuple; we can't access Args... ourselves here. + constexpr typename traits::args const* type_resolver = nullptr; + static_assert( + all_args_are_trivially_copyable(type_resolver), + "All arguments of register_op must be TriviallyCopyable as they must support memcpy"); + + auto const next_opcode = handlers.size(); + handlers.emplace_back(make_recv_functor(std::move(handler), type_resolver)); + + return make_send_functor(next_opcode, type_resolver); + } + + explicit ThreadProxy(struct wl_event_loop* event_loop) + : fds{setup_socketpair()}, + source{ + wl_event_loop_add_fd( + event_loop, + fds[Fd::Wayland], + WL_EVENT_READABLE, + &ThreadProxy::socket_readable, + this)}, + // We have to manually implement the destructor() thunk rather than using register_op because + // the send_functor returned by register_op takes shared reference to this, care of + // shared_from_this(). But during construction `this` is not a shared_ptr, so shared_from_this() + // isn't usable. + destructor{ + [this]() + { + send_message(0); + }} + { + // We must run wl_event_source_remove() on the Wayland mainloop, too. + handlers.emplace_back( + [this](void*) + { + wl_event_source_remove(source); + }); + } + + ~ThreadProxy() + { + destructor(); + close(fds[0]); + close(fds[1]); + } + +private: + static int socket_readable(int fd, uint32_t /*mask*/, void* data) noexcept + { + auto const& me = *static_cast(data); + char buffer[max_message_size]; + + recv(fd, buffer, sizeof(buffer), 0); + + auto const opcode = *reinterpret_cast(buffer); + me.handlers[opcode](buffer + sizeof(uint32_t)); + + return 0; + } + + static constexpr ssize_t max_arguments_size{1024}; + static constexpr ssize_t max_message_size = max_arguments_size + sizeof(uint32_t); + enum Fd + { + Wayland = 0, + Client + }; + std::array const fds; + struct wl_event_source* const source; + std::mutex mutable message_serialiser; + std::vector> handlers; + std::function const destructor; +}; +} + + +#endif //WLCS_THREAD_PROXY_H_ diff --git a/src/version_specifier.cpp b/src/version_specifier.cpp new file mode 100644 index 0000000..36692ab --- /dev/null +++ b/src/version_specifier.cpp @@ -0,0 +1,89 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#include "version_specifier.h" +#include "boost/throw_exception.hpp" +#include + +wlcs::ExactlyVersion::ExactlyVersion(uint32_t version) noexcept + : version{version} +{ +} + +auto wlcs::ExactlyVersion::select_version( + uint32_t max_available_version, + uint32_t max_supported_version) const -> std::optional +{ + if (version > max_supported_version) + { + BOOST_THROW_EXCEPTION(std::logic_error( + "Required version " + + std::to_string(version) + + " is higher than the highest version supported by WLCS (" + + std::to_string(max_supported_version) + + ")")); + } + else if (version > max_available_version) + { + return {}; + } + else + { + return {version}; + } +} + +auto wlcs::ExactlyVersion::describe() const -> std::string +{ + return std::string{"= "} + std::to_string(version); +} + +wlcs::AtLeastVersion::AtLeastVersion(uint32_t version) noexcept + : version{version} +{ +} + +auto wlcs::AtLeastVersion::select_version( + uint32_t max_available_version, + uint32_t max_supported_version) const -> std::optional +{ + if (version > max_supported_version) + { + BOOST_THROW_EXCEPTION(std::logic_error( + "Required version " + + std::to_string(version) + + " is higher than the highest version supported by WLCS (" + + std::to_string(max_supported_version) + + ")")); + } + else if (version > max_available_version) + { + return {}; + } + else + { + return {std::min(max_available_version, max_supported_version)}; + } +} + +auto wlcs::AtLeastVersion::describe() const -> std::string +{ + return std::string{">= "} + std::to_string(version); +} + +wlcs::VersionSpecifier const& wlcs::AnyVersion = wlcs::AtLeastVersion{1}; diff --git a/src/xdg_output_v1.cpp b/src/xdg_output_v1.cpp new file mode 100644 index 0000000..a92e469 --- /dev/null +++ b/src/xdg_output_v1.cpp @@ -0,0 +1,144 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "xdg_output_v1.h" +#include "wl_handle.h" +#include "version_specifier.h" +#include + +struct wlcs::XdgOutputManagerV1::Impl +{ + Impl(Client& client) + : client{client}, + manager{client.bind_if_supported(AnyVersion)} + { + } + + Client& client; + WlHandle const manager; +}; + +wlcs::XdgOutputManagerV1::XdgOutputManagerV1(Client& client) + : impl{std::make_unique(client)} +{ +} + +wlcs::XdgOutputManagerV1::~XdgOutputManagerV1() = default; + +wlcs::XdgOutputManagerV1::operator zxdg_output_manager_v1*() const +{ + return impl->manager; +} + +auto wlcs::XdgOutputManagerV1::client() const -> Client& +{ + return impl->client; +} + +struct wlcs::XdgOutputV1::Impl +{ + Impl(XdgOutputManagerV1& manager, size_t output_index) + : output{zxdg_output_manager_v1_get_xdg_output( + manager, + manager.client().output_state(output_index).output)}, + version{zxdg_output_v1_get_version(output)} + { + zxdg_output_v1_add_listener(output, &Impl::listener, this); + } + + ~Impl() + { + zxdg_output_v1_destroy(output); + } + + zxdg_output_v1* const output; + uint32_t const version; + bool dirty{false}; + State _state; + + static zxdg_output_v1_listener const listener; +}; + +zxdg_output_v1_listener const wlcs::XdgOutputV1::Impl::listener = { + [] /* logical_position */ (void *data, zxdg_output_v1 *, int32_t x, int32_t y) + { + auto const impl = static_cast(data); + impl->_state.logical_position = std::make_pair(x, y); + impl->dirty = true; + }, + + [] /* logical_size */ (void *data, zxdg_output_v1 *, int32_t width, int32_t height) + { + auto const impl = static_cast(data); + impl->_state.logical_size = std::make_pair(width, height); + impl->dirty = true; + }, + + [] /* done */ (void *data, zxdg_output_v1 *) + { + auto const impl = static_cast(data); + if (impl->version < 3) + impl->dirty = false; + }, + + [] /* name */ (void *data, zxdg_output_v1 *, const char *name) + { + auto const impl = static_cast(data); + std::string str{name}; + impl->_state.name = str; + impl->dirty = true; + }, + + [] /* description */ (void *data, zxdg_output_v1 *, const char *description) + { + auto const impl = static_cast(data); + impl->_state.description = std::string{description}; + impl->dirty = true; + }, +}; + +wlcs::XdgOutputV1::XdgOutputV1(XdgOutputManagerV1& manager, size_t output_index) + : impl{std::make_shared(manager, output_index)} +{ + if (impl->version >= 3) + { + manager.client().add_output_done_notifier(output_index, [weak = std::weak_ptr(impl)]() + { + if (auto const impl = weak.lock()) + { + impl->dirty = false; + } + }); + } +} + +wlcs::XdgOutputV1::operator zxdg_output_v1*() const +{ + return impl->output; +} + +auto wlcs::XdgOutputV1::state() -> State const& +{ + if (impl->dirty) + { + std::string done_event = (impl->version >= 3 ? "wl_output.done" : "zxdg_output_v1.done"); + BOOST_THROW_EXCEPTION(std::logic_error("State change was not finished with a " + done_event + " event")); + } + + return impl->_state; +} diff --git a/src/xdg_shell_stable.cpp b/src/xdg_shell_stable.cpp new file mode 100644 index 0000000..3c6a2dc --- /dev/null +++ b/src/xdg_shell_stable.cpp @@ -0,0 +1,143 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "xdg_shell_stable.h" + +using namespace testing; + +// XdgSurfaceStable + +wlcs::XdgSurfaceStable::XdgSurfaceStable(wlcs::Client& client, wlcs::Surface& surface) +{ + EXPECT_CALL(*this, configure).Times(AnyNumber()); + if (!client.xdg_shell_stable()) + throw std::runtime_error("XDG shell stable not supported by compositor"); + shell_surface = xdg_wm_base_get_xdg_surface(client.xdg_shell_stable(), surface); + static struct xdg_surface_listener const listener = { + [](void* data, auto, auto... args) { static_cast(data)->configure(args...); }}; + xdg_surface_add_listener(shell_surface, &listener, this); +} + +wlcs::XdgSurfaceStable::~XdgSurfaceStable() +{ + xdg_surface_destroy(shell_surface); +} + +// XdgToplevelStable + +wlcs::XdgToplevelStable::State::State(int32_t width, int32_t height, struct wl_array *states) + : width{width}, + height{height}, + maximized{false}, + fullscreen{false}, + resizing{false}, + activated{false} +{ + if (!states) + return; + xdg_toplevel_state* item; + for (item = static_cast(states->data); + (char*)item < static_cast(states->data) + states->size; + item++) + { + switch (*item) + { + case XDG_TOPLEVEL_STATE_MAXIMIZED: + maximized = true; + break; + case XDG_TOPLEVEL_STATE_FULLSCREEN: + fullscreen = true; + break; + case XDG_TOPLEVEL_STATE_RESIZING: + resizing = true; + break; + case XDG_TOPLEVEL_STATE_ACTIVATED: + activated = true; + case XDG_TOPLEVEL_STATE_TILED_LEFT: + case XDG_TOPLEVEL_STATE_TILED_RIGHT: + case XDG_TOPLEVEL_STATE_TILED_BOTTOM: + case XDG_TOPLEVEL_STATE_TILED_TOP: + break; + } + } +} + +wlcs::XdgToplevelStable::XdgToplevelStable(XdgSurfaceStable& shell_surface_) + : shell_surface{&shell_surface_} +{ + EXPECT_CALL(*this, configure).Times(AnyNumber()); + EXPECT_CALL(*this, close).Times(AnyNumber()); + EXPECT_CALL(*this, configure_bounds).Times(AnyNumber()); + EXPECT_CALL(*this, wm_capabilities).Times(AnyNumber()); + toplevel = xdg_surface_get_toplevel(*shell_surface); + static struct xdg_toplevel_listener const listener = { + [](void* data, auto, auto... args) { static_cast(data)->configure(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->close(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->configure_bounds(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->wm_capabilities(args...); }}; + xdg_toplevel_add_listener(toplevel, &listener, this); +} + +wlcs::XdgToplevelStable::~XdgToplevelStable() +{ + xdg_toplevel_destroy(toplevel); +} + +wlcs::XdgPositionerStable::XdgPositionerStable(wlcs::Client& client) + : positioner{xdg_wm_base_create_positioner(client.xdg_shell_stable())} +{ +} + +wlcs::XdgPositionerStable::~XdgPositionerStable() +{ + xdg_positioner_destroy(positioner); +} + +auto wlcs::XdgPositionerStable::setup_default(std::pair size) -> XdgPositionerStable& +{ + xdg_positioner_set_size(positioner, size.first, size.second); + xdg_positioner_set_anchor_rect(positioner, 0, 0, 1, 1); + xdg_positioner_set_anchor(positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT); + xdg_positioner_set_gravity(positioner, XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT); + return *this; +} + +wlcs::XdgPopupStable::XdgPopupStable( + XdgSurfaceStable& shell_surface_, + std::optional parent, + XdgPositionerStable& positioner) + : shell_surface{&shell_surface_}, + popup{xdg_surface_get_popup( + *shell_surface, + parent ? *parent.value() : (xdg_surface*)nullptr, + positioner)} +{ + EXPECT_CALL(*this, configure).Times(AnyNumber()); + EXPECT_CALL(*this, done).Times(AnyNumber()); + EXPECT_CALL(*this, repositioned).Times(AnyNumber()); + static struct xdg_popup_listener const listener = { + [](void* data, auto, auto... args) { static_cast(data)->configure(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->done(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->repositioned(args...); }}; + xdg_popup_add_listener(popup, &listener, this); +} + +wlcs::XdgPopupStable::~XdgPopupStable() +{ + xdg_popup_destroy(popup); +} diff --git a/src/xdg_shell_v6.cpp b/src/xdg_shell_v6.cpp new file mode 100644 index 0000000..2d3f848 --- /dev/null +++ b/src/xdg_shell_v6.cpp @@ -0,0 +1,118 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "xdg_shell_v6.h" + +using namespace testing; + +// XdgSurfaceV6 + +wlcs::XdgSurfaceV6::XdgSurfaceV6(wlcs::Client& client, wlcs::Surface& surface) +{ + EXPECT_CALL(*this, configure).Times(AnyNumber()); + if (!client.xdg_shell_v6()) + throw std::runtime_error("XDG shell unstable V6 not supported by compositor"); + shell_surface = zxdg_shell_v6_get_xdg_surface(client.xdg_shell_v6(), surface); + static struct zxdg_surface_v6_listener const listener = { + [](void* data, auto, auto... args) { static_cast(data)->configure(args...); }}; + zxdg_surface_v6_add_listener(shell_surface, &listener, this); +} + +wlcs::XdgSurfaceV6::~XdgSurfaceV6() +{ + zxdg_surface_v6_destroy(shell_surface); +} + +// XdgToplevelV6 + +wlcs::XdgToplevelV6::State::State(int32_t width, int32_t height, struct wl_array *states) + : width{width}, + height{height}, + maximized{false}, + fullscreen{false}, + resizing{false}, + activated{false} +{ + if (!states) + return; + zxdg_toplevel_v6_state* item; + for (item = static_cast(states->data); + (char*)item < static_cast(states->data) + states->size; + item++) + { + switch (*item) + { + case ZXDG_TOPLEVEL_V6_STATE_MAXIMIZED: + maximized = true; + break; + case ZXDG_TOPLEVEL_V6_STATE_FULLSCREEN: + fullscreen = true; + break; + case ZXDG_TOPLEVEL_V6_STATE_RESIZING: + resizing = true; + break; + case ZXDG_TOPLEVEL_V6_STATE_ACTIVATED: + activated = true; + break; + } + } +} + +wlcs::XdgToplevelV6::XdgToplevelV6(XdgSurfaceV6& shell_surface_) + : shell_surface{&shell_surface_} +{ + EXPECT_CALL(*this, configure).Times(AnyNumber()); + EXPECT_CALL(*this, close).Times(AnyNumber()); + toplevel = zxdg_surface_v6_get_toplevel(*shell_surface); + static struct zxdg_toplevel_v6_listener const listener = { + [](void* data, auto, auto... args) { static_cast(data)->configure(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->close(args...); }}; + zxdg_toplevel_v6_add_listener(toplevel, &listener, this); +} + +wlcs::XdgToplevelV6::~XdgToplevelV6() +{ + zxdg_toplevel_v6_destroy(toplevel); +} + +wlcs::XdgPositionerV6::XdgPositionerV6(wlcs::Client& client) + : positioner{zxdg_shell_v6_create_positioner(client.xdg_shell_v6())} +{ +} + +wlcs::XdgPositionerV6::~XdgPositionerV6() +{ + zxdg_positioner_v6_destroy(positioner); +} + +wlcs::XdgPopupV6::XdgPopupV6(XdgSurfaceV6& shell_surface_, XdgSurfaceV6& parent, XdgPositionerV6& positioner) + : shell_surface{&shell_surface_}, + popup{zxdg_surface_v6_get_popup(*shell_surface, parent, positioner)} +{ + EXPECT_CALL(*this, configure).Times(AnyNumber()); + EXPECT_CALL(*this, done).Times(AnyNumber()); + static struct zxdg_popup_v6_listener const listener = { + [](void* data, auto, auto... args) { static_cast(data)->configure(args...); }, + [](void* data, auto, auto... args) { static_cast(data)->done(args...); }}; + zxdg_popup_v6_add_listener(popup, &listener, this); +} + +wlcs::XdgPopupV6::~XdgPopupV6() +{ + zxdg_popup_v6_destroy(popup); +} diff --git a/src/xfail_supporting_test_listener.cpp b/src/xfail_supporting_test_listener.cpp new file mode 100644 index 0000000..77e1a64 --- /dev/null +++ b/src/xfail_supporting_test_listener.cpp @@ -0,0 +1,212 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#include "xfail_supporting_test_listener.h" +#include +#include +#include "termcolor.hpp" + +testing::XFailSupportingTestListenerWrapper::XFailSupportingTestListenerWrapper(std::unique_ptr&& wrapped) + : delegate{std::move(wrapped)} +{ +} + +void testing::XFailSupportingTestListenerWrapper::OnTestProgramStart(testing::UnitTest const& unit_test) +{ + delegate->OnTestProgramStart(unit_test); +} + +void testing::XFailSupportingTestListenerWrapper::OnTestIterationStart(testing::UnitTest const& unit_test, int iteration) +{ + failed_test_names.clear(); + skipped_test_names.clear(); + delegate->OnTestIterationStart(unit_test, iteration); +} + +void testing::XFailSupportingTestListenerWrapper::OnEnvironmentsSetUpStart(testing::UnitTest const& unit_test) +{ + delegate->OnEnvironmentsSetUpStart(unit_test); +} + +void testing::XFailSupportingTestListenerWrapper::OnEnvironmentsSetUpEnd(testing::UnitTest const& unit_test) +{ + delegate->OnEnvironmentsSetUpEnd(unit_test); +} + +void testing::XFailSupportingTestListenerWrapper::OnTestCaseStart(testing::TestCase const& test_case) +{ + delegate->OnTestCaseStart(test_case); +} + +void testing::XFailSupportingTestListenerWrapper::OnTestStart(testing::TestInfo const& test_info) +{ + current_test_info = &test_info; + current_test_start = std::chrono::steady_clock::now(); + delegate->OnTestStart(test_info); +} + +void testing::XFailSupportingTestListenerWrapper::OnTestPartResult(testing::TestPartResult const& test_part_result) +{ + if (test_part_result.failed()) + { + // Search for expected-failure property + for (int i= 0; i < current_test_info->result()->test_property_count() ; ++i) + { + auto prop = current_test_info->result()->GetTestProperty(i); + if (strcmp(prop.key(), "wlcs-skip-test") == 0) + { + if (!current_skip_reasons) + { + current_skip_reasons = std::vector{}; + } + current_skip_reasons->push_back(prop.value()); + } + } + if (current_skip_reasons) + { + skipped_test_names.insert(std::string{current_test_info->test_case_name()} + "." + current_test_info->name()); + return; + } + + failed_test_names.insert(std::string{current_test_info->test_case_name()} + "." + current_test_info->name()); + } + delegate->OnTestPartResult(test_part_result); +} + +void testing::XFailSupportingTestListenerWrapper::OnTestEnd(testing::TestInfo const& test_info) +{ + if (current_skip_reasons) + { + for (auto const& reason : *current_skip_reasons) + { + std::cout + << termcolor::yellow << "[ ]" + << termcolor::reset << " " << reason << std::endl; + } + auto elapsed_time = std::chrono::steady_clock::now() - current_test_start; + + std::cout + << termcolor::yellow << "[ SKIP ]" + << termcolor::reset << " " + << test_info.test_case_name() << "." << test_info.name() + << " (" + << std::chrono::duration_cast(elapsed_time).count() + << "ms)" << std::endl; + } + else + { + delegate->OnTestEnd(test_info); + } + current_skip_reasons = {}; +} + +void testing::XFailSupportingTestListenerWrapper::OnTestCaseEnd(testing::TestCase const& test_case) +{ + delegate->OnTestCaseEnd(test_case); +} + +void testing::XFailSupportingTestListenerWrapper::OnEnvironmentsTearDownStart(testing::UnitTest const& unit_test) +{ + delegate->OnEnvironmentsTearDownStart(unit_test); +} + +void testing::XFailSupportingTestListenerWrapper::OnEnvironmentsTearDownEnd(testing::UnitTest const& unit_test) +{ + delegate->OnEnvironmentsTearDownEnd(unit_test); +} + +namespace +{ +std::string singular_or_plural(char const* base, int count) +{ + if (count == 1) + { + return base; + } + return std::string{base} + "s"; +} +} + +void testing::XFailSupportingTestListenerWrapper::OnTestIterationEnd(testing::UnitTest const& unit_test, int /*iteration*/) +{ + std::cout + << termcolor::green << "[==========] " + << termcolor::reset + << unit_test.test_to_run_count() + << " tests from " + << unit_test.test_case_to_run_count() + << " test cases run. (" + << unit_test.elapsed_time() + << "ms total elapsed)" << std::endl; + + std::cout + << termcolor::green << "[ PASSED ]" + << termcolor::reset + << " " + << unit_test.successful_test_count() + << singular_or_plural(" test", unit_test.successful_test_count()) << std::endl; + + auto const skipped_tests = skipped_test_names.size(); + if (skipped_tests > 0) + { + std::cout + << termcolor::yellow << "[ SKIPPED ] " + << termcolor::reset + << skipped_tests + << singular_or_plural(" test", skipped_tests) + << " skipped:" << std::endl; + for (auto const& name : skipped_test_names) + { + std::cout + << termcolor::yellow << "[ SKIPPED ] " + << termcolor::reset << name << std::endl; + } + } + auto const failed_tests = failed_test_names.size(); + if (failed_tests > 0) + { + /* Mark the test run as a failure; if multiple iterations are run + * only one might fail, making failed_test_names() an unreliable + * indicator. + */ + failed_ = true; + + std::cout + << termcolor::red << "[ FAILED ] " + << termcolor::reset + << failed_tests + << singular_or_plural(" test", failed_tests) + << " failed:" << std::endl; + for (auto const& name : failed_test_names) + { + std::cout + << termcolor::red << "[ FAILED ] " + << termcolor::reset << name << std::endl; + } + } +} + +void testing::XFailSupportingTestListenerWrapper::OnTestProgramEnd(testing::UnitTest const& unit_test) +{ + delegate->OnTestProgramEnd(unit_test); +} + +bool testing::XFailSupportingTestListenerWrapper::failed() const +{ + return failed_; +} diff --git a/src/xfail_supporting_test_listener.h b/src/xfail_supporting_test_listener.h new file mode 100644 index 0000000..491c561 --- /dev/null +++ b/src/xfail_supporting_test_listener.h @@ -0,0 +1,78 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Christopher James Halse Rogers + */ + +#ifndef WLCS_XFAIL_SUPPORTING_TEST_LISTENER_H_ +#define WLCS_XFAIL_SUPPORTING_TEST_LISTENER_H_ + +#include + +#include +#include +#include +#include +#include + +namespace testing +{ +class XFailSupportingTestListenerWrapper : public testing::TestEventListener +{ +public: + explicit XFailSupportingTestListenerWrapper(std::unique_ptr&& wrapped); + + void OnTestProgramStart(testing::UnitTest const& unit_test) override; + + void OnTestIterationStart(testing::UnitTest const& unit_test, int iteration) override; + + void OnEnvironmentsSetUpStart(testing::UnitTest const& unit_test) override; + + void OnEnvironmentsSetUpEnd(testing::UnitTest const& unit_test) override; + + void OnTestCaseStart(testing::TestCase const& test_case) override; + + void OnTestStart(testing::TestInfo const& test_info) override; + + void OnTestPartResult(testing::TestPartResult const& test_part_result) override; + + void OnTestEnd(testing::TestInfo const& test_info) override; + void OnTestCaseEnd(testing::TestCase const& test_case) override; + + void OnEnvironmentsTearDownStart(testing::UnitTest const& unit_test) override; + + void OnEnvironmentsTearDownEnd(testing::UnitTest const& unit_test) override; + + void OnTestIterationEnd(testing::UnitTest const& unit_test, int iteration) override; + + void OnTestProgramEnd(testing::UnitTest const& unit_test) override; + + bool failed() const; +private: + std::unique_ptr const delegate; + + std::chrono::steady_clock::time_point current_test_start; + ::testing::TestInfo const* current_test_info; + std::optional> current_skip_reasons; + + std::unordered_set failed_test_names; + std::unordered_set skipped_test_names; + + bool failed_{false}; +}; + +} + +#endif //WLCS_XFAIL_SUPPORTING_TEST_LISTENER_H_ diff --git a/tests/copy_cut_paste.cpp b/tests/copy_cut_paste.cpp new file mode 100644 index 0000000..de29435 --- /dev/null +++ b/tests/copy_cut_paste.cpp @@ -0,0 +1,127 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "data_device.h" +#include "helpers.h" +#include "in_process_server.h" +#include "wl_handle.h" +#include "version_specifier.h" + +#include + +#include + +using namespace testing; +using namespace wlcs; + +namespace +{ + +auto static const any_width = 100; +auto static const any_height = 100; +auto static const any_mime_type = "AnyMimeType"; + +struct CCnPSource : Client +{ + using Client::Client; + + Surface const surface{create_visible_surface(any_width, any_height)}; + WlHandle const manager{ + this->bind_if_supported(AnyVersion)}; + DataDevice data_device{wl_data_device_manager_get_data_device(manager,seat())}; + DataSource data{wl_data_device_manager_create_data_source(manager)}; + + void offer(char const* mime_type) + { + wl_data_source_offer(data, mime_type); + // TODO: collect a serial from the "event that triggered" the selection + // (This works while Mir fails to validate the serial) + uint32_t const serial = 0; + wl_data_device_set_selection(data_device, data, serial); + roundtrip(); + } +}; + +struct MockDataDeviceListener : DataDeviceListener +{ + using DataDeviceListener::DataDeviceListener; + + MOCK_METHOD2(data_offer, void(struct wl_data_device* wl_data_device, struct wl_data_offer* id)); +}; + +struct MockDataOfferListener : DataOfferListener +{ + using DataOfferListener::DataOfferListener; + + MOCK_METHOD2(offer, void(struct wl_data_offer* data_offer, char const* MimeType)); +}; + +struct CCnPSink : Client +{ + using Client::Client; + + WlHandle const manager{ + this->bind_if_supported(AnyVersion)}; + DataDevice sink_data{wl_data_device_manager_get_data_device(manager, seat())}; + MockDataDeviceListener listener{sink_data}; + + Surface create_surface_with_focus() + { + return Surface{create_visible_surface(any_width, any_height)}; + } +}; + +struct CopyCutPaste : StartedInProcessServer +{ + CCnPSource source{the_server()}; + CCnPSink sink{the_server()}; + + MockDataOfferListener mdol; + + void TearDown() override + { + source.roundtrip(); + sink.roundtrip(); + StartedInProcessServer::TearDown(); + } +}; +} + +TEST_F(CopyCutPaste, given_source_has_offered_when_sink_gets_focus_it_sees_offer) +{ + source.offer(any_mime_type); + + EXPECT_CALL(mdol, offer(_, StrEq(any_mime_type))); + EXPECT_CALL(sink.listener, data_offer(_,_)) + .WillOnce(Invoke([&](struct wl_data_device*, struct wl_data_offer* id) + { mdol.listen_to(id); })); + + sink.create_surface_with_focus(); +} + +TEST_F(CopyCutPaste, given_sink_has_focus_when_source_makes_offer_sink_sees_offer) +{ + auto sink_surface_with_focus = sink.create_surface_with_focus(); + + EXPECT_CALL(mdol, offer(_, StrEq(any_mime_type))); + EXPECT_CALL(sink.listener, data_offer(_,_)) + .WillOnce(Invoke([&](struct wl_data_device*, struct wl_data_offer* id) + { mdol.listen_to(id); })); + + source.offer(any_mime_type); +} diff --git a/tests/frame_submission.cpp b/tests/frame_submission.cpp new file mode 100644 index 0000000..c0db929 --- /dev/null +++ b/tests/frame_submission.cpp @@ -0,0 +1,146 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "helpers.h" +#include "in_process_server.h" + +#include + +using namespace testing; +using namespace wlcs; + +namespace +{ + +struct FrameSubmission : StartedInProcessServer +{ + Client client{the_server()}; + Surface surface{client.create_visible_surface(200, 200)}; + + void submit_frame(bool& frame_consumed); + + void wait_for_frame(bool const& frame_consumed); +}; + +void FrameSubmission::submit_frame(bool& consumed_flag) +{ + static wl_callback_listener const frame_listener + { + [](void *data, struct wl_callback* callback, uint32_t /*callback_data*/) + { + *static_cast(data) = true; + wl_callback_destroy(callback); + } + }; + + consumed_flag = false; + wl_callback_add_listener(wl_surface_frame(surface), &frame_listener, &consumed_flag); + auto buffer = std::make_shared(client, 200, 200); + wl_surface_attach(surface, *buffer, 0, 0); + wl_surface_commit(surface); +} + +void FrameSubmission::wait_for_frame(bool const& consumed_flag) +{ + // TODO timeout + client.dispatch_until([&consumed_flag]() { return consumed_flag; }); +} +} + +TEST_F(FrameSubmission, post_one_frame_at_a_time) +{ + for (auto i = 0; i != 10; ++i) + { + auto frame_consumed = false; + + submit_frame(frame_consumed); + wait_for_frame(frame_consumed); + + EXPECT_THAT(frame_consumed, Eq(true)); + } +} + +// Regression test for https://github.com/MirServer/mir/issues/2960 +TEST_F(FrameSubmission, test_buffer_can_be_deleted_after_attached) +{ + using namespace testing; + + wlcs::Client client{the_server()}; + auto surface = client.create_visible_surface(200, 200); + + auto buffer = std::make_shared(client, 200, 200); + wl_surface_attach(surface, *buffer, 0, 0); + buffer.reset(); + /* Check that the destroyed buffer doesn't crash the server + * It's not clear what the *correct* behaviour is in this case: + * Weston treats this as committing a null buffer, wlroots appears to + * treat this as committing the content of the buffer before deletion. + * + * *Whatever* the correct behaviour is, "crash" is *definitely* + * incorrect. + */ + wl_surface_commit(surface); + + // Roundtrip to ensure the server has processed our weirdness + client.roundtrip(); +} + +/* Firefox has a lovely habit of sending an endless stream of + * wl_surface.frame() requests (maybe it's trying to estimate vsync?!) + * + * If a compositor responds immediately to the frame on commit, Firefox + * ends up looping endlessly. If the compositor *doesn't* respond to the frame + * request, Firefox decides not to draw anything. 🤷‍♀️ + */ +TEST_F(FrameSubmission, when_client_endlessly_requests_frame_then_callbacks_are_throttled) +{ + using namespace testing; + using namespace std::chrono_literals; + + wlcs::Client annoying_client{the_server()}; + auto surface = client.create_visible_surface(200, 200); + + bool frame_callback_called{false}; + + wl_callback_listener const listener = { + .done = [](void* data, struct wl_callback* callback, [[maybe_unused]]uint32_t timestamp) + { + wl_callback_destroy(callback); + + auto callback_called = static_cast(data); + *callback_called = true; + } + }; + + auto const timeout = std::chrono::steady_clock::now() + 10s; + + do + { + frame_callback_called = false; + wl_callback_add_listener(wl_surface_frame(surface), &listener, &frame_callback_called); + wl_surface_commit(surface); + client.roundtrip(); + /* This roundtrip ensures the server has processed everything prior. + * In particular, if the server sends the frame callback in response to wl_surface.commit, that frame callback + * will have been processed by now. + */ + } + while (frame_callback_called && std::chrono::steady_clock::now() < timeout); + + EXPECT_THAT(std::chrono::steady_clock::now(), Lt(timeout)) << "Timed out looping in frame callback storm"; +} diff --git a/tests/gtk_primary_selection.cpp b/tests/gtk_primary_selection.cpp new file mode 100644 index 0000000..85f0b93 --- /dev/null +++ b/tests/gtk_primary_selection.cpp @@ -0,0 +1,230 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "gtk_primary_selection.h" + +#include "in_process_server.h" +#include "version_specifier.h" + +#include +#include + +using namespace wlcs; +using namespace testing; + +namespace +{ +char const any_mime_type[] = "AnyMimeType"; +char const any_mime_data[] = "AnyMimeData"; + +struct SourceApp : Client +{ + using Client::Client; + + WlHandle manager{ + this->bind_if_supported(AnyVersion)}; + GtkPrimarySelectionSource source{manager}; + GtkPrimarySelectionDevice device{manager, seat()}; + + void set_selection() + { + gtk_primary_selection_device_set_selection(device, source, 0); + roundtrip(); + } + + void offer(char const* mime_type) + { + gtk_primary_selection_source_offer(source, mime_type); + roundtrip(); + } +}; + +struct SinkApp : Client +{ + explicit SinkApp(Server& server) : Client{server} { roundtrip(); } + + WlHandle const manager{ + this->bind_if_supported(AnyVersion)}; + GtkPrimarySelectionDevice device{manager, seat()}; +}; + +struct GtkPrimarySelection : StartedInProcessServer +{ + SourceApp source_app{the_server()}; + SinkApp sink_app{the_server()}; + + void TearDown() override + { + source_app.roundtrip(); + sink_app.roundtrip(); + StartedInProcessServer::TearDown(); + } +}; + +struct MockGtkPrimarySelectionDeviceListener : GtkPrimarySelectionDeviceListener +{ + using GtkPrimarySelectionDeviceListener::GtkPrimarySelectionDeviceListener; + + MOCK_METHOD2(data_offer, void(gtk_primary_selection_device* device, gtk_primary_selection_offer* offer)); + MOCK_METHOD2(selection, void(gtk_primary_selection_device* device, gtk_primary_selection_offer* offer)); +}; + +struct MockGtkPrimarySelectionOfferListener : GtkPrimarySelectionOfferListener +{ + using GtkPrimarySelectionOfferListener::GtkPrimarySelectionOfferListener; + + MOCK_METHOD2(offer, void(gtk_primary_selection_offer* offer, const char* mime_type)); +}; + +struct MockGtkPrimarySelectionSourceListener : GtkPrimarySelectionSourceListener +{ + using GtkPrimarySelectionSourceListener::GtkPrimarySelectionSourceListener; + + MOCK_METHOD3(send, void(gtk_primary_selection_source* source, const char* mime_type, int32_t fd)); + + MOCK_METHOD1(cancelled, void(gtk_primary_selection_source*)); +}; + +struct StubGtkPrimarySelectionDeviceListener : GtkPrimarySelectionDeviceListener +{ + StubGtkPrimarySelectionDeviceListener( + gtk_primary_selection_device* device, + GtkPrimarySelectionOfferListener& offer_listener) : + GtkPrimarySelectionDeviceListener{device}, + offer_listener{offer_listener} + { + } + + void data_offer(gtk_primary_selection_device* device, gtk_primary_selection_offer* offer) override + { + offer_listener.listen_to(offer); + GtkPrimarySelectionDeviceListener::data_offer(device, offer); + } + + void selection(gtk_primary_selection_device* device, gtk_primary_selection_offer* offer) override + { + selected = offer; + GtkPrimarySelectionDeviceListener::selection(device, offer); + } + + GtkPrimarySelectionOfferListener& offer_listener; + gtk_primary_selection_offer* selected = nullptr; +}; + +struct StubGtkPrimarySelectionSourceListener : GtkPrimarySelectionSourceListener +{ + using GtkPrimarySelectionSourceListener::GtkPrimarySelectionSourceListener; + + void send(gtk_primary_selection_source*, const char*, int32_t fd) + { + ASSERT_THAT(write(fd, any_mime_data, sizeof any_mime_data), Eq(ssize_t(sizeof any_mime_data))); + close(fd); + } +}; + +struct Pipe +{ + int source; + int sink; + + Pipe() { socketpair(AF_LOCAL, SOCK_STREAM, 0, &source); } + Pipe(Pipe const&) = delete; + Pipe& operator=(Pipe const&) = delete; + + ~Pipe() { close(source); close(sink); } +}; +} + +TEST_F(GtkPrimarySelection, source_can_offer) +{ + source_app.offer(any_mime_type); + source_app.set_selection(); +} + +TEST_F(GtkPrimarySelection, sink_can_listen) +{ + MockGtkPrimarySelectionDeviceListener device_listener{sink_app.device}; + MockGtkPrimarySelectionOfferListener offer_listener; + + InSequence seq; + EXPECT_CALL(device_listener, data_offer(_, _)) + .WillOnce(Invoke([&](auto*, auto* id) { offer_listener.listen_to(id); })); + + EXPECT_CALL(offer_listener, offer(_, StrEq(any_mime_type))); + + EXPECT_CALL(device_listener, selection(_, _)); + + source_app.offer(any_mime_type); + source_app.set_selection(); + + sink_app.roundtrip(); +} + +TEST_F(GtkPrimarySelection, sink_can_request) +{ + GtkPrimarySelectionOfferListener offer_listener; + StubGtkPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener}; + source_app.offer(any_mime_type); + source_app.set_selection(); + sink_app.roundtrip(); + ASSERT_THAT(device_listener.selected, NotNull()); + + Pipe pipe; + gtk_primary_selection_offer_receive(device_listener.selected, any_mime_type, pipe.source); + sink_app.roundtrip(); +} + +TEST_F(GtkPrimarySelection, source_sees_request) +{ + MockGtkPrimarySelectionSourceListener source_listener{source_app.source}; + GtkPrimarySelectionOfferListener offer_listener; + StubGtkPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener}; + source_app.offer(any_mime_type); + source_app.set_selection(); + sink_app.roundtrip(); + ASSERT_THAT(device_listener.selected, NotNull()); + + EXPECT_CALL(source_listener, send(_, _, _)) + .Times(1) + .WillRepeatedly(Invoke([&](auto*, auto*, int fd) { close(fd); })); + + Pipe pipe; + gtk_primary_selection_offer_receive(device_listener.selected, any_mime_type, pipe.source); + sink_app.roundtrip(); + source_app.roundtrip(); +} + +TEST_F(GtkPrimarySelection, source_can_supply_request) +{ + StubGtkPrimarySelectionSourceListener source_listener{source_app.source}; + GtkPrimarySelectionOfferListener offer_listener; + StubGtkPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener}; + source_app.offer(any_mime_type); + source_app.set_selection(); + sink_app.roundtrip(); + ASSERT_THAT(device_listener.selected, NotNull()); + + Pipe pipe; + gtk_primary_selection_offer_receive(device_listener.selected, any_mime_type, pipe.source); + sink_app.roundtrip(); + source_app.roundtrip(); + + char buffer[128]; + EXPECT_THAT(read(pipe.sink, buffer, sizeof buffer), Eq(ssize_t(sizeof any_mime_data))); + EXPECT_THAT(buffer, StrEq(any_mime_data)); +} diff --git a/tests/pointer_constraints.cpp b/tests/pointer_constraints.cpp new file mode 100644 index 0000000..6ee856f --- /dev/null +++ b/tests/pointer_constraints.cpp @@ -0,0 +1,301 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "pointer_constraints_unstable_v1.h" +#include "helpers.h" +#include "in_process_server.h" + +#include + +#include + +using testing::AnyNumber; +using testing::Eq; +using testing::Ne; +using testing::NotNull; + +using namespace wlcs; + +namespace +{ +auto const any_width = 300; +auto const any_height = 300; +auto const nw_middle_x = any_width / 2; +auto const nw_middle_y = any_height / 2; +auto const se_middle_x = any_width + nw_middle_x; +auto const se_middle_y = any_height + nw_middle_y; + +struct PointerConstraints : StartedInProcessServer +{ + // Create a client with two surface (ne & sw) and a cursor centered on the nw_surface + Client client{the_server()}; + Surface se_surface{client.create_visible_surface(any_width, any_height)}; + Surface nw_surface{client.create_visible_surface(any_width, any_height)}; + wl_pointer* const pointer = client.the_pointer(); + + Pointer cursor = the_server().create_pointer(); + + ZwpPointerConstraintsV1 pointer_constraints{client}; + + std::unique_ptr locked_ptr = nullptr; + std::unique_ptr confined_ptr = nullptr; + + void SetUp() override + { + StartedInProcessServer::SetUp(); + + // Get the surface in a known position with the cursor over it + the_server().move_surface_to(nw_surface, 0, 0); + cursor.move_to(nw_middle_x, nw_middle_y); + the_server().move_surface_to(se_surface, any_width, any_height); + } + + void TearDown() override + { + locked_ptr.reset(); + confined_ptr.reset(); + client.roundtrip(); + StartedInProcessServer::TearDown(); + } + + void setup_locked_ptr_on(Surface& surface, zwp_pointer_constraints_v1_lifetime lifetime = ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT) + { + locked_ptr = std::make_unique(pointer_constraints, surface, pointer, nullptr, lifetime); + } + + void setup_confined_ptr_on(Surface& surface, zwp_pointer_constraints_v1_lifetime lifetime = ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT) + { + confined_ptr = std::make_unique(pointer_constraints, surface, pointer, nullptr, lifetime); + } + + void setup_sync() + { + client.roundtrip(); + using namespace testing; + if (locked_ptr) Mock::VerifyAndClearExpectations(locked_ptr.get()); + if (confined_ptr) Mock::VerifyAndClearExpectations(confined_ptr.get()); + } + + void select_se_window() + { + cursor.move_to(se_middle_x, se_middle_y); + cursor.left_click(); + client.roundtrip(); + } + + void select_nw_window() + { + cursor.move_to(nw_middle_x, nw_middle_y); + cursor.left_click(); + client.roundtrip(); + } +}; +} + +TEST_F(PointerConstraints, can_get_locked_pointer) +{ + setup_locked_ptr_on(nw_surface); + + EXPECT_THAT(*locked_ptr, NotNull()); +} + +TEST_F(PointerConstraints, locked_pointer_on_initially_focussed_surface_gets_locked_notification) +{ + setup_locked_ptr_on(nw_surface); + + EXPECT_CALL(*locked_ptr, locked()).Times(1); + + client.roundtrip(); +} + +TEST_F(PointerConstraints, locked_pointer_does_not_move) +{ + auto const initial_pointer_position = client.pointer_position(); + setup_locked_ptr_on(nw_surface); + EXPECT_CALL(*locked_ptr, locked()).Times(AnyNumber()); + setup_sync(); + + cursor.move_by(10, 10); + client.roundtrip(); + + EXPECT_THAT(client.pointer_position(), Eq(initial_pointer_position)); +} + +TEST_F(PointerConstraints, locked_pointer_on_initially_unfocussed_surface_gets_no_locked_notification) +{ + setup_locked_ptr_on(se_surface); + + EXPECT_CALL(*locked_ptr, locked()).Times(0); + + client.roundtrip(); +} + +TEST_F(PointerConstraints, when_surface_is_selected_locked_pointer_gets_locked_notification) +{ + setup_locked_ptr_on(se_surface); + setup_sync(); + + EXPECT_CALL(*locked_ptr, locked()).Times(1); + + select_se_window(); +} + +TEST_F(PointerConstraints, when_surface_is_unselected_locked_pointer_gets_unlocked_notification) +{ + setup_locked_ptr_on(nw_surface); + EXPECT_CALL(*locked_ptr, locked()).Times(AnyNumber()); + setup_sync(); + + EXPECT_CALL(*locked_ptr, unlocked()).Times(1); + + // A new surface will be given focus + Surface a_new_surface{client.create_visible_surface(any_width, any_height)}; + client.roundtrip(); +} + +TEST_F(PointerConstraints, when_surface_is_reselected_persistent_locked_pointer_gets_notifications) +{ + setup_locked_ptr_on(nw_surface, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + EXPECT_CALL(*locked_ptr, locked()).Times(AnyNumber()); + setup_sync(); + + for (auto i = 3; i-- != 0;) + { + { + EXPECT_CALL(*locked_ptr, unlocked()).Times(1); + // A new surface will be given focus + Surface a_new_surface{client.create_visible_surface(any_width, any_height)}; + setup_sync(); + + EXPECT_CALL(*locked_ptr, locked()).Times(1); + } + client.roundtrip(); + } +} + +TEST_F(PointerConstraints, can_get_confined_pointer) +{ + setup_confined_ptr_on(nw_surface); + EXPECT_THAT(*confined_ptr, NotNull()); +} + +TEST_F(PointerConstraints, confined_pointer_on_initially_focussed_surface_gets_confined_notification) +{ + setup_confined_ptr_on(nw_surface); + + EXPECT_CALL(*confined_ptr, confined()).Times(1); + + client.roundtrip(); +} + +TEST_F(PointerConstraints, confined_pointer_does_move) +{ + auto const initial_pointer_position = client.pointer_position(); + setup_confined_ptr_on(nw_surface); + EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber()); + setup_sync(); + + cursor.move_by(10, 10); + client.roundtrip(); + + EXPECT_THAT(client.pointer_position(), Ne(initial_pointer_position)); +} + +TEST_F(PointerConstraints, confined_pointer_movement_is_constrained) +{ + auto const top = wl_fixed_from_int(0); + auto const left = wl_fixed_from_int(0); + auto const bottom = wl_fixed_from_int(any_height-1); + auto const right = wl_fixed_from_int(any_width-1); + + setup_confined_ptr_on(nw_surface); + EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber()); + setup_sync(); + + cursor.move_by(2*any_width, 2*any_height); + client.roundtrip(); + + EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(bottom, right))); + + cursor.move_by(-2*any_width, -2*any_height); + client.roundtrip(); + + EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(top, left))); + + cursor.move_by(-2*any_width, 2*any_height); + client.roundtrip(); + + EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(top, right))); + + cursor.move_by(2*any_width, -2*any_height); + client.roundtrip(); + + EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(bottom, left))); +} + +TEST_F(PointerConstraints, confined_pointer_on_initially_unfocussed_surface_gets_no_confined_notification) +{ + setup_confined_ptr_on(se_surface); + + EXPECT_CALL(*confined_ptr, confined()).Times(0); + + client.roundtrip(); +} + +TEST_F(PointerConstraints, when_surface_is_selected_confined_pointer_gets_confined_notification) +{ + setup_confined_ptr_on(se_surface); + setup_sync(); + + EXPECT_CALL(*confined_ptr, confined()).Times(1); + + select_se_window(); +} + +TEST_F(PointerConstraints, when_surface_is_unselected_confined_pointer_gets_unconfined_notification) +{ + setup_confined_ptr_on(nw_surface); + EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber()); + setup_sync(); + + EXPECT_CALL(*confined_ptr, unconfined()).Times(1); + + // A new surface will be given focus + Surface a_new_surface{client.create_visible_surface(any_width, any_height)}; + client.roundtrip(); +} + +TEST_F(PointerConstraints, when_surface_is_reselected_persistent_confined_pointer_gets_notifications) +{ + setup_confined_ptr_on(nw_surface, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber()); + setup_sync(); + + for (auto i = 3; i-- != 0;) + { + { + EXPECT_CALL(*confined_ptr, unconfined()).Times(1); + // A new surface will be given focus + Surface a_new_surface{client.create_visible_surface(any_width, any_height)}; + setup_sync(); + + EXPECT_CALL(*confined_ptr, confined()).Times(1); + } + client.roundtrip(); + } +} diff --git a/tests/primary_selection.cpp b/tests/primary_selection.cpp new file mode 100644 index 0000000..2f18a4d --- /dev/null +++ b/tests/primary_selection.cpp @@ -0,0 +1,231 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "primary_selection.h" + +#include "in_process_server.h" +#include "version_specifier.h" + +#include +#include + +using namespace wlcs; +using namespace testing; + +namespace +{ +char const any_mime_type[] = "AnyMimeType"; +char const any_mime_data[] = "AnyMimeData"; + +struct SourceApp : Client +{ + using Client::Client; + + WlHandle const manager{ + this->bind_if_supported(AnyVersion)}; + PrimarySelectionSource source{manager}; + PrimarySelectionDevice device{manager, seat()}; + + void set_selection() + { + zwp_primary_selection_device_v1_set_selection(device, source, 0); + roundtrip(); + } + + void offer(char const* mime_type) + { + zwp_primary_selection_source_v1_offer(source, mime_type); + roundtrip(); + } +}; + +struct SinkApp : Client +{ + explicit SinkApp(Server& server) : Client{server} { roundtrip(); } + + WlHandle const manager{ + this->bind_if_supported(AnyVersion)}; + PrimarySelectionDevice device{manager, seat()}; +}; + +struct PrimarySelection : StartedInProcessServer +{ + SourceApp source_app{the_server()}; + SinkApp sink_app{the_server()}; + Surface surface{sink_app.create_visible_surface(10, 10)}; + + void TearDown() override + { + source_app.roundtrip(); + sink_app.roundtrip(); + StartedInProcessServer::TearDown(); + } +}; + +struct MockPrimarySelectionDeviceListener : PrimarySelectionDeviceListener +{ + using PrimarySelectionDeviceListener::PrimarySelectionDeviceListener; + + MOCK_METHOD2(data_offer, void(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer)); + MOCK_METHOD2(selection, void(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer)); +}; + +struct MockPrimarySelectionOfferListener : PrimarySelectionOfferListener +{ + using PrimarySelectionOfferListener::PrimarySelectionOfferListener; + + MOCK_METHOD2(offer, void(zwp_primary_selection_offer_v1* offer, const char* mime_type)); +}; + +struct MockPrimarySelectionSourceListener : PrimarySelectionSourceListener +{ + using PrimarySelectionSourceListener::PrimarySelectionSourceListener; + + MOCK_METHOD3(send, void(zwp_primary_selection_source_v1* source, const char* mime_type, int32_t fd)); + + MOCK_METHOD1(cancelled, void(zwp_primary_selection_source_v1*)); +}; + +struct StubPrimarySelectionDeviceListener : PrimarySelectionDeviceListener +{ + StubPrimarySelectionDeviceListener( + zwp_primary_selection_device_v1* device, + PrimarySelectionOfferListener& offer_listener) : + PrimarySelectionDeviceListener{device}, + offer_listener{offer_listener} + { + } + + void data_offer(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer) override + { + offer_listener.listen_to(offer); + PrimarySelectionDeviceListener::data_offer(device, offer); + } + + void selection(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer) override + { + selected = offer; + PrimarySelectionDeviceListener::selection(device, offer); + } + + PrimarySelectionOfferListener& offer_listener; + zwp_primary_selection_offer_v1* selected = nullptr; +}; + +struct StubPrimarySelectionSourceListener : PrimarySelectionSourceListener +{ + using PrimarySelectionSourceListener::PrimarySelectionSourceListener; + + void send(zwp_primary_selection_source_v1*, const char*, int32_t fd) + { + ASSERT_THAT(write(fd, any_mime_data, sizeof any_mime_data), Eq(ssize_t(sizeof any_mime_data))); + close(fd); + } +}; + +struct Pipe +{ + int source; + int sink; + + Pipe() { socketpair(AF_LOCAL, SOCK_STREAM, 0, &source); } + Pipe(Pipe const&) = delete; + Pipe& operator=(Pipe const&) = delete; + + ~Pipe() { close(source); close(sink); } +}; +} + +TEST_F(PrimarySelection, source_can_offer) +{ + source_app.offer(any_mime_type); + source_app.set_selection(); +} + +TEST_F(PrimarySelection, sink_can_listen) +{ + MockPrimarySelectionDeviceListener device_listener{sink_app.device}; + MockPrimarySelectionOfferListener offer_listener; + + InSequence seq; + EXPECT_CALL(device_listener, data_offer(_, _)) + .WillOnce(Invoke([&](auto*, auto* id) { offer_listener.listen_to(id); })); + + EXPECT_CALL(offer_listener, offer(_, StrEq(any_mime_type))); + + EXPECT_CALL(device_listener, selection(_, _)); + + source_app.offer(any_mime_type); + source_app.set_selection(); + + sink_app.roundtrip(); +} + +TEST_F(PrimarySelection, sink_can_request) +{ + PrimarySelectionOfferListener offer_listener; + StubPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener}; + source_app.offer(any_mime_type); + source_app.set_selection(); + sink_app.roundtrip(); + ASSERT_THAT(device_listener.selected, NotNull()); + + Pipe pipe; + zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source); + sink_app.roundtrip(); +} + +TEST_F(PrimarySelection, source_sees_request) +{ + MockPrimarySelectionSourceListener source_listener{source_app.source}; + PrimarySelectionOfferListener offer_listener; + StubPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener}; + source_app.offer(any_mime_type); + source_app.set_selection(); + sink_app.roundtrip(); + ASSERT_THAT(device_listener.selected, NotNull()); + + EXPECT_CALL(source_listener, send(_, _, _)) + .Times(1) + .WillRepeatedly(Invoke([&](auto*, auto*, int fd) { close(fd); })); + + Pipe pipe; + zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source); + sink_app.roundtrip(); + source_app.roundtrip(); +} + +TEST_F(PrimarySelection, source_can_supply_request) +{ + StubPrimarySelectionSourceListener source_listener{source_app.source}; + PrimarySelectionOfferListener offer_listener; + StubPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener}; + source_app.offer(any_mime_type); + source_app.set_selection(); + sink_app.roundtrip(); + ASSERT_THAT(device_listener.selected, NotNull()); + + Pipe pipe; + zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source); + sink_app.roundtrip(); + source_app.roundtrip(); + + char buffer[128]; + EXPECT_THAT(read(pipe.sink, buffer, sizeof buffer), Eq(ssize_t(sizeof any_mime_data))); + EXPECT_THAT(buffer, StrEq(any_mime_data)); +} diff --git a/tests/relative_pointer.cpp b/tests/relative_pointer.cpp new file mode 100644 index 0000000..c5c5358 --- /dev/null +++ b/tests/relative_pointer.cpp @@ -0,0 +1,97 @@ +/* + * Copyright © 2020 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "relative_pointer_unstable_v1.h" +#include "helpers.h" +#include "in_process_server.h" + +#include + +using testing::AnyNumber; +using testing::IsTrue; +using testing::NotNull; +using testing::_; + +using namespace wlcs; + +namespace +{ +auto const any_width = 300; +auto const any_height = 300; +auto const nw_middle_x = any_width / 2; +auto const nw_middle_y = any_height / 2; + +struct RelativePointer : StartedInProcessServer +{ + // Create a client with a surface and a cursor centered on the surface + Client a_client{the_server()}; + Surface a_surface{a_client.create_visible_surface(any_width, any_height)}; + Pointer cursor = the_server().create_pointer(); + + ZwpRelativePointerManagerV1 manager{a_client}; + ZwpRelativePointerV1 pointer{manager, a_client.the_pointer()}; + + void SetUp() override + { + StartedInProcessServer::SetUp(); + + // Get the surface in a known position with the cursor over it + the_server().move_surface_to(a_surface, 0, 0); + cursor.move_to(nw_middle_x, nw_middle_y); + } + + void TearDown() override + { + a_client.roundtrip(); + StartedInProcessServer::TearDown(); + } +}; +} + +TEST_F(RelativePointer, can_get_relative_pointer) +{ + EXPECT_THAT(pointer, NotNull()); +} + +TEST_F(RelativePointer, relative_pointer_gets_movement) +{ + auto const move_x = any_width/6; + auto const move_y = any_height/6; + + EXPECT_CALL(pointer, relative_motion(_, _, + wl_fixed_from_int(move_x), wl_fixed_from_int(move_y), + wl_fixed_from_int(move_x), wl_fixed_from_int(move_y))).Times(1); + + cursor.move_by(move_x, move_y); +} + +// #1959 +TEST_F(RelativePointer, default_pointer_still_gets_movement) +{ + auto const move_x = any_width/6; + auto const move_y = any_height/6; + EXPECT_CALL(pointer, relative_motion(_, _, _, _, _, _)).Times(AnyNumber()); + + bool moved = false; + a_client.add_pointer_motion_notification([&](auto...) { moved = true; return true; }); + + cursor.move_by(move_x, move_y); + + a_client.roundtrip(); + EXPECT_THAT(moved, IsTrue()); +} diff --git a/tests/self_test.cpp b/tests/self_test.cpp new file mode 100644 index 0000000..66f78b8 --- /dev/null +++ b/tests/self_test.cpp @@ -0,0 +1,215 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Alan Griffiths + */ + +#include "data_device.h" +#include "helpers.h" +#include "gtest_helpers.h" +#include "in_process_server.h" +#include "version_specifier.h" + +#include + +#include + +using namespace testing; +using namespace wlcs; + +namespace +{ + +struct SelfTest : StartedInProcessServer +{ + Client client1{the_server()}; +}; + +auto static const any_width = 100; +auto static const any_height = 100; +} + +TEST_F(SelfTest, when_creating_second_client_nothing_bad_happens) +{ + Client client2{the_server()}; +} + +TEST_F(SelfTest, given_second_client_when_roundtripping_first_client_nothing_bad_happens) +{ + Client client2{the_server()}; + client1.roundtrip(); +} + +TEST_F(SelfTest, given_second_client_when_roundtripping_both_clients_nothing_bad_happens) +{ + Client client2{the_server()}; + + for (auto i = 0; i != 10; ++i) + { + client1.roundtrip(); + client2.roundtrip(); + } +} + +TEST_F(SelfTest, when_a_client_creates_a_surface_nothing_bad_happens) +{ + Surface const surface1{client1.create_visible_surface(any_width, any_height)}; + client1.roundtrip(); +} + +TEST_F(SelfTest, given_second_client_when_first_creates_a_surface_nothing_bad_happens) +{ + Client client2{the_server()}; + + Surface const surface1{client1.create_visible_surface(any_width, any_height)}; + + for (auto i = 0; i != 10; ++i) + { + client1.roundtrip(); + client2.roundtrip(); + } +} + +TEST_F(SelfTest, given_second_client_when_both_create_a_surface_nothing_bad_happens) +{ + Client client2{the_server()}; + + Surface const surface1{client1.create_visible_surface(any_width, any_height)}; + + Surface const surface2{client2.create_visible_surface(any_width, any_height)}; + + for (auto i = 0; i != 10; ++i) + { + client1.roundtrip(); + client2.roundtrip(); + } +} + +TEST_F(SelfTest, xfail_failure_is_noted) +{ + ::testing::Test::RecordProperty("wlcs-skip-test", "Reason goes here"); + + FAIL() << "This message shouldn't be seen"; +} + +TEST_F(SelfTest, expected_missing_extension_is_xfail) +{ + throw wlcs::ExtensionExpectedlyNotSupported("xdg_not_really_an_extension", wlcs::AtLeastVersion{1}); +} + +TEST_F(SelfTest, acquiring_unsupported_extension_is_xfail) +{ + auto const extension_list = the_server().supported_extensions(); + + if (!extension_list) + { + ::testing::Test::RecordProperty("wlcs-skip-test", "Compositor Integration module is too old for expected extension failures"); + FAIL() << "Requires unsupported feature from module under test"; + } + + Client client{the_server()}; + + wl_interface unsupported_interface = wl_shell_interface; + unsupported_interface.name = "wlcs_non_existent_extension"; + client.bind_if_supported(unsupported_interface, AnyVersion); + + FAIL() << "We should have (x)failed at acquiring the interface"; +} + +TEST_F(SelfTest, acquiring_unsupported_extension_version_is_xfail) +{ + auto const extension_list = the_server().supported_extensions(); + + if (!extension_list) + { + ::testing::Test::RecordProperty("wlcs-skip-test", "Compositor Integration module is too old for expected extension failures"); + FAIL() << "Requires unsupported feature from module under test"; + } + + Client client{the_server()}; + + wl_interface interface_with_unsupported_version = wl_shell_interface; + interface_with_unsupported_version.version += 1; + client.bind_if_supported( + interface_with_unsupported_version, + AtLeastVersion{static_cast(interface_with_unsupported_version.version)}); + + FAIL() << "We should have (x)failed at acquiring the interface"; +} + +TEST_F(SelfTest, does_not_acquire_version_newer_than_wlcs_supports) +{ + auto const extension_list = the_server().supported_extensions(); + + if (!extension_list) + { + ::testing::Test::RecordProperty("wlcs-skip-test", "Compositor Integration module is too old for expected extension failures"); + FAIL() << "Requires unsupported feature from module under test"; + } + + Client client{the_server()}; + + auto const proxy_latest = static_cast(client.bind_if_supported(wl_seat_interface, AnyVersion)); + ASSERT_THAT(wl_seat_get_version(proxy_latest), Gt(1)); + + wl_interface interface_with_old_version = wl_seat_interface; + interface_with_old_version.version = wl_seat_get_version(proxy_latest) - 1; + auto const proxy_old = static_cast(client.bind_if_supported(interface_with_old_version, AnyVersion)); + EXPECT_THAT(wl_seat_get_version(proxy_old), Eq(interface_with_old_version.version)); + + wl_seat_destroy(proxy_latest); + wl_seat_destroy(proxy_old); +} + +TEST_F(SelfTest, dispatch_until_times_out_on_failure) +{ + Client client{the_server()}; + + // Ensure that there's some events happening on the Wayland socket + auto dummy = client.create_visible_surface(300, 300); + dummy.attach_buffer(300, 300); + wl_surface_commit(dummy); + + try + { + client.dispatch_until([]() { return false; }, std::chrono::seconds{1}); + } + catch (wlcs::Timeout const&) + { + return; + } + FAIL() << "Dispatch did not raise a wlcs::Timeout exception"; +} + +TEST_F(SelfTest, dispatch_until_times_out_at_the_right_time) +{ + using namespace std::literals::chrono_literals; + + Client client{the_server()}; + + auto const timeout = 5s; + auto const expected_end = std::chrono::steady_clock::now() + timeout; + try + { + client.dispatch_until([]() { return false; }, timeout); + } + catch (wlcs::Timeout const&) + { + EXPECT_THAT(std::chrono::steady_clock::now(), Gt(expected_end)); + EXPECT_THAT(std::chrono::steady_clock::now(), Lt(expected_end + 5s)); + return; + } + FAIL() << "Dispatch did not raise a wlcs::Timeout exception"; +} diff --git a/tests/subsurfaces.cpp b/tests/subsurfaces.cpp new file mode 100644 index 0000000..2343e11 --- /dev/null +++ b/tests/subsurfaces.cpp @@ -0,0 +1,936 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "xdg_shell_v6.h" + +#include + +#include + +using namespace testing; + +struct AbstractInputDevice +{ + virtual void to_screen_position(int x, int y) = 0; + virtual wl_surface* focused_window() = 0; + virtual std::pair position_on_window() = 0; + virtual ~AbstractInputDevice() = default; +}; + +struct PointerInputDevice : AbstractInputDevice +{ + PointerInputDevice(wlcs::Server& server, wlcs::Client& client): + client{client}, + pointer{server.create_pointer()} + { + } + + void to_screen_position(int x, int y) override + { + pointer.move_to(0, 0); + pointer.move_to(x, y); + } + + wl_surface* focused_window() override + { + return client.window_under_cursor(); + } + + std::pair position_on_window() override + { + return client.pointer_position(); + } + + wlcs::Client& client; + wlcs::Pointer pointer; +}; + +struct TouchInputDevice : AbstractInputDevice +{ + TouchInputDevice(wlcs::Server& server, wlcs::Client& client): + client{client}, + touch{server.create_touch()} + { + } + + void to_screen_position(int x, int y) override + { + touch.up(); + touch.down_at(x, y); + } + + wl_surface* focused_window() override + { + return client.touched_window(); + } + + std::pair position_on_window() override + { + return client.touch_position(); + } + + wlcs::Client& client; + wlcs::Touch touch; +}; + +struct SubsurfaceTestParams +{ + std::string name; + std::function(wlcs::InProcessServer& server, + wlcs::Client& client, + int x, int y, + int width, int height)> make_surface; + std::function(wlcs::Server& server, + wlcs::Client& client)> make_input_device; +}; + +std::ostream& operator<<(std::ostream& out, SubsurfaceTestParams const& param) +{ + return out << param.name; +} + +class SubsurfaceTest : + public wlcs::StartedInProcessServer, + public testing::WithParamInterface +{ +public: + static int const surface_width = 200, surface_height = 300; + static int const subsurface_width = 50, subsurface_height = 50; + static int const surface_x = 20, surface_y = 30; + + SubsurfaceTest(): + client{the_server()}, + main_surface{std::move(*GetParam().make_surface( + *this, client, surface_x, surface_y, surface_width, surface_height))}, + subsurface{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)}, + input_device{GetParam().make_input_device(the_server(), client)} + { + client.roundtrip(); + } + + void move_subsurface_to(int x, int y) + { + wl_subsurface_set_position(subsurface, x, y); + wl_surface_commit(main_surface); + client.roundtrip(); + } + + wlcs::Client client{the_server()}; + wlcs::Surface main_surface; + wlcs::Subsurface subsurface; + std::unique_ptr input_device; +}; + +class SubsurfaceMultilevelTest : + public wlcs::StartedInProcessServer, + public testing::WithParamInterface +{ +public: + static int const surface_width = 200, surface_height = 300; + static int const subsurface_width = 50, subsurface_height = 50; + static int const surface_x = 20, surface_y = 30; + + SubsurfaceMultilevelTest(): + client{the_server()}, + main_surface{std::move(*GetParam().make_surface( + *this, client, surface_x, surface_y, surface_width, surface_height))}, + parent_subsurface{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)}, + child_subsurface{wlcs::Subsurface::create_visible(parent_subsurface, 0, 0, subsurface_width, subsurface_height)}, + input_device{GetParam().make_input_device(the_server(), client)} + { + client.roundtrip(); + } + + wlcs::Client client{the_server()}; + wlcs::Surface main_surface; + wlcs::Subsurface parent_subsurface; + wlcs::Subsurface child_subsurface; + std::unique_ptr input_device; +}; + +TEST_P(SubsurfaceTest, subsurface_has_correct_parent) +{ + EXPECT_THAT(&subsurface.parent(), Eq(&main_surface)); +} + +TEST_P(SubsurfaceTest, subsurface_gets_pointer_input) +{ + int const pointer_x = surface_x + 10, pointer_y = surface_y + 5; + + input_device->to_screen_position(pointer_x, pointer_y); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface"; + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)); + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - surface_x), + wl_fixed_from_int(pointer_y - surface_y)))); +} + +TEST_P(SubsurfaceTest, pointer_input_correctly_offset_for_subsurface) +{ + int const pointer_x = surface_x + 13, pointer_y = surface_y + 24; + int const subsurface_x = 8, subsurface_y = 17; + + move_subsurface_to(subsurface_x, subsurface_y); + + input_device->to_screen_position(pointer_x, pointer_y); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface"; + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)); + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - surface_x - subsurface_x), + wl_fixed_from_int(pointer_y - surface_y - subsurface_y)))); +} + +TEST_P(SubsurfaceTest, sync_subsurface_moves_when_only_parent_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y); + // Position is applied when parent (main_surface) commits, so subsurface does not need to be committed + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))) + << "Subsurface did not move after parent commit"; +} + +TEST_P(SubsurfaceTest, desync_subsurface_moves_when_only_parent_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_desync(subsurface); + + wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y); + // Position is applied when parent (main_surface) commits, so subsurface does not need to be committed + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))) + << "Subsurface did not move after parent commit"; +} + +TEST_P(SubsurfaceTest, subsurface_does_not_move_when_parent_not_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_desync(subsurface); + + wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y); + wl_surface_commit(subsurface); + // We don't call wl_surface_commit(main_surface), so position should not be applied + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface moved to new location without parent being committed"; +} + +TEST_P(SubsurfaceTest, subsurface_extends_parent_input_region) +{ + int const pointer_x = surface_x - 5, pointer_y = surface_y + surface_height + 8; + int const subsurface_x = -10, subsurface_y = surface_height - 10; + + move_subsurface_to(subsurface_x, subsurface_y); + + input_device->to_screen_position(pointer_x, pointer_y); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface"; + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)); + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - surface_x - subsurface_x), + wl_fixed_from_int(pointer_y - surface_y - subsurface_y)))); +} + +TEST_P(SubsurfaceTest, input_falls_through_empty_subsurface_input_region) +{ + int const pointer_x = surface_x + 10, pointer_y = surface_y + 5; + + auto const wl_region = wl_compositor_create_region(client.compositor()); + wl_surface_set_input_region(subsurface, wl_region); + wl_region_destroy(wl_region); + wl_surface_commit(subsurface); + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x, pointer_y); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface)) << "input was incorrectly caught by subsurface"; + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)); + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - surface_x), + wl_fixed_from_int(pointer_y - surface_y)))); +} + +TEST_P(SubsurfaceTest, gets_input_over_surface_with_empty_region) +{ + int const pointer_x = surface_x + 32, pointer_y = surface_y + 21; + + auto const wl_region = wl_compositor_create_region(client.compositor()); + wl_surface_set_input_region(main_surface, wl_region); + wl_region_destroy(wl_region); + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x, pointer_y); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface"; + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)); + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - surface_x), + wl_fixed_from_int(pointer_y - surface_y)))); +} + +TEST_P(SubsurfaceTest, one_subsurface_to_another_fallthrough) +{ + int const pointer_x_0 = 3, pointer_y_0 = 3; + int const pointer_x_1 = 3, pointer_y_1 = 10; + int const pointer_x_2 = 10, pointer_y_2 = 3; + int const subsurface_x = 0, subsurface_y = 5; + int const subsurface_top_x = 5, subsurface_top_y = 0; + move_subsurface_to(subsurface_x, subsurface_y); + auto subsurface_top{wlcs::Subsurface::create_visible(main_surface, subsurface_top_x, subsurface_top_y, subsurface_width, subsurface_height)}; + + input_device->to_screen_position(pointer_x_0 + surface_x, pointer_y_0 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)) << "main surface not focused"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x_0), + wl_fixed_from_int(pointer_y_0)))); + + input_device->to_screen_position(pointer_x_1 + surface_x, pointer_y_1 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "lower subsurface not focused"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x_1 - subsurface_x), + wl_fixed_from_int(pointer_y_1 - subsurface_y)))); + + input_device->to_screen_position(pointer_x_2 + surface_x, pointer_y_2 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface_top)) << "upper subsurface not focused"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x_2 - subsurface_top_x), + wl_fixed_from_int(pointer_y_2 - subsurface_top_y)))); +} + +TEST_P(SubsurfaceTest, place_below_simple) +{ + auto subsurface_moving_down{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)}; + wl_subsurface_place_below(subsurface_moving_down, subsurface); + wl_surface_commit(subsurface_moving_down); + wl_surface_commit(subsurface); + wl_surface_commit(main_surface); + + input_device->to_screen_position(5 + surface_x, 5 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface_moving_down)) + << "subsurface.place_below() did not have an effect"; + + ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface)) + << "wrong surface/subsurface on top"; +} + +TEST_P(SubsurfaceTest, place_above_simple) +{ + auto subsurface_being_covered{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)}; + wl_subsurface_place_above(subsurface, subsurface_being_covered); + wl_surface_commit(subsurface); + wl_surface_commit(subsurface_being_covered); + wl_surface_commit(main_surface); + + input_device->to_screen_position(5 + surface_x, 5 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface_being_covered)) + << "subsurface.place_above() did not have an effect"; + + ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface)) + << "wrong surface/subsurface on top"; +} + +TEST_P(SubsurfaceTest, subsurface_of_a_subsurface_handled) +{ + int const pointer_x_0 = 3, pointer_y_0 = 3; + int const pointer_x_1 = 3, pointer_y_1 = 10; + int const pointer_x_2 = 10, pointer_y_2 = 3; + int const subsurface_x = 0, subsurface_y = 5; + int const subsurface_top_x = 5, subsurface_top_y = -5; + move_subsurface_to(subsurface_x, subsurface_y); + auto subsurface_top{wlcs::Subsurface::create_visible(subsurface, subsurface_top_x, subsurface_top_y, subsurface_width, subsurface_height)}; + + input_device->to_screen_position(pointer_x_0 + surface_x, pointer_y_0 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)) << "main surface not focused"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x_0), + wl_fixed_from_int(pointer_y_0)))); + + input_device->to_screen_position(pointer_x_1 + surface_x, pointer_y_1 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "lower subsurface not focused"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x_1 - subsurface_x), + wl_fixed_from_int(pointer_y_1 - subsurface_y)))); + + input_device->to_screen_position(pointer_x_2 + surface_x, pointer_y_2 + surface_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface_top)) << "subsurface of subsurface not focused"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x_2 - subsurface_top_x - subsurface_x), + wl_fixed_from_int(pointer_y_2 - subsurface_top_y - subsurface_y)))); +} + +TEST_P(SubsurfaceTest, subsurface_moves_under_input_device_once) +{ + int const input_x = surface_x + 10, input_y = surface_y + 5; + int const subsurface_x = -23, subsurface_y = -17; + + input_device->to_screen_position(input_x, input_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "precondition failed"; + ASSERT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(input_x - surface_x), + wl_fixed_from_int(input_y - surface_y)))) << "precondition failed"; + + wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y); + wl_surface_commit(subsurface); + wl_surface_commit(main_surface); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "subsurface not focuesed"; + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(input_x - surface_x), + wl_fixed_from_int(input_y - surface_y)))) << "input device did not get new location"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(input_x - surface_x - subsurface_x), + wl_fixed_from_int(input_y - surface_y - subsurface_y)))) << "input device in wrong location"; +} + +TEST_P(SubsurfaceTest, subsurface_moves_under_input_device_twice) +{ + int const input_x = surface_x + 10, input_y = surface_y + 5; + int const subsurface_x_0 = 4, subsurface_y_0 = 2; + int const subsurface_x_1 = -23, subsurface_y_1 = -17; + + input_device->to_screen_position(input_x, input_y); + wl_subsurface_set_position(subsurface, subsurface_x_0, subsurface_y_0); + wl_surface_commit(subsurface); + wl_surface_commit(main_surface); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "precondition failed"; + ASSERT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(input_x - surface_x - subsurface_x_0), + wl_fixed_from_int(input_y - surface_y - subsurface_y_0)))) << "precondition failed"; + + wl_subsurface_set_position(subsurface, subsurface_x_1, subsurface_y_1); + wl_surface_commit(subsurface); + wl_surface_commit(main_surface); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "subsurface not focuesed"; + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(input_x - surface_x - subsurface_x_0), + wl_fixed_from_int(input_y - surface_y - subsurface_y_0)))) << "input device did not get new location"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(input_x - surface_x - subsurface_x_1), + wl_fixed_from_int(input_y - surface_y - subsurface_y_1)))) << "input device in wrong location"; +} + +TEST_P(SubsurfaceTest, subsurface_moves_out_from_under_input_device) +{ + int const input_x = surface_x + 10, input_y = surface_y + 5; + int const subsurface_x = input_x - surface_x + 10, subsurface_y = input_y - surface_y + 10; + + input_device->to_screen_position(input_x, input_y); + client.roundtrip(); + + ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "precondition failed"; + ASSERT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(input_x - surface_x), + wl_fixed_from_int(input_y - surface_y)))) << "precondition failed"; + + wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y); + wl_surface_commit(subsurface); + wl_surface_commit(main_surface); + client.roundtrip(); + + EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)) << "main surface not focuesed"; + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(input_x - surface_x), + wl_fixed_from_int(input_y - surface_y)))) << "input device in wrong location"; +} + +INSTANTIATE_TEST_SUITE_P( + WlShellSubsurfaces, + SubsurfaceTest, + testing::Values( + SubsurfaceTestParams{ + "wl_shell_surface", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_wl_shell_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +INSTANTIATE_TEST_SUITE_P( + XdgShellV6Subsurfaces, + SubsurfaceTest, + testing::Values( + SubsurfaceTestParams{ + "xdg_v6_surface", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_xdg_shell_v6_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +INSTANTIATE_TEST_SUITE_P( + XdgShellStableSubsurfaces, + SubsurfaceTest, + testing::Values( + SubsurfaceTestParams{ + "xdg_stable_surface", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_xdg_shell_stable_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +INSTANTIATE_TEST_SUITE_P( + TouchInputSubsurfaces, + SubsurfaceTest, + testing::Values( + SubsurfaceTestParams{ + "touch_input_subsurfaces", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_xdg_shell_v6_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +TEST_P(SubsurfaceMultilevelTest, subsurface_with_sync_parent_does_not_move_when_only_grandparent_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y); + // We don't call wl_surface_commit(parent_subsurface), so position should not be applied + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface moved without parent being committed"; +} + +TEST_P(SubsurfaceMultilevelTest, subsurface_with_desync_parent_does_not_move_when_only_grandparent_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_desync(parent_subsurface); + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y); + // We don't call wl_surface_commit(parent_subsurface), so position should not be applied + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface moved without parent being committed"; +} + +TEST_P(SubsurfaceMultilevelTest, subsurface_with_sync_parent_does_not_move_when_only_parent_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y); + wl_surface_commit(parent_subsurface); + // We don't call wl_surface_commit(main_surface), which should be required before position is applied because + // parent_subsurface is sync + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface of sync parent moved without grandparent being committed"; +} + +TEST_P(SubsurfaceMultilevelTest, subsurface_with_desync_parent_moves_when_only_parent_committed) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_desync(parent_subsurface); + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y); + wl_surface_commit(parent_subsurface); + // wl_surface_commit(main_surface) should NOT be required for position to be applied because parent_subsurface is + // desync + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))) + << "Did not move on desync parent commit"; +} + +TEST_P(SubsurfaceMultilevelTest, subsurface_does_not_move_when_grandparent_commit_is_before_sync_parent_commit) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y); + wl_surface_commit(main_surface); + wl_surface_commit(parent_subsurface); + // Committing main_surface would need to happend AFTER parent_subsurface in order for position to be applied + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface moved when hierarchy commits were in the wrong order"; +} + +TEST_P(SubsurfaceMultilevelTest, subsurface_moves_after_both_sync_parent_and_grandparent_commit) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y); + wl_surface_commit(parent_subsurface); + wl_surface_commit(main_surface); + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))) + << "Did not move after parent and grandparent both comitted"; +} + +TEST_P(SubsurfaceMultilevelTest, by_default_subsurface_is_sync) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_x); + wl_surface_commit(parent_subsurface); + // Not calling wl_surface_commit(main_surface), so new position should not be applied + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface moved without parent commit (it should have been 'sync' by default, but is acting as desync)"; +} + +TEST_P(SubsurfaceMultilevelTest, subsurface_can_be_set_to_sync) +{ + int const pointer_x = 30, pointer_y = 30; + int const subsurface_x = 20, subsurface_y = 20; + + wl_subsurface_set_desync(child_subsurface); + wl_subsurface_set_sync(child_subsurface); + + wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_x); + wl_surface_commit(parent_subsurface); + // Not calling wl_surface_commit(main_surface), so new position should not be applied + client.roundtrip(); + + input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y); + client.roundtrip(); + + EXPECT_THAT(input_device->position_on_window(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x), + wl_fixed_from_int(pointer_y)))); + + EXPECT_THAT(input_device->position_on_window(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - subsurface_x), + wl_fixed_from_int(pointer_y - subsurface_y)))) + << "Subsurface moved without parent commit (it should have been 'sync' by default, but is acting as desync)"; +} + +INSTANTIATE_TEST_SUITE_P( + WlShellSubsurfaces, + SubsurfaceMultilevelTest, + testing::Values( + SubsurfaceTestParams{ + "wl_shell_surface", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_wl_shell_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +INSTANTIATE_TEST_SUITE_P( + XdgShellV6Subsurfaces, + SubsurfaceMultilevelTest, + testing::Values( + SubsurfaceTestParams{ + "xdg_v6_surface", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_xdg_shell_v6_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +INSTANTIATE_TEST_SUITE_P( + XdgShellStableSubsurfaces, + SubsurfaceMultilevelTest, + testing::Values( + SubsurfaceTestParams{ + "xdg_stable_surface", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_xdg_shell_stable_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +INSTANTIATE_TEST_SUITE_P( + TouchInputSubsurfaces, + SubsurfaceMultilevelTest, + testing::Values( + SubsurfaceTestParams{ + "touch_input_subsurfaces", + [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height) + -> std::unique_ptr + { + auto surface = client.create_xdg_shell_v6_surface( + width, + height); + server.the_server().move_surface_to(surface, x, y); + return std::make_unique(std::move(surface)); + }, + [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr + { + return std::make_unique(server, client); + } + } + )); + +// TODO: combinations of sync and desync at various levels of the tree +// TODO: "bad_surface" error +// TODO: seitch to the new surface/input method abstraction diff --git a/tests/surface_input_regions.cpp b/tests/surface_input_regions.cpp new file mode 100644 index 0000000..e109d81 --- /dev/null +++ b/tests/surface_input_regions.cpp @@ -0,0 +1,837 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "surface_builder.h" +#include "input_method.h" + +#include + +#include +#include +#include + +using namespace testing; + +enum class RegionAction +{ + add, + subtract, +}; + +struct Region +{ + struct Element + { + RegionAction action; + std::pair top_left; + std::pair size; + }; + + std::string name; + std::pair surface_size; + std::vector elements; + + void apply_to_surface(wlcs::Client& client, wl_surface* surface) const + { + if (elements.empty()) + return; + auto const wl_region = wl_compositor_create_region(client.compositor()); + for (auto const& e: elements) + { + switch(e.action) + { + case RegionAction::add: + wl_region_add(wl_region, e.top_left.first, e.top_left.second, e.size.first, e.size.second); + break; + case RegionAction::subtract: + wl_region_subtract(wl_region, e.top_left.first, e.top_left.second, e.size.first, e.size.second); + break; + } + } + wl_surface_set_input_region(surface, wl_region); + wl_region_destroy(wl_region); + wl_surface_commit(surface); + client.roundtrip(); + } +}; + +struct RegionWithTestPoints +{ + RegionWithTestPoints() + : name{"default constructed"}, + region{"default constructed", {0, 0}, {}}, + on_surface{0, 0}, + off_surface{0, 0} + { + } + + RegionWithTestPoints( + std::string const& name, + Region region, + std::pair on_surface, + std::pair delta) + : name{name}, + region{region}, + on_surface{on_surface}, + off_surface{on_surface.first + delta.first, on_surface.second + delta.second} + { + } + + std::string name; + Region region; + std::pair on_surface; + std::pair off_surface; +}; + +std::ostream& operator<<(std::ostream& out, RegionWithTestPoints const& param) +{ + return out << param.region.name << " " << param.name; +} + +auto const all_surface_types = ValuesIn(wlcs::SurfaceBuilder::all_surface_types()); +auto const toplevel_surface_types = ValuesIn(wlcs::SurfaceBuilder::toplevel_surface_types()); +auto const xdg_stable_surface_type = Values( + std::static_pointer_cast(std::make_shared(0, 0, 0, 0))); + +auto const all_input_types = ValuesIn(wlcs::InputMethod::all_input_methods()); + +class RegionSurfaceInputCombinations : + public wlcs::InProcessServer, + public testing::WithParamInterface, + std::shared_ptr>> +{ +}; + +auto const surface_size{std::make_pair(215, 108)}; + +Region const default_region{"default", surface_size, {}}; + +auto const default_edges = Values( + RegionWithTestPoints{"left edge", default_region, + {0, surface_size.second / 2}, + {-1, 0}}, + RegionWithTestPoints{"bottom edge", default_region, + {surface_size.first / 2, surface_size.second - 1}, + {0, 1}}, + RegionWithTestPoints{"right edge", default_region, + {surface_size.first - 1, surface_size.second / 2}, + {1, 0}}, + RegionWithTestPoints{"top edge", default_region, + {surface_size.first / 2, 0}, + {0, -1}}); + +Region const full_surface_region{"explicitly specified full surface", surface_size, { + {RegionAction::add, {0, 0}, surface_size}}}; + +auto const full_surface_edges = Values( + RegionWithTestPoints{"left edge", full_surface_region, + {0, surface_size.second / 2}, + {-1, 0}}, + RegionWithTestPoints{"bottom edge", full_surface_region, + {surface_size.first / 2, surface_size.second - 1}, + {0, 1}}, + RegionWithTestPoints{"right edge", full_surface_region, + {surface_size.first - 1, surface_size.second / 2}, + {1, 0}}, + RegionWithTestPoints{"top edge", full_surface_region, + {surface_size.first / 2, 0}, + {0, -1}}); + +auto const region_inset = std::make_pair(12, 17); + +Region const smaller_region{"smaller", surface_size, { + {RegionAction::add, region_inset, { + surface_size.first - region_inset.first * 2, + surface_size.second - region_inset.second * 2}}}}; + +auto const smaller_region_edges = Values( + RegionWithTestPoints{"left edge", smaller_region, + {region_inset.first, surface_size.second / 2}, + {-1, 0}}, + RegionWithTestPoints{"bottom edge", smaller_region, + {surface_size.first / 2, surface_size.second - region_inset.second - 1}, + {0, 1}}, + RegionWithTestPoints{"right edge", smaller_region, + {surface_size.first - region_inset.first - 1, surface_size.second / 2}, + {1, 0}}, + RegionWithTestPoints{"top edge", smaller_region, + {surface_size.first / 2, region_inset.second}, + {0, -1}}); + +// If a region is larger then the surface it should be clipped + +auto const region_outset = std::make_pair(12, 17); + +Region const larger_region{"larger", surface_size, { + {RegionAction::add, {-region_outset.first, -region_outset.second}, { + surface_size.first + region_inset.first * 2, + surface_size.second + region_inset.second * 2}}}}; + +auto const larger_region_edges = Values( + RegionWithTestPoints{"left edge", larger_region, + {0, surface_size.second / 2}, + {-1, 0}}, + RegionWithTestPoints{"bottom edge", larger_region, + {surface_size.first / 2, surface_size.second - 1}, + {0, 1}}, + RegionWithTestPoints{"right edge", larger_region, + {surface_size.first - 1, surface_size.second / 2}, + {1, 0}}, + RegionWithTestPoints{"top edge", larger_region, + {surface_size.first / 2, 0}, + {0, -1}}); + +int const small_rect_inset = 16; + +// Looks something like this: +// (dotted line is real surface, solid line is input region rectangles) +// _______A_______ +// | | +// B| |C +// |_D___________E_| +// : | | : +// : F| |G : +// '---|---H---|---' +// |_______| +// I +Region const multi_rect_region{"multi-rect", surface_size, { + // upper rect + {RegionAction::add, + {0, 0}, + {surface_size.first, surface_size.second / 2}}, + // lower rect + {RegionAction::add, + {small_rect_inset, surface_size.second / 2}, + {surface_size.first - small_rect_inset * 2, surface_size.second / 2 + 20}}}}; + +auto const multi_rect_edges = Values( + RegionWithTestPoints{"top region edge at surface top edge", multi_rect_region, // A in diagram + {surface_size.first / 2, 0}, + {0, -1}}, + RegionWithTestPoints{"right region edge at surface right edge", multi_rect_region, // C in diagram + {surface_size.first - 1, surface_size.second / 4}, + {1, 0}}, + RegionWithTestPoints{"left region edge inside surface", multi_rect_region, // F in diagram + {small_rect_inset, surface_size.second * 3 / 4}, + {-1, 0}}, + RegionWithTestPoints{"step edge", multi_rect_region, // D in diagram + {small_rect_inset / 2, surface_size.second / 2 - 1}, + {0, 1}}, + RegionWithTestPoints{"bottom clipped edge", multi_rect_region, // I in diagram + {surface_size.first / 2, surface_size.second - 1}, + {0, 1}}); + +auto const multi_rect_corners = Values( + RegionWithTestPoints{ + "top-left corner", multi_rect_region, // AxB in diagram + {0, 0}, + {-1, -1}}, + RegionWithTestPoints{ + "top-right corner", multi_rect_region, // AxC in diagram + {surface_size.first - 1, 0}, + {1, -1}}, + RegionWithTestPoints{ + "bottom-left corner", multi_rect_region, // HxF in diagram + {small_rect_inset, surface_size.second - 1}, + {-1, 1}}, + RegionWithTestPoints{ + "bottom-right corner", multi_rect_region, // HxG in diagram + {surface_size.first - small_rect_inset - 1, surface_size.second - 1}, + {1, 1}}, + RegionWithTestPoints{ + "left interior corner", multi_rect_region, // HxF in diagram + {small_rect_inset, surface_size.second / 2 - 1}, + {-1, 1}}, + RegionWithTestPoints{ + "right interior corner", multi_rect_region, // HxG in diagram + {surface_size.first - small_rect_inset - 1, surface_size.second / 2 - 1}, + {1, 1}}); + +// TODO: test subtract +// TODO: test empty region +// TODO: test default region + +TEST_P(RegionSurfaceInputCombinations, input_inside_region_seen) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + RegionWithTestPoints region; + std::shared_ptr builder; + std::shared_ptr input; + + std::tie(region, builder, input) = GetParam(); + + auto surface = builder->build( + the_server(), + client, + top_left, + region.region.surface_size); + region.region.apply_to_surface(client, *surface); + struct wl_surface* const wl_surface = *surface; + + auto const device = input->create_device(the_server()); + device->down_at({ + top_left.first + region.on_surface.first, + top_left.second + region.on_surface.second}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Eq(wl_surface)) + << input << " not seen by "<< builder << " when inside " << region.name << " of " << region.region.name << " region"; + + if (input->current_surface(client) == wl_surface) + { + EXPECT_THAT(input->position_on_surface(client), Eq(region.on_surface)) + << input << " in the wrong place over " << builder << " while testing " << region; + } +} + +TEST_P(RegionSurfaceInputCombinations, input_not_seen_after_leaving_region) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + RegionWithTestPoints region; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(region, builder, input) = GetParam(); + + auto surface = builder->build( + the_server(), + client, + top_left, + region.region.surface_size); + region.region.apply_to_surface(client, *surface); + struct wl_surface* const wl_surface = *surface; + + auto const device = input->create_device(the_server()); + device->down_at({ + top_left.first + region.on_surface.first, + top_left.second + region.on_surface.second}); + client.roundtrip(); + device->up(); + device->down_at({ + top_left.first + region.off_surface.first, + top_left.second + region.off_surface.second}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(wl_surface)) + << input << " seen by " << builder << " when outside " << region.name << " of " << region.region.name << " region"; +} + +class SurfaceInputCombinations : + public wlcs::InProcessServer, + public testing::WithParamInterface, + std::shared_ptr>> +{ +}; + +TEST_P(SurfaceInputCombinations, input_not_seen_in_region_after_null_buffer_committed) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto const region = full_surface_region; + + auto surface = builder->build( + the_server(), + client, + top_left, + region.surface_size); + region.apply_to_surface(client, *surface); + struct wl_surface* const wl_surface = *surface; + wl_surface_attach(wl_surface, nullptr, 0, 0); + wl_surface_commit(wl_surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at(top_left); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(wl_surface)) + << input << " seen by " << builder << " after null buffer committed"; +} + +TEST_P(SurfaceInputCombinations, input_not_seen_in_surface_without_region_after_null_buffer_committed) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto surface = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const wl_surface = *surface; + wl_surface_attach(wl_surface, nullptr, 0, 0); + wl_surface_commit(wl_surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at(top_left); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(wl_surface)) + << input << " seen by " << builder << " after null buffer committed"; +} + +TEST_P(SurfaceInputCombinations, input_not_seen_over_empty_region) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto surface = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const wl_surface = *surface; + + auto const wl_region = wl_compositor_create_region(client.compositor()); + wl_surface_set_input_region(*surface, wl_region); + wl_region_destroy(wl_region); + wl_surface_commit(*surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + 4, top_left.second + 4}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(wl_surface)) + << input << " seen by " << builder << " with empty input region"; +} + +TEST_P(SurfaceInputCombinations, input_hits_parent_after_falling_through_subsurface) +{ + auto const top_left = std::make_pair(64, 49); + auto const input_offset = std::make_pair(4, 4); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto parent = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const parent_wl_surface = *parent; + auto subsurface = std::make_unique(wlcs::Subsurface::create_visible( + *parent, + 0, 0, + surface_size.first, surface_size.second)); + wl_subsurface_set_desync(*subsurface); + struct wl_surface* const sub_wl_surface = *subsurface; + + Region const region{"region", surface_size, {{RegionAction::add, {0, 0}, {1, 1}}}}; + region.apply_to_surface(client, sub_wl_surface); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + input_offset.first, top_left.second + input_offset.second}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(sub_wl_surface)) + << input << " seen by subsurface when not over region"; + + EXPECT_THAT(input->current_surface(client), Eq(parent_wl_surface)) + << input << " not seen by " << builder << " when it should have fallen through the subsurface input region"; + + EXPECT_THAT(input->position_on_surface(client), Eq(input_offset)) + << input << " seen in the wrong place"; +} + +TEST_P(SurfaceInputCombinations, unmapping_parent_stops_subsurface_getting_input) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto parent = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const parent_wl_surface = *parent; + auto subsurface = std::make_unique(wlcs::Subsurface::create_visible( + *parent, + 0, 0, + surface_size.first, surface_size.second)); + wl_subsurface_set_desync(*subsurface); + struct wl_surface* const sub_wl_surface = *subsurface; + client.roundtrip(); + + wl_surface_attach(parent_wl_surface, nullptr, 0, 0); + wl_surface_commit(parent_wl_surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + 4, top_left.second + 4}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(parent_wl_surface)) + << input << " seen by " << builder << " after it was unmapped"; + + EXPECT_THAT(input->current_surface(client), Ne(sub_wl_surface)) + << input << " seen by subsurface after parent " << builder << " was unmapped"; +} + +TEST_P(SurfaceInputCombinations, input_falls_through_subsurface_when_unmapped) +{ + auto const top_left = std::make_pair(200, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto lower = client.create_visible_surface(surface_size.first, surface_size.second); + the_server().move_surface_to(lower, top_left.first - 100, top_left.second); + struct wl_surface* const lower_wl_surface = lower; + + auto parent = builder->build( + the_server(), + client, + top_left, + surface_size); + ASSERT_THAT(surface_size.first, Gt(100)); + struct wl_surface* const parent_wl_surface = *parent; + auto subsurface = std::make_unique(wlcs::Subsurface::create_visible( + *parent, + -100, 0, + surface_size.first, surface_size.second)); + wl_subsurface_set_desync(*subsurface); + struct wl_surface* const sub_wl_surface = *subsurface; + client.roundtrip(); + + wl_surface_attach(sub_wl_surface, nullptr, 0, 0); + wl_surface_commit(sub_wl_surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first - 90, top_left.second + 10}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(sub_wl_surface)) + << input << " seen by subsurface after it was unmapped"; + + EXPECT_THAT(input->current_surface(client), Ne(parent_wl_surface)) + << input << " seen by " << builder << " even through it shouldn't have been over it's input region"; + + EXPECT_THAT(input->current_surface(client), Eq(lower_wl_surface)) + << input << " not seen by lower surface"; +} + +TEST_P(SurfaceInputCombinations, input_falls_through_subsurface_when_parent_unmapped) +{ + auto const top_left = std::make_pair(200, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto lower = client.create_visible_surface(surface_size.first, surface_size.second); + the_server().move_surface_to(lower, top_left.first - 100, top_left.second); + struct wl_surface* const lower_wl_surface = lower; + + auto parent = builder->build( + the_server(), + client, + top_left, + surface_size); + ASSERT_THAT(surface_size.first, Gt(100)); + struct wl_surface* const parent_wl_surface = *parent; + auto subsurface = std::make_unique(wlcs::Subsurface::create_visible( + *parent, + -100, 0, + surface_size.first, surface_size.second)); + wl_subsurface_set_desync(*subsurface); + struct wl_surface* const sub_wl_surface = *subsurface; + client.roundtrip(); + + wl_surface_attach(parent_wl_surface, nullptr, 0, 0); + wl_surface_commit(parent_wl_surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first - 90, top_left.second + 10}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(sub_wl_surface)) + << input << " seen by subsurface after parent was unmapped"; + + EXPECT_THAT(input->current_surface(client), Ne(parent_wl_surface)) + << input << " seen by " << builder << " after being unmapped (also input should have gone to subsurface)"; + + EXPECT_THAT(input->current_surface(client), Eq(lower_wl_surface)) + << input << " not seen by lower surface"; +} + +TEST_P(SurfaceInputCombinations, input_seen_after_surface_unmapped_and_remapped) +{ + auto const top_left = std::make_pair(200, 49); + auto const input_offset = std::make_pair(4, 4); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto surface = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const wl_surface = *surface; + + wl_surface_attach(wl_surface, nullptr, 0, 0); + wl_surface_commit(wl_surface); + client.roundtrip(); + + surface->attach_visible_buffer(surface_size.first, surface_size.second); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + input_offset.first, top_left.second + input_offset.second}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Eq(wl_surface)) + << input << " not seen by " << builder << " after it was unmapped and remapped"; + + EXPECT_THAT(input->position_on_surface(client), Eq(input_offset)) + << input << " seen in the wrong place"; +} + +TEST_P(SurfaceInputCombinations, input_seen_by_subsurface_after_parent_unmapped_and_remapped) +{ + auto const top_left = std::make_pair(200, 49); + auto const input_offset = std::make_pair(-90, 10); + auto const subsurface_offset = std::make_pair(-100, 0); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto parent = builder->build( + the_server(), + client, + top_left, + surface_size); + ASSERT_THAT(surface_size.first, Gt(100)); + struct wl_surface* const parent_wl_surface = *parent; + auto subsurface = std::make_unique(wlcs::Subsurface::create_visible( + *parent, + subsurface_offset.first, subsurface_offset.second, + surface_size.first, surface_size.second)); + wl_subsurface_set_desync(*subsurface); + struct wl_surface* const sub_wl_surface = *subsurface; + client.roundtrip(); + + wl_surface_attach(parent_wl_surface, nullptr, 0, 0); + wl_surface_commit(parent_wl_surface); + client.roundtrip(); + + parent->attach_visible_buffer(surface_size.first, surface_size.second); + //the_server().move_surface_to(*parent, top_left.first, top_left.second); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + input_offset.first, top_left.second + input_offset.second}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(parent_wl_surface)) + << input << " seen by " << builder << " when it should be seen by it's subsurface"; + + EXPECT_THAT(input->current_surface(client), Eq(sub_wl_surface)) + << input << " not seen by subsurface after parent was unmapped and remapped"; + + EXPECT_THAT(input->position_on_surface(client), Eq( + std::make_pair(input_offset.first - subsurface_offset.first, input_offset.second - subsurface_offset.second))) + << input << " seen in the wrong place"; +} + +TEST_P(SurfaceInputCombinations, input_seen_after_dragged_off_surface) +{ + auto const top_left = std::make_pair(200, 49); + auto const input_offset = std::make_pair(-5, 5); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto other = client.create_visible_surface(100, 100); + the_server().move_surface_to(other, top_left.first - 102, top_left.second); + struct wl_surface* const other_wl_surface = other; + + auto main = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const main_wl_surface = *main; + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + 5, top_left.second + 5}); + client.roundtrip(); + device->move_to({top_left.first + input_offset.first, top_left.second + input_offset.second}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(other_wl_surface)) + << input << " seen by second surface event though it was dragged from first"; + + EXPECT_THAT(input->current_surface(client), Eq(main_wl_surface)) + << input << " not seen by " << builder << " after being dragged away"; + + EXPECT_THAT(input->position_on_surface(client), Eq(input_offset)) + << input << " not seen by " << builder << " after being dragged away"; +} + +TEST_P(SurfaceInputCombinations, input_seen_by_second_surface_after_drag_off_first_and_up) +{ + auto const top_left = std::make_pair(200, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto other = builder->build( + the_server(), + client, + std::make_pair(top_left.first - 102, top_left.second), + std::make_pair(100, 100)); + struct wl_surface* const other_wl_surface = *other; + + auto main = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const main_wl_surface = *main; + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at({top_left.first + 5, top_left.second + 5}); + client.roundtrip(); + device->move_to({top_left.first - 80, top_left.second + 5}); + client.roundtrip(); + device->up(); + device->down_at({top_left.first - 80, top_left.second + 5}); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(main_wl_surface)) + << input << " seen by first " << builder << " after being up"; + + EXPECT_THAT(input->current_surface(client), Eq(other_wl_surface)) + << input << " not seen by second " << builder << " after being up"; +} + +// Will only be instantiated with toplevel surfaces +class ToplevelInputCombinations : + public wlcs::InProcessServer, + public testing::WithParamInterface, + std::shared_ptr>> +{ +}; + +TEST_P(ToplevelInputCombinations, input_falls_through_surface_without_region_after_null_buffer_committed) +{ + auto const top_left = std::make_pair(64, 49); + wlcs::Client client{the_server()}; + std::shared_ptr builder; + std::shared_ptr input; + std::tie(builder, input) = GetParam(); + + auto lower = client.create_visible_surface(surface_size.first, surface_size.second); + the_server().move_surface_to(lower, top_left.first, top_left.second); + struct wl_surface* const lower_wl_surface = lower; + + auto upper = builder->build( + the_server(), + client, + top_left, + surface_size); + struct wl_surface* const upper_wl_surface = *upper; + wl_surface_attach(upper_wl_surface, nullptr, 0, 0); + wl_surface_commit(upper_wl_surface); + client.roundtrip(); + + auto const device = input->create_device(the_server()); + device->down_at(top_left); + client.roundtrip(); + + EXPECT_THAT(input->current_surface(client), Ne(upper_wl_surface)) + << input << " seen by " << builder << " after null buffer committed"; + + EXPECT_THAT(input->current_surface(client), Eq(lower_wl_surface)) + << input << " not seen by lower toplevel after null buffer committed to " << builder; +} + +// There's way too many region edges/surface type/input device combinations, so we don't run all of them +// multi_rect_edges covers most cases, so we test it against all surface type/input device combinations +// We test the rest against just XDG toplevel + +INSTANTIATE_TEST_SUITE_P( + MultiRectEdges, + RegionSurfaceInputCombinations, + Combine(multi_rect_edges, all_surface_types, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + DefaultEdges, + RegionSurfaceInputCombinations, + Combine(default_edges, all_surface_types, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + FullSurface, + RegionSurfaceInputCombinations, + Combine(full_surface_edges, xdg_stable_surface_type, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + SmallerRegion, + RegionSurfaceInputCombinations, + Combine(smaller_region_edges, xdg_stable_surface_type, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + ClippedLargerRegion, + RegionSurfaceInputCombinations, + Combine(larger_region_edges, xdg_stable_surface_type, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + MultiRectCorners, + RegionSurfaceInputCombinations, + Combine(multi_rect_corners, xdg_stable_surface_type, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + SurfaceInputRegions, + SurfaceInputCombinations, + Combine(all_surface_types, all_input_types)); + +INSTANTIATE_TEST_SUITE_P( + ToplevelInputRegions, + ToplevelInputCombinations, + Combine(toplevel_surface_types, all_input_types)); diff --git a/tests/test_bad_buffer.cpp b/tests/test_bad_buffer.cpp new file mode 100644 index 0000000..8e77938 --- /dev/null +++ b/tests/test_bad_buffer.cpp @@ -0,0 +1,182 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2017 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include + +#include "helpers.h" + +#include "in_process_server.h" + +/* tests, that attempt to crash the compositor on purpose */ + +static struct wl_buffer * +create_bad_shm_buffer(wlcs::Client& client, int width, int height) +{ + struct wl_shm *shm = client.shm(); + int stride = width * 4; + int size = stride * height; + struct wl_shm_pool *pool; + struct wl_buffer *buffer; + int fd; + + fd = wlcs::helpers::create_anonymous_file(size); + + pool = wl_shm_create_pool(shm, fd, size); + buffer = wl_shm_pool_create_buffer( + pool, + 0, + width, + height, + stride, + WL_SHM_FORMAT_ARGB8888); + wl_shm_pool_destroy(pool); + + /* Truncate the file to a small size, so that the compositor + * will access it out-of-bounds, and hit SIGBUS. + */ + assert(ftruncate(fd, 12) == 0); + close(fd); + + return buffer; +} + +using BadBufferTest = wlcs::InProcessServer; + +TEST_F(BadBufferTest, test_truncated_shm_file) +{ + using namespace testing; + + wlcs::Client client{the_server()}; + + bool buffer_consumed{false}; + + auto surface = client.create_visible_surface(200, 200); + + wl_buffer* bad_buffer = create_bad_shm_buffer(client, 200, 200); + + wl_surface_attach(surface, bad_buffer, 0, 0); + wl_surface_damage(surface, 0, 0, 200, 200); + + surface.add_frame_callback([&buffer_consumed](int) { buffer_consumed = true; }); + + wl_surface_commit(surface); + + try + { + client.dispatch_until([&buffer_consumed]() { return buffer_consumed; }); + } + catch (wlcs::ProtocolError const& err) + { + wl_buffer_destroy(bad_buffer); + EXPECT_THAT(err.error_code(), Eq(WL_SHM_ERROR_INVALID_FD)); + EXPECT_THAT(err.interface(), Eq(&wl_buffer_interface)); + return; + } + + FAIL() << "Expected protocol error not raised"; +} + +TEST_F(BadBufferTest, client_lies_about_buffer_size) +{ + using namespace testing; + + wlcs::Client client{the_server()}; + + auto surface = client.create_visible_surface(200, 200); + + auto const width = 200, height = 200; + auto const incorrect_stride = width; + + auto fd = wlcs::helpers::create_anonymous_file(height * incorrect_stride); + + auto shm_pool = wl_shm_create_pool(client.shm(), fd, height * incorrect_stride); + + auto bad_buffer = wl_shm_pool_create_buffer( + shm_pool, + 0, + width, height, + incorrect_stride, // Stride is in bytes, not pixels, so this is ¼ the correct value. + WL_SHM_FORMAT_ARGB8888); + + try + { + /* Buffer creation should fail, so all we need is for the create_buffer + * call to be processed + */ + client.roundtrip(); + } + catch (wlcs::ProtocolError const& err) + { + wl_buffer_destroy(bad_buffer); + EXPECT_THAT(err.error_code(), Eq(WL_SHM_ERROR_INVALID_STRIDE)); + EXPECT_THAT(err.interface(), Eq(&wl_shm_pool_interface)); + return; + } + + FAIL() << "Expected protocol error not raised"; +} + +// This should be identical to the first tests case of the previous test. It tests if a 2nd instance of the server can +// successfully handle a bad buffer. There have been issues with the server installing a SIGBUS handler (via +// wl_shm_buffer_begin_access()) that only works for the first server instance. + +using SecondBadBufferTest = wlcs::InProcessServer; + +TEST_F(SecondBadBufferTest, DISABLED_test_truncated_shm_file) +{ + using namespace testing; + + wlcs::Client client{the_server()}; + + bool buffer_consumed{false}; + + auto surface = client.create_visible_surface(200, 200); + + wl_buffer* bad_buffer = create_bad_shm_buffer(client, 200, 200); + + wl_surface_attach(surface, bad_buffer, 0, 0); + wl_surface_damage(surface, 0, 0, 200, 200); + + surface.add_frame_callback([&buffer_consumed](int) { buffer_consumed = true; }); + + wl_surface_commit(surface); + + try + { + client.dispatch_until([&buffer_consumed]() { return buffer_consumed; }); + } + catch (wlcs::ProtocolError const& err) + { + wl_buffer_destroy(bad_buffer); + EXPECT_THAT(err.error_code(), Eq(WL_SHM_ERROR_INVALID_FD)); + EXPECT_THAT(err.interface(), Eq(&wl_buffer_interface)); + return; + } + + FAIL() << "Expected protocol error not raised"; +} diff --git a/tests/test_surface_events.cpp b/tests/test_surface_events.cpp new file mode 100644 index 0000000..bc5e638 --- /dev/null +++ b/tests/test_surface_events.cpp @@ -0,0 +1,576 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2017 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "helpers.h" +#include "in_process_server.h" + +#include +#include +#include +#include + +#include + +using ClientSurfaceEventsTest = wlcs::InProcessServer; + +// +//static void +//check_pointer(struct client *client, int x, int y) +//{ +// int sx, sy; +// +// /* check that the client got the global pointer update */ +// assert(client->test->pointer_x == x); +// assert(client->test->pointer_y == y); +// +// /* Does global pointer map onto the surface? */ +// if (surface_contains(client->surface, x, y)) { +// /* check that the surface has the pointer focus */ +// assert(client->input->pointer->focus == client->surface); +// +// /* +// * check that the local surface pointer maps +// * to the global pointer. +// */ +// sx = client->input->pointer->x + client->surface->x; +// sy = client->input->pointer->y + client->surface->y; +// assert(sx == x); +// assert(sy == y); +// } else { +// /* +// * The global pointer does not map onto surface. So +// * check that it doesn't have the pointer focus. +// */ +// assert(client->input->pointer->focus == NULL); +// } +//} +// +//static void +//check_pointer_move(struct client *client, int x, int y) +//{ +// weston_test_move_pointer(client->test->weston_test, x, y); +// client_roundtrip(client); +// check_pointer(client, x, y); +//} +// +//TEST(test_pointer_surface_move) +//{ +// struct client *client; +// +// client = create_client_and_test_surface(100, 100, 100, 100); +// assert(client); +// +// /* move pointer outside of client */ +// assert(!surface_contains(client->surface, 50, 50)); +// check_pointer_move(client, 50, 50); +// +// /* move client center to pointer */ +// move_client(client, 0, 0); +// assert(surface_contains(client->surface, 50, 50)); +// check_pointer(client, 50, 50); +//} +// +//static int +//output_contains_client(struct client *client) +//{ +// struct output *output = client->output; +// struct surface *surface = client->surface; +// +// return !(output->x >= surface->x + surface->width +// || output->x + output->width <= surface->x +// || output->y >= surface->y + surface->height +// || output->y + output->height <= surface->y); +//} +// +//static void +//check_client_move(struct client *client, int x, int y) +//{ +// move_client(client, x, y); +// +// if (output_contains_client(client)) { +// assert(client->surface->output == client->output); +// } else { +// assert(client->surface->output == NULL); +// } +//} +// +//TEST(test_surface_output) +//{ +// struct client *client; +// int x, y; +// +// client = create_client_and_test_surface(100, 100, 100, 100); +// assert(client); +// +// assert(output_contains_client(client)); +// +// /* not visible */ +// x = 0; +// y = -client->surface->height; +// check_client_move(client, x, y); +// +// /* visible */ +// check_client_move(client, x, ++y); +// +// /* not visible */ +// x = -client->surface->width; +// y = 0; +// check_client_move(client, x, y); +// +// /* visible */ +// check_client_move(client, ++x, y); +// +// /* not visible */ +// x = client->output->width; +// y = 0; +// check_client_move(client, x, y); +// +// /* visible */ +// check_client_move(client, --x, y); +// assert(output_contains_client(client)); +// +// /* not visible */ +// x = 0; +// y = client->output->height; +// check_client_move(client, x, y); +// assert(!output_contains_client(client)); +// +// /* visible */ +// check_client_move(client, x, --y); +// assert(output_contains_client(client)); +//} + +struct PointerMotion +{ + static int constexpr window_width = 231; + static int constexpr window_height = 220; + std::string name; + int initial_x, initial_y; // Relative to surface top-left + int dx, dy; +}; + +std::ostream& operator<<(std::ostream& out, PointerMotion const& motion) +{ + return out << motion.name; +} + +class SurfacePointerMotionTest : + public wlcs::InProcessServer, + public testing::WithParamInterface +{ +}; + +TEST_P(SurfacePointerMotionTest, pointer_movement) +{ + using namespace testing; + + auto pointer = the_server().create_pointer(); + + wlcs::Client client{the_server()}; + + auto const params = GetParam(); + + auto surface = client.create_visible_surface( + params.window_width, + params.window_height); + + int const top_left_x = 23, top_left_y = 231; + the_server().move_surface_to(surface, top_left_x, top_left_y); + + auto const wl_surface = static_cast(surface); + + pointer.move_to(top_left_x + params.initial_x, top_left_y + params.initial_y); + + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Ne(wl_surface)); + + /* move pointer; it should now be inside the surface */ + pointer.move_by(params.dx, params.dy); + + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Eq(wl_surface)); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(params.initial_x + params.dx), + wl_fixed_from_int(params.initial_y + params.dy)))); + + /* move pointer back; it should now be outside the surface */ + pointer.move_by(-params.dx, -params.dy); + + client.roundtrip(); + EXPECT_THAT(client.window_under_cursor(), Ne(wl_surface)); +} + +INSTANTIATE_TEST_SUITE_P( + PointerCrossingSurfaceCorner, + SurfacePointerMotionTest, + testing::Values( + PointerMotion{"Top-left", -1, -1, 1, 1}, + PointerMotion{"Bottom-left", -1, PointerMotion::window_height, 1, -1}, + PointerMotion{"Bottom-right", PointerMotion::window_width, PointerMotion::window_height, -1, -1}, + PointerMotion{"Top-right", PointerMotion::window_width, -1, -1, 1} + )); + +INSTANTIATE_TEST_SUITE_P( + PointerCrossingSurfaceEdge, + SurfacePointerMotionTest, + testing::Values( + PointerMotion{ + "Centre-left", + -1, PointerMotion::window_height / 2, + 1, 0}, + PointerMotion{ + "Bottom-centre", + PointerMotion::window_width / 2, PointerMotion::window_height, + 0, -1}, + PointerMotion{ + "Centre-right", + PointerMotion::window_width, PointerMotion::window_height / 2, + -1, 0}, + PointerMotion{ + "Top-centre", + PointerMotion::window_width / 2, -1, + 0, 1} + )); + +TEST_F(ClientSurfaceEventsTest, surface_moves_under_pointer) +{ + using namespace testing; + + auto pointer = the_server().create_pointer(); + + wlcs::Client client{the_server()}; + + auto surface = client.create_visible_surface(100, 100); + auto const wl_surface = static_cast(surface); + + /* Set up the pointer outside the surface */ + the_server().move_surface_to(surface, 0, 0); + pointer.move_to(500, 500); + + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Ne(wl_surface)); + + /* move the surface so that it is under the pointer */ + the_server().move_surface_to(surface, 450, 450); + + client.dispatch_until( + [wl_surface, &client]() + { + return client.window_under_cursor() == wl_surface; + }); + + EXPECT_THAT(client.window_under_cursor(), Eq(wl_surface)); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(50), + wl_fixed_from_int(50)))); +} + +TEST_F(ClientSurfaceEventsTest, surface_moves_over_surface_under_pointer) +{ + using namespace testing; + + auto pointer = the_server().create_pointer(); + + wlcs::Client client{the_server()}; + + auto first_surface = client.create_visible_surface(100, 100); + auto second_surface = client.create_visible_surface(100, 100); + + /* Set up the pointer outside the surface */ + the_server().move_surface_to(first_surface, 0, 0); + the_server().move_surface_to(second_surface, 0, 0); + pointer.move_to(500, 500); + + client.roundtrip(); + + /* move the first surface so that it is under the pointer */ + the_server().move_surface_to(first_surface, 450, 450); + + bool first_surface_focused{false}; + client.add_pointer_enter_notification( + [&first_surface_focused, &first_surface](wl_surface* surf, auto, auto) + { + if (surf == first_surface) + { + first_surface_focused = true; + } + return false; + }); + + // Wait until the first surface is focused + client.dispatch_until( + [&first_surface_focused]() + { + return first_surface_focused; + }); + + client.add_pointer_leave_notification( + [&first_surface_focused, &first_surface](wl_surface* surf) + { + if (surf == first_surface) + { + first_surface_focused = false; + } + return false; + }); + + bool second_surface_focused{false}; + client.add_pointer_enter_notification( + [&first_surface_focused, &second_surface_focused, &second_surface](auto surf, auto x, auto y) + { + if (surf == second_surface) + { + /* + * Protocol requires that the pointer-leave event is sent before pointer-enter + */ + EXPECT_FALSE(first_surface_focused); + second_surface_focused = true; + EXPECT_THAT(x, Eq(wl_fixed_from_int(50))); + EXPECT_THAT(y, Eq(wl_fixed_from_int(50))); + } + return false; + }); + + the_server().move_surface_to(second_surface, 450, 450); + + client.dispatch_until( + [&second_surface_focused]() + { + return second_surface_focused; + }); +} + +TEST_F(ClientSurfaceEventsTest, surface_resizes_under_pointer) +{ + using namespace testing; + + auto pointer = the_server().create_pointer(); + + wlcs::Client client{the_server()}; + + auto surface = client.create_visible_surface(100, 100); + + /* Set up the pointer outside the surface */ + the_server().move_surface_to(surface, 400, 400); + pointer.move_to(500, 500); + + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Ne(static_cast(surface))); + + bool surface_entered{false}; + client.add_pointer_enter_notification( + [&surface_entered, &surface](wl_surface* entered_surf, wl_fixed_t x, wl_fixed_t y) + { + EXPECT_THAT(surface, Eq(entered_surf)); + EXPECT_THAT(x, Eq(wl_fixed_from_int(100))); + EXPECT_THAT(y, Eq(wl_fixed_from_int(100))); + surface_entered = true; + return false; + }); + client.add_pointer_leave_notification( + [&surface_entered, &surface](wl_surface* left_surf) + { + EXPECT_THAT(surface, Eq(left_surf)); + surface_entered = false; + return false; + }); + + auto larger_buffer = wlcs::ShmBuffer{client, 200, 200}; + auto smaller_buffer = wlcs::ShmBuffer{client, 50, 50}; + + // Resize the surface so that the pointer is now over the top... + wl_surface_attach(surface, larger_buffer, 0, 0); + wl_surface_commit(surface); + + client.dispatch_until( + [&surface_entered]() + { + return surface_entered; + }); + + // Resize the surface so that the pointer is no longer over the top... + wl_surface_attach(surface, smaller_buffer, 0, 0); + wl_surface_commit(surface); + + client.dispatch_until( + [&surface_entered]() + { + return !surface_entered; + }); +} + +TEST_F(ClientSurfaceEventsTest, surface_moves_while_under_pointer) +{ + using namespace testing; + + auto pointer = the_server().create_pointer(); + + wlcs::Client client{the_server()}; + + auto surface = client.create_visible_surface(100, 100); + + the_server().move_surface_to(surface, 450, 450); + pointer.move_to(500, 500); + + std::deque> surface_movements = { + std::make_pair(445, 455), + std::make_pair(460, 405), + std::make_pair(420, 440), + std::make_pair(430, 460), + std::make_pair(0, 0) // The last motion is not checked for + }; + + client.dispatch_until( + [&client, &surface]() + { + if (client.window_under_cursor() == surface) + { + EXPECT_THAT(client.pointer_position().first, Eq(wl_fixed_from_int(50))); + EXPECT_THAT(client.pointer_position().second, Eq(wl_fixed_from_int(50))); + return true; + } + return false; + }); + + int expected_x{55}, expected_y{45}; + client.add_pointer_motion_notification( + [this, &surface, &surface_movements, &expected_x, &expected_y] + (wl_fixed_t x, wl_fixed_t y) + { + EXPECT_THAT(x, Eq(wl_fixed_from_int(expected_x))); + EXPECT_THAT(y, Eq(wl_fixed_from_int(expected_y))); + + auto next_movement = surface_movements.front(); + surface_movements.pop_front(); + + the_server().move_surface_to( + surface, + next_movement.first, + next_movement.second); + + expected_x = 500 - next_movement.first; + expected_y = 500 - next_movement.second; + return true; + }); + + // Do the initial surface move + the_server().move_surface_to( + surface, + surface_movements.front().first, + surface_movements.front().second); + surface_movements.pop_front(); + + client.dispatch_until( + [&surface_movements]() + { + return surface_movements.empty(); + }); +} + +TEST_F(ClientSurfaceEventsTest, frame_timestamp_increases) +{ + using namespace testing; + using namespace std::chrono_literals; + + wlcs::Client client{the_server()}; + + auto surface = client.create_visible_surface(100, 100); + + std::array buffers = {{ + wlcs::ShmBuffer{client, 100, 100}, + wlcs::ShmBuffer{client, 100, 100}, + wlcs::ShmBuffer{client, 100, 100} + }}; + + /* + * The first buffer must never be released, since it is replaced before + * it is committed, therefore it never becomes busy. + */ + wl_surface_attach(surface, buffers[0], 0, 0); + wl_surface_attach(surface, buffers[1], 0, 0); + + uint32_t prev_frame_time = 0; + int frame_callback_count = 0; + + auto const check_time_and_increment_count = + [&](uint32_t timestamp) + { + EXPECT_THAT(timestamp, Gt(prev_frame_time)); + prev_frame_time = timestamp; + frame_callback_count++; + }; + + surface.add_frame_callback(check_time_and_increment_count); + wl_surface_commit(surface); + + /* We don't need to wait for the server, but we *do* need + * the server to see this commit + */ + client.flush(); + + /** + * When run against a *real* compositor we should not need any + * delay here - when running on a real display, we would expect + * the second commit to wait for the next refresh cycle. + * + * But we're probably not running on a real display, so make + * things easier for the integration by waiting a simulated + * refresh cycle (at 60Hz) before submitting the next buffer. + */ + std::this_thread::sleep_for(std::chrono::ceil(1.0s/60)); + + wl_surface_attach(surface, buffers[2], 0, 0); + surface.add_frame_callback(check_time_and_increment_count); + wl_surface_commit(surface); + + client.dispatch_until([&frame_callback_count]() + { + return frame_callback_count == 2; + }); +} + +TEST_F(ClientSurfaceEventsTest, surface_enters_output) +{ + using namespace testing; + + wlcs::Client client{the_server()}; + + auto surface = client.create_visible_surface(100, 100); + client.roundtrip(); + + EXPECT_THAT(surface.current_outputs(), Not(IsEmpty())) << "Surface did not initially enter output"; +} + +// TODO: test surfaces can leave outputs once we can create multiple outputs + +// TODO: make parameterized for different types of shell surfaces diff --git a/tests/text_input_v3_with_input_method_v2.cpp b/tests/text_input_v3_with_input_method_v2.cpp new file mode 100644 index 0000000..2142cea --- /dev/null +++ b/tests/text_input_v3_with_input_method_v2.cpp @@ -0,0 +1,298 @@ +/* + * Copyright © 2021 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "in_process_server.h" +#include "mock_text_input_v3.h" +#include "mock_input_method_v2.h" +#include "version_specifier.h" +#include "xdg_shell_stable.h" + +#include +#include + +using namespace testing; + +struct TextInputV3WithInputMethodV2Test : wlcs::StartedInProcessServer +{ + TextInputV3WithInputMethodV2Test() + : StartedInProcessServer{}, + pointer{the_server().create_pointer()}, + app_client{the_server()}, + text_input_manager{app_client.bind_if_supported(wlcs::AnyVersion)}, + text_input{zwp_text_input_manager_v3_get_text_input(text_input_manager, app_client.seat())}, + input_client{the_server()}, + input_method_manager{input_client.bind_if_supported(wlcs::AnyVersion)}, + input_method{zwp_input_method_manager_v2_get_input_method(input_method_manager, input_client.seat())} + { + } + + void create_focussed_surface() + { + app_surface.emplace(app_client.create_visible_surface(100, 100)); + the_server().move_surface_to(app_surface.value(), 0, 0); + pointer.move_to(10, 10); + pointer.left_click(); + app_client.roundtrip(); + } + + wlcs::Pointer pointer; + wlcs::Client app_client; + wlcs::WlHandle text_input_manager; + NiceMock text_input; + std::optional app_surface; + wlcs::Client input_client; + wlcs::WlHandle input_method_manager; + NiceMock input_method; +}; + +TEST_F(TextInputV3WithInputMethodV2Test, text_input_enters_surface_on_focus) +{ + wl_surface* entered{nullptr}; + EXPECT_CALL(text_input, enter(_)) + .WillOnce(SaveArg<0>(&entered)); + create_focussed_surface(); + EXPECT_THAT(entered, Eq(app_surface.value().wl_surface())); +} + +TEST_F(TextInputV3WithInputMethodV2Test, text_input_leaves_surface_on_unfocus) +{ + create_focussed_surface(); + // Text input will not .enter() other surface because it belongs to a different client + EXPECT_CALL(text_input, enter(_)).Times(0); + EXPECT_CALL(text_input, leave(app_surface.value().wl_surface())); + + // Create a 2nd client with a focused surface + wlcs::Client other_client{the_server()}; + auto other_surface = other_client.create_visible_surface(100, 100); + the_server().move_surface_to(other_surface, 200, 200); + pointer.move_to(210, 210); + pointer.left_click(); + app_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, input_method_can_be_enabled) +{ + create_focussed_surface(); + InSequence seq; + EXPECT_CALL(input_method, activate()); + EXPECT_CALL(input_method, done()); + zwp_text_input_v3_enable(text_input); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, input_method_can_be_disabled) +{ + create_focussed_surface(); + InSequence seq; + EXPECT_CALL(input_method, activate()); + EXPECT_CALL(input_method, done()); + zwp_text_input_v3_enable(text_input); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); + EXPECT_CALL(input_method, deactivate()); + EXPECT_CALL(input_method, done()); + zwp_text_input_v3_disable(text_input); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, input_method_disabled_when_text_input_destroyed) +{ + create_focussed_surface(); + { + NiceMock text_input{ + zwp_text_input_manager_v3_get_text_input(text_input_manager, app_client.seat())}; + InSequence seq; + EXPECT_CALL(input_method, activate()); + EXPECT_CALL(input_method, done()); + zwp_text_input_v3_enable(text_input); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); + EXPECT_CALL(input_method, deactivate()); + EXPECT_CALL(input_method, done()); + } + app_client.roundtrip(); + input_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, text_field_state_can_be_set) +{ + auto const text = "some text"; + auto const cursor = 2; + auto const anchor = 1; + auto const change_cause = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER; + auto const content_hint = + ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION | + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA; + auto const content_purpose = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME; + + create_focussed_surface(); + + Expectation a = EXPECT_CALL(input_method, activate()); + Expectation b = EXPECT_CALL(input_method, surrounding_text(text, cursor, anchor)).After(a); + Expectation c = EXPECT_CALL(input_method, text_change_cause(change_cause)).After(a); + Expectation d = EXPECT_CALL(input_method, content_type(content_hint, content_purpose)).After(a); + EXPECT_CALL(input_method, done()).After(a, b, c, d); + + zwp_text_input_v3_enable(text_input); + zwp_text_input_v3_set_surrounding_text(text_input, text, cursor, anchor); + zwp_text_input_v3_set_text_change_cause(text_input, change_cause); + zwp_text_input_v3_set_content_type(text_input, content_hint, content_purpose); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, input_method_can_send_text) +{ + auto const text = "some text"; + auto const delete_left = 1; + auto const delete_right = 2; + + create_focussed_surface(); + zwp_text_input_v3_enable(text_input); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); + + Expectation a = EXPECT_CALL(text_input, commit_string(text)); + Expectation b = EXPECT_CALL(text_input, delete_surrounding_text(delete_left, delete_right)); + // Expected serial is 1 because we've sent exactly 1 commit + EXPECT_CALL(text_input, done(1)).After(a, b); + zwp_input_method_v2_commit_string(input_method, text); + zwp_input_method_v2_delete_surrounding_text(input_method, delete_left, delete_right); + zwp_input_method_v2_commit(input_method, input_method.done_count()); + input_client.roundtrip(); + app_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, input_method_can_send_preedit) +{ + auto const text = "preedit"; + auto const cursor_begin = 1; + auto const cursor_end = 2; + + create_focussed_surface(); + zwp_text_input_v3_enable(text_input); + zwp_text_input_v3_commit(text_input); + app_client.roundtrip(); + input_client.roundtrip(); + + InSequence seq; + EXPECT_CALL(text_input, preedit_string(text, cursor_begin, cursor_end)); + // Expected serial is 1 because we've sent exactly 1 commit + EXPECT_CALL(text_input, done(1)); + zwp_input_method_v2_set_preedit_string(input_method, text, cursor_begin, cursor_end); + zwp_input_method_v2_commit(input_method, input_method.done_count()); + input_client.roundtrip(); + app_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, text_input_does_not_enter_non_grabbing_popup) +{ + auto parent_surface = std::make_unique(app_client); + EXPECT_CALL(text_input, enter(parent_surface->wl_surface())); + auto parent_xdg_surface = std::make_shared(app_client, *parent_surface); + auto parent_xdg_toplevel = std::make_shared(*parent_xdg_surface); + parent_surface->attach_visible_buffer(20, 20); + app_client.roundtrip(); + Mock::VerifyAndClearExpectations(&text_input); + auto child_surface = std::make_unique(app_client); + EXPECT_CALL(text_input, leave(_)).Times(0); + EXPECT_CALL(text_input, enter(_)).Times(0); + auto child_xdg_surface = std::make_shared(app_client, *child_surface); + auto child_xdg_popup = std::make_shared( + *child_xdg_surface, + parent_xdg_surface.get(), + wlcs::XdgPositionerStable{app_client}.setup_default({20, 20})); + child_surface->attach_visible_buffer(20, 20); + app_client.roundtrip(); +} + +TEST_F(TextInputV3WithInputMethodV2Test, text_input_enters_grabbing_popup) +{ + InSequence seq; + auto parent_surface = std::make_unique(app_client); + EXPECT_CALL(text_input, enter(parent_surface->wl_surface())); + auto parent_xdg_surface = std::make_shared(app_client, *parent_surface); + auto parent_xdg_toplevel = std::make_shared(*parent_xdg_surface); + parent_surface->attach_visible_buffer(20, 20); + the_server().move_surface_to(*parent_surface, 0, 0); + app_client.roundtrip(); + Mock::VerifyAndClearExpectations(&text_input); + + // This is needed to get a serial, which will be used later on + auto pointer = the_server().create_pointer(); + pointer.move_to(2, 2); + pointer.left_click(); + app_client.roundtrip(); + + auto child_surface = std::make_unique(app_client); + EXPECT_CALL(text_input, leave(parent_surface->wl_surface())); + EXPECT_CALL(text_input, enter(child_surface->wl_surface())); + auto child_xdg_surface = std::make_shared(app_client, *child_surface); + auto child_xdg_popup = std::make_shared( + *child_xdg_surface, + parent_xdg_surface.get(), + wlcs::XdgPositionerStable{app_client}.setup_default({20, 20})); + xdg_popup_grab(*child_xdg_popup, app_client.seat(), app_client.latest_serial().value()); + child_surface->attach_visible_buffer(20, 20); + app_client.roundtrip(); +} + +/// Regression test for https://github.com/MirServer/mir/issues/2189 +TEST_F(TextInputV3WithInputMethodV2Test, text_input_enters_parent_surface_after_child_destroyed) +{ + InSequence seq; + auto parent_surface = std::make_unique(app_client); + EXPECT_CALL(text_input, enter(parent_surface->wl_surface())); + auto parent_xdg_surface = std::make_shared(app_client, *parent_surface); + auto parent_xdg_toplevel = std::make_shared(*parent_xdg_surface); + parent_surface->attach_visible_buffer(20, 20); + app_client.roundtrip(); + Mock::VerifyAndClearExpectations(&text_input); + + { + auto child_surface = std::make_unique(app_client); + EXPECT_CALL(text_input, leave(parent_surface->wl_surface())); + EXPECT_CALL(text_input, enter(child_surface->wl_surface())); + auto child_xdg_surface = std::make_shared(app_client, *child_surface); + auto child_xdg_toplevel = std::make_shared(*child_xdg_surface); + xdg_toplevel_set_parent(*child_xdg_toplevel, *parent_xdg_toplevel); + child_surface->attach_visible_buffer(20, 20); + app_client.roundtrip(); + Mock::VerifyAndClearExpectations(&text_input); + + EXPECT_CALL(text_input, leave(nullptr)); // Child surface will be destroyed by the time the message comes in + EXPECT_CALL(text_input, enter(parent_surface->wl_surface())); + } + + app_client.roundtrip(); +} diff --git a/tests/touches.cpp b/tests/touches.cpp new file mode 100644 index 0000000..63a0610 --- /dev/null +++ b/tests/touches.cpp @@ -0,0 +1,170 @@ +/* + * Copyright © 2018 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "xdg_shell_stable.h" +#include "surface_builder.h" + +#include + +#include +#include + +using namespace testing; + +class TouchTest: + public wlcs::InProcessServer, + public testing::WithParamInterface> +{ +}; + +TEST_P(TouchTest, touch_on_surface_seen) +{ + int const window_width = 300, window_height = 300; + int const window_top_left_x = 64, window_top_left_y = 7; + + wlcs::Client client{the_server()}; + auto const surface = GetParam()->build( + the_server(), client, {window_top_left_x, window_top_left_y}, {window_width, window_height}); + auto const wl_surface = static_cast(*surface); + + auto touch = the_server().create_touch(); + int touch_x = window_top_left_x + 27; + int touch_y = window_top_left_y + 8; + + touch.down_at(touch_x, touch_y); + client.roundtrip(); + ASSERT_THAT(client.touched_window(), Eq(wl_surface)) << "touch did not register on surface"; + ASSERT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(touch_x - window_top_left_x), + wl_fixed_from_int(touch_y - window_top_left_y)))) << "touch came down in the wrong place"; + touch.up(); + client.roundtrip(); +} + +TEST_P(TouchTest, touch_and_drag_on_surface_seen) +{ + int const window_width = 300, window_height = 300; + int const window_top_left_x = 64, window_top_left_y = 12; + int const touch_x = window_top_left_x + 27, touch_y = window_top_left_y + 140; + int const dx = 37, dy = -52; + + wlcs::Client client{the_server()}; + auto const surface = GetParam()->build( + the_server(), client, {window_top_left_x, window_top_left_y}, {window_width, window_height}); + auto const wl_surface = static_cast(*surface); + + auto touch = the_server().create_touch(); + + touch.down_at(touch_x, touch_y); + client.roundtrip(); + ASSERT_THAT(client.touched_window(), Eq(wl_surface)) << "touch did not register on surface"; + ASSERT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(touch_x - window_top_left_x), + wl_fixed_from_int(touch_y - window_top_left_y)))) << "touch came down in the wrong place"; + + touch.move_to(touch_x + dx, touch_y + dy); + client.roundtrip(); + ASSERT_THAT(client.touched_window(), Eq(wl_surface)) << "surface was unfocused when it shouldn't have been"; + ASSERT_THAT(client.touch_position(), + Ne(std::make_pair( + wl_fixed_from_int(touch_x - window_top_left_x), + wl_fixed_from_int(touch_y - window_top_left_y)))) << "touch did not move"; + ASSERT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(touch_x - window_top_left_x + dx), + wl_fixed_from_int(touch_y - window_top_left_y + dy)))) << "touch did not end up in the right place"; + + touch.up(); + client.roundtrip(); +} + +TEST_P(TouchTest, touch_drag_outside_of_surface_and_back_not_lost) +{ + int const window_width = 300, window_height = 300; + int const window_top_left_x = 64, window_top_left_y = 12; + int const touch_a_x = window_top_left_x + 27, touch_a_y = window_top_left_y + 12; + int const touch_b_x = window_top_left_x - 6, touch_b_y = window_top_left_y + window_width + 8; + + wlcs::Client client{the_server()}; + auto const surface = GetParam()->build( + the_server(), client, {window_top_left_x, window_top_left_y}, {window_width, window_height}); + auto const wl_surface = static_cast(*surface); + + auto touch = the_server().create_touch(); + + touch.down_at(touch_a_x, touch_a_y); + client.roundtrip(); + ASSERT_THAT(client.touched_window(), Eq(wl_surface)) << "touch did not register on surface"; + ASSERT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(touch_a_x - window_top_left_x), + wl_fixed_from_int(touch_a_y - window_top_left_y)))) << "touch came down in the wrong place"; + + touch.move_to(touch_b_x, touch_b_y); + client.roundtrip(); + EXPECT_THAT(client.touched_window(), Eq(wl_surface)) << "touch was lost when it moved out of the surface"; + if (client.touched_window() == wl_surface) + { + EXPECT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(touch_b_x - window_top_left_x), + wl_fixed_from_int(touch_b_y - window_top_left_y)))) << "touch did not end up in the right place outside of the surface"; + } + + touch.move_to(touch_a_x, touch_a_y); + client.roundtrip(); + EXPECT_THAT(client.touched_window(), Eq(wl_surface)) << "touch did not come back onto surface"; + EXPECT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(touch_a_x - window_top_left_x), + wl_fixed_from_int(touch_a_y - window_top_left_y)))) << "touch came back in the wrong place"; + + touch.up(); + client.roundtrip(); +} + +TEST_P(TouchTest, sends_touch_up_on_surface_destroy) +{ + int const window_width = 300, window_height = 300; + int const window_top_left_x = 64, window_top_left_y = 7; + + wlcs::Client client{the_server()}; + auto surface = GetParam()->build( + the_server(), client, {window_top_left_x, window_top_left_y}, {window_width, window_height}); + + auto touch = the_server().create_touch(); + int touch_x = window_top_left_x + 27; + int touch_y = window_top_left_y + 8; + touch.down_at(touch_x, touch_y); + client.roundtrip(); + + surface.reset(); + client.roundtrip(); + + ASSERT_THAT(client.touched_window(), Eq(nullptr)) << "Touch did not leave surface when surface was destroyed"; +} + +INSTANTIATE_TEST_SUITE_P( + AllSurfaceTypes, + TouchTest, + ValuesIn(wlcs::SurfaceBuilder::all_surface_types()), + wlcs::SubsurfaceBuilder::surface_builder_to_string); diff --git a/tests/wl_output.cpp b/tests/wl_output.cpp new file mode 100644 index 0000000..9b0fb3b --- /dev/null +++ b/tests/wl_output.cpp @@ -0,0 +1,57 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "version_specifier.h" + +#include + +#include + +using namespace testing; +using namespace wlcs; + +using WlOutputTest = wlcs::InProcessServer; + +TEST_F(WlOutputTest, wl_output_properties_set) +{ + wlcs::Client client{the_server()}; + + ASSERT_THAT(client.output_count(), Ge(1u)); + auto output = client.output_state(0); + + EXPECT_THAT((bool)output.geometry_position, Eq(true)); + EXPECT_THAT((bool)output.mode_size, Eq(true)); + EXPECT_THAT((bool)output.scale, Eq(true)); +} + +TEST_F(WlOutputTest, wl_output_release) +{ + wlcs::Client client{the_server()}; + + { + // Acquire *any* wl_output; we don't care which + auto const output = + client.bind_if_supported(wlcs::AtLeastVersion{WL_OUTPUT_RELEASE_SINCE_VERSION}); + client.roundtrip(); + } + // output is now released + + client.roundtrip(); +} diff --git a/tests/wlr_foreign_toplevel_management_v1.cpp b/tests/wlr_foreign_toplevel_management_v1.cpp new file mode 100644 index 0000000..b824d3d --- /dev/null +++ b/tests/wlr_foreign_toplevel_management_v1.cpp @@ -0,0 +1,902 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "helpers.h" +#include "version_specifier.h" +#include "in_process_server.h" +#include "xdg_shell_stable.h" +#include "generated/wlr-foreign-toplevel-management-unstable-v1-client.h" + +#include +#include +#include + +using namespace testing; + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwlr_foreign_toplevel_manager_v1) +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwlr_foreign_toplevel_handle_v1) +} + +namespace +{ +int const w = 10, h = 15; + +class ForeignToplevelHandle +{ +public: + ForeignToplevelHandle(zwlr_foreign_toplevel_handle_v1* handle); + ForeignToplevelHandle(ForeignToplevelHandle const&) = delete; + ForeignToplevelHandle& operator=(ForeignToplevelHandle const&) = delete; + + auto is_dirty() const { return dirty_; } + auto title() const { return title_; } + auto app_id() const { return app_id_; } + auto outputs() const -> std::vector const& { return outputs_; } + auto maximized() const { return maximized_; } + auto minimized() const { return minimized_; } + auto activated() const { return activated_; } + auto fullscreen() const { return fullscreen_; } + auto destroyed() const { return destroyed_; } + + operator zwlr_foreign_toplevel_handle_v1*() const { return handle; } + wlcs::WlHandle const handle; + +private: + static auto get_self(void* data) -> ForeignToplevelHandle* + { + return static_cast(data); + } + + bool dirty_{false}; + std::optional title_; + std::optional app_id_; + std::vector outputs_; + bool maximized_{false}, minimized_{false}, activated_{false}, fullscreen_{false}; + + bool destroyed_{false}; +}; + +ForeignToplevelHandle::ForeignToplevelHandle(zwlr_foreign_toplevel_handle_v1* handle) + : handle{handle} +{ + static zwlr_foreign_toplevel_handle_v1_listener const listener = { + [] /* title */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*, + char const* title) + { + auto self = get_self(data); + self->title_ = title; + self->dirty_ = true; + }, + [] /* app_id */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*, + char const* app_id) + { + auto self = get_self(data); + self->app_id_ = app_id; + self->dirty_ = true; + }, + [] /* output_enter */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*, + wl_output* output) + { + auto self = get_self(data); + self->outputs_.push_back(output); + self->dirty_ = true; + }, + [] /* output_leave */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*, + wl_output* output) + { + auto self = get_self(data); + std::remove( + self->outputs_.begin(), + self->outputs_.end(), + output); + self->dirty_ = true; + }, + [] /*state */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*, + wl_array* state) + { + auto self = get_self(data); + + self->maximized_ = false; + self->minimized_ = false; + self->activated_ = false; + self->fullscreen_ = false; + + for (auto item = static_cast(state->data); + (char*)item < static_cast(state->data) + state->size; + item++) + { + switch (*item) + { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: + self->maximized_ = true; + break; + + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: + self->minimized_ = true; + break; + + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: + self->activated_ = true; + break; + + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: + self->fullscreen_ = true; + break; + } + } + self->dirty_ = true; + }, + [] /* done */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*) + { + auto self = get_self(data); + self->dirty_ = false; + }, + [] /* closed */ ( + void* data, + zwlr_foreign_toplevel_handle_v1*) + { + auto self = get_self(data); + self->destroyed_ = true; + self->dirty_ = false; + } + }; + zwlr_foreign_toplevel_handle_v1_add_listener(handle, &listener, this); +} + +class ForeignToplevelManager +{ +public: + ForeignToplevelManager(wlcs::Client& client); + + auto toplevels() -> std::vector> const& + { + toplevels_.erase( + std::remove_if( + toplevels_.begin(), + toplevels_.end(), + [](auto const& toplevel) { return toplevel->destroyed(); }), + toplevels_.end()); + + return toplevels_; + } + + wlcs::WlHandle const manager; + +private: + std::vector> toplevels_; +}; + +ForeignToplevelManager::ForeignToplevelManager(wlcs::Client& client) + : manager{client.bind_if_supported(wlcs::AnyVersion)} +{ + static zwlr_foreign_toplevel_manager_v1_listener const listener = { + +[] /* toplevel */ ( + void* data, + zwlr_foreign_toplevel_manager_v1*, + zwlr_foreign_toplevel_handle_v1* toplevel) + { + auto self = static_cast(data); + auto handle = std::make_unique(toplevel); + self->toplevels_.push_back(std::move(handle)); + }, + +[] /* finished */ ( + void* /*data*/, + zwlr_foreign_toplevel_manager_v1 *) + { + } + }; + zwlr_foreign_toplevel_manager_v1_add_listener(manager, &listener, this); +} + +class ForeignToplevelManagerTest + : public wlcs::StartedInProcessServer +{ +}; + +class ForeignToplevelHandleTest + : public wlcs::StartedInProcessServer +{ +public: + ForeignToplevelHandleTest() + : client{the_server()}, + manager{client}, + surface{client}, + xdg_surface{client, surface}, + xdg_toplevel{xdg_surface} + { + } + + auto toplevel() -> ForeignToplevelHandle const& + { + if (manager.toplevels().empty()) + BOOST_THROW_EXCEPTION(std::runtime_error("Manager does not know about any toplevels")); + if (manager.toplevels().size() > 1u) + BOOST_THROW_EXCEPTION(std::runtime_error( + "Manager knows about " + std::to_string(manager.toplevels().size()) + " toplevels")); + if (manager.toplevels()[0]->is_dirty()) + BOOST_THROW_EXCEPTION(std::runtime_error("Toplevel has pending updates")); + return *manager.toplevels()[0]; + } + + auto toplevel(std::string const& app_id) -> ForeignToplevelHandle const& + { + std::optional match; + for (auto const& i : manager.toplevels()) + { + if (i->app_id() == app_id) + { + if (match) + BOOST_THROW_EXCEPTION(std::runtime_error("Multiple toplevels have the same app ID " + app_id)); + else + match = i.get(); + } + } + if (!match) + BOOST_THROW_EXCEPTION(std::runtime_error("No toplevels have the app ID " + app_id)); + if (match.value()->is_dirty()) + BOOST_THROW_EXCEPTION(std::runtime_error("Toplevel has pending updates")); + return *match.value(); + } + + wlcs::Client client; + ForeignToplevelManager manager; + wlcs::Surface surface; + wlcs::XdgSurfaceStable xdg_surface; + wlcs::XdgToplevelStable xdg_toplevel; +}; +} + +TEST_F(ForeignToplevelManagerTest, does_not_detect_toplevels_when_test_creates_none) +{ + wlcs::Client client{the_server()}; + ForeignToplevelManager manager{client}; + client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(0u)); +} + +TEST_F(ForeignToplevelManagerTest, detects_toplevel_from_same_client) +{ + wlcs::Client client{the_server()}; + + auto surface{client.create_visible_surface(w, h)}; + + ForeignToplevelManager manager{client}; + client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(1u)); +} + +TEST_F(ForeignToplevelManagerTest, detects_toplevel_from_different_client) +{ + wlcs::Client foreign_client{the_server()}; + wlcs::Client observer_client{the_server()}; + + auto surface{foreign_client.create_visible_surface(w, h)}; + + ForeignToplevelManager manager{observer_client}; + observer_client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(1u)); +} + +TEST_F(ForeignToplevelManagerTest, detects_toplevel_created_after_manager) +{ + wlcs::Client client{the_server()}; + + ForeignToplevelManager manager{client}; + client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(0u)); + + auto surface{client.create_visible_surface(w, h)}; + + client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(1u)); +} + +TEST_F(ForeignToplevelManagerTest, detects_multiple_toplevels_from_multiple_clients) +{ + wlcs::Client foreign_client{the_server()}; + wlcs::Client observer_client{the_server()}; + + auto foreign_surface{foreign_client.create_visible_surface(w, h)}; + auto observer_surface{observer_client.create_visible_surface(w, h)}; + + ForeignToplevelManager manager{observer_client}; + observer_client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(2u)); +} + +TEST_F(ForeignToplevelManagerTest, detects_toplevel_closed) +{ + wlcs::Client client{the_server()}; + + ForeignToplevelManager manager{client}; + client.roundtrip(); + EXPECT_THAT(manager.toplevels().size(), Eq(0u)); + + { + wlcs::Surface other{client}; + wlcs::XdgSurfaceStable other_xdg{client, other}; + wlcs::XdgToplevelStable other_toplevel{other_xdg}; + other.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(manager.toplevels().size(), Eq(1u)); + } + + client.roundtrip(); + ASSERT_THAT(manager.toplevels().size(), Eq(0u)); +} + +TEST_F(ForeignToplevelHandleTest, gets_title) +{ + std::string title = "Test Title @!\\-"; + + xdg_toplevel_set_title(xdg_toplevel, title.c_str()); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT((bool)toplevel().title(), Eq(true)); + EXPECT_THAT(toplevel().title().value(), Eq(title)); +} + +TEST_F(ForeignToplevelHandleTest, title_gets_updated) +{ + std::string title_a = "Test Title @!\\-"; + std::string title_b = "Title 2"; + + xdg_toplevel_set_title(xdg_toplevel, title_a.c_str()); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT((bool)toplevel().title(), Eq(true)); + ASSERT_THAT(toplevel().title().value(), Eq(title_a)); + + xdg_toplevel_set_title(xdg_toplevel, title_b.c_str()); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT((bool)toplevel().title(), Eq(true)); + EXPECT_THAT(toplevel().title().value(), Eq(title_b)); +} + +TEST_F(ForeignToplevelHandleTest, gets_app_id) +{ + std::string app_id = "fake.wlcs.app.id"; + + xdg_toplevel_set_app_id(xdg_toplevel, app_id.c_str()); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT((bool)toplevel().app_id(), Eq(true)); + EXPECT_THAT(toplevel().app_id().value(), Eq(app_id)); +} + +TEST_F(ForeignToplevelHandleTest, gets_maximized) +{ + xdg_toplevel_set_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel().maximized(), Eq(true)); + + xdg_toplevel_unset_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel().maximized(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, gets_minimized) +{ + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(false)); + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, gets_fullscreen) +{ + xdg_toplevel_set_fullscreen(xdg_toplevel, nullptr); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel().fullscreen(), Eq(true)); + + xdg_toplevel_unset_fullscreen(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel().maximized(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, gets_activated) +{ + std::string app_id = "fake.wlcs.app.id"; + xdg_toplevel_set_app_id(xdg_toplevel, app_id.c_str()); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel().activated(), Eq(true)); + + std::string other_app_id = "other.app.id"; + wlcs::Surface other{client}; + wlcs::XdgSurfaceStable other_xdg{client, other}; + wlcs::XdgToplevelStable other_toplevel{other_xdg}; + xdg_toplevel_set_app_id(other_toplevel, other_app_id.c_str()); + other.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(toplevel(app_id).activated(), Eq(false)); + EXPECT_THAT(toplevel(other_app_id).activated(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, can_maximize_foreign) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + xdg_toplevel_unset_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(toplevel().maximized(), Eq(false)); + + zwlr_foreign_toplevel_handle_v1_set_maximized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(toplevel().maximized(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, can_unmaximize_foreign) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + xdg_toplevel_set_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(toplevel().maximized(), Eq(true)); + + zwlr_foreign_toplevel_handle_v1_unset_maximized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(toplevel().maximized(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, can_fullscreen_foreign) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(toplevel().fullscreen(), Eq(false)); + + zwlr_foreign_toplevel_handle_v1_set_fullscreen(toplevel(), nullptr); + client.roundtrip(); + + EXPECT_THAT(state.fullscreen, Eq(true)); + EXPECT_THAT(toplevel().fullscreen(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, can_unfullscreen_foreign) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + xdg_toplevel_set_fullscreen(xdg_toplevel, nullptr); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(state.fullscreen, Eq(true)); + EXPECT_THAT(toplevel().fullscreen(), Eq(true)); + + zwlr_foreign_toplevel_handle_v1_unset_fullscreen(toplevel()); + client.roundtrip(); + + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(toplevel().fullscreen(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, can_minimize_foreign) +{ + std::string app_id = "fake.wlcs.app.id"; + + wlcs::Surface below_surface{client.create_visible_surface(w, h)}; + the_server().move_surface_to(below_surface, 0, 0); + client.roundtrip(); + + xdg_toplevel_set_app_id(xdg_toplevel, app_id.c_str()); + surface.attach_visible_buffer(w, h); + the_server().move_surface_to(surface, 0, 0); + client.roundtrip(); + + auto pointer = the_server().create_pointer(); + pointer.move_to(1, 1); + client.roundtrip(); + + ASSERT_THAT(toplevel(app_id).minimized(), Eq(false)) << "precondition failed"; + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)surface)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_set_minimized(toplevel(app_id)); + client.roundtrip(); + + EXPECT_THAT(toplevel(app_id).minimized(), Eq(true)); + + pointer.move_to(2, 2); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Ne((wl_surface*)surface)) + << "surface under pointer when it should have been minimized"; + EXPECT_THAT(client.window_under_cursor(), Eq((wl_surface*)below_surface)) + << "surface under pointer not correct"; +} + +TEST_F(ForeignToplevelHandleTest, can_unminimize_foreign) +{ + std::string app_id = "fake.wlcs.app.id"; + + wlcs::Surface below_surface{client.create_visible_surface(w, h)}; + the_server().move_surface_to(below_surface, 0, 0); + + xdg_toplevel_set_app_id(xdg_toplevel, app_id.c_str()); + surface.attach_visible_buffer(w, h); + the_server().move_surface_to(surface, 0, 0); + client.roundtrip(); + + xdg_toplevel_set_minimized(xdg_toplevel); + client.roundtrip(); + + auto pointer = the_server().create_pointer(); + pointer.move_to(1, 1); + client.roundtrip(); + + ASSERT_THAT(toplevel(app_id).minimized(), Eq(true)) << "precondition failed"; + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)below_surface)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_unset_minimized(toplevel(app_id)); + client.roundtrip(); + + EXPECT_THAT(toplevel(app_id).minimized(), Eq(false)); + + pointer.move_to(2, 2); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Ne((wl_surface*)below_surface)) + << "surface under pointer when it should have been occluded by unminimized surface"; + EXPECT_THAT(client.window_under_cursor(), Eq((wl_surface*)surface)) + << "surface under pointer not correct"; +} + +TEST_F(ForeignToplevelHandleTest, can_unminimize_foreign_to_restored) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + surface.attach_visible_buffer(w, h); + xdg_toplevel_unset_maximized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().maximized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_unset_minimized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(false)); + EXPECT_THAT(toplevel().maximized(), Eq(false)); + EXPECT_THAT(state.maximized, Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, can_unminimize_foreign_to_maximized) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + surface.attach_visible_buffer(w, h); + xdg_toplevel_set_maximized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().maximized(), Eq(true)) << "precondition failed"; + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_unset_minimized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(false)); + EXPECT_THAT(toplevel().maximized(), Eq(true)); + EXPECT_THAT(state.maximized, Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, gets_activated_when_unminimized) +{ + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_unset_minimized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(toplevel().activated(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, gets_unminimized_when_activated) +{ + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_activate(toplevel(), client.seat()); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, gets_unminimized_when_maximized) +{ + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_set_maximized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(false)); + EXPECT_THAT(toplevel().maximized(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, gets_unminimized_when_fullscreened) +{ + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_minimized(xdg_toplevel); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(toplevel().minimized(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_set_fullscreen(toplevel(), nullptr); + client.roundtrip(); + + EXPECT_THAT(toplevel().minimized(), Eq(false)); + EXPECT_THAT(toplevel().fullscreen(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, can_unfullscreen_foreign_to_restored) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + xdg_toplevel_unset_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(state.maximized, Eq(false)) << "precondition failed"; + ASSERT_THAT(toplevel().maximized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_fullscreen(xdg_toplevel, nullptr); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(state.fullscreen, Eq(true)) << "precondition failed"; + ASSERT_THAT(toplevel().fullscreen(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_unset_fullscreen(toplevel()); + client.roundtrip(); + + ASSERT_THAT(state.fullscreen, Eq(false)) << "precondition failed"; + ASSERT_THAT(toplevel().fullscreen(), Eq(false)) << "precondition failed"; + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(toplevel().maximized(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, can_unfullscreen_foreign_to_maximized) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + xdg_toplevel_set_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(state.maximized, Eq(true)) << "precondition failed"; + ASSERT_THAT(toplevel().maximized(), Eq(true)) << "precondition failed"; + + xdg_toplevel_set_fullscreen(xdg_toplevel, nullptr); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(state.fullscreen, Eq(true)) << "precondition failed"; + ASSERT_THAT(toplevel().fullscreen(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_unset_fullscreen(toplevel()); + client.roundtrip(); + + ASSERT_THAT(state.fullscreen, Eq(false)) << "precondition failed"; + ASSERT_THAT(toplevel().fullscreen(), Eq(false)) << "precondition failed"; + + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(toplevel().maximized(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, can_maximize_foreign_while_fullscreen) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + xdg_toplevel_unset_maximized(xdg_toplevel); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + ASSERT_THAT(state.maximized, Eq(false)) << "precondition failed"; + ASSERT_THAT(toplevel().maximized(), Eq(false)) << "precondition failed"; + + xdg_toplevel_set_fullscreen(xdg_toplevel, nullptr); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(state.fullscreen, Eq(true)) << "precondition failed"; + ASSERT_THAT(toplevel().fullscreen(), Eq(true)) << "precondition failed"; + + zwlr_foreign_toplevel_handle_v1_set_maximized(toplevel()); + client.roundtrip(); + + EXPECT_THAT(state.fullscreen, Eq(true)) << "XDG toplevel became not fullscreen after requesting maximized"; + EXPECT_THAT(toplevel().fullscreen(), Eq(true)) << "foreign toplevel became not fullscreen after maximize"; + + zwlr_foreign_toplevel_handle_v1_unset_fullscreen(toplevel()); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(state.fullscreen, Eq(false)) << "precondition failed"; + ASSERT_THAT(toplevel().fullscreen(), Eq(false)) << "precondition failed"; + + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(toplevel().maximized(), Eq(true)); +} + +TEST_F(ForeignToplevelHandleTest, can_activate_foreign) +{ + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; + ON_CALL(xdg_toplevel, configure).WillByDefault([&state](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + std::string app_id = "fake.wlcs.app.id"; + xdg_toplevel_set_app_id(xdg_toplevel, app_id.c_str()); + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + std::string other_app_id = "other.app.id"; + wlcs::Surface other{client}; + wlcs::XdgSurfaceStable other_xdg{client, other}; + wlcs::XdgToplevelStable other_toplevel{other_xdg}; + xdg_toplevel_set_app_id(other_toplevel, other_app_id.c_str()); + other.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_THAT(state.activated, Eq(false)); + EXPECT_THAT(toplevel(app_id).activated(), Eq(false)); + EXPECT_THAT(toplevel(other_app_id).activated(), Eq(true)); + + zwlr_foreign_toplevel_handle_v1_activate(toplevel(app_id), client.seat()); + client.roundtrip(); + + EXPECT_THAT(state.activated, Eq(true)); + EXPECT_THAT(toplevel(app_id).activated(), Eq(true)); + EXPECT_THAT(toplevel(other_app_id).activated(), Eq(false)); +} + +TEST_F(ForeignToplevelHandleTest, can_close_foreign) +{ + EXPECT_CALL(xdg_toplevel, close()).Times(0); + + surface.attach_visible_buffer(w, h); + client.roundtrip(); + + EXPECT_CALL(xdg_toplevel, close()).Times(1); + + zwlr_foreign_toplevel_handle_v1_close(toplevel()); + client.roundtrip(); +} + +// TODO: test toplevel outputs +// TODO: test fullscreening toplevel on a specific output +// TODO: test popup does not come up as toplevel +// TODO: test zwlr_foreign_toplevel_handle_v1_set_rectangle somehow diff --git a/tests/wlr_layer_shell_v1.cpp b/tests/wlr_layer_shell_v1.cpp new file mode 100644 index 0000000..1e67b35 --- /dev/null +++ b/tests/wlr_layer_shell_v1.cpp @@ -0,0 +1,1125 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "layer_shell_v1.h" +#include "xdg_shell_stable.h" +#include "version_specifier.h" +#include "geometry/rectangle.h" + +#include + +using namespace testing; +using wlcs::X, wlcs::Y, wlcs::DeltaX, wlcs::DeltaY, wlcs::Width, wlcs::Height, wlcs::Size, wlcs::Point, wlcs::Rectangle; + +namespace +{ +class LayerSurfaceTest: public wlcs::StartedInProcessServer +{ +public: + LayerSurfaceTest() + : client{the_server()}, + surface{client}, + layer_surface{client, surface} + { + } + + void commit_and_wait_for_configure() + { + wl_surface_commit(surface); + layer_surface.dispatch_until_configure(); + } + + void expect_surface_is_at_position(Point pos, wlcs::Surface const& expected) + { + auto pointer = the_server().create_pointer(); + pointer.move_to(pos.x.as_int() + 10, pos.y.as_int() + 10); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Eq((wl_surface*)expected)); + if (client.window_under_cursor() == expected) + { + EXPECT_THAT(wl_fixed_to_int(client.pointer_position().first), Eq(10)); + EXPECT_THAT(wl_fixed_to_int(client.pointer_position().second), Eq(10)); + } + } + + void expect_surface_is_at_position(Point pos) + { + expect_surface_is_at_position(pos, surface); + } + + auto output_rect() const -> Rectangle + { + EXPECT_THAT(client.output_count(), Ge(1u)) << "There are no outputs to get a size from"; + EXPECT_THAT(client.output_count(), Eq(1u)) << "Unclear which output the layer shell surface will be placed on"; + auto output_state = client.output_state(0); + EXPECT_THAT((bool)output_state.mode_size, Eq(true)) << "Output has no size"; + EXPECT_THAT((bool)output_state.geometry_position, Eq(true)) << "Output has no position"; + auto size = output_state.mode_size.value(); + if (output_state.scale) + { + size.first /= output_state.scale.value(); + size.second /= output_state.scale.value(); + } + auto pos = output_state.geometry_position.value(); + return {{pos.first, pos.second}, {size.first, size.second}}; + } + + wlcs::Client client; + wlcs::Surface surface; + wlcs::LayerSurfaceV1 layer_surface; + + Size static constexpr default_size{200, 300}; + int static const default_width = default_size.width.as_int(); + int static const default_height = default_size.height.as_int(); +}; + +struct LayerSurfaceLayout +{ + struct Anchor + { + bool left, right, top, bottom; + }; + struct Margin + { + DeltaX left, right; + DeltaY top, bottom; + }; + + static auto get_all() -> std::vector + { + bool const range[]{false, true}; + std::vector result; + for (auto left: range) + { + for (auto right: range) + { + for (auto top: range) + { + for (auto bottom: range) + { + result.emplace_back(Anchor{left, right, top, bottom}); + result.emplace_back( + Anchor{left, right, top, bottom}, + Margin{DeltaX{6}, DeltaX{9}, DeltaY{12}, DeltaY{15}}); + } + } + } + } + return result; + } + + LayerSurfaceLayout(Anchor anchor) + : anchor{anchor}, + margin{{}, {}, {}, {}} + { + } + + LayerSurfaceLayout(Anchor anchor, Margin margin) + : anchor{anchor}, + margin{margin} + { + } + + operator uint32_t() const + { + return + (anchor.left ? ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT : 0) | + (anchor.right ? ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT : 0) | + (anchor.top ? ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP : 0) | + (anchor.bottom ? ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM : 0); + } + + auto h_expand() const -> bool { return anchor.left && anchor.right; } + auto v_expand() const -> bool { return anchor.top && anchor.bottom; } + + auto placement_rect(Rectangle const& output) const -> Rectangle + { + auto const config_size = configure_size(output); + auto const width = config_size.width.as_int() ? config_size.width : LayerSurfaceTest::default_size.width; + auto const height = config_size.height.as_int() ? config_size.height : LayerSurfaceTest::default_size.height; + X const x = + (anchor.left ? + output.top_left.x + margin.left : + (anchor.right ? + (output.top_left.x + as_delta(output.size.width) - as_delta(width) - margin.right) : + (output.top_left.x + (output.size.width - width) / 2) + ) + ); + Y const y = + (anchor.top ? + output.top_left.y + margin.top : + (anchor.bottom ? + (output.top_left.y + as_delta(output.size.height) - as_delta(height) - margin.bottom) : + (output.top_left.y + (output.size.height - height) / 2) + ) + ); + return {{x, y}, {width, height}}; + } + + auto request_size() const -> Size + { + return { + h_expand() ? Width{} : LayerSurfaceTest::default_size.width, + v_expand() ? Height{} : LayerSurfaceTest::default_size.height}; + } + + auto configure_size(Rectangle const& output) const -> Size + { + auto const configure_width = h_expand() ? output.size.width - margin.left - margin.right : Width{}; + auto const configure_height = v_expand() ? output.size.height - margin.top - margin.bottom : Height{}; + return {configure_width, configure_height}; + } + + // Will always either return 0, or a single enum value + auto attached_edge() const -> zwlr_layer_surface_v1_anchor + { + if (anchor.top == anchor.bottom) + { + if (anchor.left && !anchor.right) + return ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; + else if (anchor.right && !anchor.left) + return ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + } + else if (anchor.left == anchor.right) + { + if (anchor.top && !anchor.bottom) + return ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + else if (anchor.bottom && !anchor.top) + return ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + } + return (zwlr_layer_surface_v1_anchor)0; + } + + Anchor const anchor; + Margin const margin; +}; + +static void invoke_zwlr_layer_surface_v1_set_margin( + zwlr_layer_surface_v1* layer_surface, + LayerSurfaceLayout::Margin const& margin) +{ + zwlr_layer_surface_v1_set_margin( + layer_surface, + margin.top.as_int(), + margin.right.as_int(), + margin.bottom.as_int(), + margin.left.as_int()); +} + +std::ostream& operator<<(std::ostream& os, const LayerSurfaceLayout& layout) +{ + std::vector strs; + if (layout.anchor.left) + strs.emplace_back("left"); + if (layout.anchor.right) + strs.emplace_back("right"); + if (layout.anchor.top) + strs.emplace_back("top"); + if (layout.anchor.bottom) + strs.emplace_back("bottom"); + if (strs.empty()) + strs.emplace_back("none"); + os << "Anchor{"; + for (auto i = 0u; i < strs.size(); i++) + { + if (i > 0) + os << " | "; + os << strs[i]; + } + os << "}, Margin{"; + if (layout.margin.left.as_int() || + layout.margin.right.as_int() || + layout.margin.top.as_int() || + layout.margin.bottom.as_int()) + { + os << "l: " << layout.margin.left << ", "; + os << "r: " << layout.margin.right << ", "; + os << "t: " << layout.margin.top << ", "; + os << "b: " << layout.margin.bottom; + } + os << "}"; + return os; +} + +class LayerSurfaceLayoutTest: + public LayerSurfaceTest, + public testing::WithParamInterface +{ +}; + +struct LayerLayerParams +{ + std::optional below; + std::optional above; +}; + +std::ostream& operator<<(std::ostream& os, const LayerLayerParams& layer) +{ + os << "layers:"; + for (auto const& i : {layer.below, layer.above}) + { + os << " "; + os << "Layer{"; + if (i) + { + switch (auto const value = i.value()) + { + case ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND: + os << "background"; + break; + case ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM: + os << "bottom"; + break; + case ZWLR_LAYER_SHELL_V1_LAYER_TOP: + os << "top"; + break; + case ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY: + os << "overlay"; + break; + default: + os << "INVALID(" << value << ")"; + } + } + else + { + os << "none"; + } + os << "}"; + } + return os; +} + +class LayerSurfaceLayerTest: + public wlcs::StartedInProcessServer, + public testing::WithParamInterface +{ +public: + struct SurfaceOnLayer + { + SurfaceOnLayer( + wlcs::Client& client, + std::optional layer) + : surface{layer ? wlcs::Surface{client} : client.create_visible_surface(width, height)} + { + if (layer) + { + layer_surface.emplace(client, surface, layer.value()); + zwlr_layer_surface_v1_set_size(layer_surface.value(), width, height); + surface.attach_visible_buffer(width, height); + } + } + + wlcs::Surface surface; + std::optional layer_surface; + }; + + LayerSurfaceLayerTest() + : client(the_server()) + { + } + + static const int width{200}, height{100}; + wlcs::Client client; +}; + +struct SizeAndAnchors +{ + uint32_t width, height; + LayerSurfaceLayout anchors; +}; + +class LayerSurfaceErrorsTest: + public LayerSurfaceTest, + public testing::WithParamInterface +{ +}; + +} + +TEST_F(LayerSurfaceTest, specifying_no_size_without_anchors_is_an_error) +{ + try + { + // Protocol specifies that a size of (0,0) is the default + commit_and_wait_for_configure(); + } + catch (wlcs::ProtocolError const& err) + { + EXPECT_THAT(err.interface(), Eq(&zwlr_layer_surface_v1_interface)); + // The protocol does not explicitly state what error to send here; INVALID_SIZE seems most appropriate + EXPECT_THAT(err.error_code(), Eq(ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE)); + return; + } + FAIL() << "Expected protocol error not raised"; +} + +TEST_F(LayerSurfaceTest, specifying_zero_size_without_anchors_is_an_error) +{ + try + { + zwlr_layer_surface_v1_set_size(layer_surface, 0, 0); + commit_and_wait_for_configure(); + } + catch (wlcs::ProtocolError const& err) + { + EXPECT_THAT(err.interface(), Eq(&zwlr_layer_surface_v1_interface)); + // The protocol does not explicitly state what error to send here; INVALID_SIZE seems most appropriate + EXPECT_THAT(err.error_code(), Eq(ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE)); + return; + } + FAIL() << "Expected protocol error not raised"; +} + +TEST_P(LayerSurfaceErrorsTest, specifying_zero_size_without_corresponding_anchors_is_an_error) +{ + try + { + zwlr_layer_surface_v1_set_size(layer_surface, GetParam().width, GetParam().height); + zwlr_layer_surface_v1_set_anchor(layer_surface, GetParam().anchors); + commit_and_wait_for_configure(); + } + catch (wlcs::ProtocolError const& err) + { + EXPECT_THAT(err.interface(), Eq(&zwlr_layer_surface_v1_interface)); + // The protocol does not explicitly state what error to send here; INVALID_SIZE seems most appropriate + EXPECT_THAT(err.error_code(), Eq(ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE)); + return; + } + FAIL() << "Expected protocol error not raised"; +} + +INSTANTIATE_TEST_SUITE_P( + Anchors, + LayerSurfaceErrorsTest, + testing::Values( + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, false, false, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{true, false, false, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, true, false, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{true, true, false, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, false, true, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{true, false, true, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, true, true, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{true, true, true, false}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, false, false, true}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{true, false, false, true}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, true, false, true}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{true, true, false, true}}}, + SizeAndAnchors{0, 0, LayerSurfaceLayout{{false, true, true, true}}}, + SizeAndAnchors{200, 0, LayerSurfaceLayout{{false, false, false, true}}}, + SizeAndAnchors{200, 0, LayerSurfaceLayout{{false, false, true, false}}}, + SizeAndAnchors{0, 200, LayerSurfaceLayout{{true, false, false, true}}}, + SizeAndAnchors{0, 200, LayerSurfaceLayout{{false, true, true, false}}} + )); + +TEST_F(LayerSurfaceTest, can_open_layer_surface) +{ + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + commit_and_wait_for_configure(); +} + +TEST_F(LayerSurfaceTest, gets_configured_with_supplied_size_when_set) +{ + int width = 123, height = 546; + zwlr_layer_surface_v1_set_size(layer_surface, width, height); + commit_and_wait_for_configure(); + EXPECT_THAT(layer_surface.last_size(), Eq(Size{width, height})); +} + +TEST_F(LayerSurfaceTest, gets_configured_with_supplied_size_even_when_anchored_to_edges) +{ + int width = 321, height = 218; + zwlr_layer_surface_v1_set_anchor(layer_surface, LayerSurfaceLayout({true, true, true, true})); + zwlr_layer_surface_v1_set_size(layer_surface, width, height); + commit_and_wait_for_configure(); + EXPECT_THAT(layer_surface.last_size(), Eq(Size{width, height})); +} + +TEST_F(LayerSurfaceTest, when_anchored_to_all_edges_gets_configured_with_output_size) +{ + zwlr_layer_surface_v1_set_anchor(layer_surface, LayerSurfaceLayout({true, true, true, true})); + commit_and_wait_for_configure(); + ASSERT_THAT(layer_surface.last_size(), Eq(output_rect().size)); +} + +TEST_F(LayerSurfaceTest, gets_configured_after_anchor_change) +{ + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + commit_and_wait_for_configure(); + zwlr_layer_surface_v1_set_size(layer_surface, 0, 0); + zwlr_layer_surface_v1_set_anchor(layer_surface, LayerSurfaceLayout({true, true, true, true})); + commit_and_wait_for_configure(); + EXPECT_THAT(layer_surface.last_size().width, Gt(Width{})); + EXPECT_THAT(layer_surface.last_size().height, Gt(Height{})); +} + +TEST_F(LayerSurfaceTest, destroy_request_supported) +{ + wlcs::Client client{the_server()}; + + { + auto const layer_shell = client.bind_if_supported( + wlcs::AtLeastVersion{ZWLR_LAYER_SHELL_V1_DESTROY_SINCE_VERSION}); + client.roundtrip(); + } + // layer_shell is now destroyed + + client.roundtrip(); +} + +TEST_F(LayerSurfaceTest, destroy_request_not_sent_when_not_supported) +{ + wlcs::Client client{the_server()}; + + { + auto const layer_shell = client.bind_if_supported( + wlcs::ExactlyVersion{ZWLR_LAYER_SHELL_V1_DESTROY_SINCE_VERSION - 1}); + client.roundtrip(); + } + // layer_shell is now destroyed + + client.roundtrip(); +} + +TEST_F(LayerSurfaceTest, does_not_take_keyboard_focus_without_keyboard_interactivity) +{ + auto normal_surface = client.create_visible_surface(100, 100); + the_server().move_surface_to(normal_surface, 0, default_height); + auto pointer = the_server().create_pointer(); + pointer.move_to(5, default_height + 5); + pointer.left_click(); + client.roundtrip(); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Could not run test because normal surface was not given keyboeard focus"; + + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + + EXPECT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Normal surface lost keyboard focus when non-keyboard layer surface appeared"; +} + +TEST_F(LayerSurfaceTest, takes_keyboard_focus_with_exclusive_keyboard_interactivity) +{ + auto normal_surface = client.create_visible_surface(100, 100); + the_server().move_surface_to(normal_surface, 0, default_height); + auto pointer = the_server().create_pointer(); + pointer.move_to(5, default_height + 5); + pointer.left_click(); + client.roundtrip(); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Could not run test because normal surface was not given keyboeard focus"; + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + + EXPECT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Layer surface not given keyboard focus in exclusive mode"; +} + +TEST_F(LayerSurfaceTest, takes_keyboard_focus_after_click_with_on_demand_keyboard_interactivity) +{ + auto normal_surface = client.create_visible_surface(100, 100); + the_server().move_surface_to(normal_surface, 0, default_height); + auto pointer = the_server().create_pointer(); + pointer.move_to(5, default_height + 5); + pointer.left_click(); + client.roundtrip(); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Could not run test because normal surface was not given keyboeard focus"; + + { + wlcs::Client client{the_server()}; + auto const layer_shell = client.bind_if_supported( + wlcs::AtLeastVersion{ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND_SINCE_VERSION}); + client.roundtrip(); + } + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + client.roundtrip(); + + pointer.move_to(5, 5); + pointer.left_click(); + client.roundtrip(); + + EXPECT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Layer surface not given keyboard focus in on-demand mode"; +} + +TEST_F(LayerSurfaceTest, does_not_lose_keyboard_focus_with_exclusive_keyboard_interactivity) +{ + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Layer surface not given keyboard focus in exclusive mode"; + + auto normal_surface = client.create_visible_surface(100, 100); + the_server().move_surface_to(normal_surface, 0, default_height); + auto pointer = the_server().create_pointer(); + pointer.move_to(5, default_height + 5); + pointer.left_click(); + client.roundtrip(); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Creating a normal surface caused the layer surface in exclusive mode to lose keyboard focus"; +} + +TEST_F(LayerSurfaceTest, can_lose_keyboard_focus_with_on_demand_keyboard_interactivity) +{ + { + wlcs::Client client{the_server()}; + auto const layer_shell = client.bind_if_supported( + wlcs::AtLeastVersion{ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND_SINCE_VERSION}); + client.roundtrip(); + } + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Layer surface not given keyboard focus in exclusive mode"; + + auto normal_surface = client.create_visible_surface(100, 100); + the_server().move_surface_to(normal_surface, 0, default_height); + auto pointer = the_server().create_pointer(); + pointer.move_to(5, default_height + 5); + pointer.left_click(); + client.roundtrip(); + ASSERT_THAT(client.keyboard_focused_window(), Ne(static_cast(surface))) + << "Creating a normal surface did not cause the layer surface in on-demand mode to lose keyboard focus"; +} + +TEST_F(LayerSurfaceTest, takes_keyboard_focus_when_interactivity_changes_to_exclusive) +{ + auto normal_surface = client.create_visible_surface(100, 100); + the_server().move_surface_to(normal_surface, 0, default_height); + auto pointer = the_server().create_pointer(); + pointer.move_to(5, default_height + 5); + pointer.left_click(); + client.roundtrip(); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Could not run test because normal surface was not given keyboeard focus"; + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Could not run test because normal surface was not given keyboeard focus"; + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Layer surface did not take keyboard focus when mode changed to exclusive"; + + pointer.left_click(); + client.roundtrip(); + + EXPECT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Layer surface did not hold exclusive keyboard focus"; +} + +TEST_F(LayerSurfaceTest, loses_keybaord_focus_when_interactivity_changes_to_none) +{ + auto normal_surface = client.create_visible_surface(100, 100); + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(normal_surface))) + << "Could not run test because normal surface was not given keyboeard focus"; + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + zwlr_layer_surface_v1_set_anchor( + layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(default_width, default_height); + + ASSERT_THAT(client.keyboard_focused_window(), Eq(static_cast(surface))) + << "Could not run test because surface not given keyboard focus despite exclusive interactivity"; + + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); + wl_surface_commit(surface); + client.roundtrip(); + + ASSERT_THAT(client.keyboard_focused_window(), Ne(static_cast(surface))) + << "Layer surface did not lose keyboard focus when interactivity changed to none"; +} + +TEST_P(LayerSurfaceLayoutTest, is_initially_positioned_correctly_for_anchor) +{ + auto const layout = GetParam(); + auto const output = output_rect(); + auto const rect = layout.placement_rect(output); + auto const request_size = layout.request_size(); + + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_size(layer_surface, request_size.width.as_int(), request_size.height.as_int()); + commit_and_wait_for_configure(); + + auto configure_size = layout.configure_size(output); + if (configure_size.width.as_int()) + { + EXPECT_THAT(layer_surface.last_size().width, Eq(configure_size.width)); + } + if (configure_size.height.as_int()) + { + EXPECT_THAT(layer_surface.last_size().height, Eq(configure_size.height)); + } + + surface.attach_visible_buffer(rect.size.width.as_int(), rect.size.height.as_int()); + expect_surface_is_at_position(rect.top_left); +} + +TEST_P(LayerSurfaceLayoutTest, is_positioned_correctly_when_explicit_size_does_not_match_buffer_size) +{ + auto const layout = GetParam(); + int const initial_width{52}, initial_height{74}; + auto const rect = layout.placement_rect(output_rect()); + auto const request_size = layout.request_size(); + + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_size(layer_surface, request_size.width.as_int(), request_size.height.as_int()); + commit_and_wait_for_configure(); + + surface.attach_visible_buffer(initial_width, initial_height); + + expect_surface_is_at_position(rect.top_left); +} + +TEST_P(LayerSurfaceLayoutTest, is_positioned_correctly_when_layout_changed) +{ + auto const layout = GetParam(); + auto const output = output_rect(); + auto const result_rect = layout.placement_rect(output); + auto const request_size = layout.request_size(); + + zwlr_layer_surface_v1_set_size(layer_surface, default_width - 5, default_height - 2); + commit_and_wait_for_configure(); + + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_size(layer_surface, request_size.width.as_int(), request_size.height.as_int()); + surface.attach_visible_buffer(result_rect.size.width.as_int(), result_rect.size.height.as_int()); + wl_surface_commit(surface); + client.roundtrip(); // Sometimes we get a configure, sometimes we don't + + auto configure_size = layout.configure_size(output); + if (configure_size.width.as_int()) + { + EXPECT_THAT(layer_surface.last_size().width, Eq(configure_size.width)); + } + if (configure_size.height.as_int()) + { + EXPECT_THAT(layer_surface.last_size().height, Eq(configure_size.height)); + } + expect_surface_is_at_position(result_rect.top_left); +} + +TEST_P(LayerSurfaceLayoutTest, is_positioned_correctly_after_multiple_changes) +{ + auto const layout = GetParam(); + auto const output = output_rect(); + auto const result_rect = layout.placement_rect(output); + auto const request_size = layout.request_size(); + + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + commit_and_wait_for_configure(); + + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_size(layer_surface, request_size.width.as_int(), request_size.height.as_int()); + surface.attach_visible_buffer(result_rect.size.width.as_int(), result_rect.size.height.as_int()); + wl_surface_commit(surface); + client.roundtrip(); // Sometimes we get a configure, sometimes we don't + + zwlr_layer_surface_v1_set_anchor(layer_surface, 0); + zwlr_layer_surface_v1_set_margin(layer_surface, 0, 0, 0, 0); + zwlr_layer_surface_v1_set_size(layer_surface, default_width, default_height); + surface.attach_visible_buffer(default_width, default_height); + wl_surface_commit(surface); + client.roundtrip(); // Sometimes we get a configure, sometimes we don't + + EXPECT_THAT(layer_surface.last_size().width, Eq(default_size.width)); + EXPECT_THAT(layer_surface.last_size().height, Eq(default_size.height)); + auto const expected_top_left = output.top_left + as_displacement( + as_size(as_displacement(output.size) - as_displacement(default_size)) / 2); + expect_surface_is_at_position(expected_top_left); +} + +TEST_P(LayerSurfaceLayoutTest, is_positioned_to_accommodate_other_surfaces_exclusive_zone) +{ + auto const layout = GetParam(); + auto const request_size = layout.request_size(); + auto const initial_rect = layout.placement_rect(output_rect()); + auto const exclusive = 12; + + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_size(layer_surface, request_size.width.as_int(), request_size.height.as_int()); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(initial_rect.size.width.as_int(), initial_rect.size.height.as_int()); + + // Create layer surfaces with exclusive zones on the top and left of the output to push our surface out of the way + + wlcs::Surface top_surface{client}; + wlcs::LayerSurfaceV1 top_layer_surface{client, top_surface}; + zwlr_layer_surface_v1_set_anchor(top_layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP); + zwlr_layer_surface_v1_set_exclusive_zone(top_layer_surface, exclusive); + zwlr_layer_surface_v1_set_size(top_layer_surface, exclusive, exclusive); + wl_surface_commit(top_surface); + top_layer_surface.dispatch_until_configure(); + top_surface.attach_visible_buffer(exclusive, exclusive); + wl_surface_commit(top_surface); + + wlcs::Surface left_surface{client}; + wlcs::LayerSurfaceV1 left_layer_surface{client, left_surface}; + zwlr_layer_surface_v1_set_anchor(left_layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + zwlr_layer_surface_v1_set_exclusive_zone(left_layer_surface, exclusive); + zwlr_layer_surface_v1_set_size(left_layer_surface, exclusive, exclusive); + wl_surface_commit(left_surface); + left_layer_surface.dispatch_until_configure(); + left_surface.attach_visible_buffer(exclusive, exclusive); + wl_surface_commit(left_surface); + + client.roundtrip(); + + auto non_exlusive_zone = output_rect(); + non_exlusive_zone.top_left.x += DeltaX{exclusive}; + non_exlusive_zone.top_left.y += DeltaY{exclusive}; + non_exlusive_zone.size.width -= DeltaX{exclusive}; + non_exlusive_zone.size.height -= DeltaY{exclusive}; + + auto expected_config_size = layout.configure_size(non_exlusive_zone); + if (expected_config_size.width.as_int()) + { + EXPECT_THAT(layer_surface.last_size().width, Eq(expected_config_size.width)); + } + if (expected_config_size.height.as_int()) + { + EXPECT_THAT(layer_surface.last_size().height, Eq(expected_config_size.height)); + } + + auto const expected_placement = layout.placement_rect(non_exlusive_zone); + surface.attach_visible_buffer(expected_placement.size.width.as_int(), expected_placement.size.height.as_int()); + expect_surface_is_at_position(expected_placement.top_left); +} + +TEST_P(LayerSurfaceLayoutTest, maximized_xdg_toplevel_is_shrunk_for_exclusive_zone) +{ + int const exclusive_zone = 25; + int width = 0, height = 0; + wlcs::Surface other_surface{client}; + wlcs::XdgSurfaceStable xdg_surface{client, other_surface}; + wlcs::XdgToplevelStable toplevel{xdg_surface}; + ON_CALL(toplevel, configure).WillByDefault([&](auto w, auto h, wl_array* /*states*/) + { + if (w != width || h != height) + { + width = w; + height = h; + if (w == 0) + w = 100; + if (h == 0) + h = 150; + other_surface.attach_buffer(w, h); + xdg_surface_set_window_geometry(xdg_surface, 0, 0, w, h); + } + }); + ON_CALL(xdg_surface, configure).WillByDefault([&](uint32_t serial) + { + xdg_surface_ack_configure(xdg_surface, serial); + wl_surface_commit(other_surface); + }); + xdg_toplevel_set_maximized(toplevel); + wl_surface_commit(other_surface); + client.dispatch_until([&](){ return width > 0; }); + + int const initial_width = width; + int const initial_height = height; + + ASSERT_THAT(initial_width, Gt(0)) << "Can't test as shell did not configure XDG surface with a size"; + ASSERT_THAT(initial_height, Gt(0)) << "Can't test as shell did not configure XDG surface with a size"; + + auto const layout = GetParam(); + auto const request_size = layout.request_size(); + auto const rect = layout.placement_rect(output_rect()); + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, exclusive_zone); + zwlr_layer_surface_v1_set_size(layer_surface, request_size.width.as_int(), request_size.height.as_int()); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(rect.size.width.as_int(), rect.size.height.as_int()); + + int const new_width = width; + int const new_height = height; + + int expected_width = initial_width; + int expected_height = initial_height; + + switch (layout.attached_edge()) + { + case ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT: + expected_width -= exclusive_zone + layout.margin.left.as_int(); + break; + + case ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT: + expected_width -= exclusive_zone + layout.margin.right.as_int(); + break; + + case ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP: + expected_height -= exclusive_zone + layout.margin.top.as_int(); + break; + + case ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM: + expected_height -= exclusive_zone + layout.margin.bottom.as_int(); + break; + + default: ; + } + + EXPECT_THAT(new_width, Eq(expected_width)); + EXPECT_THAT(new_height, Eq(expected_height)); +} + +TEST_P(LayerSurfaceLayoutTest, simple_popup_positioned_correctly) +{ + auto const layout = GetParam(); + auto const output = output_rect(); + auto const layer_surface_rect = layout.placement_rect(output); + auto const layer_surface_request_size = layout.request_size(); + + zwlr_layer_surface_v1_set_anchor(layer_surface, layout); + invoke_zwlr_layer_surface_v1_set_margin(layer_surface, layout.margin); + zwlr_layer_surface_v1_set_size(layer_surface, layer_surface_request_size.width.as_int(), layer_surface_request_size.height.as_int()); + commit_and_wait_for_configure(); + surface.attach_visible_buffer(layer_surface_rect.size.width.as_int(), layer_surface_rect.size.height.as_int()); + + auto const popup_size = std::make_pair(30, 30); + wlcs::XdgPositionerStable positioner{client}; + xdg_positioner_set_size(positioner, popup_size.first, popup_size.second); + xdg_positioner_set_anchor_rect( + positioner, + 5, 5, + layer_surface_rect.size.width.as_int() - 10, layer_surface_rect.size.height.as_int() - 10); + xdg_positioner_set_anchor(positioner, 0); + xdg_positioner_set_gravity(positioner, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + + wlcs::Surface popup_wl_surface{client}; + wlcs::XdgSurfaceStable popup_xdg_surface{client, popup_wl_surface}; + wlcs::XdgPopupStable popup_xdg_popup{popup_xdg_surface, std::nullopt, positioner}; + zwlr_layer_surface_v1_get_popup(layer_surface, popup_xdg_popup); + + int popup_surface_configure_count = 0; + Point popup_configured_position; + ON_CALL(popup_xdg_surface, configure).WillByDefault([&](uint32_t serial) + { + xdg_surface_ack_configure(popup_xdg_surface, serial); + popup_surface_configure_count++; + }); + ON_CALL(popup_xdg_popup, configure).WillByDefault([&](int32_t x, int32_t y, int32_t, int32_t) + { + popup_configured_position.x = X{x}; + popup_configured_position.y = Y{y}; + }); + + popup_wl_surface.attach_visible_buffer(popup_size.first, popup_size.second); + client.dispatch_until([&](){ return popup_surface_configure_count > 0; }); + + EXPECT_THAT( + popup_configured_position, + Eq(as_point(layer_surface_rect.size / 2))); + + expect_surface_is_at_position( + layer_surface_rect.top_left + as_displacement(layer_surface_rect.size / 2), + popup_wl_surface); +} + +INSTANTIATE_TEST_SUITE_P( + Anchor, + LayerSurfaceLayoutTest, + testing::ValuesIn(LayerSurfaceLayout::get_all())); + +TEST_P(LayerSurfaceLayerTest, surface_on_lower_layer_is_initially_placed_below) +{ + auto const& param = GetParam(); + + SurfaceOnLayer above{client, param.above}; + SurfaceOnLayer below{client, param.below}; + + the_server().move_surface_to(above.surface, 100, 0); + the_server().move_surface_to(below.surface, 0, 0); + + auto pointer = the_server().create_pointer(); + pointer.move_to(1, 1); + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)below.surface)) + << "Test could not run because below surface was not detected when above surface was not in the way"; + + the_server().move_surface_to(above.surface, 0, 0); + the_server().move_surface_to(below.surface, 0, 0); + + pointer.move_to(2, 3); + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Ne((wl_surface*)below.surface)) + << "Wrong wl_surface was on top"; + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)above.surface)) + << "Correct surface was not on top"; +} + +TEST_P(LayerSurfaceLayerTest, below_surface_can_not_be_raised_with_click) +{ + auto const& param = GetParam(); + + SurfaceOnLayer above{client, param.above}; + SurfaceOnLayer below{client, param.below}; + + the_server().move_surface_to(above.surface, width / 2, 0); + the_server().move_surface_to(below.surface, 0, 0); + + auto pointer = the_server().create_pointer(); + pointer.move_to(1, 1); + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)below.surface)) + << "Test could not run because below surface was not detected and clicked on"; + + pointer.left_button_down(); + client.roundtrip(); + pointer.left_button_up(); + client.roundtrip(); + pointer.move_to(width / 2 + 2, 1); + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Ne((wl_surface*)below.surface)) + << "Wrong wl_surface was on top"; + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)above.surface)) + << "Correct surface was not on top"; +} + +TEST_P(LayerSurfaceLayerTest, surface_can_be_moved_to_layer) +{ + client.bind_if_supported(wlcs::AtLeastVersion{ZWLR_LAYER_SURFACE_V1_SET_LAYER_SINCE_VERSION}); + auto const& param = GetParam(); + + auto initial_below{param.below}, initial_above{param.above}; + if (initial_below) + { + initial_below = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + } + if (initial_above) + { + initial_above = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; + } + SurfaceOnLayer above{client, initial_above}; + SurfaceOnLayer below{client, initial_below}; + + client.roundtrip(); + + if (above.layer_surface) + { + zwlr_layer_surface_v1_set_layer(above.layer_surface.value(), param.above.value()); + } + if (below.layer_surface) + { + zwlr_layer_surface_v1_set_layer(below.layer_surface.value(), param.below.value()); + } + + wl_surface_commit(above.surface); + wl_surface_commit(below.surface); + + auto pointer = the_server().create_pointer(); + the_server().move_surface_to(above.surface, 0, 0); + the_server().move_surface_to(below.surface, 0, 0); + pointer.move_to(2, 3); + + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Ne((wl_surface*)below.surface)) + << "Wrong wl_surface was on top"; + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)above.surface)) + << "Correct surface was not on top"; +} + +INSTANTIATE_TEST_SUITE_P( + Layer, + LayerSurfaceLayerTest, + testing::Values( + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM}, + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, ZWLR_LAYER_SHELL_V1_LAYER_TOP}, + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_TOP, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY}, + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY}, + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, ZWLR_LAYER_SHELL_V1_LAYER_TOP}, + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, std::nullopt}, + LayerLayerParams{ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, std::nullopt}, + LayerLayerParams{std::nullopt, ZWLR_LAYER_SHELL_V1_LAYER_TOP}, + LayerLayerParams{std::nullopt, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY} + )); + +// TODO: test it gets put on a specified output +// TODO: test margin +// TODO: test keyboard interactivity diff --git a/tests/wlr_virtual_pointer_v1.cpp b/tests/wlr_virtual_pointer_v1.cpp new file mode 100644 index 0000000..a0d4c18 --- /dev/null +++ b/tests/wlr_virtual_pointer_v1.cpp @@ -0,0 +1,361 @@ +/* + * Copyright © 2022 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "in_process_server.h" +#include "version_specifier.h" +#include "xdg_output_v1.h" +#include "generated/wlr-virtual-pointer-unstable-v1-client.h" + +#include "linux/input.h" +#include +#include + +using namespace testing; +using namespace std::chrono_literals; + +namespace wlcs +{ +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwlr_virtual_pointer_manager_v1) +WLCS_CREATE_INTERFACE_DESCRIPTOR(zwlr_virtual_pointer_v1) +} + +namespace +{ +class PointerListener +{ +public: + PointerListener(wl_seat* seat); + ~PointerListener(); + + MOCK_METHOD(void, enter, (uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y)); + MOCK_METHOD(void, leave, (uint32_t serial, wl_surface* surface)); + MOCK_METHOD(void, motion, (uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y)); + MOCK_METHOD(void, button, (uint32_t serial, uint32_t time, uint32_t button, uint32_t state)); + MOCK_METHOD(void, axis, (uint32_t time, uint32_t axis, wl_fixed_t value)); + MOCK_METHOD(void, frame, ()); + MOCK_METHOD(void, axis_source, (uint32_t axis_source)); + MOCK_METHOD(void, axis_stop, (uint32_t time, uint32_t axis)); + MOCK_METHOD(void, axis_discrete, (uint32_t axis, int32_t discrete)); + +private: + wl_pointer* const proxy; +}; + +PointerListener::PointerListener(wl_seat* seat) + : proxy{wl_seat_get_pointer(seat)} +{ +#define FORWARD_TO_MOCK(method) \ + [](void* data, wl_pointer*, auto... args){ static_cast(data)->method(args...); } + static const wl_pointer_listener listener = { + FORWARD_TO_MOCK(enter), + FORWARD_TO_MOCK(leave), + FORWARD_TO_MOCK(motion), + FORWARD_TO_MOCK(button), + FORWARD_TO_MOCK(axis), + FORWARD_TO_MOCK(frame), + FORWARD_TO_MOCK(axis_source), + FORWARD_TO_MOCK(axis_stop), + FORWARD_TO_MOCK(axis_discrete), + }; +#undef FORWARD_TO_MOCK + wl_pointer_add_listener(proxy, &listener, this); +} + +PointerListener::~PointerListener() +{ + wl_pointer_destroy(proxy); +} + +class VirtualPointerV1Test: public wlcs::StartedInProcessServer +{ +public: + VirtualPointerV1Test() + : receive_client{the_server()}, + send_client{the_server()}, + surface{receive_client.create_visible_surface(surface_width, surface_height)}, + pointer{the_server().create_pointer()}, + listener{receive_client.seat()}, + manager{send_client.bind_if_supported(wlcs::AnyVersion)} + { + EXPECT_CALL(listener, enter(_, surface.wl_surface(), _, _)); + EXPECT_CALL(listener, motion(_, _, _)).Times(AnyNumber()); + EXPECT_CALL(listener, frame()).Times(AnyNumber()); + the_server().move_surface_to(surface, 0, 0); + pointer.move_to(pointer_start_x, pointer_start_y); + send_client.roundtrip(); + receive_client.roundtrip(); + Mock::VerifyAndClearExpectations(&listener); + } + + wlcs::Client receive_client; + wlcs::Client send_client; + wlcs::Surface surface; + wlcs::Pointer pointer; + StrictMock listener; + wlcs::WlHandle manager; + + static int const surface_width = 400; + static int const surface_height = 400; + static int const pointer_start_x = 20; + static int const pointer_start_y = 30; +}; +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_is_moved_client_sees_motion) +{ + int const motion_x = 7; + int const motion_y = 22; + + EXPECT_CALL(listener, motion(_, + wl_fixed_from_int(pointer_start_x + motion_x), + wl_fixed_from_int(pointer_start_y + motion_y))); + + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_motion(handle, 0, wl_fixed_from_int(motion_x), wl_fixed_from_int(motion_y)); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_is_moved_multiple_times_client_sees_motion) +{ + int const motion1_x = 7; + int const motion1_y = 22; + int const motion2_x = 5; + int const motion2_y = -12; + + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + + EXPECT_CALL(listener, motion(_, _, _)).Times(AnyNumber()); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AnyNumber()).WillOnce(Invoke([&]{ recieved_frame = true; })); + zwlr_virtual_pointer_v1_motion(handle, 0, wl_fixed_from_int(motion1_x), wl_fixed_from_int(motion1_y)); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); + + EXPECT_CALL(listener, motion(_, + wl_fixed_from_int(pointer_start_x + motion1_x + motion2_x), + wl_fixed_from_int(pointer_start_y + motion1_y + motion2_y))); + recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + zwlr_virtual_pointer_v1_motion(handle, 0, wl_fixed_from_int(motion2_x), wl_fixed_from_int(motion2_y)); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_left_clicks_client_sees_button_down) +{ + EXPECT_CALL(listener, button(_, _, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED)); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_left_releases_client_sees_button_up) +{ + EXPECT_CALL(listener, button(_, _, _, _)).Times(AnyNumber()); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AnyNumber()).WillOnce(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); + Mock::VerifyAndClearExpectations(&listener); + + EXPECT_CALL(listener, button(_, _, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED)); + recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_given_multiple_button_presses_at_once_client_sees_all) +{ + EXPECT_CALL(listener, button(_, _, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED)); + EXPECT_CALL(listener, button(_, _, BTN_MIDDLE, WL_POINTER_BUTTON_STATE_PRESSED)); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_MIDDLE, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_presses_and_releases_different_buttons_on_same_frame_client_sees_correct_events) +{ + EXPECT_CALL(listener, button(_, _, _, _)).Times(AnyNumber()); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AnyNumber()).WillRepeatedly(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); + Mock::VerifyAndClearExpectations(&listener); + + EXPECT_CALL(listener, button(_, _, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED)); + EXPECT_CALL(listener, button(_, _, BTN_RIGHT, WL_POINTER_BUTTON_STATE_PRESSED)); + recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_RIGHT, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); + + // This is a hack around https://github.com/MirServer/mir/issues/2971 + std::this_thread::sleep_for(1ms); + receive_client.roundtrip(); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_scrolls_client_sees_axis) +{ + EXPECT_CALL(listener, axis(_, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(5))); + EXPECT_CALL(listener, axis_source(_)).Times(AnyNumber()); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_axis(handle, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(5)); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_scrolls_with_steps_client_sees_axis_descrete) +{ + EXPECT_CALL(listener, axis(_, WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_int(5))); + EXPECT_CALL(listener, axis_discrete(WL_POINTER_AXIS_HORIZONTAL_SCROLL, 4)); + EXPECT_CALL(listener, axis_source(_)).Times(AnyNumber()); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_axis_discrete(handle, 0, WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_int(5), 4); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_specifies_axis_source_client_sees_axis_source) +{ + EXPECT_CALL(listener, axis(_, _, _)).Times(AnyNumber()); + EXPECT_CALL(listener, axis_source(WL_POINTER_AXIS_SOURCE_CONTINUOUS)); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_axis(handle, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(5)); + zwlr_virtual_pointer_v1_axis_source(handle, WL_POINTER_AXIS_SOURCE_CONTINUOUS); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); + auto axis_source = false; + EXPECT_CALL(listener, axis_source(WL_POINTER_AXIS_SOURCE_WHEEL)).WillOnce(Invoke([&]{ axis_source = true; })); + zwlr_virtual_pointer_v1_axis(handle, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(5)); + zwlr_virtual_pointer_v1_axis_source(handle, WL_POINTER_AXIS_SOURCE_WHEEL); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return axis_source; }); +} + +TEST_F(VirtualPointerV1Test, if_frame_is_not_sent_client_sees_no_events) +{ + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_motion(handle, 0, wl_fixed_from_int(6), wl_fixed_from_int(7)); + zwlr_virtual_pointer_v1_motion_absolute(handle, 0, 2, 4, 10, 10); + zwlr_virtual_pointer_v1_button(handle, 0, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_axis(handle, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(5)); + zwlr_virtual_pointer_v1_axis_source(handle, WL_POINTER_AXIS_SOURCE_WHEEL); + // Should produce no events because there has been no frame + EXPECT_CALL(listener, motion(_,_,_)).Times(0); + EXPECT_CALL(listener, button(_,_,_,_)).Times(0); + EXPECT_CALL(listener, axis(_,_,_)).Times(0); + EXPECT_CALL(listener, axis_source(_)).Times(0); + EXPECT_CALL(listener, frame()).Times(0); + + send_client.roundtrip(); + std::this_thread::sleep_for(1ms); + receive_client.roundtrip(); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_is_moved_with_absolute_coordinates_with_the_extent_of_the_output_client_sees_motion) +{ + ASSERT_THAT(send_client.output_count(), Ge(1u)); + wlcs::XdgOutputManagerV1 xdg_output_manager{send_client}; + wlcs::XdgOutputV1 xdg_output{xdg_output_manager, 0}; + send_client.roundtrip(); + auto const& output_state = xdg_output.state(); + ASSERT_THAT(output_state.logical_size.operator bool(), Eq(true)); + auto const output_size = output_state.logical_size.value(); + + int const move_to_x = 22; + int const move_to_y = 33; + + EXPECT_CALL(listener, motion(_, + wl_fixed_from_int(move_to_x), + wl_fixed_from_int(move_to_y))); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_motion_absolute(handle, 0, move_to_x, move_to_y, output_size.first, output_size.second); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} + +TEST_F(VirtualPointerV1Test, when_virtual_pointer_is_moved_with_absolute_coordinates_with_the_extent_twice_of_the_output_client_sees_motion) +{ + ASSERT_THAT(send_client.output_count(), Ge(1u)); + wlcs::XdgOutputManagerV1 xdg_output_manager{send_client}; + wlcs::XdgOutputV1 xdg_output{xdg_output_manager, 0}; + send_client.roundtrip(); + auto const& output_state = xdg_output.state(); + ASSERT_THAT(output_state.logical_size.operator bool(), Eq(true)); + auto const output_size = output_state.logical_size.value(); + + int const move_to_x = 22; + int const move_to_y = 33; + + EXPECT_CALL(listener, motion(_, + wl_fixed_from_int(move_to_x), + wl_fixed_from_int(move_to_y))); + auto recieved_frame = false; + EXPECT_CALL(listener, frame()).Times(AtLeast(1)).WillOnce(Invoke([&]{ recieved_frame = true; })); + + auto const handle = zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager, nullptr); + zwlr_virtual_pointer_v1_motion_absolute( + handle, 0, + move_to_x * 2, move_to_y * 2, + output_size.first * 2, output_size.second * 2); + zwlr_virtual_pointer_v1_frame(handle); + send_client.roundtrip(); + receive_client.dispatch_until([&] { return recieved_frame; }); +} diff --git a/tests/xdg_output_v1.cpp b/tests/xdg_output_v1.cpp new file mode 100644 index 0000000..9b1a187 --- /dev/null +++ b/tests/xdg_output_v1.cpp @@ -0,0 +1,43 @@ +/* + * Copyright © 2019 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: William Wold + */ + +#include "in_process_server.h" +#include "xdg_output_v1.h" + +#include + +using namespace testing; +using namespace wlcs; + +using XdgOutputV1Test = wlcs::InProcessServer; + +TEST_F(XdgOutputV1Test, xdg_output_properties_set) +{ + Client client{the_server()}; + ASSERT_THAT(client.output_count(), Ge(1u)); + + XdgOutputManagerV1 xdg_output_manager{client}; + XdgOutputV1 xdg_output{xdg_output_manager, 0}; + client.roundtrip(); + + auto const& state = xdg_output.state(); + ASSERT_THAT((bool)state.logical_position, true); + ASSERT_THAT((bool)state.logical_size, true); + ASSERT_THAT((bool)state.name, true); + // Description is optional +} diff --git a/tests/xdg_popup.cpp b/tests/xdg_popup.cpp new file mode 100644 index 0000000..99c2a0b --- /dev/null +++ b/tests/xdg_popup.cpp @@ -0,0 +1,1428 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2018 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "xdg_shell_stable.h" +#include "xdg_shell_v6.h" +#include "layer_shell_v1.h" +#include "version_specifier.h" + +#include + +#include + +using namespace testing; + +int const window_width = 400, window_height = 500; +int const popup_width = 60, popup_height = 40; + +namespace +{ +uint32_t anchor_stable_to_v6(xdg_positioner_anchor anchor) +{ + switch (anchor) + { + case XDG_POSITIONER_ANCHOR_NONE: + return ZXDG_POSITIONER_V6_ANCHOR_NONE; + case XDG_POSITIONER_ANCHOR_TOP: + return ZXDG_POSITIONER_V6_ANCHOR_TOP; + case XDG_POSITIONER_ANCHOR_BOTTOM: + return ZXDG_POSITIONER_V6_ANCHOR_BOTTOM; + case XDG_POSITIONER_ANCHOR_LEFT: + return ZXDG_POSITIONER_V6_ANCHOR_LEFT; + case XDG_POSITIONER_ANCHOR_RIGHT: + return ZXDG_POSITIONER_V6_ANCHOR_RIGHT; + case XDG_POSITIONER_ANCHOR_TOP_LEFT: + return ZXDG_POSITIONER_V6_ANCHOR_TOP | ZXDG_POSITIONER_V6_ANCHOR_LEFT; + case XDG_POSITIONER_ANCHOR_BOTTOM_LEFT: + return ZXDG_POSITIONER_V6_ANCHOR_BOTTOM | ZXDG_POSITIONER_V6_ANCHOR_LEFT; + case XDG_POSITIONER_ANCHOR_TOP_RIGHT: + return ZXDG_POSITIONER_V6_ANCHOR_TOP | ZXDG_POSITIONER_V6_ANCHOR_RIGHT; + case XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT: + return ZXDG_POSITIONER_V6_ANCHOR_BOTTOM | ZXDG_POSITIONER_V6_ANCHOR_RIGHT; + default: + return ZXDG_POSITIONER_V6_ANCHOR_NONE; + } +} + +uint32_t gravity_stable_to_v6(xdg_positioner_gravity gravity) +{ + switch (gravity) + { + case XDG_POSITIONER_GRAVITY_NONE: + return ZXDG_POSITIONER_V6_GRAVITY_NONE; + case XDG_POSITIONER_GRAVITY_TOP: + return ZXDG_POSITIONER_V6_GRAVITY_TOP; + case XDG_POSITIONER_GRAVITY_BOTTOM: + return ZXDG_POSITIONER_V6_GRAVITY_BOTTOM; + case XDG_POSITIONER_GRAVITY_LEFT: + return ZXDG_POSITIONER_V6_GRAVITY_LEFT; + case XDG_POSITIONER_GRAVITY_RIGHT: + return ZXDG_POSITIONER_V6_GRAVITY_RIGHT; + case XDG_POSITIONER_GRAVITY_TOP_LEFT: + return ZXDG_POSITIONER_V6_GRAVITY_TOP | ZXDG_POSITIONER_V6_GRAVITY_LEFT; + case XDG_POSITIONER_GRAVITY_BOTTOM_LEFT: + return ZXDG_POSITIONER_V6_GRAVITY_BOTTOM | ZXDG_POSITIONER_V6_GRAVITY_LEFT; + case XDG_POSITIONER_GRAVITY_TOP_RIGHT: + return ZXDG_POSITIONER_V6_ANCHOR_TOP | ZXDG_POSITIONER_V6_ANCHOR_RIGHT; + case XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT: + return ZXDG_POSITIONER_V6_GRAVITY_BOTTOM | ZXDG_POSITIONER_V6_GRAVITY_RIGHT; + default: + return ZXDG_POSITIONER_V6_GRAVITY_NONE; + } +} + +uint32_t constraint_adjustment_stable_to_v6(xdg_positioner_constraint_adjustment ca) +{ + return ca; // the two enums have the same values +} + +struct PositionerParams +{ + PositionerParams() + : popup_size{popup_width, popup_height}, + anchor_rect{{0, 0}, {window_width, window_height}} + { + } + + auto with_size(int x, int y) -> PositionerParams& { popup_size = {x, y}; return *this; } + auto with_anchor_rect(int x, int y, int w, int h) -> PositionerParams& { anchor_rect = {{x, y}, {w, h}}; return *this; } + auto with_anchor(xdg_positioner_anchor value) -> PositionerParams& { anchor_stable = {value}; return *this; } + auto with_gravity(xdg_positioner_gravity value) -> PositionerParams& { gravity_stable = {value}; return *this; } + auto with_constraint_adjustment(uint32_t value) -> PositionerParams& { constraint_adjustment_stable = static_cast(value); return *this; } + auto with_offset(int x, int y) -> PositionerParams& { offset = {{x, y}}; return *this; } + auto with_reactive(bool enable = true) -> PositionerParams& { reactive = enable; return *this; } + auto with_grab(bool enable = true) -> PositionerParams& { grab = enable; return *this; } + + std::pair popup_size; // will default to XdgPopupStableTestBase::popup_(width|height) if nullopt + std::pair, std::pair> anchor_rect; // will default to the full window rect + std::optional anchor_stable; + std::optional gravity_stable; + std::optional constraint_adjustment_stable; + std::optional> offset; + bool reactive{false}; + bool grab{false}; +}; + +struct PositionerTestParams +{ + PositionerTestParams(std::string name, int expected_x, int expected_y, PositionerParams const& positioner) + : name{name}, + expected_positon{expected_x, expected_y}, + expected_size{popup_width, popup_height}, + positioner{positioner}, + parent_position_func{std::nullopt} + { + } + + PositionerTestParams( + std::string name, + int expected_x, int expected_y, + int expected_width, int expected_height, + PositionerParams const& positioner, + std::function(int, int)> parent_position_func) + : name{name}, + expected_positon{expected_x, expected_y}, + expected_size{expected_width, expected_height}, + positioner{positioner}, + parent_position_func{std::move(parent_position_func)} + { + } + + std::string name; + std::pair expected_positon; + std::pair expected_size; + PositionerParams positioner; + /// parent_position_func is called with the size of the output + std::optional(int output_width, int output_height)>> parent_position_func; +}; + +std::ostream& operator<<(std::ostream& out, PositionerTestParams const& param) +{ + return out << param.name; +} + +class XdgPopupManagerBase +{ +public: + struct State + { + int x; + int y; + int width; + int height; + }; + + static int const window_x, window_y; + + XdgPopupManagerBase(wlcs::Server& server, std::shared_ptr client) + : the_server{server}, + client{client}, + surface{*client}, + parent_position_{window_x, window_y} + { + surface.add_frame_callback([this](auto) { surface_rendered = true; }); + } + + virtual ~XdgPopupManagerBase() = default; + + void wait_for_frame_to_render() + { + surface.attach_buffer(window_width, window_height); + surface_rendered = false; + wl_surface_commit(surface); + client->dispatch_until([this]() { return surface_rendered; }); + the_server.move_surface_to(surface, parent_position_.first, parent_position_.second); + } + + void map_popup(PositionerParams const& params) + { + popup_surface.emplace(*client); + setup_popup(params); + wl_surface_commit(popup_surface.value()); + dispatch_until_popup_configure(); + popup_surface.value().attach_buffer(params.popup_size.first, params.popup_size.second); + bool surface_rendered{false}; + popup_surface.value().add_frame_callback([&surface_rendered](auto) { surface_rendered = true; }); + wl_surface_commit(popup_surface.value()); + client->dispatch_until([&surface_rendered]() { return surface_rendered; }); + } + + void set_parent_position( + std::function(int output_width, int output_height)> const& parent_position_func) + { + auto const output_size = client->output_state(0).mode_size.value(); + parent_position_ = parent_position_func(output_size.first, output_size.second); + the_server.move_surface_to(surface, parent_position_.first, parent_position_.second); + client->roundtrip(); + } + + auto parent_position() -> std::pair + { + return parent_position_; + } + + void unmap_popup() + { + clear_popup(); + popup_surface = std::nullopt; + } + + virtual auto create_child_popup() -> std::unique_ptr = 0; + virtual void dispatch_until_popup_configure() = 0; + + MOCK_METHOD0(popup_done, void()); + + wlcs::Server& the_server; + + std::shared_ptr const client; + wlcs::Surface surface; + std::optional popup_surface; + + std::pair parent_position_; + std::optional state; + +protected: + virtual void setup_popup(PositionerParams const& params) = 0; + virtual void clear_popup() = 0; + + bool surface_rendered{true}; +}; + +int const XdgPopupManagerBase::window_x = 500; +int const XdgPopupManagerBase::window_y = 500; + +class XdgPopupStableManager : public XdgPopupManagerBase +{ +public: + XdgPopupStableManager(wlcs::Server& server) + : XdgPopupManagerBase{server, std::make_shared(server)}, + xdg_shell_surface{std::in_place, *client, surface}, + toplevel{std::in_place, xdg_shell_surface.value()}, + parent{&*xdg_shell_surface} + { + wait_for_frame_to_render(); + } + + XdgPopupStableManager(wlcs::Server& server, std::shared_ptr client, wlcs::XdgSurfaceStable* parent) + : XdgPopupManagerBase{server, client}, + parent{parent} + { + } + + void dispatch_until_popup_configure() override + { + client->dispatch_until( + [prev_count = popup_surface_configure_count, ¤t_count = popup_surface_configure_count]() + { + return current_count > prev_count; + }); + } + + static void setup_positioner(wlcs::XdgPositionerStable& positioner, PositionerParams const& param) + { + // size must always be set + xdg_positioner_set_size(positioner, param.popup_size.first, param.popup_size.second); + + // anchor rect must always be set + xdg_positioner_set_anchor_rect( + positioner, + param.anchor_rect.first.first, + param.anchor_rect.first.second, + param.anchor_rect.second.first, + param.anchor_rect.second.second); + + if (param.anchor_stable) + xdg_positioner_set_anchor(positioner, param.anchor_stable.value()); + + if (param.gravity_stable) + xdg_positioner_set_gravity(positioner, param.gravity_stable.value()); + + if (param.constraint_adjustment_stable) + xdg_positioner_set_constraint_adjustment(positioner, param.constraint_adjustment_stable.value()); + + if (param.offset) + xdg_positioner_set_offset(positioner, param.offset.value().first, param.offset.value().second); + + if (param.reactive) + { + if (xdg_positioner_get_version(positioner) < XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION) + { + BOOST_THROW_EXCEPTION(std::logic_error("XDG shell version does not support reactive popups")); + } + xdg_positioner_set_reactive(positioner); + } + } + + void setup_popup(PositionerParams const& param) override + { + wlcs::XdgPositionerStable positioner{*client}; + setup_positioner(positioner, param); + + popup_xdg_surface.emplace(*client, popup_surface.value()); + + popup.emplace(popup_xdg_surface.value(), parent, positioner); + if (param.grab) + { + if (!client->latest_serial()) + { + BOOST_THROW_EXCEPTION(std::runtime_error("client does not have a serial")); + } + xdg_popup_grab(popup.value().popup, client->seat(), client->latest_serial().value()); + } + + ON_CALL(popup_xdg_surface.value(), configure).WillByDefault([&](auto serial) + { + xdg_surface_ack_configure(popup_xdg_surface.value(), serial); + popup_surface_configure_count++; + }); + ON_CALL(popup.value(), configure).WillByDefault([&](auto... args) + { + state = State{args...}; + }); + ON_CALL(popup.value(), done()).WillByDefault([this](){ popup_done(); }); + } + + void move_parent_using_pointer(std::pair to) + { + auto pointer = the_server.create_pointer(); + pointer.move_to(parent_position_.first + 1, parent_position_.second + 1); + pointer.left_button_down(); + client->roundtrip(); + if (client->window_under_cursor() != surface) + { + BOOST_THROW_EXCEPTION(std::runtime_error("surface not detected at expected position")); + } + xdg_toplevel_move(toplevel.value(), client->seat(), client->latest_serial().value()); + client->roundtrip(); + pointer.move_to(to.first + 1, to.second + 1); + parent_position_ = to; + pointer.left_button_up(); + client->roundtrip(); + } + + void clear_popup() override + { + popup = std::nullopt; + popup_xdg_surface = std::nullopt; + } + + auto create_child_popup() -> std::unique_ptr override + { + return std::make_unique(the_server, client, &popup_xdg_surface.value()); + } + + std::optional xdg_shell_surface; + std::optional toplevel; + wlcs::XdgSurfaceStable* const parent; + + std::optional popup_xdg_surface; + std::optional popup; + + int popup_surface_configure_count{0}; +}; + +class XdgPopupV6Manager : public XdgPopupManagerBase +{ +public: + XdgPopupV6Manager(wlcs::Server& server) + : XdgPopupManagerBase{server, std::make_shared(server)}, + xdg_shell_surface{std::in_place, *client, surface}, + toplevel{std::in_place, xdg_shell_surface.value()}, + parent{&*xdg_shell_surface} + { + wait_for_frame_to_render(); + } + + XdgPopupV6Manager(wlcs::Server& server, std::shared_ptr client, wlcs::XdgSurfaceV6* parent) + : XdgPopupManagerBase{server, client}, + parent{parent} + { + } + + void dispatch_until_popup_configure() override + { + client->dispatch_until( + [prev_count = popup_surface_configure_count, ¤t_count = popup_surface_configure_count]() + { + return current_count > prev_count; + }); + } + + void setup_popup(PositionerParams const& param) override + { + wlcs::XdgPositionerV6 positioner{*client}; + + // size must always be set + zxdg_positioner_v6_set_size(positioner, param.popup_size.first, param.popup_size.second); + + // anchor rect must always be set + zxdg_positioner_v6_set_anchor_rect( + positioner, + param.anchor_rect.first.first, + param.anchor_rect.first.second, + param.anchor_rect.second.first, + param.anchor_rect.second.second); + + if (param.anchor_stable) + { + uint32_t v6_anchor = anchor_stable_to_v6(param.anchor_stable.value()); + zxdg_positioner_v6_set_anchor(positioner, v6_anchor); + } + + if (param.gravity_stable) + { + uint32_t v6_gravity = gravity_stable_to_v6(param.gravity_stable.value()); + zxdg_positioner_v6_set_gravity(positioner, v6_gravity); + } + + if (param.constraint_adjustment_stable) + { + uint32_t v6_constraint_adjustment = + constraint_adjustment_stable_to_v6(param.constraint_adjustment_stable.value()); + zxdg_positioner_v6_set_constraint_adjustment(positioner, v6_constraint_adjustment); + } + + if (param.offset) + { + zxdg_positioner_v6_set_offset(positioner, param.offset.value().first, param.offset.value().second); + } + + popup_xdg_surface.emplace(*client, popup_surface.value()); + popup.emplace(popup_xdg_surface.value(), *parent, positioner); + if (param.grab) + { + if (!client->latest_serial()) + { + BOOST_THROW_EXCEPTION(std::runtime_error("client does not have a serial")); + } + zxdg_popup_v6_grab(popup.value().popup, client->seat(), client->latest_serial().value()); + } + + ON_CALL(popup.value(), done).WillByDefault([this](){ popup_done(); }); + ON_CALL(popup_xdg_surface.value(), configure).WillByDefault([&](auto serial) + { + zxdg_surface_v6_ack_configure(popup_xdg_surface.value(), serial); + popup_surface_configure_count++; + }); + ON_CALL(popup.value(), configure).WillByDefault([this](auto... args) + { + state = State{args...}; + }); + } + + void clear_popup() override + { + popup = std::nullopt; + popup_xdg_surface = std::nullopt; + } + + auto create_child_popup() -> std::unique_ptr override + { + return std::make_unique(the_server, client, &popup_xdg_surface.value()); + } + + std::optional xdg_shell_surface; + std::optional toplevel; + wlcs::XdgSurfaceV6* const parent; + + std::optional popup_xdg_surface; + std::optional popup; + + int popup_surface_configure_count{0}; +}; + +class LayerV1PopupManager : public XdgPopupManagerBase +{ +public: + LayerV1PopupManager(wlcs::Server& server) + : XdgPopupManagerBase{server, std::make_shared(server)}, + layer_surface{*client, surface} + { + { + wlcs::Client client{server}; + auto const layer_shell = client.bind_if_supported( + wlcs::AtLeastVersion{ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND_SINCE_VERSION}); + client.roundtrip(); + } + zwlr_layer_surface_v1_set_size(layer_surface, window_width, window_height); + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); + wait_for_frame_to_render(); + } + + void dispatch_until_popup_configure() override + { + client->dispatch_until( + [prev_count = popup_surface_configure_count, ¤t_count = popup_surface_configure_count]() + { + return current_count > prev_count; + }); + } + + void setup_popup(PositionerParams const& param) override + { + wlcs::XdgPositionerStable positioner{*client}; + + // size must always be set + xdg_positioner_set_size(positioner, param.popup_size.first, param.popup_size.second); + + // anchor rect must always be set + xdg_positioner_set_anchor_rect( + positioner, + param.anchor_rect.first.first, + param.anchor_rect.first.second, + param.anchor_rect.second.first, + param.anchor_rect.second.second); + + if (param.anchor_stable) + xdg_positioner_set_anchor(positioner, param.anchor_stable.value()); + + if (param.gravity_stable) + xdg_positioner_set_gravity(positioner, param.gravity_stable.value()); + + if (param.constraint_adjustment_stable) + xdg_positioner_set_constraint_adjustment(positioner, param.constraint_adjustment_stable.value()); + + if (param.offset) + xdg_positioner_set_offset(positioner, param.offset.value().first, param.offset.value().second); + + popup_xdg_surface.emplace(*client, popup_surface.value()); + popup.emplace(popup_xdg_surface.value(), std::nullopt, positioner); + zwlr_layer_surface_v1_get_popup(layer_surface, popup.value()); + if (param.grab) + { + if (!client->latest_serial()) + { + BOOST_THROW_EXCEPTION(std::runtime_error("client does not have a serial")); + } + xdg_popup_grab(popup.value().popup, client->seat(), client->latest_serial().value()); + } + + ON_CALL(popup_xdg_surface.value(), configure).WillByDefault([&](uint32_t serial) + { + xdg_surface_ack_configure(popup_xdg_surface.value(), serial); + popup_surface_configure_count++; + }); + ON_CALL(popup.value(), configure).WillByDefault([this](auto... args) + { + state = State{args...}; + }); + ON_CALL(popup.value(), done()).WillByDefault([this](){ popup_done(); }); + } + + void clear_popup() override + { + popup = std::nullopt; + popup_xdg_surface = std::nullopt; + } + + auto create_child_popup() -> std::unique_ptr override + { + return std::make_unique(the_server, client, &popup_xdg_surface.value()); + } + + wlcs::LayerSurfaceV1 layer_surface; + + std::optional popup_xdg_surface; + std::optional popup; + + int popup_surface_configure_count{0}; +}; + +auto surface_actually_at_location( + wlcs::Server& server, + wlcs::Client& client, + wl_surface* surface, + std::pair location) -> bool +{ + auto pointer = server.create_pointer(); + int offset_x = 1, offset_y = 1; + if (location.first < 0) + { + offset_x -= location.first; + } + if (location.second < 0) + { + offset_y -= location.second; + } + pointer.move_to(location.first + offset_x, location.second + offset_y); + client.roundtrip(); + return ( + client.window_under_cursor() == surface && + client.pointer_position() == std::make_pair(wl_fixed_from_int(offset_x), wl_fixed_from_int(offset_y))); +} + +} + +class XdgPopupPositionerTest: + public wlcs::StartedInProcessServer, + public testing::WithParamInterface +{ +}; + +TEST_P(XdgPopupPositionerTest, xdg_shell_stable_popup_placed_correctly) +{ + auto manager = std::make_unique(the_server()); + auto const& param = GetParam(); + + if (param.parent_position_func) + { + manager->set_parent_position(param.parent_position_func.value()); + } + manager->map_popup(param.positioner); + + ASSERT_THAT( + manager->state, + Ne(std::nullopt)) << "popup configure event not sent"; + + EXPECT_THAT( + std::make_pair(manager->state.value().x, manager->state.value().y), + Eq(param.expected_positon)) << "popup placed in incorrect position"; + + EXPECT_THAT( + std::make_pair(manager->state.value().width, manager->state.value().height), + Eq(param.expected_size)) << "popup has incorrect size"; + + EXPECT_THAT( + surface_actually_at_location( + the_server(), + *manager->client, + manager->popup_surface.value(), + std::make_pair( + manager->parent_position().first + param.expected_positon.first, + manager->parent_position().second + param.expected_positon.second)), + IsTrue()); +} + +TEST_P(XdgPopupPositionerTest, xdg_shell_unstable_v6_popup_placed_correctly) +{ + auto manager = std::make_unique(the_server()); + auto const& param = GetParam(); + + if (param.parent_position_func) + { + manager->set_parent_position(param.parent_position_func.value()); + } + manager->map_popup(param.positioner); + + ASSERT_THAT( + manager->state, + Ne(std::nullopt)) << "popup configure event not sent"; + + EXPECT_THAT( + std::make_pair(manager->state.value().x, manager->state.value().y), + Eq(param.expected_positon)) << "popup placed in incorrect position"; + + EXPECT_THAT( + std::make_pair(manager->state.value().width, manager->state.value().height), + Eq(param.expected_size)) << "popup has incorrect size"; + + EXPECT_THAT( + surface_actually_at_location( + the_server(), + *manager->client, + manager->popup_surface.value(), + std::make_pair( + manager->parent_position().first + param.expected_positon.first, + manager->parent_position().second + param.expected_positon.second)), + IsTrue()); +} + +TEST_P(XdgPopupPositionerTest, layer_shell_popup_placed_correctly) +{ + auto manager = std::make_unique(the_server()); + auto const& param = GetParam(); + + if (param.parent_position_func) + { + manager->set_parent_position(param.parent_position_func.value()); + } + manager->map_popup(param.positioner); + + ASSERT_THAT( + manager->state, + Ne(std::nullopt)) << "popup configure event not sent"; + + + EXPECT_THAT( + std::make_pair(manager->state.value().x, manager->state.value().y), + Eq(param.expected_positon)) << "popup placed in incorrect position"; + + EXPECT_THAT( + std::make_pair(manager->state.value().width, manager->state.value().height), + Eq(param.expected_size)) << "popup has incorrect size"; + + EXPECT_THAT( + surface_actually_at_location( + the_server(), + *manager->client, + manager->popup_surface.value(), + std::make_pair( + manager->parent_position().first + param.expected_positon.first, + manager->parent_position().second + param.expected_positon.second)), + IsTrue()); +} + +INSTANTIATE_TEST_SUITE_P( + Default, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"default values", (window_width - popup_width) / 2, (window_height - popup_height) / 2, PositionerParams()} + )); + +INSTANTIATE_TEST_SUITE_P( + Anchor, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"anchor left", -popup_width / 2, (window_height - popup_height) / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_LEFT)}, + + PositionerTestParams{"anchor right", window_width - popup_width / 2, (window_height - popup_height) / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_RIGHT)}, + + PositionerTestParams{"anchor top", (window_width - popup_width) / 2, -popup_height / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_TOP)}, + + PositionerTestParams{"anchor bottom", (window_width - popup_width) / 2, window_height - popup_height / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM)}, + + PositionerTestParams{"anchor top left", -popup_width / 2, -popup_height / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)}, + + PositionerTestParams{"anchor top right", window_width - popup_width / 2, -popup_height / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT)}, + + PositionerTestParams{"anchor bottom left", -popup_width / 2, window_height - popup_height / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT)}, + + PositionerTestParams{"anchor bottom right", window_width - popup_width / 2, window_height - popup_height / 2, + PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)} + )); + +INSTANTIATE_TEST_SUITE_P( + Gravity, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"gravity none", (window_width - popup_width) / 2, (window_height - popup_height) / 2, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_NONE)}, + + PositionerTestParams{"gravity left", window_width / 2 - popup_width, (window_height - popup_height) / 2, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_LEFT)}, + + PositionerTestParams{"gravity right", window_width / 2, (window_height - popup_height) / 2, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_RIGHT)}, + + PositionerTestParams{"gravity top", (window_width - popup_width) / 2, window_height / 2 - popup_height, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_TOP)}, + + PositionerTestParams{"gravity bottom", (window_width - popup_width) / 2, window_height / 2, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM)}, + + PositionerTestParams{"gravity top left", window_width / 2 - popup_width, window_height / 2 - popup_height, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)}, + + PositionerTestParams{"gravity top right", window_width / 2, window_height / 2 - popup_height, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT)}, + + PositionerTestParams{"gravity bottom left", window_width / 2 - popup_width, window_height / 2, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT)}, + + PositionerTestParams{"gravity bottom right", window_width / 2, window_height / 2, + PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)} + )); + +INSTANTIATE_TEST_SUITE_P( + AnchorRect, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"explicit defaultPositionerParams anchor rect", (window_width - popup_width) / 2, (window_height - popup_height) / 2, + PositionerParams().with_anchor_rect(0, 0, window_width, window_height)}, + + PositionerTestParams{"upper left anchor rect", (window_width - 40 - popup_width) / 2, (window_height - 30 - popup_height) / 2, + PositionerParams().with_anchor_rect(0, 0, window_width - 40, window_height - 30)}, + + PositionerTestParams{"upper right anchor rect", (window_width + 40 - popup_width) / 2, (window_height - 30 - popup_height) / 2, + PositionerParams().with_anchor_rect(40, 0, window_width - 40, window_height - 30)}, + + PositionerTestParams{"lower left anchor rect", (window_width - 40 - popup_width) / 2, (window_height + 30 - popup_height) / 2, + PositionerParams().with_anchor_rect(0, 30, window_width - 40, window_height - 30)}, + + PositionerTestParams{"lower right anchor rect", (window_width + 40 - popup_width) / 2, (window_height + 30 - popup_height) / 2, + PositionerParams().with_anchor_rect(40, 30, window_width - 40, window_height - 30)}, + + PositionerTestParams{"offset anchor rect", (window_width - 40 - popup_width) / 2, (window_height - 80 - popup_height) / 2, + PositionerParams().with_anchor_rect(20, 20, window_width - 80, window_height - 120)} + )); + +INSTANTIATE_TEST_SUITE_P( + ConstraintAdjustmentNone, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"middle of screen", + -popup_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE), + [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }}, + PositionerTestParams{"off top left edge", + -popup_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE), + [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }}, + PositionerTestParams{"off top right edge", + window_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT) + .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE), + [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }}, + PositionerTestParams{"off bottom left edge", + -popup_width, window_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT) + .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE), + [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }}, + PositionerTestParams{"off bottom right edge", + window_width, window_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE), + [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }} + )); + +INSTANTIATE_TEST_SUITE_P( + ConstraintAdjustmentSlide, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"middle of screen", + -popup_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y), + [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }}, + PositionerTestParams{"off top left edge", + -5, -5, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y), + [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }}, + PositionerTestParams{"off top right edge", + window_width - popup_width + 5, -5, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y), + [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }}, + PositionerTestParams{"off bottom left edge", -5, + window_height - popup_height + 5, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y), + [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }}, + PositionerTestParams{"off bottom right edge", + window_width - popup_width + 5, window_height - popup_height + 5, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y), + [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }} + )); + +INSTANTIATE_TEST_SUITE_P( + ConstraintAdjustmentFlip, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"middle of screen", + -popup_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y), + [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }}, + PositionerTestParams{"off top left edge", + window_width, window_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y), + [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }}, + PositionerTestParams{"off top right edge", + -popup_width, window_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y), + [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }}, + PositionerTestParams{"off bottom left edge", + window_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y), + [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }}, + PositionerTestParams{"off bottom right edge", + -popup_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y), + [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }} + )); + +INSTANTIATE_TEST_SUITE_P( + ConstraintAdjustmentResize, + XdgPopupPositionerTest, + testing::Values( + PositionerTestParams{"middle of screen", + -popup_width, -popup_height, + popup_width, popup_height, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y), + [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }}, + PositionerTestParams{"off top left edge", + -5, -5, + 5, 5, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y), + [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }}, + PositionerTestParams{"off top right edge", + window_width, -5, + 5, 5, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y), + [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }}, + PositionerTestParams{"off bottom left edge", + -5, window_height, + 5, 5, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y), + [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }}, + PositionerTestParams{"off bottom right edge", + window_width, window_height, + 5, 5, + PositionerParams() + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y), + [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }} + )); + +struct XdgPopupTestParam +{ + std::function(wlcs::InProcessServer* const)> build; +}; + +std::ostream& operator<<(std::ostream& out, XdgPopupTestParam const&) { return out; } + +class XdgPopupTest: + public wlcs::StartedInProcessServer, + public testing::WithParamInterface +{ +}; + +TEST_P(XdgPopupTest, pointer_focus_goes_to_popup) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + auto pointer = the_server().create_pointer(); + pointer.move_to(manager->window_x + 1, manager->window_y + 1); + manager->client->roundtrip(); + + EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->surface)); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + manager->map_popup(positioner); + manager->client->roundtrip(); + + pointer.move_to(manager->window_x + 2, manager->window_y + 1); + manager->client->roundtrip(); + + EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->popup_surface.value())); +} + +TEST_P(XdgPopupTest, popup_gives_up_pointer_focus_when_gone) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + auto pointer = the_server().create_pointer(); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + manager->map_popup(positioner); + manager->client->roundtrip(); + + pointer.move_to(manager->window_x + 2, manager->window_y + 1); + manager->client->roundtrip(); + + EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->popup_surface.value())); + + manager->unmap_popup(); + manager->client->roundtrip(); + pointer.move_to(manager->window_x + 3, manager->window_y + 1); + manager->client->roundtrip(); + + EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->surface)); +} + +TEST_P(XdgPopupTest, grabbed_popup_gets_done_event_when_new_toplevel_created) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + auto pointer = the_server().create_pointer(); + + // This is needed to get a serial, which will be used later on + pointer.move_to(manager->window_x + 2, manager->window_y + 2); + pointer.left_click(); + manager->client->roundtrip(); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_grab(); + manager->map_popup(positioner); + manager->client->roundtrip(); + + EXPECT_CALL(*manager, popup_done()); + + manager->client->create_visible_surface(window_width, window_height); +} + +TEST_P(XdgPopupTest, grabbed_popups_get_done_events_in_correct_order) +{ + auto const& param = GetParam(); + + wlcs::Client background_client{the_server()}; + wlcs::Surface background_surface{background_client.create_visible_surface(10, 10)}; + the_server().move_surface_to( + background_surface, + XdgPopupManagerBase::window_x - 5, + XdgPopupManagerBase::window_y - 5); + + auto top_popup_manager = param.build(this); + auto pointer = the_server().create_pointer(); + + // This is needed to get a serial, which will be used later on + pointer.move_to(top_popup_manager->window_x + 2, top_popup_manager->window_y + 2); + pointer.left_click(); + top_popup_manager->client->roundtrip(); + + auto positioner = PositionerParams{} + .with_anchor_rect(20, 20, 1, 1) + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_grab(); + top_popup_manager->map_popup(positioner); + top_popup_manager->client->roundtrip(); + auto sub_popup_manager = top_popup_manager->create_child_popup(); + sub_popup_manager->map_popup(positioner); + top_popup_manager->client->roundtrip(); + + InSequence seq; + EXPECT_CALL(*sub_popup_manager, popup_done()); + EXPECT_CALL(*top_popup_manager, popup_done()); + + // Click on background surface so it is focused and grabbed popups are dismissed + pointer.move_to(top_popup_manager->window_x - 2, top_popup_manager->window_y - 2); + pointer.left_click(); + top_popup_manager->client->roundtrip(); +} + +TEST_P(XdgPopupTest, grabbed_popup_gets_keyboard_focus) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + auto pointer = the_server().create_pointer(); + + // This is needed to get a serial, which will be used later on + pointer.move_to(manager->window_x + 2, manager->window_y + 2); + pointer.left_click(); + manager->client->roundtrip(); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_grab(); + manager->map_popup(positioner); + manager->client->roundtrip(); + + EXPECT_THAT( + manager->client->keyboard_focused_window(), + Eq(static_cast(manager->popup_surface.value()))) + << "grabbed popup not given keyboard focus"; +} + +TEST_P(XdgPopupTest, non_grabbed_popup_does_not_get_keyboard_focus) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + manager->map_popup(positioner); + manager->client->roundtrip(); + + EXPECT_THAT( + manager->client->keyboard_focused_window(), + Ne(static_cast(manager->popup_surface.value()))) + << "popup given keyboard focus"; + EXPECT_THAT( + manager->client->keyboard_focused_window(), + Eq(static_cast(manager->surface))); +} + +TEST_P(XdgPopupTest, does_not_get_popup_done_event_before_button_press) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + auto pointer = the_server().create_pointer(); + + // This is needed to get a serial, which will be used later on + pointer.move_to(manager->window_x + 2, manager->window_y + 2); + pointer.left_click(); + manager->client->roundtrip(); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT) + .with_grab(); + manager->map_popup(positioner); + manager->client->roundtrip(); + + // This may or may not be sent, but a button press should not come in after it if it is sent + bool got_popup_done = false; + EXPECT_CALL(*manager, popup_done()).Times(AnyNumber()).WillRepeatedly([&]() + { + got_popup_done = true; + }); + + manager->client->add_pointer_button_notification([&](auto, auto, auto) + { + EXPECT_THAT(got_popup_done, IsFalse()) << "pointer button sent after popup done"; + return true; + }); + + pointer.move_to(manager->window_x + 32, manager->window_y + 32); + pointer.left_click(); + manager->client->roundtrip(); +} + +TEST_F(XdgPopupTest, zero_size_anchor_rect_stable) +{ + auto manager = std::make_unique(the_server()); + + auto positioner = PositionerParams{} + .with_anchor_rect(window_width / 2, window_height / 2, 0, 0); + + manager->map_popup(positioner); + manager->client->roundtrip(); + + ASSERT_THAT( + std::make_pair(manager->state.value().x, manager->state.value().y), + Eq(std::make_pair( + (window_width - popup_width) / 2, + (window_height - popup_height) / 2))) << "popup placed in incorrect position"; +} + +// regression test for https://github.com/MirServer/mir/issues/836 +TEST_P(XdgPopupTest, popup_configure_is_valid) +{ + auto const& param = GetParam(); + auto manager = param.build(this); + + auto positioner = PositionerParams{} + .with_size(30, 30) + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + manager->map_popup(positioner); + manager->client->roundtrip(); + + ASSERT_THAT(manager->state, Ne(std::nullopt)); + EXPECT_THAT(manager->state.value().width, Gt(0)); + EXPECT_THAT(manager->state.value().height, Gt(0)); +} + +TEST_F(XdgPopupTest, when_parent_surface_is_moved_a_reactive_popup_is_moved) +{ + XdgPopupStableManager manager{the_server()}; + + manager.client->bind_if_supported(wlcs::AtLeastVersion{XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION}); + + auto positioner = PositionerParams{} + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y) + .with_reactive(); + + manager.map_popup(positioner); + // repositioned should not be called unless reposition is called + EXPECT_CALL(manager.popup.value(), repositioned).Times(0); + manager.client->roundtrip(); + + ASSERT_THAT( + manager.state, + Ne(std::nullopt)) << "popup configure event not sent"; + + ASSERT_THAT( + std::make_pair(manager.state.value().x, manager.state.value().y), + Eq(std::make_pair(-popup_width, -popup_height))) << "popup initially placed in incorrect position"; + + manager.move_parent_using_pointer({5, 5}); + + ASSERT_THAT( + std::make_pair(manager.state.value().x, manager.state.value().y), + Ne(std::make_pair(-popup_width, -popup_height))) << "reactive popup was not moved"; + + EXPECT_THAT( + std::make_pair(manager.state.value().x, manager.state.value().y), + Eq(std::make_pair(-5, -5))) << "reactive popup placed in incorrect position"; + + EXPECT_THAT( + surface_actually_at_location( + the_server(), + *manager.client, + manager.popup_surface.value(), + std::make_pair(0, 0)), + IsTrue()); +} + +TEST_F(XdgPopupTest, when_parent_surface_is_moved_a_nonreactive_popup_is_not_moved) +{ + XdgPopupStableManager manager{the_server()}; + + manager.client->bind_if_supported(wlcs::AtLeastVersion{XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION}); + + auto positioner = PositionerParams{} + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT) + .with_constraint_adjustment( + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y); + + manager.map_popup(positioner); + // repositioned should not be called unless reposition is called + EXPECT_CALL(manager.popup.value(), repositioned).Times(0); + manager.client->roundtrip(); + + ASSERT_THAT( + manager.state, + Ne(std::nullopt)) << "popup configure event not sent"; + + ASSERT_THAT( + std::make_pair(manager.state.value().x, manager.state.value().y), + Eq(std::make_pair(-popup_width, -popup_height))) << "popup initially placed in incorrect position"; + + manager.move_parent_using_pointer({5, 5}); + + EXPECT_THAT( + std::make_pair(manager.state.value().x, manager.state.value().y), + Eq(std::make_pair(-popup_width, -popup_height))) << "nonreactive popup was moved"; + + EXPECT_THAT( + surface_actually_at_location( + the_server(), + *manager.client, + manager.popup_surface.value(), + std::make_pair(5 - popup_width, 5 - popup_height)), + IsTrue()); +} + +TEST_F(XdgPopupTest, popup_can_be_repositioned) +{ + XdgPopupStableManager manager{the_server()}; + + manager.client->bind_if_supported(wlcs::AtLeastVersion{XDG_POPUP_REPOSITION_SINCE_VERSION}); + + auto positioner_a = PositionerParams{} + .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT) + .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT); + + manager.map_popup(positioner_a); + EXPECT_CALL(manager.popup.value(), repositioned).Times(0); + manager.client->roundtrip(); + + auto positioner_b = PositionerParams{} + .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT) + .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT); + wlcs::XdgPositionerStable xdg_positioner{*manager.client}; + XdgPopupStableManager::setup_positioner(xdg_positioner, positioner_b); + xdg_popup_reposition(manager.popup.value(), xdg_positioner, 1); + EXPECT_CALL(manager.popup.value(), repositioned(1)); + manager.client->roundtrip(); + + ASSERT_THAT( + manager.state, + Ne(std::nullopt)) << "popup configure event not sent"; + + EXPECT_THAT( + std::make_pair(manager.state.value().x, manager.state.value().y), + Eq(std::make_pair(window_width, window_height))); + + EXPECT_THAT( + surface_actually_at_location( + the_server(), + *manager.client, + manager.popup_surface.value(), + std::make_pair( + manager.parent_position().first + window_width, + manager.parent_position().second + window_height)), + IsTrue()); +} + +INSTANTIATE_TEST_SUITE_P( + XdgPopupStable, + XdgPopupTest, + testing::Values(XdgPopupTestParam{ + [](wlcs::InProcessServer* const server) + { + return std::make_unique(server->the_server()); + }})); + +INSTANTIATE_TEST_SUITE_P( + XdgPopupUnstableV6, + XdgPopupTest, + testing::Values(XdgPopupTestParam{ + [](wlcs::InProcessServer* const server) + { + return std::make_unique(server->the_server()); + }})); + +INSTANTIATE_TEST_SUITE_P( + LayerShellPopup, + XdgPopupTest, + testing::Values(XdgPopupTestParam{ + [](wlcs::InProcessServer* const server) + { + return std::make_unique(server->the_server()); + }})); + +// TODO: test that positioner is always overlapping or adjacent to parent +// TODO: test that positioner is copied immediately after use +// TODO: test that error is raised when incomplete positioner is used (positioner without size and anchor rect set) +// TODO: test set_size +// TODO: test that set_window_geometry affects anchor rect +// TODO: test set_offset +// TODO: test that a zero size anchor rect fails on v6 diff --git a/tests/xdg_surface_stable.cpp b/tests/xdg_surface_stable.cpp new file mode 100644 index 0000000..c0a5437 --- /dev/null +++ b/tests/xdg_surface_stable.cpp @@ -0,0 +1,176 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2018 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "xdg_shell_stable.h" +#include "wl_handle.h" +#include "version_specifier.h" + +#include + +using namespace testing; +using XdgSurfaceStableTest = wlcs::InProcessServer; +using wlcs::AnyVersion; + +TEST_F(XdgSurfaceStableTest, supports_xdg_shell_stable_protocol) +{ + wlcs::Client client{the_server()}; + ASSERT_THAT(client.xdg_shell_stable(), NotNull()); + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_surface{client, surface}; +} + +TEST_F(XdgSurfaceStableTest, gets_configure_event) +{ + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_surface{client, surface}; + + EXPECT_CALL(xdg_surface, configure) + .WillOnce([&](auto serial) + { + xdg_surface_ack_configure(xdg_surface, serial); + }); + + wlcs::XdgToplevelStable toplevel{xdg_surface}; + surface.attach_buffer(600, 400); + + client.roundtrip(); +} + +TEST_F(XdgSurfaceStableTest, creating_xdg_surface_from_wl_surface_with_existing_role_is_an_error) +{ + wlcs::Client client{the_server()}; + + auto const xdg_wm_base = client.bind_if_supported(AnyVersion); + + // We need a parent for the subsurface + auto const parent = client.create_visible_surface(300, 300); + + auto const surface = wlcs::wrap_wl_object(wl_compositor_create_surface(client.compositor())); + + // We need some way of assigning a role to a wl_surface. wl_subcompositor is as good a way as any. + auto const subsurface = + wlcs::wrap_wl_object(wl_subcompositor_get_subsurface(client.subcompositor(), surface, parent)); + + client.roundtrip(); + + try + { + xdg_wm_base_get_xdg_surface(xdg_wm_base, surface); + client.roundtrip(); + } + catch(wlcs::ProtocolError const& error) + { + EXPECT_THAT(error.interface(), Eq(&xdg_wm_base_interface)); + EXPECT_THAT(error.error_code(), Eq(XDG_WM_BASE_ERROR_ROLE)); + return; + } + + FAIL() << "Expected protocol error not received"; +} + + +TEST_F(XdgSurfaceStableTest, creating_xdg_surface_from_wl_surface_with_attached_buffer_is_an_error) +{ + wlcs::Client client{the_server()}; + + auto const xdg_wm_base = client.bind_if_supported(AnyVersion); + + auto const surface = wlcs::wrap_wl_object(wl_compositor_create_surface(client.compositor())); + wlcs::ShmBuffer buffer{client, 300, 300}; + wl_surface_attach(surface, buffer, 0, 0); + client.roundtrip(); + + try + { + xdg_wm_base_get_xdg_surface(xdg_wm_base, surface); + client.roundtrip(); + } + catch(wlcs::ProtocolError const& error) + { + EXPECT_THAT(error.interface(), Eq(&xdg_wm_base_interface)); + EXPECT_THAT(error.error_code(), Eq(XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE)); + return; + } + + FAIL() << "Expected protocol error not received"; +} + +TEST_F(XdgSurfaceStableTest, creating_xdg_surface_from_wl_surface_with_committed_buffer_is_an_error) +{ + wlcs::Client client{the_server()}; + + auto const xdg_wm_base = client.bind_if_supported(AnyVersion); + + auto const surface = wlcs::wrap_wl_object(wl_compositor_create_surface(client.compositor())); + wlcs::ShmBuffer buffer{client, 300, 300}; + wl_surface_attach(surface, buffer, 0, 0); + wl_surface_commit(surface); + client.roundtrip(); + + try + { + xdg_wm_base_get_xdg_surface(xdg_wm_base, surface); + client.roundtrip(); + } + catch(wlcs::ProtocolError const& error) + { + EXPECT_THAT(error.interface(), Eq(&xdg_wm_base_interface)); + EXPECT_THAT(error.error_code(), Eq(XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE)); + return; + } + + FAIL() << "Expected protocol error not received"; +} + +TEST_F(XdgSurfaceStableTest, attaching_buffer_to_unconfigured_xdg_surface_is_an_error) +{ + wlcs::Client client{the_server()}; + + auto const xdg_wm_base = client.bind_if_supported(AnyVersion); + + auto const surface = wlcs::wrap_wl_object(wl_compositor_create_surface(client.compositor())); + wlcs::ShmBuffer buffer{client, 300, 300}; + client.roundtrip(); + + try + { + xdg_wm_base_get_xdg_surface(xdg_wm_base, surface); + wl_surface_attach(surface, buffer, 0, 0); + client.roundtrip(); + } + catch(wlcs::ProtocolError const& error) + { + EXPECT_THAT(error.interface(), Eq(&xdg_surface_interface)); + EXPECT_THAT(error.error_code(), Eq(XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER)); + return; + } + + FAIL() << "Expected protocol error not received"; +} diff --git a/tests/xdg_surface_v6.cpp b/tests/xdg_surface_v6.cpp new file mode 100644 index 0000000..5039bd5 --- /dev/null +++ b/tests/xdg_surface_v6.cpp @@ -0,0 +1,60 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2018 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "xdg_shell_v6.h" + +#include + +using namespace testing; +using XdgSurfaceV6Test = wlcs::InProcessServer; + +TEST_F(XdgSurfaceV6Test, supports_xdg_shell_v6_protocol) +{ + wlcs::Client client{the_server()}; + ASSERT_THAT(client.xdg_shell_v6(), NotNull()); + wlcs::Surface surface{client}; + wlcs::XdgSurfaceV6 xdg_surface{client, surface}; +} + +TEST_F(XdgSurfaceV6Test, gets_configure_event) +{ + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceV6 xdg_surface{client, surface}; + + EXPECT_CALL(xdg_surface, configure).WillOnce([&](uint32_t serial) + { + zxdg_surface_v6_ack_configure(xdg_surface, serial); + }); + + wlcs::XdgToplevelV6 toplevel{xdg_surface}; + surface.attach_buffer(600, 400); + + client.roundtrip(); +} diff --git a/tests/xdg_toplevel_stable.cpp b/tests/xdg_toplevel_stable.cpp new file mode 100644 index 0000000..676fc6b --- /dev/null +++ b/tests/xdg_toplevel_stable.cpp @@ -0,0 +1,664 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2018 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "version_specifier.h" +#include "xdg_shell_stable.h" + +#include + +#include + +using namespace testing; + +namespace +{ +class ConfigurationWindow +{ +public: + int const window_width = 200, window_height = 320; + + ConfigurationWindow(wlcs::Client& client) + : client{client}, + surface{client}, + xdg_shell_surface{client, surface}, + toplevel{xdg_shell_surface} + { + ON_CALL(xdg_shell_surface, configure).WillByDefault([&](auto serial) + { + xdg_surface_ack_configure(xdg_shell_surface, serial); + surface_configure_count++; + }); + + ON_CALL(toplevel, configure).WillByDefault([&](auto... args) + { + state = wlcs::XdgToplevelStable::State{args...}; + }); + + wl_surface_commit(surface); + client.roundtrip(); + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + dispatch_until_configure(); + } + + void dispatch_until_configure() + { + client.dispatch_until( + [prev_count = surface_configure_count, ¤t_count = surface_configure_count]() + { + return current_count > prev_count; + }); + } + + operator wlcs::Surface&() {return surface;} + + operator wl_surface*() const {return surface;} + operator xdg_surface*() const {return xdg_shell_surface;} + operator xdg_toplevel*() const {return toplevel;} + + wlcs::Client& client; + wlcs::Surface surface; + wlcs::XdgSurfaceStable xdg_shell_surface; + wlcs::XdgToplevelStable toplevel; + + int surface_configure_count{0}; + wlcs::XdgToplevelStable::State state{0, 0, nullptr}; +}; +} + +using XdgToplevelStableTest = wlcs::InProcessServer; + +TEST_F(XdgToplevelStableTest, wm_capabilities_are_sent) +{ + wlcs::Client client{the_server()}; + client.bind_if_supported(wlcs::AtLeastVersion{XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION}); + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_shell_surface{client, surface}; + wlcs::XdgToplevelStable toplevel{xdg_shell_surface}; + EXPECT_CALL(toplevel, wm_capabilities).Times(1); + client.roundtrip(); +} + +// there *could* be a bug in these tests, but also the window manager may not be behaving properly +// lets take another look when we've updated the window manager +TEST_F(XdgToplevelStableTest, pointer_respects_window_geom_offset) +{ + const int offset_x = 35, offset_y = 12; + const int window_pos_x = 200, window_pos_y = 280; + const int pointer_x = window_pos_x + 20, pointer_y = window_pos_y + 30; + + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + xdg_surface_set_window_geometry(window.xdg_shell_surface, + offset_x, + offset_y, + window.window_width - offset_x, + window.window_height - offset_y); + wl_surface_commit(window.surface); + the_server().move_surface_to(window.surface, window_pos_x, window_pos_y); + + auto pointer = the_server().create_pointer(); + pointer.move_to(pointer_x, pointer_y); + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)window.surface)); + ASSERT_THAT(client.pointer_position(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x), + wl_fixed_from_int(pointer_y - window_pos_y)))) << "set_window_geometry offset was ignored"; + ASSERT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x + offset_x), + wl_fixed_from_int(pointer_y - window_pos_y + offset_y)))); +} + +TEST_F(XdgToplevelStableTest, touch_respects_window_geom_offset) +{ + const int offset_x = 35, offset_y = 12; + const int window_pos_x = 200, window_pos_y = 280; + const int pointer_x = window_pos_x + 20, pointer_y = window_pos_y + 30; + + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + xdg_surface_set_window_geometry(window.xdg_shell_surface, + offset_x, + offset_y, + window.window_width - offset_x, + window.window_height - offset_y); + wl_surface_commit(window.surface); + the_server().move_surface_to(window.surface, window_pos_x, window_pos_y); + + auto touch = the_server().create_touch(); + touch.down_at(pointer_x, pointer_y); + client.roundtrip(); + + ASSERT_THAT(client.touched_window(), Eq((wl_surface*)window.surface)); + ASSERT_THAT(client.touch_position(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x), + wl_fixed_from_int(pointer_y - window_pos_y)))) << "set_window_geometry offset was ignored"; + ASSERT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x + offset_x), + wl_fixed_from_int(pointer_y - window_pos_y + offset_y)))); +} + +// TODO: set_window_geometry window size (something will need to be added to wlcs) + +TEST_F(XdgToplevelStableTest, surface_can_be_moved_interactively) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + int dx = 60, dy = -40; + int end_x = window_x + dx + 20, end_y = window_y + dy + 20; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_shell_surface{client, surface}; + wlcs::XdgToplevelStable toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + xdg_toplevel_move(toplevel, client.seat(), last_serial); + client.roundtrip(); + pointer.move_to(start_x + dx, start_x + dy); + pointer.left_button_up(); + client.roundtrip(); + + pointer.move_to(end_x, end_y); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Eq(static_cast(surface))); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(end_x - window_x - dx), + wl_fixed_from_int(end_y - window_y - dy)))); + + client.roundtrip(); +} + +// Tests https://github.com/MirServer/mir/issues/1792 +TEST_F(XdgToplevelStableTest, touch_can_not_steal_pointer_based_move) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_shell_surface{client, surface}; + wlcs::XdgToplevelStable toplevel{xdg_shell_surface}; + surface.attach_visible_buffer(window_width, window_height); + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + auto touch = the_server().create_touch(); + + bool button_down{false}; + uint32_t last_pointer_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_pointer_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + touch.down_at(start_x, start_y); + + client.dispatch_until([&](){ + return button_down; + }); + + xdg_toplevel_move(toplevel, client.seat(), last_pointer_serial); + client.roundtrip(); + pointer.left_button_up(); + touch.move_to(0, 0); + client.roundtrip(); + + // The move should have either been ignored entirly or been based on the pointer (which didn't move) + // Either way, the window should be in the same place it started + EXPECT_THAT(client.window_under_cursor(), Eq(static_cast(surface))); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(start_x - window_x), + wl_fixed_from_int(start_y - window_y)))); +} + +TEST_F(XdgToplevelStableTest, pointer_leaves_surface_during_interactive_move) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_shell_surface{client, surface}; + wlcs::XdgToplevelStable toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + xdg_toplevel_move(toplevel, client.seat(), last_serial); + client.dispatch_until([&](){ + return !client.window_under_cursor(); + }); +} + +TEST_F(XdgToplevelStableTest, surface_can_be_resized_interactively) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + int dx = 60, dy = -40; + int end_x = window_x + dx + 20, end_y = window_y + dy + 20; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_shell_surface{client, surface}; + wlcs::XdgToplevelStable toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + xdg_toplevel_resize(toplevel, client.seat(), last_serial, XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT); + client.roundtrip(); + pointer.move_to(start_x + dx, start_x + dy); + pointer.left_button_up(); + client.roundtrip(); + + pointer.move_to(end_x, end_y); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Eq(static_cast(surface))); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(end_x - window_x - dx), + wl_fixed_from_int(end_y - window_y - dy)))); + + client.roundtrip(); +} + +TEST_F(XdgToplevelStableTest, pointer_leaves_surface_during_interactive_resize) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceStable xdg_shell_surface{client, surface}; + wlcs::XdgToplevelStable toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + xdg_toplevel_resize(toplevel, client.seat(), last_serial, XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT); + client.dispatch_until([&](){ + return !client.window_under_cursor(); + }); +} + +TEST_F(XdgToplevelStableTest, parent_can_be_set) +{ + const int window_pos_x = 200, window_pos_y = 280; + + wlcs::Client client{the_server()}; + + ConfigurationWindow parent{client}; + the_server().move_surface_to(parent.surface, window_pos_x, window_pos_y); + + ConfigurationWindow child{client}; + the_server().move_surface_to(child.surface, window_pos_x, window_pos_y); + + xdg_toplevel_set_parent(child, parent); + wl_surface_commit(child.surface); + client.roundtrip(); +} + +TEST_F(XdgToplevelStableTest, null_parent_can_be_set) +{ + const int window_pos_x = 200, window_pos_y = 280; + + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + the_server().move_surface_to(window.surface, window_pos_x, window_pos_y); + + xdg_toplevel_set_parent(window, nullptr); + wl_surface_commit(window.surface); + client.roundtrip(); +} + +TEST_F(XdgToplevelStableTest, when_parent_is_set_to_self_error_is_raised) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + xdg_toplevel_set_parent(window, window); + wl_surface_commit(window); + try + { + client.roundtrip(); + } + catch (wlcs::ProtocolError const& err) + { + return; + } + FAIL() << "Protocol error not raised"; +} + +TEST_F(XdgToplevelStableTest, when_parent_is_set_to_child_descendant_error_is_raised) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow parent{client}; + ConfigurationWindow child{client}; + xdg_toplevel_set_parent(child, parent); + wl_surface_commit(child); + client.roundtrip(); + + ConfigurationWindow grandchild{client}; + xdg_toplevel_set_parent(grandchild, child); + wl_surface_commit(grandchild); + client.roundtrip(); + + xdg_toplevel_set_parent(parent, grandchild); + wl_surface_commit(parent); + + try + { + client.roundtrip(); + } + catch (wlcs::ProtocolError const& err) + { + return; + } + FAIL() << "Protocol error not raised"; +} + +using XdgToplevelStableConfigurationTest = wlcs::InProcessServer; + +TEST_F(XdgToplevelStableConfigurationTest, defaults) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + // default values + EXPECT_THAT(state.width, Eq(0)); + EXPECT_THAT(state.height, Eq(0)); + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, window_can_maximize_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + ASSERT_THAT(state.maximized, Eq(false)) << "test could not run as precondition failed"; + + xdg_toplevel_set_maximized(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.width, Gt(0)); + EXPECT_THAT(state.height, Gt(0)); + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, window_can_unmaximize_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + xdg_toplevel_set_maximized(window); + window.dispatch_until_configure(); + + ASSERT_THAT(state.maximized, Eq(true)) << "test could not run as precondition failed"; + + xdg_toplevel_unset_maximized(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, window_can_fullscreen_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + xdg_toplevel_set_fullscreen(window, nullptr); + window.dispatch_until_configure(); + + EXPECT_THAT(state.width, Gt(0)); + EXPECT_THAT(state.height, Gt(0)); + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(true)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, window_can_unfullscreen_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + xdg_toplevel_set_fullscreen(window, nullptr); + window.dispatch_until_configure(); + + ASSERT_THAT(state.fullscreen, Eq(true)) << "test could not run as precondition failed"; + + xdg_toplevel_unset_fullscreen(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, DISABLED_window_stays_maximized_after_fullscreen) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + xdg_toplevel_set_maximized(window); + window.dispatch_until_configure(); + + ASSERT_THAT(state.maximized, Eq(true)) << "test could not run as precondition failed"; + + xdg_toplevel_set_fullscreen(window, nullptr); + window.dispatch_until_configure(); + + ASSERT_THAT(state.fullscreen, Eq(true)) << "test could not run as precondition failed"; + + xdg_toplevel_unset_fullscreen(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.width, Gt(0)); + EXPECT_THAT(state.height, Gt(0)); + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, DISABLED_window_can_maximize_itself_while_fullscreen) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + ASSERT_THAT(state.maximized, Eq(false)) << "test could not run as precondition failed"; + + xdg_toplevel_set_fullscreen(window, nullptr); + window.dispatch_until_configure(); + + ASSERT_THAT(state.fullscreen, Eq(true)) << "test could not run as precondition failed"; + + xdg_toplevel_set_maximized(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.maximized, Eq(true)); + + xdg_toplevel_unset_fullscreen(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.width, Gt(0)); + EXPECT_THAT(state.height, Gt(0)); + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelStableConfigurationTest, activated_state_follows_pointer) +{ + wlcs::Client client{the_server()}; + + ConfigurationWindow window_a{client}; + auto& state_a = window_a.state; + int const a_x = 12, a_y = 15; + the_server().move_surface_to(window_a, a_x, a_y); + + ConfigurationWindow window_b{client}; + auto& state_b = window_b.state; + int const b_x = a_x + window_a.window_width + 27, b_y = 15; + the_server().move_surface_to(window_b, b_x, b_y); + + auto pointer = the_server().create_pointer(); + + pointer.move_to(a_x + 10, a_y + 10); + pointer.left_click(); + client.roundtrip(); + + ASSERT_THAT(state_a.activated, Eq(true)); + ASSERT_THAT(state_b.activated, Eq(false)); + + pointer.move_to(b_x + 10, b_y + 10); + pointer.left_click(); + client.roundtrip(); + + EXPECT_THAT(state_a.activated, Eq(false)); + EXPECT_THAT(state_b.activated, Eq(true)); +} diff --git a/tests/xdg_toplevel_v6.cpp b/tests/xdg_toplevel_v6.cpp new file mode 100644 index 0000000..4595125 --- /dev/null +++ b/tests/xdg_toplevel_v6.cpp @@ -0,0 +1,514 @@ +/* + * Copyright © 2012 Intel Corporation + * Copyright © 2013 Collabora, Ltd. + * Copyright © 2018 Canonical Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "helpers.h" +#include "in_process_server.h" +#include "xdg_shell_v6.h" + +#include + +#include + +using namespace testing; + +namespace +{ +class ConfigurationWindow +{ +public: + int const window_width = 200, window_height = 320; + + ConfigurationWindow(wlcs::Client& client) + : client{client}, + surface{client}, + xdg_surface{client, surface}, + toplevel{xdg_surface} + { + ON_CALL(xdg_surface, configure).WillByDefault([&](auto serial) + { + zxdg_surface_v6_ack_configure(xdg_surface, serial); + surface_configure_count++; + }); + + ON_CALL(toplevel, configure).WillByDefault([this](auto... args) + { + state = wlcs::XdgToplevelV6::State{args...}; + }); + + wl_surface_commit(surface); + client.roundtrip(); + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + dispatch_until_configure(); + } + + ~ConfigurationWindow() + { + client.roundtrip(); + } + + void dispatch_until_configure() + { + client.dispatch_until( + [prev_count = surface_configure_count, ¤t_count = surface_configure_count]() + { + return current_count > prev_count; + }); + } + + operator wlcs::Surface&() {return surface;} + + operator wl_surface*() const {return surface;} + operator zxdg_surface_v6*() const {return xdg_surface;} + operator zxdg_toplevel_v6*() const {return toplevel;} + + wlcs::Client& client; + wlcs::Surface surface; + wlcs::XdgSurfaceV6 xdg_surface; + wlcs::XdgToplevelV6 toplevel; + + int surface_configure_count{0}; + wlcs::XdgToplevelV6::State state{0, 0, nullptr}; +}; +} + +using XdgToplevelV6Test = wlcs::InProcessServer; + +// there *could* be a bug in these tests, but also the window manager may not be behaving properly +// lets take another look when we've updated the window manager +TEST_F(XdgToplevelV6Test, pointer_respects_window_geom_offset) +{ + const int offset_x = 35, offset_y = 12; + const int window_pos_x = 200, window_pos_y = 280; + const int pointer_x = window_pos_x + 20, pointer_y = window_pos_y + 30; + + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + zxdg_surface_v6_set_window_geometry(window.xdg_surface, + offset_x, + offset_y, + window.window_width - offset_x, + window.window_height - offset_y); + wl_surface_commit(window.surface); + the_server().move_surface_to(window.surface, window_pos_x, window_pos_y); + + auto pointer = the_server().create_pointer(); + pointer.move_to(pointer_x, pointer_y); + client.roundtrip(); + + ASSERT_THAT(client.window_under_cursor(), Eq((wl_surface*)window.surface)); + ASSERT_THAT(client.pointer_position(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x), + wl_fixed_from_int(pointer_y - window_pos_y)))) << "set_window_geometry offset was ignored"; + ASSERT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x + offset_x), + wl_fixed_from_int(pointer_y - window_pos_y + offset_y)))); +} + +TEST_F(XdgToplevelV6Test, touch_respects_window_geom_offset) +{ + const int offset_x = 35, offset_y = 12; + const int window_pos_x = 200, window_pos_y = 280; + const int pointer_x = window_pos_x + 20, pointer_y = window_pos_y + 30; + + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + zxdg_surface_v6_set_window_geometry(window.xdg_surface, + offset_x, + offset_y, + window.window_width - offset_x, + window.window_height - offset_y); + wl_surface_commit(window.surface); + the_server().move_surface_to(window.surface, window_pos_x, window_pos_y); + + auto touch = the_server().create_touch(); + touch.down_at(pointer_x, pointer_y); + client.roundtrip(); + + ASSERT_THAT(client.touched_window(), Eq((wl_surface*)window.surface)); + ASSERT_THAT(client.touch_position(), + Ne(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x), + wl_fixed_from_int(pointer_y - window_pos_y)))) << "set_window_geometry offset was ignored"; + ASSERT_THAT(client.touch_position(), + Eq(std::make_pair( + wl_fixed_from_int(pointer_x - window_pos_x + offset_x), + wl_fixed_from_int(pointer_y - window_pos_y + offset_y)))); +} + +// TODO: set_window_geometry window size (something will need to be added to wlcs) + +TEST_F(XdgToplevelV6Test, surface_can_be_moved_interactively) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + int dx = 60, dy = -40; + int end_x = window_x + dx + 20, end_y = window_y + dy + 20; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceV6 xdg_surface{client, surface}; + wlcs::XdgToplevelV6 toplevel{xdg_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + zxdg_toplevel_v6_move(toplevel, client.seat(), last_serial); + client.roundtrip(); + pointer.move_to(start_x + dx, start_x + dy); + pointer.left_button_up(); + client.roundtrip(); + + client.dispatch_until([&](){ + return !button_down; + }); + + pointer.move_to(end_x, end_y); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Eq(static_cast(surface))); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(end_x - window_x - dx), + wl_fixed_from_int(end_y - window_y - dy)))); + + client.roundtrip(); +} + + +TEST_F(XdgToplevelV6Test, pointer_leaves_surface_during_interactive_move) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceV6 xdg_shell_surface{client, surface}; + wlcs::XdgToplevelV6 toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + zxdg_toplevel_v6_move(toplevel, client.seat(), last_serial); + client.dispatch_until([&](){ + return !client.window_under_cursor(); + }); +} + +TEST_F(XdgToplevelV6Test, surface_can_be_resized_interactively) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + int dx = 60, dy = -40; + int end_x = window_x + dx + 20, end_y = window_y + dy + 20; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceV6 xdg_shell_surface{client, surface}; + wlcs::XdgToplevelV6 toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + zxdg_toplevel_v6_resize(toplevel, client.seat(), last_serial, ZXDG_TOPLEVEL_V6_RESIZE_EDGE_TOP_LEFT); + client.roundtrip(); + pointer.move_to(start_x + dx, start_x + dy); + pointer.left_button_up(); + client.roundtrip(); + + pointer.move_to(end_x, end_y); + client.roundtrip(); + + EXPECT_THAT(client.window_under_cursor(), Eq(static_cast(surface))); + EXPECT_THAT(client.pointer_position(), + Eq(std::make_pair( + wl_fixed_from_int(end_x - window_x - dx), + wl_fixed_from_int(end_y - window_y - dy)))); + + client.roundtrip(); +} + +TEST_F(XdgToplevelV6Test, pointer_leaves_surface_during_interactive_resize) +{ + int window_x = 100, window_y = 100; + int window_width = 420, window_height = 390; + int start_x = window_x + 5, start_y = window_y + 5; + + wlcs::Client client{the_server()}; + wlcs::Surface surface{client}; + wlcs::XdgSurfaceV6 xdg_shell_surface{client, surface}; + wlcs::XdgToplevelV6 toplevel{xdg_shell_surface}; + surface.attach_buffer(window_width, window_height); + wl_surface_commit(surface); + client.roundtrip(); + + the_server().move_surface_to(surface, window_x, window_y); + + auto pointer = the_server().create_pointer(); + + bool button_down{false}; + uint32_t last_serial{0}; + + client.add_pointer_button_notification([&](uint32_t serial, uint32_t, bool is_down) -> bool { + last_serial = serial; + button_down = is_down; + return true; + }); + + pointer.move_to(start_x, start_y); + pointer.left_button_down(); + + client.dispatch_until([&](){ + return button_down; + }); + + zxdg_toplevel_v6_resize(toplevel, client.seat(), last_serial, ZXDG_TOPLEVEL_V6_RESIZE_EDGE_TOP_LEFT); + client.dispatch_until([&](){ + return !client.window_under_cursor(); + }); +} + +TEST_F(XdgToplevelV6Test, parent_can_be_set) +{ + const int window_pos_x = 200, window_pos_y = 280; + + wlcs::Client client{the_server()}; + + ConfigurationWindow parent{client}; + the_server().move_surface_to(parent.surface, window_pos_x, window_pos_y); + + ConfigurationWindow child{client}; + the_server().move_surface_to(child.surface, window_pos_x, window_pos_y); + + zxdg_toplevel_v6_set_parent(child, parent); + wl_surface_commit(child.surface); + client.roundtrip(); +} + +TEST_F(XdgToplevelV6Test, null_parent_can_be_set) +{ + const int window_pos_x = 200, window_pos_y = 280; + + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + the_server().move_surface_to(window.surface, window_pos_x, window_pos_y); + + zxdg_toplevel_v6_set_parent(window, nullptr); + wl_surface_commit(window.surface); + client.roundtrip(); +} + +// TODO: interactive resize +// This would probably make sense as a parameterized test, with resizing in all directions +// Like move, resize is not implemented in the current WLCS window manager, and should not be tested until it is + +using XdgToplevelV6ConfigurationTest = wlcs::InProcessServer; + +TEST_F(XdgToplevelV6ConfigurationTest, defaults) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + // default values + EXPECT_THAT(state.width, Eq(0)); + EXPECT_THAT(state.height, Eq(0)); + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelV6ConfigurationTest, window_can_maximize_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + zxdg_toplevel_v6_set_maximized(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.width, Gt(0)); + EXPECT_THAT(state.height, Gt(0)); + EXPECT_THAT(state.maximized, Eq(true)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelV6ConfigurationTest, window_can_unmaximize_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + zxdg_toplevel_v6_set_maximized(window); + window.dispatch_until_configure(); + + ASSERT_THAT(state.maximized, Eq(true)) << "test could not run as precondition failed"; + + zxdg_toplevel_v6_unset_maximized(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelV6ConfigurationTest, window_can_fullscreen_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + zxdg_toplevel_v6_set_fullscreen(window, nullptr); + window.dispatch_until_configure(); + + EXPECT_THAT(state.width, Gt(0)); + EXPECT_THAT(state.height, Gt(0)); + EXPECT_THAT(state.maximized, Eq(false)); // is this right? should it not be maximized, even when fullscreen? + EXPECT_THAT(state.fullscreen, Eq(true)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelV6ConfigurationTest, window_can_unfullscreen_itself) +{ + wlcs::Client client{the_server()}; + ConfigurationWindow window{client}; + auto& state = window.state; + + zxdg_toplevel_v6_set_fullscreen(window, nullptr); + window.dispatch_until_configure(); + + EXPECT_THAT(state.fullscreen, Eq(true)) << "test could not run as precondition failed"; + + zxdg_toplevel_v6_unset_fullscreen(window); + window.dispatch_until_configure(); + + EXPECT_THAT(state.maximized, Eq(false)); + EXPECT_THAT(state.fullscreen, Eq(false)); + EXPECT_THAT(state.resizing, Eq(false)); + EXPECT_THAT(state.activated, Eq(true)); +} + +TEST_F(XdgToplevelV6ConfigurationTest, activated_state_follows_pointer) +{ + wlcs::Client client{the_server()}; + + ConfigurationWindow window_a{client}; + auto& state_a = window_a.state; + int const a_x = 12, a_y = 15; + the_server().move_surface_to(window_a, a_x, a_y); + + ConfigurationWindow window_b{client}; + auto& state_b = window_b.state; + int const b_x = a_x + window_a.window_width + 27, b_y = 15; + the_server().move_surface_to(window_b, b_x, b_y); + + auto pointer = the_server().create_pointer(); + + pointer.move_to(a_x + 10, a_y + 10); + pointer.left_click(); + client.roundtrip(); + + ASSERT_THAT(state_a.activated, Eq(true)); + ASSERT_THAT(state_b.activated, Eq(false)); + + pointer.move_to(b_x + 10, b_y + 10); + pointer.left_click(); + client.roundtrip(); + + EXPECT_THAT(state_a.activated, Eq(false)); + EXPECT_THAT(state_b.activated, Eq(true)); +} diff --git a/tools/make_release_tarball b/tools/make_release_tarball new file mode 100755 index 0000000..92c0c4e --- /dev/null +++ b/tools/make_release_tarball @@ -0,0 +1,64 @@ +#!/bin/bash + +set -ex + +RELEASE_TAG=HEAD + +while [ $# -gt 0 ] +do + if [ "$1" == "--skip-checks" ] + then + skip_checks=true + fi + if [ "$1" == "--release-tag" ] + then + shift + RELEASE_TAG=$1 + fi + shift +done + +if [ ! -e tools/make_release_tarball -o "$1" != "" ] +then + echo "Handy script for creating tarball: Use from the checkout directory" + exit 0 +fi + +set -e + +VERSION=$(grep -E "project\(wlcs VERSION [[:digit:]]+(\.[[:digit:]]+)+\)" CMakeLists.txt | grep -E --only-matching "[[:digit:]]+(.[[:digit:]])+") + +VERSIONED_NAME=wlcs-$VERSION + +SCRATCH_DIR=$(mktemp -d) +BUILD_DIR=$(mktemp -d) +INSTALL_DIR=$(mktemp -d) + +function cleanup() { + ARG=$? + [ -d $SCRATCH_DIR ] && rm -r $SCRATCH_DIR + [ -d $BUILD_DIR ] && rm -r $BUILD_DIR + [ -d $INSTALL_DIR ] && rm -r $INSTALL_DIR + exit $ARG +} + +trap cleanup EXIT + +echo "Generating WLCS tarball…" +git archive --format=tar --prefix=$VERSIONED_NAME/ $RELEASE_TAG | xz -9 > $SCRATCH_DIR/$VERSIONED_NAME.tar.xz + +if [ ! -v skip_checks ] +then + (cd $SCRATCH_DIR; tar xvJf $SCRATCH_DIR/$VERSIONED_NAME.tar.xz) + + echo "Testing that the tarball is buildable" + (cd $BUILD_DIR ; cmake $SCRATCH_DIR/$VERSIONED_NAME ) + make -C $BUILD_DIR -j $(nproc) + + echo "Testing that the tarball is installable" + make -C $BUILD_DIR install DESTDIR=$INSTALL_DIR +fi + +mv $SCRATCH_DIR/$VERSIONED_NAME.tar.xz . +echo "$VERSIONED_NAME.tar.xz successfully created and tested" + diff --git a/tools/ppa-upload.sh b/tools/ppa-upload.sh new file mode 100755 index 0000000..f80df48 --- /dev/null +++ b/tools/ppa-upload.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +set -e + +LP_CREDS="$1" +if [ -z "${LP_CREDS}" ]; then + echo "ERROR: pass the Launchpad credentials file as argument" >&2 + exit 2 +fi + +if [ -z "${RELEASE}" ]; then + echo "ERROR: RELEASE environment variable needs to be set to the" >&2 + echo " target Ubuntu version." >&2 + exit 1 +fi + +GIT_BRANCH=${GITHUB_REF-$( git rev-parse --abbrev-ref HEAD )} +# determine the patch release +if ! [[ "${GIT_BRANCH}" =~ ^(refs/(heads|tags)/)?(main|(release/|v)([0-9\.]+))$ ]]; then + echo "ERROR: This script should only run on main or release tags" >&2 + echo " or branches." >&2 + exit 3 +fi + +source <( python <&2 + exit 2 +fi + +GIT_REVISION=$( git rev-parse --short HEAD ) + +if [[ "${GIT_BRANCH}" =~ ^(refs/(heads|tags)/)?(release/|v)([0-9\.]+)$ ]]; then + # we're on a release branch + TARGET_PPA=ppa:mir-team/rc + WLCS_SERIES=${BASH_REMATCH[4]} + if [[ "$( git describe --tags --exact-match )" =~ ^v([0-9\.]+)$ ]]; then + # this is a final release, use the tag version + WLCS_VERSION=${BASH_REMATCH[1]} + else + # find the last tagged patch version + PATCH_VERSION=$( git describe --abbrev=0 --match "v${WLCS_SERIES}*" \ + 2> /dev/null | sed 's/^v//') + if [ -z "${PATCH_VERSION}" ]; then + # start with patch version 0 + WLCS_VERSION=${WLCS_SERIES}.0 + else + # increment the patch version + WLCS_VERSION=$( echo ${PATCH_VERSION} | perl -pe 's/^((\d+\.)*)(\d+)$/$1.($3+1)/e' ) + fi + + # use the number of commits since main + GIT_COMMITS=$( git rev-list --count origin/main..HEAD ) + WLCS_VERSION=${WLCS_VERSION}~rc${GIT_COMMITS}.git${GIT_REVISION} + fi +else + # look for a release tag within parents 2..n + PARENT=2 + while git rev-parse HEAD^${PARENT} >/dev/null 2>&1; do + if [[ "$( git describe --exact-match HEAD^${PARENT} )" =~ ^v([0-9\.]+)$ ]]; then + # copy packages from ppa:mir-team/rc to ppa:mir-team/release + RELEASE_VERSION=${BASH_REMATCH[1]}-0ubuntu${UBUNTU_VERSION} + echo "Copying wlcs_${RELEASE_VERSION} from ppa:mir-team/rc to ppa:mir-team/release…" + python - ${RELEASE_VERSION} < ../wlcs_${WLCS_VERSION}.orig.tar.xz + +dpkg-buildpackage \ + -I".git" \ + -I"build" \ + -i"^.git|^build" \ + -d -S + +dput ${TARGET_PPA} ../wlcs_${PPA_VERSION}_source.changes diff --git a/wlcs.pc.in b/wlcs.pc.in new file mode 100644 index 0000000..e4bc59f --- /dev/null +++ b/wlcs.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=@PKGCONFIG_BINDIR@ +libexecdir=@PKGCONFIG_LIBEXECDIR@ +includedir=@PKGCONFIG_INCLUDEDIR@ + +test_runner=${libexecdir}/wlcs/wlcs + +Name: wlcs +Description: Wayland Conformance Suite test harness +Version: @PROJECT_VERSION@ +Requires.private: wayland-client +Cflags: -I${includedir} -- 2.30.2