SDL: emscripten: send drag and drop events

From 88926f2b731792a6f50b98b3ed2029352e106ec7 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Fri, 22 Nov 2024 03:25:12 +0100
Subject: [PATCH] emscripten: send drag and drop events

---
 src/video/emscripten/SDL_emscriptenevents.c | 145 ++++++++++++++++++++
 1 file changed, 145 insertions(+)

diff --git a/src/video/emscripten/SDL_emscriptenevents.c b/src/video/emscripten/SDL_emscriptenevents.c
index 18d9117fc3da3..aeffef64cca40 100644
--- a/src/video/emscripten/SDL_emscriptenevents.c
+++ b/src/video/emscripten/SDL_emscriptenevents.c
@@ -26,6 +26,7 @@
 #include <emscripten/html5.h>
 #include <emscripten/dom_pk_codes.h>
 
+#include "../../events/SDL_dropevents_c.h"
 #include "../../events/SDL_events_c.h"
 #include "../../events/SDL_keyboard_c.h"
 #include "../../events/SDL_touch_c.h"
@@ -806,6 +807,144 @@ static void Emscripten_unset_pointer_event_callbacks(SDL_WindowData *data)
     }, data->canvas_id);
 }
 
+// IF YOU CHANGE THIS STRUCTURE, YOU NEED TO UPDATE THE JAVASCRIPT THAT FILLS IT IN: makeDropEventCStruct, below.
+typedef struct Emscripten_DropEvent
+{
+    int x;
+    int y;
+} Emscripten_DropEvent;
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_SendDragEvent(SDL_WindowData *window_data, const Emscripten_DropEvent *event)
+{
+    SDL_SendDropPosition(window_data->window, event->x, event->y);
+}
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_SendDragCompleteEvent(SDL_WindowData *window_data)
+{
+    SDL_SendDropComplete(window_data->window);
+}
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_SendDragTextEvent(SDL_WindowData *window_data, char *text)
+{
+    SDL_SendDropText(window_data->window, text);
+}
+
+EMSCRIPTEN_KEEPALIVE void Emscripten_SendDragFileEvent(SDL_WindowData *window_data, char *filename)
+{
+    SDL_SendDropFile(window_data->window, NULL, filename);
+}
+
+EM_JS_DEPS(dragndrop, "$writeArrayToMemory");
+
+static void Emscripten_set_drag_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 makeDropEventCStruct = function(event) {
+                var ptr = 0;
+                ptr = _SDL_malloc($2);
+                if (ptr != 0) {
+                    var idx = ptr >> 2;
+                    var rect = target.getBoundingClientRect();
+                    HEAP32[idx++] = event.clientX - rect.left;
+                    HEAP32[idx++] = event.clientY - rect.top;
+                }
+                return ptr;
+            };
+
+            SDL3.eventHandlerDropDragover = function(event) {
+                event.preventDefault();
+                var d = makeDropEventCStruct(event); if (d != 0) { _Emscripten_SendDragEvent(data, d); _SDL_free(d); }
+            };
+            target.addEventListener("dragover", SDL3.eventHandlerDropDragover);
+
+            SDL3.drop_count = 0;
+            FS.mkdir("/tmp/filedrop");
+            SDL3.eventHandlerDropDrop = function(event) {
+                event.preventDefault();
+                if (event.dataTransfer.types.includes("text/plain")) {
+                    let plain_text = stringToNewUTF8(event.dataTransfer.getData("text/plain"));
+                    _Emscripten_SendDragTextEvent(data, plain_text);
+                    _free(plain_text);
+                } else if (event.dataTransfer.types.includes("Files")) {
+                    for (let i = 0; i < event.dataTransfer.files.length; i++) {
+                        const file = event.dataTransfer.files.item(i);
+                        const file_reader = new FileReader();
+                        file_reader.readAsArrayBuffer(file);
+                        file_reader.onload = function(event) {
+                            const fs_dropdir = `/tmp/filedrop/${SDL3.drop_count}`;
+                            SDL3.drop_count += 1;
+
+                            const fs_filepath = `${fs_dropdir}/${file.name}`;
+                            const c_fs_filepath = stringToNewUTF8(fs_filepath);
+                            const contents_array8 = new Uint8Array(event.target.result);
+
+                            FS.mkdir(fs_dropdir);
+                            var stream = FS.open(fs_filepath, "w");
+                            FS.write(stream, contents_array8, 0, contents_array8.length, 0);
+                            FS.close(stream);
+
+                            _Emscripten_SendDragFileEvent(data, c_fs_filepath);
+                            _free(c_fs_filepath);
+                            _Emscripten_SendDragCompleteEvent(data);
+                        };
+                    }
+                }
+                _Emscripten_SendDragCompleteEvent(data);
+            };
+            target.addEventListener("drop", SDL3.eventHandlerDropDrop);
+
+            SDL3.eventHandlerDropDragend = function(event) {
+                event.preventDefault();
+                _Emscripten_SendDragCompleteEvent(data);
+            };
+            target.addEventListener("dragend", SDL3.eventHandlerDropDragend);
+            target.addEventListener("dragleave", SDL3.eventHandlerDropDragend);
+        }
+    }, data, data->canvas_id, sizeof (Emscripten_DropEvent));
+}
+
+static void Emscripten_unset_drag_event_callbacks(SDL_WindowData *data)
+{
+    MAIN_THREAD_EM_ASM({
+        var target = document.querySelector(UTF8ToString($0));
+        if (target) {
+            var SDL3 = Module['SDL3'];
+            target.removeEventListener("dragleave", SDL3.eventHandlerDropDragend);
+            target.removeEventListener("dragend", SDL3.eventHandlerDropDragend);
+            target.removeEventListener("drop", SDL3.eventHandlerDropDrop);
+            SDL3.drop_count = undefined;
+
+            function recursive_remove(dirpath) {
+                FS.readdir(dirpath).forEach((filename) => {
+                    const p = `${dirpath}/${filename}`;
+                    const p_s = FS.stat(p);
+                    if (FS.isFile(p_s.mode)) {
+                        FS.unlink(p);
+                    } else if (FS.isDir(p)) {
+                        recursive_remove(p);
+                    }
+                });
+                FS.rmdir(dirpath);
+            }("/tmp/filedrop");
+
+            FS.rmdir("/tmp/filedrop");
+            target.removeEventListener("dragover", SDL3.eventHandlerDropDragover);
+            SDL3.eventHandlerDropDragover = undefined;
+            SDL3.eventHandlerDropDrop = undefined;
+            SDL3.eventHandlerDropDragend = undefined;
+        }
+    }, data->canvas_id);
+}
+
 void Emscripten_RegisterEventHandlers(SDL_WindowData *data)
 {
     const char *keyElement;
@@ -852,12 +991,18 @@ void Emscripten_RegisterEventHandlers(SDL_WindowData *data)
     // !!! 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);
+
+    // !!! FIXME: currently Emscripten doesn't have a Drop Events functions like emscripten_set_*_callback, but we should use those when they do:
+    Emscripten_set_drag_event_callbacks(data);
 }
 
 void Emscripten_UnregisterEventHandlers(SDL_WindowData *data)
 {
     const char *target;
 
+    // !!! FIXME: currently Emscripten doesn't have a Drop Events functions like emscripten_set_*_callback, but we should use those when they do:
+    Emscripten_unset_drag_event_callbacks(data);
+
     // !!! 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);