SDL_gesture: cmake: add interface-only SDL2_gesture::SDL2_gesture target + test

From 9ed3b76db83e6d8ce3aee49979a7da469b65b544 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Sat, 17 Dec 2022 04:31:07 +0100
Subject: [PATCH] cmake: add interface-only SDL2_gesture::SDL2_gesture target +
 test

---
 CMakeLists.txt                   |  48 +++++
 SDL_gesture.h                    |  93 +++++----
 cmake/CommonFindSDL2.cmake       |  22 +++
 cmake/FindPrivateSDL2.cmake      |  45 +++++
 cmake/FindSDL2main.cmake         |  29 +++
 cmake/FindSDL2test.cmake         |  22 +++
 cmake/SDL_gestureConfig.cmake.in |   7 +
 test/CMakeLists.txt              |  53 ++++++
 test/testgesture.c               | 316 +++++++++++++++++++++++++++++++
 9 files changed, 588 insertions(+), 47 deletions(-)
 create mode 100644 CMakeLists.txt
 create mode 100644 cmake/CommonFindSDL2.cmake
 create mode 100644 cmake/FindPrivateSDL2.cmake
 create mode 100644 cmake/FindSDL2main.cmake
 create mode 100644 cmake/FindSDL2test.cmake
 create mode 100644 cmake/SDL_gestureConfig.cmake.in
 create mode 100644 test/CMakeLists.txt
 create mode 100644 test/testgesture.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..c80a46d
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,48 @@
+cmake_minimum_required(VERSION 3.0)
+project(SDL_gesture VERSION 1.0.0 LANGUAGES C)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+option(SDLGESTURE_TESTS "Build SDL_gesture tests" ON)
+
+add_library(SDL_gesture INTERFACE)
+add_library(SDL_gesture::SDL_gesture ALIAS SDL_gesture)
+target_include_directories(SDL_gesture INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
+
+if(SDLGESTURE_TESTS)
+    add_subdirectory(test)
+endif()
+
+if(WINDOWS AND NOT MINGW)
+    set(SDLGESTURE_INSTALL_CMAKEDIR_ROOT_DEFAULT "cmake")
+else()
+    set(SDLGESTURE_INSTALL_CMAKEDIR_ROOT_DEFAULT "${CMAKE_INSTALL_LIBDIR}/cmake")
+endif()
+set(SDLGESTURE_INSTALL_CMAKEDIR_ROOT "${SDLGESTURE_INSTALL_CMAKEDIR_ROOT_DEFAULT}" CACHE STRING "Root folder where to install SDLConfig.cmake related files (SDL_gesture subfolder for MSVC projects)")
+
+if(WINDOWS AND NOT MINGW)
+    set(SDLGESTURE_INSTALL_CMAKEDIR "${SDLGESTURE_INSTALL_CMAKEDIR_ROOT}")
+    set(LICENSES_PREFIX "licenses/SDL_gesture")
+else()
+    set(SDLGESTURE_INSTALL_CMAKEDIR "${SDLGESTURE_INSTALL_CMAKEDIR_ROOT}/SDL_gesture")
+    set(LICENSES_PREFIX "${CMAKE_INSTALL_DATAROOTDIR}/licenses/SDL_gesture")
+endif()
+
+configure_package_config_file(cmake/SDL_gestureConfig.cmake.in SDL_gestureConfig.cmake
+    INSTALL_DESTINATION "${SDLGESTURE_INSTALL_CMAKEDIR}"
+    INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}"
+    PATH_VARS "CMAKE_INSTALL_FULL_INCLUDEDIR"
+    NO_SET_AND_CHECK_MACRO
+)
+write_basic_package_version_file(SDL_gestureConfigVersion.cmake
+    COMPATIBILITY AnyNewerVersion
+)
+
+install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SDL_gesture.h"
+    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+)
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/SDL_gestureConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/SDL_gestureConfigVersion.cmake"
+    DESTINATION "${SDLGESTURE_INSTALL_CMAKEDIR}"
+)
+install(FILES "LICENSE.txt" DESTINATION "${LICENSES_PREFIX}")
diff --git a/SDL_gesture.h b/SDL_gesture.h
index d7d8b32..d3bf44e 100644
--- a/SDL_gesture.h
+++ b/SDL_gesture.h
@@ -24,15 +24,16 @@
 #ifndef INCL_SDL_GESTURE_H
 #define INCL_SDL_GESTURE_H
 
-#if !defined(SDL_VERSION_MAJOR)
+#if !defined(SDL_MAJOR_VERSION)
 #error Please include SDL.h before including this header.
-#elif SDL_VERSION_MAJOR < 2
+#elif SDL_MAJOR_VERSION < 2
 #error This header requires SDL2 or later.
-#elif SDL_VERSION_MAJOR == 2
+#elif SDL_MAJOR_VERSION == 2
 /* building against SDL2? Just use the built-in SDL2 implementation. */
 #define Gesture_Init() (0)
 #define Gesture_Quit()
 #define Gesture_ID SDL_GestureID
+#define Gesture_LoadDollarTemplates SDL_LoadDollarTemplates
 #define Gesture_RecordGesture SDL_RecordGesture
 #define Gesture_SaveAllDollarTemplates SDL_SaveAllDollarTemplates
 #define Gesture_SaveDollarTemplate SDL_SaveDollarTemplate
@@ -40,7 +41,7 @@
 #define GESTURE_DOLLARRECORD SDL_DOLLARRECORD
 #define GESTURE_MULTIGESTURE SDL_MULTIGESTURE
 #define Gesture_MultiGestureEvent SDL_MultiGestureEvent
-#define Gesture_DollarGestureEvent SDL_MultiGestureEvent
+#define Gesture_DollarGestureEvent SDL_DollarGestureEvent
 #else
 
 /* Set up for C function definitions, even when using C++ */
@@ -77,7 +78,7 @@ typedef struct Gesture_DollarGestureEvent
     Uint32 type;
     Uint32 timestamp;
     SDL_TouchID touchId;
-    SDL_GestureID gestureId;
+    Gesture_ID gestureId;
     Uint32 numFingers;
     float error;
     float x;
@@ -137,7 +138,7 @@ extern int SDLCALL Gesture_SaveAllDollarTemplates(SDL_RWops *dst);
  * \sa SDL_LoadDollarTemplates
  * \sa SDL_SaveAllDollarTemplates
  */
-extern int SDLCALL Gesture_SaveDollarTemplate(SDL_GestureID gestureId,SDL_RWops *dst);
+extern int SDLCALL Gesture_SaveDollarTemplate(Gesture_ID gestureId, SDL_RWops *dst);
 
 /**
  * Load Dollar Gesture templates from a file.
@@ -159,9 +160,6 @@ extern int SDLCALL Gesture_LoadDollarTemplates(SDL_TouchID touchId, SDL_RWops *s
 }
 #endif
 
-#endif /* !defined INCL_SDL_GESTURE_H */
-
-
 #if defined(SDL_GESTURE_IMPLEMENTATION)
 
 #define GESTURE_MAX_DOLLAR_PATH_SIZE 1024
@@ -200,7 +198,7 @@ static SDL_bool GestureRecordAll = SDL_FALSE;
 
 static void GestureProcessEvent(const SDL_Event *event);
 
-static int SDLCALL *GestureEventWatch(void *userdata, SDL_Event *event)
+static int SDLCALL GestureEventWatch(void *userdata, SDL_Event *event)
 {
     GestureProcessEvent(event);
     return 1;
@@ -264,7 +262,7 @@ static GestureTouch *GestureGetTouch(const SDL_TouchID touchId)
     return NULL;
 }
 
-int SDL_RecordGesture(SDL_TouchID touchId)
+int Gesture_RecordGesture(SDL_TouchID touchId)
 {
     const int numtouchdevs = SDL_GetNumTouchDevices();
     int i;
@@ -350,7 +348,7 @@ static int GestureSaveTemplate(GestureDollarTemplate *templ, SDL_RWops *dst)
 }
 
 DECLSPEC int SDLCALL
-SDL_SaveAllDollarTemplates(SDL_RWops *dst)
+Gesture_SaveAllDollarTemplates(SDL_RWops *dst)
 {
     int i, j, rtrn = 0;
     for (i = 0; i < GestureNumTouches; i++) {
@@ -363,7 +361,7 @@ SDL_SaveAllDollarTemplates(SDL_RWops *dst)
 }
 
 DECLSPEC int SDLCALL
-SDL_SaveDollarTemplate(SDL_GestureID gestureId, SDL_RWops *dst)
+Gesture_SaveDollarTemplate(Gesture_ID gestureId, SDL_RWops *dst)
 {
     int i, j;
     for (i = 0; i < GestureNumTouches; i++) {
@@ -422,7 +420,7 @@ static int GestureAddDollar(GestureTouch *inTouch, SDL_FPoint *path)
 }
 
 DECLSPEC int SDLCALL
-SDL_LoadDollarTemplates(SDL_TouchID touchId, SDL_RWops *src)
+Gesture_LoadDollarTemplates(SDL_TouchID touchId, SDL_RWops *src)
 {
     int i, loaded = 0;
     GestureTouch *touch = NULL;
@@ -442,7 +440,7 @@ SDL_LoadDollarTemplates(SDL_TouchID touchId, SDL_RWops *src)
 
     while (1) {
         GestureDollarTemplate templ;
-        const Sint64 bytes = sizeof(templ->path[0]) * GESTURE_DOLLARNPOINTS;
+        const Sint64 bytes = sizeof(templ.path[0]) * GESTURE_DOLLARNPOINTS;
 
         if (SDL_RWread(src, templ.path, bytes) < bytes) {
             if (loaded == 0) {
@@ -653,46 +651,46 @@ static float GestureDollarRecognize(const GestureDollarPath *path, int *bestTemp
 
 static void GestureSendMulti(GestureTouch *touch, float dTheta, float dDist)
 {
-    if (SDL_GetEventState(SDL_MULTIGESTURE) == SDL_ENABLE) {
-        SDL_Event event;
-        event.type = GESTURE_MULTIGESTURE;
-        event.common.timestamp = 0;
-        event.mgesture.touchId = touch->touchId;
-        event.mgesture.x = touch->centroid.x;
-        event.mgesture.y = touch->centroid.y;
-        event.mgesture.dTheta = dTheta;
-        event.mgesture.dDist = dDist;
-        event.mgesture.numFingers = touch->numDownFingers;
-        SDL_PushEvent(&event);
+    if (SDL_GetEventState(GESTURE_MULTIGESTURE) == SDL_ENABLE) {
+        Gesture_MultiGestureEvent mgesture;
+        mgesture.type = GESTURE_MULTIGESTURE;
+        mgesture.timestamp = 0;
+        mgesture.touchId = touch->touchId;
+        mgesture.x = touch->centroid.x;
+        mgesture.y = touch->centroid.y;
+        mgesture.dTheta = dTheta;
+        mgesture.dDist = dDist;
+        mgesture.numFingers = touch->numDownFingers;
+        SDL_PushEvent((SDL_Event*)&mgesture);
     }
 }
 
-static void GestureSendDollar(GestureTouch *touch, SDL_GestureID gestureId, float error)
+static void GestureSendDollar(GestureTouch *touch, Gesture_ID gestureId, float error)
 {
-    if (SDL_GetEventState(SDL_DOLLARGESTURE) == SDL_ENABLE) {
-        SDL_Event event;
-        event.type = GESTURE_DOLLARGESTURE;
-        event.common.timestamp = 0;
-        event.dgesture.touchId = touch->touchId;
-        event.dgesture.x = touch->centroid.x;
-        event.dgesture.y = touch->centroid.y;
-        event.dgesture.gestureId = gestureId;
-        event.dgesture.error = error;
+    if (SDL_GetEventState(GESTURE_DOLLARGESTURE) == SDL_ENABLE) {
+        Gesture_DollarGestureEvent dgesture;
+        dgesture.type = GESTURE_DOLLARGESTURE;
+        dgesture.timestamp = 0;
+        dgesture.touchId = touch->touchId;
+        dgesture.x = touch->centroid.x;
+        dgesture.y = touch->centroid.y;
+        dgesture.gestureId = gestureId;
+        dgesture.error = error;
         /* A finger came up to trigger this event. */
-        event.dgesture.numFingers = touch->numDownFingers + 1;
-        SDL_PushEvent(&event);
+        dgesture.numFingers = touch->numDownFingers + 1;
+        SDL_PushEvent((SDL_Event*)&dgesture);
     }
 }
 
-static void GestureSendDollarRecord(GestureTouch *touch, SDL_GestureID gestureId)
+static void GestureSendDollarRecord(GestureTouch *touch, Gesture_ID gestureId)
 {
-    if (SDL_GetEventState(SDL_DOLLARRECORD) == SDL_ENABLE) {
-        SDL_Event event;
-        event.type = GESTURE_DOLLARRECORD;
-        event.common.timestamp = 0;
-        event.dgesture.touchId = touch->touchId;
-        event.dgesture.gestureId = gestureId;
-        SDL_PushEvent(&event);
+    if (SDL_GetEventState(GESTURE_DOLLARRECORD) == SDL_ENABLE) {
+        Gesture_DollarGestureEvent dgesture;
+        dgesture.type = GESTURE_DOLLARRECORD;
+        dgesture.timestamp = 0;
+        dgesture.touchId = touch->touchId;
+        dgesture.gestureId = gestureId;
+        SDL_PushEvent((SDL_Event*)&dgesture);
     }
 }
 
@@ -850,6 +848,7 @@ static void GestureProcessEvent(const SDL_Event *event)
 }
 
 #endif  /* defined(SDL_GESTURE_IMPLEMENTATION) */
-#endif  /* SDL vesion > 2 */
+#endif  /* SDL version > 2 */
+#endif /* INCL_SDL_GESTURE_H */
 
 /* vi: set sts=4 ts=4 sw=4 expandtab: */
diff --git a/cmake/CommonFindSDL2.cmake b/cmake/CommonFindSDL2.cmake
new file mode 100644
index 0000000..59939dc
--- /dev/null
+++ b/cmake/CommonFindSDL2.cmake
@@ -0,0 +1,22 @@
+# Common variables for FindSDL2*.cmake modules
+
+set(_inc_suffixes include)
+set(_lib_suffixes)
+if(MSVC)
+    if(CMAKE_SIZEOF_VOID_P EQUAL 4)
+        list(APPEND _lib_suffixes "lib/x86")
+    endif()
+    if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+        list(APPEND _lib_suffixes "lib/x64")
+    endif()
+endif()
+if(MINGW)
+    if(CMAKE_SIZEOF_VOID_P EQUAL 4)
+        list(APPEND _lib_suffixes "i686-w64-mingw32/lib")
+        list(APPEND _inc_suffixes "i686-w64-mingw32/include")
+    endif()
+    if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+        list(APPEND _lib_suffixes "x86_64-w64-mingw32/lib")
+        list(APPEND _inc_suffixes "x86_64-w64-mingw32/include")
+    endif()
+endif()
diff --git a/cmake/FindPrivateSDL2.cmake b/cmake/FindPrivateSDL2.cmake
new file mode 100644
index 0000000..ba0732b
--- /dev/null
+++ b/cmake/FindPrivateSDL2.cmake
@@ -0,0 +1,45 @@
+include(FindPackageHandleStandardArgs)
+include("${CMAKE_CURRENT_LIST_DIR}/CommonFindSDL2.cmake")
+
+find_library(SDL2_LIBRARY
+    NAMES SDL2
+    HINTS ${SDL2_DIR} ENV SDL2_DIR
+    PATH_SUFFIXES ${_lib_suffixes}
+)
+
+find_path(SDL2_INCLUDE_DIR
+    NAMES SDL_haptic.h
+    PATH_SUFFIXES SDL2
+    HINTS ${SDL2_DIR} ENV SDL2_DIR
+    PATH_SUFFIXES ${_inc_suffixes}
+)
+
+set(SDL2_VERSION)
+if(SDL2_INCLUDE_DIR)
+    file(READ "${SDL2_INCLUDE_DIR}/SDL_version.h" _sdl_version_h)
+    string(REGEX MATCH "#define[ \t]+SDL_MAJOR_VERSION[ \t]+([0-9]+)" _sdl2_major_re "${_sdl_version_h}")
+    set(_sdl2_major "${CMAKE_MATCH_1}")
+    string(REGEX MATCH "#define[ \t]+SDL_MINOR_VERSION[ \t]+([0-9]+)" _sdl2_minor_re "${_sdl_version_h}")
+    set(_sdl2_minor "${CMAKE_MATCH_1}")
+    string(REGEX MATCH "#define[ \t]+SDL_PATCHLEVEL[ \t]+([0-9]+)" _sdl2_patch_re "${_sdl_version_h}")
+    set(_sdl2_patch "${CMAKE_MATCH_1}")
+    if(_sdl2_major_re AND _sdl2_minor_re AND _sdl2_patch_re)
+        set(SDL2_VERSION "${_sdl2_major}.${_sdl2_minor}.${_sdl2_patch}")
+    endif()
+endif()
+
+find_package_handle_standard_args(PrivateSDL2
+    REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR
+    VERSION_VAR SDL2_VERSION
+)
+
+if(PrivateSDL2_FOUND)
+    if(NOT TARGET PrivateSDL2::PrivateSDL2)
+        add_library(PrivateSDL2::PrivateSDL2 UNKNOWN IMPORTED)
+        set_target_properties(PrivateSDL2::PrivateSDL2 PROPERTIES
+            INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INCLUDE_DIR}"
+            IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+            IMPORTED_LOCATION "${SDL2_LIBRARY}"
+        )
+    endif()
+endif()
diff --git a/cmake/FindSDL2main.cmake b/cmake/FindSDL2main.cmake
new file mode 100644
index 0000000..7c67ba1
--- /dev/null
+++ b/cmake/FindSDL2main.cmake
@@ -0,0 +1,29 @@
+include(FindPackageHandleStandardArgs)
+include("${CMAKE_CURRENT_LIST_DIR}/CommonFindSDL2.cmake")
+
+find_library(SDL2_MAIN_LIBRARY
+    NAMES SDL2main SDL2_main
+    HINTS ${SDL2_DIR} ENV SDL2_DIR
+    PATH_SUFFIXES ${_lib_suffixes}
+)
+
+find_package_handle_standard_args(SDL2main
+    REQUIRED_VARS SDL2_MAIN_LIBRARY
+)
+
+if(SDL2main_FOUND)
+    if(NOT TARGET SDL2::SDL2main)
+        add_library(SDL2::SDL2main UNKNOWN IMPORTED)
+        set_target_properties(SDL2::SDL2main PROPERTIES
+            IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+            IMPORTED_LOCATION "${SDL2_MAIN_LIBRARY}"
+        )
+        if(MINGW)
+            if(CMAKE_SIZEOF_VOID_P EQUAL 4)
+                set_target_properties(SDL2::SDL2main PROPERTIES INTERFACE_LINK_LIBRARIES "$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>:-Wl,--undefined=_WinMain@16>")
+            else()
+                set_target_properties(SDL2::SDL2main PROPERTIES INTERFACE_LINK_LIBRARIES "$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>:-Wl,--undefined=WinMain>")
+            endif()
+        endif()
+    endif()
+endif()
diff --git a/cmake/FindSDL2test.cmake b/cmake/FindSDL2test.cmake
new file mode 100644
index 0000000..56613b8
--- /dev/null
+++ b/cmake/FindSDL2test.cmake
@@ -0,0 +1,22 @@
+include(FindPackageHandleStandardArgs)
+include("${CMAKE_CURRENT_LIST_DIR}/CommonFindSDL2.cmake")
+
+find_library(SDL2_TEST_LIBRARY
+    NAMES SDL2test SDL2_test
+    HINTS ${SDL2_DIR} ENV SDL2_DIR
+    PATH_SUFFIXES ${_lib_suffixes}
+)
+
+find_package_handle_standard_args(SDL2test
+    REQUIRED_VARS SDL2_TEST_LIBRARY
+)
+
+if(SDL2test_FOUND)
+    if(NOT TARGET SDL2::SDL2test)
+        add_library(SDL2::SDL2test UNKNOWN IMPORTED)
+        set_target_properties(SDL2::SDL2test PROPERTIES
+            IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+            IMPORTED_LOCATION "${SDL2_TEST_LIBRARY}"
+        )
+    endif()
+endif()
diff --git a/cmake/SDL_gestureConfig.cmake.in b/cmake/SDL_gestureConfig.cmake.in
new file mode 100644
index 0000000..c1ab70a
--- /dev/null
+++ b/cmake/SDL_gestureConfig.cmake.in
@@ -0,0 +1,7 @@
+@PACKAGE_INIT@
+
+if(NOT TARGET SDL_gesture::SDL_gesture)
+    add_library(SDL_gesture INTERFACE)
+    target_include_directories(SDL_gesture INTERFACE "@PACKAGE_CMAKE_INSTALL_FULL_INCLUDEDIR@")
+    add_library(SDL_gesture::SDL_gesture ALIAS SDL_gesture)
+endif()
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..8b5b310
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,53 @@
+cmake_minimum_required(VERSION 3.0)
+project(SDL_gesture_tests C)
+
+option(SDLGESTURE_TESTS_STATIC "Link to static SDLx library" OFF)
+
+if(SDLGESTURE_TESTS_STATIC)
+    set(SDL2_COMPONENTS "SDL2-static")
+    set(SDL2_TARGET "SDL2::SDL2-static")
+
+    set(SDL3_COMPONENTS "SDL3-static")
+    set(SDL3_TARGET "SDL3::SDL3-static")
+else()
+    set(SDL2_COMPONENTS "SDL2")
+    set(SDL2_TARGET "SDL2::SDL2")
+
+    set(SDL3_COMPONENTS "SDL3")
+    set(SDL3_TARGET "SDL3::SDL3")
+endif()
+
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../cmake")
+
+find_package(SDL2 CONFIG COMPONENTS ${SDL2_COMPONENTS} SDL2main SDL2test)
+if(NOT SDL2_FOUND OR NOT TARGET SDL2::SDL2)
+    find_package(PrivateSDL2 MODULE REQUIRED)
+    set(SDL2_TARGET "PrivateSDL2::PrivateSDL2")
+endif()
+if(NOT TARGET SDL2::SDL2main)
+    find_package(SDL2main MODULE QUIET)
+    if(NOT SDL2main_FOUND)
+        add_library(SDL2::SDL2main INTERFACE)
+    endif()
+endif()
+if(NOT TARGET SDL2::SDL2test)
+    find_package(SDL2test MODULE REQUIRED)
+endif()
+
+find_package(SDL3 QUIET CONFIG COMPONENTS ${SDL3_COMPONENTS} SDL3_test)
+if(NOT SDL3_FOUND)
+    message(STATUS "SDL3 not found -> not testing SDL_gesture with SDL3")
+endif()
+
+if(NOT TARGET SDL_gesture::SDL_gesture)
+    find_package(SDL_gesture REQUIRED)
+endif()
+
+add_executable(testgesture_sdl2 testgesture.c)
+target_link_libraries(testgesture_sdl2 PRIVATE SDL2::SDL2test SDL_gesture::SDL_gesture SDL2::SDL2main  ${SDL2_TARGET})
+
+if(TARGET ${SDL3_TARGET})
+    add_executable(testgesture_sdl3 testgesture.c)
+    target_compile_definitions(testgesture_sdl3 PRIVATE TESTGESTURE_SDL3)
+    target_link_libraries(testgesture_sdl3 PRIVATE SDL3::SDL3_test SDL_gesture::SDL_gesture ${SDL3_TARGET})
+endif()
diff --git a/test/testgesture.c b/test/testgesture.c
new file mode 100644
index 0000000..1584ee3
--- /dev/null
+++ b/test/testgesture.c
@@ -0,0 +1,316 @@
+/*
+  Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+
+/*  Usage:
+ *  Spacebar to begin recording a gesture on all touches.
+ *  s to save all touches into "./gestureSave"
+ *  l to load all touches from "./gestureSave"
+ */
+
+#if defined(TESTGESTURE_SDL3)
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_test.h>
+#else
+#include "SDL.h"
+#include "SDL_test.h"
+#endif
+
+#define SDL_GESTURE_IMPLEMENTATION 1
+#include "SDL_gesture.h"
+
+#ifdef __EMSCRIPTEN__
+#include <emscripten/emscripten.h>
+#endif
+
+
+#define WIDTH  640
+#define HEIGHT 480
+#define BPP    4
+
+/* MUST BE A POWER OF 2! */
+#define EVENT_BUF_SIZE 256
+
+#define VERBOSE 0
+
+static SDLTest_CommonState *state;
+static SDL_Event events[EVENT_BUF_SIZE];
+static int eventWrite;
+static int colors[7] = { 0xFF, 0xFF00, 0xFF0000, 0xFFFF00, 0x00FFFF, 0xFF00FF, 0xFFFFFF };
+static int quitting = 0;
+
+typedef struct
+{
+    float x, y;
+} Point;
+
+typedef struct
+{
+    float ang, r;
+    Point p;
+} Knob;
+
+static Knob knob = { 0.0f, 0.1f, { 0.0f, 0.0f } };
+
+static void
+setpix(SDL_Surface *screen, float _x, float _y, unsigned int col)
+{
+    Uint32 *pixmem32;
+    Uint32 colour;
+    Uint8 r, g, b;
+    const int x = (int)_x;
+    const int y = (int)_y;
+    float a;
+
+    if ((x < 0) || (x >= screen->w) || (y < 0) || (y >= screen->h)) {
+        return;
+    }
+
+    pixmem32 = (Uint32 *)screen->pixels + y * screen->pitch / BPP + x;
+
+    SDL_memcpy(&colour, pixmem32, screen->format->BytesPerPixel);
+
+    SDL_GetRGB(colour, screen->format, &r, &g, &b);
+
+    /* r = 0;g = 0; b = 0; */
+    a = (float)((col >> 24) & 0xFF);
+    if (a == 0) {
+        a = 0xFF; /* Hack, to make things easier. */
+    }
+
+    a = (a == 0.0f) ? 1 : (a / 255.0f);
+    r = (Uint8)(r * (1 - a) + ((col >> 16) & 0xFF) * a);
+    g = (Uint8)(g * (1 - a) + ((col >> 8) & 0xFF) * a);
+    b = (Uint8)(b * (1 - a) + ((col >> 0) & 0xFF) * a);
+    colour = SDL_MapRGB(screen->format, r, g, b);
+
+    *pixmem32 = colour;
+}
+
+#if 0 /* unused */
+static void
+drawLine(SDL_Surface *screen, float x0, float y0, float x1, float y1, unsigned int col)
+{
+    float t;
+    for (t = 0; t < 1; t += (float) (1.0f / SDL_max(SDL_fabs(x0 - x1), SDL_fabs(y0 - y1)))) {
+        setpix(screen, x1 + t * (x0 - x1), y1 + t * (y0 - y1), col);
+    }
+}
+#endif
+
+static void
+drawCircle(SDL_Surface *screen, float x, float y, float r, unsigned int c)
+{
+    float tx, ty, xr;
+    for (ty = (float)-SDL_fabs(r); ty <= (float)SDL_fabs((int)r); ty++) {
+        xr = (float)SDL_sqrt(r * r - ty * ty);
+        if (r > 0) { /* r > 0 ==> filled circle */
+            for (tx = -xr + 0.5f; tx <= xr - 0.5f; tx++) {
+                setpix(screen, x + tx, y + ty, c);
+            }
+        } else {
+            setpix(screen, x - xr + 0.5f, y + ty, c);
+            setpix(screen, x + xr - 0.5f, y + ty, c);
+        }
+    }
+}
+
+static void
+drawKnob(SDL_Surface *screen, const Knob *k)
+{
+    drawCircle(screen, k->p.x * screen->w, k->p.y * screen->h, k->r * screen->w, 0xFFFFFF);
+    drawCircle(screen, (k->p.x + k->r / 2 * SDL_cosf(k->ang)) * screen->w,
+               (k->p.y + k->r / 2 * SDL_sinf(k->ang)) * screen->h, k->r / 4 * screen->w, 0);
+}
+
+static void
+DrawScreen(SDL_Window *window)
+{
+    SDL_Surface *screen = SDL_GetWindowSurface(window);
+    int i;
+
+    if (screen == NULL) {
+        return;
+    }
+
+    SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 75, 75, 75));
+
+    /* draw Touch History */
+    for (i = eventWrite; i < eventWrite + EVENT_BUF_SIZE; ++i) {
+        const SDL_Event *event = &events[i & (EVENT_BUF_SIZE - 1)];
+        const float age = (float)(i - eventWrite) / EVENT_BUF_SIZE;
+        float x, y;
+        unsigned int c, col;
+
+        if ((event->type == SDL_FINGERMOTION) ||
+            (event->type == SDL_FINGERDOWN) ||
+            (event->type == SDL_FINGERUP)) {
+            x = event->tfinger.x;
+            y = event->tfinger.y;
+
+            /* draw the touch: */
+            c = colors[event->tfinger.fingerId % 7];
+            col = ((unsigned int)(c * (0.1f + 0.85f))) | (unsigned int)(0xFF * age) << 24;
+
+            if (event->type == SDL_FINGERMOTION) {
+                drawCircle(screen, x * screen->w, y * screen->h, 5, col);
+            } else if (event->type == SDL_FINGERDOWN) {
+                drawCircle(screen, x * screen->w, y * screen->h, -10, col);
+            }
+        }
+    }
+
+    if (knob.p.x > 0) {
+        drawKnob(screen, &knob);
+    }
+
+    SDL_UpdateWindowSurface(window);
+}
+
+static void
+loop(void)
+{
+    union {
+        SDL_Event event;
+        Gesture_MultiGestureEvent mgesture;
+        Gesture_DollarGestureEvent dgesture;
+    } u_event;
+    SDL_RWops *stream;
+    int i;
+
+    while (SDL_PollEvent(&u_event.event)) {
+        SDLTest_CommonEvent(state, &u_event.event, &quitting);
+
+        /* Record _all_ events */
+        events[eventWrite & (EVENT_BUF_SIZE - 1)] = u_event.event;
+        eventWrite++;
+
+        switch (u_event.event.type) {
+        case SDL_KEYDOWN:
+            switch (u_event.event.key.keysym.sym) {
+            case SDLK_i:
+            {
+                for (i = 0; i < SDL_GetNumTouchDevices(); ++i) {
+                    const SDL_TouchID id = SDL_GetTouchDevice(i);
+#if SDL_VERSION_ATLEAST(2, 22, 0)
+                    const char *name = SDL_GetTouchName(i);
+#else
+                    const char *name = "<unknown>";
+#endif
+                    SDL_Log("Fingers Down on device %" SDL_PRIs64 " (%s): %d", id, name, SDL_GetNumTouchFingers(id));
+                }
+                break;
+            }
+
+            case SDLK_SPACE:
+                Gesture_RecordGesture(-1);
+                break;
+
+            case SDLK_s:
+                stream = SDL_RWFromFile("gestureSave", "w");
+                SDL_Log("Wrote %i templates", Gesture_SaveAllDollarTemplates(stream));
+                SDL_RWclose(stream);
+                break;
+
+            case SDLK_l:
+                stream = SDL_RWFromFile("gestureSave", "r");
+                SDL_Log("Loaded: %i", Gesture_LoadDollarTemplates(-1, stream));
+                SDL_RWclose(stream);
+                break;
+            }
+            break;
+
+#if VERBOSE
+        case SDL_FINGERMOTION:
+            SDL_Log("Finger: %" SDL_PRIs64 ", x: %f, y: %f", u_event.event.tfinger.fingerId,
+                    u_event.event.tfinger.x, u_event.event.tfinger.y);
+            break;
+
+        case SDL_FINGERDOWN:
+            SDL_Log("Finger: %" SDL_PRIs64 " down - x: %f, y: %f",
+                    u_event.event.tfinger.fingerId, u_event.event.tfinger.x, u_event.event.tfinger.y);
+            break;
+
+        case SDL_FINGERUP:
+            SDL_Log("Finger: %" SDL_PRIs64 " up - x: %f, y: %f",
+                    u_event.event.tfinger.fingerId, u_event.event.tfinger.x, u_event.event.tfinger.y);
+            break;
+#endif
+
+        case GESTURE_MULTIGESTURE:
+#if VERBOSE
+            SDL_Log("Multi Gesture: x = %f, y = %f, dAng = %f, dR = %f",
+                    u_event.mgesture.x, u_event.mgesture.y,
+                    u_event.mgesture.dTheta, u_event.mgesture.dDist);
+            SDL_Log("MG: numDownTouch = %i", u_event.mgesture.numFingers);
+#endif
+
+            knob.p.x = u_event.mgesture.x;
+            knob.p.y = u_event.mgesture.y;
+            knob.ang += u_event.mgesture.dTheta;
+            knob.r += u_event.mgesture.dDist;
+            break;
+
+        case GESTURE_DOLLARGESTURE:
+            SDL_Log("Gesture %" SDL_PRIs64 " performed, error: %f",
+                    u_event.dgesture.gestureId, u_event.dgesture.error);
+            break;
+
+        case GESTURE_DOLLARRECORD:
+            SDL_Log("Recorded gesture: %" SDL_PRIs64 "", u_event.dgesture.gestureId);
+            break;
+        }
+    }
+
+    for (i = 0; i < state->num_windows; ++i) {
+        if (state->windows[i]) {
+            DrawScreen(state->windows[i]);
+        }
+    }
+
+#ifdef __EMSCRIPTEN__
+    if (quitting) {
+        emscripten_cancel_main_loop();
+    }
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+    state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO);
+    if (state == NULL) {
+        return 1;
+    }
+    Gesture_Init();
+
+    state->window_title = "Gesture Test";
+    state->window_w = WIDTH;
+    state->window_h = HEIGHT;
+    state->skip_renderer = SDL_TRUE;
+
+    if (!SDLTest_CommonDefaultArgs(state, argc, argv) || !SDLTest_CommonInit(state)) {
+        Gesture_Quit();
+        SDLTest_CommonQuit(state);
+        return 1;
+    }
+
+#ifdef __EMSCRIPTEN__
+    emscripten_set_main_loop(loop, 0, 1);
+#else
+    while (!quitting) {
+        loop();
+    }
+#endif
+    Gesture_Quit();
+    SDLTest_CommonQuit(state);
+    return 0;
+}
+