SDL: Add Drag and drop position, for x11, wayland and MACOSX

From a946a344524ed4934acf180255337ea96b362351 Mon Sep 17 00:00:00 2001
From: Sylvain <[EMAIL REDACTED]>
Date: Mon, 6 Mar 2023 11:16:18 +0100
Subject: [PATCH] Add Drag and drop position, for x11, wayland and MACOSX

---
 include/SDL3/SDL_events.h             |  7 +++--
 src/events/SDL_dropevents.c           | 26 +++++++++++++++---
 src/events/SDL_dropevents_c.h         |  1 +
 src/video/cocoa/SDL_cocoawindow.m     | 16 +++++++++++
 src/video/wayland/SDL_waylandevents.c | 26 ++++++++++++++++++
 src/video/x11/SDL_x11events.c         | 12 +++++++++
 test/testdropfile.c                   | 39 ++++++++++++++++++++-------
 7 files changed, 112 insertions(+), 15 deletions(-)

diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 8e35e283efc0..482fc6d3cc51 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -172,6 +172,7 @@ typedef enum
     SDL_EVENT_DROP_TEXT,                 /**< text/plain drag-and-drop event */
     SDL_EVENT_DROP_BEGIN,                /**< A new set of drops is beginning (NULL filename) */
     SDL_EVENT_DROP_COMPLETE,             /**< Current set of drops is now complete (NULL filename) */
+    SDL_EVENT_DROP_POSITION,             /**< Position while moving over the window */
 
     /* Audio hotplug events */
     SDL_EVENT_AUDIO_DEVICE_ADDED = 0x1100, /**< A new audio device is available */
@@ -515,10 +516,12 @@ typedef struct SDL_TouchFingerEvent
  */
 typedef struct SDL_DropEvent
 {
-    Uint32 type;        /**< ::SDL_EVENT_DROP_BEGIN or ::SDL_EVENT_DROP_FILE or ::SDL_EVENT_DROP_TEXT or ::SDL_EVENT_DROP_COMPLETE */
+    Uint32 type;        /**< ::SDL_EVENT_DROP_BEGIN or ::SDL_EVENT_DROP_FILE or ::SDL_EVENT_DROP_TEXT or ::SDL_EVENT_DROP_COMPLETE or ::SDL_EVENT_DROP_POSITION */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
     char *file;         /**< The file name, which should be freed with SDL_free(), is NULL on begin/complete */
-    SDL_WindowID windowID;/**< The window that was dropped on, if any */
+    SDL_WindowID windowID;    /**< The window that was dropped on, if any */
+    float x;            /**< X coordinate, relative to window (not on begin) */
+    float y;            /**< Y coordinate, relative to window (not on begin) */
 } SDL_DropEvent;
 
 
diff --git a/src/events/SDL_dropevents.c b/src/events/SDL_dropevents.c
index bb303625ba85..5aa4a99236e4 100644
--- a/src/events/SDL_dropevents.c
+++ b/src/events/SDL_dropevents.c
@@ -27,9 +27,11 @@
 
 #include "../video/SDL_sysvideo.h" /* for SDL_Window internals. */
 
-static int SDL_SendDrop(SDL_Window *window, const SDL_EventType evtype, const char *data)
+static int SDL_SendDrop(SDL_Window *window, const SDL_EventType evtype, const char *data, float x, float y)
 {
     static SDL_bool app_is_dropping = SDL_FALSE;
+    static float last_drop_x = 0;
+    static float last_drop_y = 0;
     int posted = 0;
 
     /* Post the event, if desired */
@@ -58,6 +60,13 @@ static int SDL_SendDrop(SDL_Window *window, const SDL_EventType evtype, const ch
         event.common.timestamp = 0;
         event.drop.file = data ? SDL_strdup(data) : NULL;
         event.drop.windowID = window ? window->id : 0;
+
+        if (evtype == SDL_EVENT_DROP_POSITION) {
+            last_drop_x = x;
+            last_drop_y = y;
+        }
+        event.drop.x = last_drop_x;
+        event.drop.y = last_drop_y;
         posted = (SDL_PushEvent(&event) > 0);
 
         if (posted && (evtype == SDL_EVENT_DROP_COMPLETE)) {
@@ -66,6 +75,9 @@ static int SDL_SendDrop(SDL_Window *window, const SDL_EventType evtype, const ch
             } else {
                 app_is_dropping = SDL_FALSE;
             }
+
+            last_drop_x = 0;
+            last_drop_y = 0;
         }
     }
     return posted;
@@ -73,15 +85,21 @@ static int SDL_SendDrop(SDL_Window *window, const SDL_EventType evtype, const ch
 
 int SDL_SendDropFile(SDL_Window *window, const char *file)
 {
-    return SDL_SendDrop(window, SDL_EVENT_DROP_FILE, file);
+    return SDL_SendDrop(window, SDL_EVENT_DROP_FILE, file, 0, 0);
+}
+
+int SDL_SendDropPosition(SDL_Window *window, const char *file, float x, float y)
+{
+    /* Don't send 'file' since this is an malloc per position, which may be forgotten to be freed */
+    return SDL_SendDrop(window, SDL_EVENT_DROP_POSITION, NULL, x, y);
 }
 
 int SDL_SendDropText(SDL_Window *window, const char *text)
 {
-    return SDL_SendDrop(window, SDL_EVENT_DROP_TEXT, text);
+    return SDL_SendDrop(window, SDL_EVENT_DROP_TEXT, text, 0, 0);
 }
 
 int SDL_SendDropComplete(SDL_Window *window)
 {
-    return SDL_SendDrop(window, SDL_EVENT_DROP_COMPLETE, NULL);
+    return SDL_SendDrop(window, SDL_EVENT_DROP_COMPLETE, NULL, 0, 0);
 }
diff --git a/src/events/SDL_dropevents_c.h b/src/events/SDL_dropevents_c.h
index d10bbd334711..9a485cd0b2ec 100644
--- a/src/events/SDL_dropevents_c.h
+++ b/src/events/SDL_dropevents_c.h
@@ -24,6 +24,7 @@
 #define SDL_dropevents_c_h_
 
 extern int SDL_SendDropFile(SDL_Window *window, const char *file);
+extern int SDL_SendDropPosition(SDL_Window *window, const char *file, float x, float y);
 extern int SDL_SendDropText(SDL_Window *window, const char *text);
 extern int SDL_SendDropComplete(SDL_Window *window);
 
diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index fb4665fb48ee..fb0f00d6c236 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -85,6 +85,7 @@ - (void)doCommandBySelector:(SEL)aSelector;
 
 /* Handle drag-and-drop of files onto the SDL window. */
 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender;
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender;
 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender;
 - (BOOL)wantsPeriodicDraggingUpdates;
 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem;
@@ -158,6 +159,21 @@ - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
     return NSDragOperationNone; /* no idea what to do with this, reject it. */
 }
 
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
+{
+    if (([sender draggingSourceOperationMask] & NSDragOperationGeneric) == NSDragOperationGeneric) {
+        SDL_Window *sdlwindow = [self findSDLWindow];
+        NSPoint point = [sender draggingLocation];
+        float x, y;
+        x = point.x;
+        y = (sdlwindow->h - point.y);
+        SDL_SendDropPosition(sdlwindow, NULL, x, y); /* FIXME, should we get the filename */
+        return NSDragOperationGeneric;
+    }
+
+    return NSDragOperationNone; /* no idea what to do with this, reject it. */
+}
+
 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
 {
     @autoreleasepool {
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 1d05b2cd45ed..d5a38c86bdaa 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -96,6 +96,8 @@ struct SDL_WaylandTouchPointList
 
 static struct SDL_WaylandTouchPointList touch_points = { NULL, NULL };
 
+static char *Wayland_URIToLocal(char *uri);
+
 static void touch_add(SDL_TouchID id, float x, float y, struct wl_surface *surface)
 {
     struct SDL_WaylandTouchPoint *tp = SDL_malloc(sizeof(struct SDL_WaylandTouchPoint));
@@ -1845,6 +1847,30 @@ static void data_device_handle_leave(void *data, struct wl_data_device *wl_data_
 static void data_device_handle_motion(void *data, struct wl_data_device *wl_data_device,
                                       uint32_t time, wl_fixed_t x, wl_fixed_t y)
 {
+    SDL_WaylandDataDevice *data_device = data;
+
+    if (data_device->drag_offer != NULL) {
+        /* TODO: SDL Support more mime types */
+        size_t length;
+        void *buffer = Wayland_data_offer_receive(data_device->drag_offer,
+                &length, FILE_MIME, SDL_TRUE);
+        if (buffer) {
+            char *saveptr = NULL;
+            char *token = SDL_strtokr((char *)buffer, "\r\n", &saveptr);
+            while (token != NULL) {
+                char *fn = Wayland_URIToLocal(token);
+                if (fn) {
+                    double dx;
+                    double dy;
+                    dx = wl_fixed_to_double(x);
+                    dy = wl_fixed_to_double(y);
+                    SDL_SendDropPosition(data_device->dnd_window, fn, (float)dx, (float)dy);
+                }
+                token = SDL_strtokr(NULL, "\r\n", &saveptr);
+            }
+            SDL_free(buffer);
+        }
+    }
 }
 
 /* Decodes URI escape sequences in string buf of len bytes
diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 99014a863797..965a811740c5 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -1222,6 +1222,18 @@ static void X11_DispatchEvent(_THIS, XEvent *xevent)
             }
             printf("Action requested by user is : %s\n", X11_XGetAtomName(display, act));
 #endif
+            {
+                /* Drag and Drop position */
+                int root_x, root_y, window_x, window_y;
+                Window ChildReturn;
+                root_x = xevent->xclient.data.l[2] >> 16;
+                root_y = xevent->xclient.data.l[2] & 0xffff;
+                /* Translate from root to current window position */
+                X11_XTranslateCoordinates(display, DefaultRootWindow(display), data->xwindow,
+                        root_x, root_y, &window_x, &window_y, &ChildReturn);
+
+                SDL_SendDropPosition(data->window, NULL, (float)window_x, (float)window_y); /* FIXME, can we get the filename ? */
+            }
 
             /* reply with status */
             SDL_memset(&m, 0, sizeof(XClientMessageEvent));
diff --git a/test/testdropfile.c b/test/testdropfile.c
index 7d33ffe366cd..21b2838f8123 100644
--- a/test/testdropfile.c
+++ b/test/testdropfile.c
@@ -29,6 +29,9 @@ int main(int argc, char *argv[])
 {
     int i, done;
     SDL_Event event;
+    SDL_bool is_hover = SDL_FALSE;
+    float x = 0.0f, y = 0.0f;
+    unsigned int windowID = 0;
 
     /* Enable standard application logging */
     SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
@@ -60,12 +63,6 @@ int main(int argc, char *argv[])
         quit(2);
     }
 
-    for (i = 0; i < state->num_windows; ++i) {
-        SDL_Renderer *renderer = state->renderers[i];
-        SDL_SetRenderDrawColor(renderer, 0xA0, 0xA0, 0xA0, 0xFF);
-        SDL_RenderClear(renderer);
-        SDL_RenderPresent(renderer);
-    }
 
     SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, SDL_TRUE);
 
@@ -75,19 +72,43 @@ int main(int argc, char *argv[])
         /* Check for events */
         while (SDL_PollEvent(&event)) {
             if (event.type == SDL_EVENT_DROP_BEGIN) {
-                SDL_Log("Drop beginning on window %u", (unsigned int)event.drop.windowID);
+                SDL_Log("Drop beginning on window %u at (%f, %f)", (unsigned int)event.drop.windowID, event.drop.x, event.drop.y);
             } else if (event.type == SDL_EVENT_DROP_COMPLETE) {
-                SDL_Log("Drop complete on window %u", (unsigned int)event.drop.windowID);
+                is_hover = SDL_FALSE;
+                SDL_Log("Drop complete on window %u at (%f, %f)", (unsigned int)event.drop.windowID, event.drop.x, event.drop.y);
             } else if ((event.type == SDL_EVENT_DROP_FILE) || (event.type == SDL_EVENT_DROP_TEXT)) {
                 const char *typestr = (event.type == SDL_EVENT_DROP_FILE) ? "File" : "Text";
                 char *dropped_filedir = event.drop.file;
-                SDL_Log("%s dropped on window %u: %s", typestr, (unsigned int)event.drop.windowID, dropped_filedir);
+                SDL_Log("%s dropped on window %u: %s at (%f, %f)", typestr, (unsigned int)event.drop.windowID, dropped_filedir, event.drop.x, event.drop.y);
                 /* Normally you'd have to do this, but this is freed in SDLTest_CommonEvent() */
                 /*SDL_free(dropped_filedir);*/
+            } else if (event.type == SDL_EVENT_DROP_POSITION) {
+                is_hover = SDL_TRUE;
+                x = event.drop.x;
+                y = event.drop.y;
+                windowID = event.drop.windowID;
+                SDL_Log("Drop position on window %u at (%f, %f) file = %s", (unsigned int)event.drop.windowID, event.drop.x, event.drop.y, event.drop.file);
             }
 
             SDLTest_CommonEvent(state, &event, &done);
         }
+
+        for (i = 0; i < state->num_windows; ++i) {
+            SDL_Renderer *renderer = state->renderers[i];
+            SDL_SetRenderDrawColor(renderer, 0xA0, 0xA0, 0xA0, 0xFF);
+            SDL_RenderClear(renderer);
+            if (is_hover) {
+                if (windowID == SDL_GetWindowID(SDL_GetRenderWindow(renderer))) {
+                    int len = 2000;
+                    SDL_SetRenderDrawColor(renderer, 0x0A, 0x0A, 0x0A, 0xFF);
+                    SDL_RenderLine(renderer, x, y - len, x, y + len);
+                    SDL_RenderLine(renderer, x - len, y, x + len, y);
+                }
+            }
+            SDL_RenderPresent(renderer);
+        }
+
+        SDL_Delay(16);
     }
 
     quit(0);