Devsh-Graphics-Programming / Nabla

Vulkan, OptiX and CUDA Interoperation Modular Rendering Library and Framework for PC/Linux/Android
http://devsh.eu
Apache License 2.0
484 stars 59 forks source link

Native drag&drop handling #92

Open sadiuk opened 3 years ago

sadiuk commented 3 years ago

Description

This issue covers native drag & drop support on several operating systems, including Windows, Linux, macOS, IOS and Android. It also contains suggestions for possible lightweight cross-platform libraries which can be used in Nabla to simplify this process.

Native Implementations

Windows

Drag&Drop on windows works in a very easy manner. To start with, you need a window that will accept files and enable Drag&Drop.

#include <windows.h>
HWND window;
window = CreateWindow("Window class", "Drag&Drop test", WS_OVERLAPPEDWINDOW, 100, 100, 1280, 960, nullptr, nullptr, nullptr, nullptr);
ShowWindow(window, SW_SHOWNORMAL); q
DragAcceptFiles(window, TRUE);

In WindowProc you also need to define proper drop handling:

LRESULT WINAPI WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
   if(message == WM_DROPFILES)
   {
      char filename[MAX_PATH];
      uint32_t num_of_files = DragQueryFile(lparam, 0xffffffff, nullptr, 0);
      for(int i = 0; i < num_of_files; i++)
      {
         DragQueryFiles(lparam, i, filename, MAX_PATH);
         std::cout << filename << "\n";
      }
   }
}

Linux

X11

With X11 the sitution is much more complicated. Unlike on Windows, Linux has no drag & drop standart, so many organizations have implemented their own protocols, so the program, written for one protocol cannot communicate via Drag&Drop with those, written for different protocols. Still there is one, most commonly used protocol - XDND.

To implement something minimal, lets make two processes:

#include <sys/types.h>
#include <X11/Xlib.h>
int main()
{
    pit_t pid = fork();
    if(pid_t == -1) return -1;
    createWindow(pid);
}

Where the createWindow function looks something like this:

void createWindow(pid_t pid)
{
  // Distribute parent/child process duties...

  Display *display = XOpenDisplay(NULL); 
  Window window = XCreateSimpleWindow(display, RootWindow(display, N), 0, 0, 1, 1, 0, BlackPixel(display, N), WhitePixel(display, N));

  // Then goes the initialization of atoms and stuff...
  // ...
  // ...
  Atom  XdndDrop = XInternAtom(display, "XdndDrop", False);
  // ...

  bool keep_listening = true;
  XEvent event;
  while(keep_listening)
  {
    XNextEvent(display, &event);
    if(event.xclient.message_type == XdndDrop)
    {
      // Extract event data... 
    }
  }
}

You can view the full code of drag & drop handling example with X11 here.

Wayland

If you think that X11 is the only counter-intuitive part of cross-platform drag & drop implementation, you're totally wrong, because it also can be applied to wayland interface.

To get access to the clipboard and drag & drop interfaces, clients can bind to the wl_data_device_manager. We’ll also need to bind to a seat:

static struct wl_data_device_manager *data_device_manager = NULL;
static struct wl_seat *seat = NULL;

static void registry_handle_global(void *data, struct wl_registry *registry,
        uint32_t name, const char *interface, uint32_t version) {
    if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
        data_device_manager = wl_registry_bind(registry, name,
            &wl_data_device_manager_interface, 3);
    } else if (strcmp(interface, wl_seat_interface.name) == 0 && seat == NULL) {
        seat = wl_registry_bind(registry, name, &wl_seat_interface, 1);
    }
}

After binding, we’ll need to create a wl_data_device object to interact with the clipboard and drag & drop on a particular seat:

struct wl_data_device *data_device = wl_data_device_manager_get_data_device(data_device_manager, seat);

and a data source listener with the source itself:

static const struct wl_data_source_listener data_source_listener = {
    // .send and .cancelled are the same as the clipboard case
    .target = data_source_handle_target,
    .action = data_source_handle_action,
};

struct wl_data_source *source =
    wl_data_device_manager_create_data_source(data_device_manager);
wl_data_source_add_listener(source, &data_source_listener, NULL);
wl_data_source_offer(source, "text/plain");

wl_data_source_set_actions(source, WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE | WL_DATA_MANAGER_DND_ACTION_COPY);

Note that wl_data_source_offer allows you to specify the data format.

Also create a surface (drag & drop area):

struct wl_surface *icon = NULL;
wl_data_device_start_drag(data_device, source, origin, icon,
    pointer_button_serial); 

After that, you can perform event handling, covered in-depth here.

macOS

On macOS the d&d features are implemented pretty easily. Actually there are several ways to do that, but they're all pretty similar so I'm only gonna cover one of them. First you need to create class, derived from NSView and specify the formats you want d&d to work with:

class TestView: NSView {
    let supportedTypes: [NSPasteboard.PasteboardType] = [.tiff, .color, .string, .fileURL]
}

Then allow the view accept these formats by overriding the registerForDraggedTypes method:

override func awakeFromNib() {
    self.registerForDraggedTypes(supportedTypes)
}

The next method will allow you to detect when the draggable object enters the dropping area (TestView):

override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
    let canReadPasteboardObjects = sender.draggingPasteboard.canReadObject(forClasses: [NSImage.self, NSColor.self, NSString.self, NSURL.self], options: nil)
    if canReadPasteboardObjects {
        return .copy
    }

    return NSDragOperation()
}

To implement the actual drop operation, override the next method:

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
   guard let pasteboardObjects = sender.draggingPasteboard.readObjects(forClasses: [NSImage.self, NSColor.self, NSString.self, NSURL.self], options: nil), pasteboardObjects.count > 0 else { return false }
   pasteboardObjects.forEach { (object) in // go through every object that's being dragged

      if let image = object as? NSImage {
        avatarInfo.setImageData(using: image)
      }

      if let color = object as? NSColor {
        avatarInfo.setColorData(using: color)
      }

      if let quote = object as? NSString {
        avatarInfo.quote = quote as String
      }

      if let url = object as? NSURL {
        self.handleFileURLObject(url as URL)
      }
   }
}

IOS

Android

devshgraphicsprogramming commented 3 years ago

Ok so windows and X11 treat drag/drop like an input event which needs to be callbacked/polled

we need the drag-n-drop in the IWindow interface I guess