SDL: pen: Emscripten support!

From b975babfa5312b9a11800708f354ff9c4d4288df Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Sun, 11 Aug 2024 17:56:49 -0400
Subject: [PATCH] pen: Emscripten support!

Reference Issue #10516.
---
 src/video/emscripten/SDL_emscriptenevents.c | 168 ++++++++++++++++++++
 1 file changed, 168 insertions(+)

diff --git a/src/video/emscripten/SDL_emscriptenevents.c b/src/video/emscripten/SDL_emscriptenevents.c
index 6cfeeaed2cba2..591ba3cfa7753 100644
--- a/src/video/emscripten/SDL_emscriptenevents.c
+++ b/src/video/emscripten/SDL_emscriptenevents.c
@@ -641,6 +641,166 @@ static const char *Emscripten_HandleBeforeUnload(int eventType, const void *rese
     return ""; /* don't trigger confirmation dialog */
 }
 
+// IF YOU CHANGE THIS STRUCTURE, YOU NEED TO UPDATE THE JAVASCRIPT THAT FILLS IT IN: makePointerEventCStruct, below.
+typedef struct Emscripten_PointerEvent
+{
+    int pointerid;
+    int button;
+    int buttons;
+    float movementX;
+    float movementY;
+    float targetX;
+    float targetY;
+    float pressure;
+    float tangential_pressure;
+    float tiltx;
+    float tilty;
+    float rotation;
+} Emscripten_PointerEvent;
+
+static void Emscripten_UpdatePointerFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event)
+{
+    const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid);
+    if (pen) {
+        /* rescale (in case canvas is being scaled)*/
+        double client_w, client_h;
+        emscripten_get_element_css_size(window_data->canvas_id, &client_w, &client_h);
+        const double xscale = window_data->window->w / client_w;
+        const double yscale = window_data->window->h / client_h;
+
+        const SDL_bool isPointerLocked = window_data->has_pointer_lock;
+        float mx, my;
+        if (isPointerLocked) {
+            mx = (float)(event->movementX * xscale);
+            my = (float)(event->movementY * yscale);
+        } else {
+            mx = (float)(event->targetX * xscale);
+            my = (float)(event->targetY * yscale);
+        }
+
+        SDL_SendPenMotion(0, pen, window_data->window, mx, my);
+
+        if (event->button == 0) {  // pen touch
+            SDL_SendPenTouch(0, pen, window_data->window, (event->buttons & 1) ? SDL_PRESSED : SDL_RELEASED, 0);
+        } else if (event->button == 5) {  // eraser touch...? Not sure if this is right...
+            SDL_SendPenTouch(0, pen, window_data->window, (event->buttons & 32) ? SDL_PRESSED : SDL_RELEASED, 1);
+        } else if (event->button == 1) {
+            SDL_SendPenButton(0, pen, window_data->window, (event->buttons & 4) ? SDL_PRESSED : SDL_RELEASED, 2);
+        } else if (event->button == 2) {
+            SDL_SendPenButton(0, pen, window_data->window, (event->buttons & 2) ? SDL_PRESSED : SDL_RELEASED, 1);
+        }
+
+        SDL_SendPenAxis(0, pen, window_data->window, SDL_PEN_AXIS_PRESSURE, event->pressure);
+        SDL_SendPenAxis(0, pen, window_data->window, SDL_PEN_AXIS_TANGENTIAL_PRESSURE, event->tangential_pressure);
+        SDL_SendPenAxis(0, pen, window_data->window, SDL_PEN_AXIS_XTILT, event->tiltx);
+        SDL_SendPenAxis(0, pen, window_data->window, SDL_PEN_AXIS_YTILT, event->tilty);
+        SDL_SendPenAxis(0, pen, window_data->window, SDL_PEN_AXIS_ROTATION, event->rotation);
+    }
+}
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_data, const Emscripten_PointerEvent *event)
+{
+    // Emscripten offers almost none of this information as specifics, but can without warning offer any of these specific things.
+    SDL_PenInfo peninfo;
+    SDL_zero(peninfo);
+    peninfo.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_TANGENTIAL_PRESSURE | SDL_PEN_CAPABILITY_ERASER;
+    peninfo.max_tilt = 90.0f;
+    peninfo.num_buttons = 2;
+    peninfo.subtype = SDL_PEN_TYPE_PEN;
+    SDL_AddPenDevice(0, NULL, &peninfo, (void *) (size_t) event->pointerid);
+    Emscripten_UpdatePointerFromEvent(window_data, event);
+}
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event)
+{
+    const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid);
+    if (pen) {
+        Emscripten_UpdatePointerFromEvent(window_data, event);  // last data updates?
+        SDL_RemovePenDevice(0, pen);
+    }
+}
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerGeneric(SDL_WindowData *window_data, const Emscripten_PointerEvent *event)
+{
+    Emscripten_UpdatePointerFromEvent(window_data, event);
+}
+
+static void Emscripten_set_pointer_event_callbacks(SDL_WindowData *data)
+{
+    MAIN_THREAD_EM_ASM({
+        var target = document.querySelector(UTF8ToString($1));
+        if (target) {
+            var data = $0;
+
+            if (typeof(Module['SDL3']) === 'undefined') {
+                Module['SDL3'] = {};
+            }
+            var SDL3 = Module['SDL3'];
+
+            var makePointerEventCStruct = function(event) {
+                var ptr = 0;
+                if (event.pointerType == "pen") {
+                    ptr = _malloc($2);
+                    if (ptr != 0) {
+                        var rect = target.getBoundingClientRect();
+                        var idx = ptr >> 2;
+                        HEAP32[idx++] = event.pointerId;
+                        HEAP32[idx++] = (typeof(event.button) !== "undefined") ? event.button : -1;
+                        HEAP32[idx++] = event.buttons;
+                        HEAPF32[idx++] = event.movementX;
+                        HEAPF32[idx++] = event.movementY;
+                        HEAPF32[idx++] = event.clientX - rect.left;
+                        HEAPF32[idx++] = event.clientY - rect.top;
+                        HEAPF32[idx++] = event.pressure;
+                        HEAPF32[idx++] = event.tangentialPressure;
+                        HEAPF32[idx++] = event.tiltX;
+                        HEAPF32[idx++] = event.tiltY;
+                        HEAPF32[idx++] = event.twist;
+                    }
+                }
+                return ptr;
+            };
+
+            SDL3.eventHandlerPointerEnter = function(event) {
+                var d = makePointerEventCStruct(event); if (d != 0) { _Emscripten_HandlePointerEnter(data, d); _free(d); }
+            };
+            target.addEventListener("pointerenter", SDL3.eventHandlerPointerEnter);
+
+            SDL3.eventHandlerPointerLeave = function(event) {
+                var d = makePointerEventCStruct(event); if (d != 0) { _Emscripten_HandlePointerLeave(data, d); _free(d); }
+            };
+            target.addEventListener("pointerleave", SDL3.eventHandlerPointerLeave);
+            target.addEventListener("pointercancel", SDL3.eventHandlerPointerLeave);  /* catch this, just in case. */
+
+            SDL3.eventHandlerPointerGeneric = function(event) {
+                var d = makePointerEventCStruct(event); if (d != 0) { _Emscripten_HandlePointerGeneric(data, d); _free(d); }
+            };
+            target.addEventListener("pointerdown", SDL3.eventHandlerPointerGeneric);
+            target.addEventListener("pointerup", SDL3.eventHandlerPointerGeneric);
+            target.addEventListener("pointermove", SDL3.eventHandlerPointerGeneric);
+        }
+    }, data, data->canvas_id, sizeof (Emscripten_PointerEvent));
+}
+
+static void Emscripten_unset_pointer_event_callbacks(SDL_WindowData *data)
+{
+    MAIN_THREAD_EM_ASM({
+        var target = document.querySelector(UTF8ToString($0));
+        if (target) {
+            var SDL3 = Module['SDL3'];
+            target.removeEventListener("pointerenter", SDL3.eventHandlerPointerEnter);
+            target.removeEventListener("pointerleave", SDL3.eventHandlerPointerLeave);
+            target.removeEventListener("pointercancel", SDL3.eventHandlerPointerLeave);
+            target.removeEventListener("pointerdown", SDL3.eventHandlerPointerGeneric);
+            target.removeEventListener("pointerup", SDL3.eventHandlerPointerGeneric);
+            target.removeEventListener("pointermove", SDL3.eventHandlerPointerGeneric);
+            SDL3.eventHandlerPointerEnter = undefined;
+            SDL3.eventHandlerPointerLeave = undefined;
+            SDL3.eventHandlerPointerGeneric = undefined;
+        }
+    }, data->canvas_id);
+}
+
 void Emscripten_RegisterEventHandlers(SDL_WindowData *data)
 {
     const char *keyElement;
@@ -683,12 +843,20 @@ void Emscripten_RegisterEventHandlers(SDL_WindowData *data)
     emscripten_set_visibilitychange_callback(data, 0, Emscripten_HandleVisibilityChange);
 
     emscripten_set_beforeunload_callback(data, Emscripten_HandleBeforeUnload);
+
+    // !!! FIXME: currently Emscripten doesn't have a Pointer Events functions like emscripten_set_*_callback, but we should use those when they do:
+    // !!! FIXME:  https://github.com/emscripten-core/emscripten/issues/7278#issuecomment-2280024621
+    Emscripten_set_pointer_event_callbacks(data);
 }
 
 void Emscripten_UnregisterEventHandlers(SDL_WindowData *data)
 {
     const char *target;
 
+    // !!! FIXME: currently Emscripten doesn't have a Pointer Events functions like emscripten_set_*_callback, but we should use those when they do:
+    // !!! FIXME:  https://github.com/emscripten-core/emscripten/issues/7278#issuecomment-2280024621
+    Emscripten_unset_pointer_event_callbacks(data);
+
     /* only works due to having one window */
     emscripten_set_mousemove_callback(data->canvas_id, NULL, 0, NULL);