From 6d4371f5965b876b1cd618a461d38a98ce9511c8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 14 May 2026 11:07:52 +0530 Subject: [PATCH] Properly pass source actions to drop events Implemented for Wayland. Needs implementation for X11 and macOS --- glfw/glfw3.h | 2 +- glfw/input.c | 1 + glfw/internal.h | 2 +- glfw/wl_window.c | 10 ++++++++++ kitty/dnd.c | 21 +++++++++++---------- kitty/dnd.h | 2 +- kitty/glfw-wrapper.h | 2 +- kitty/glfw.c | 21 +++++++++++---------- kitty/state.h | 5 +++-- kitty_tests/dnd_kitten.py | 29 ----------------------------- 10 files changed, 40 insertions(+), 55 deletions(-) diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 4e89399a5..cbacb7568 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1407,7 +1407,7 @@ typedef struct GLFWDropEvent { // Positions are only valid for GLFW_DROP_ENTER and GLFW_DROP_MOVE. // They are in window co-ordinates same as for mouse events double xpos, ypos; - struct { GLFWDragOperationType preferred; int allowed; } operation; + struct { GLFWDragOperationType preferred; int allowed, source_actions; } operation; bool from_self; // Only valid upto GLFW_DROP_DROP ssize_t (*read_data)(GLFWwindow *w, struct GLFWDropEvent* ev, char *buffer, size_t sz); // Only valid for GLFW_DROP_DATA_AVAILABLE void (*finish_drop)(GLFWwindow *w, GLFWDragOperationType op); // Only valid for GLFW_DROP_DROP and GLFW_DROP_DATA_AVAILABLE diff --git a/glfw/input.c b/glfw/input.c index f370129e4..09fd838bc 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -416,6 +416,7 @@ size_t _glfwInputDropEvent(_GLFWwindow *window, GLFWDropEventType type, double x .read_data=type == GLFW_DROP_DATA_AVAILABLE ? _glfwPlatformReadAvailableDropData : NULL, .finish_drop=type == GLFW_DROP_DATA_AVAILABLE || type == GLFW_DROP_DROP ? _glfwPlatformEndDrop : NULL, .operation.allowed = window->drop_operation.allowed, .operation.preferred = window->drop_operation.preferred, + .operation.source_actions = window->drop_operation.source_actions, }; window->callbacks.drop_event((GLFWwindow*)window, &ev); window->drop_operation.preferred = ev.operation.preferred; window->drop_operation.allowed = ev.operation.allowed; diff --git a/glfw/internal.h b/glfw/internal.h index 9d3c19183..f4917a22d 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -461,7 +461,7 @@ struct _GLFWwindow #else const bool swaps_disallowed; #endif - struct { GLFWDragOperationType preferred; int allowed; } drop_operation; + struct { GLFWDragOperationType preferred; int allowed, source_actions; } drop_operation; struct { GLFWwindowposfun pos; diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 9d0dbeb4c..37420bc17 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -2548,6 +2548,14 @@ update_drop_state(_GLFWWaylandDataOffer *d, _GLFWwindow* window, size_t accepted } } +static void +update_drop_source_actions(_GLFWwindow *window, _GLFWWaylandDataOffer *offer) { + window->drop_operation.source_actions = GLFW_DRAG_OPERATION_NONE; + if (offer->source_actions & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY) window->drop_operation.source_actions |= GLFW_DRAG_OPERATION_COPY; + if (offer->source_actions & WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE) window->drop_operation.source_actions |= GLFW_DRAG_OPERATION_MOVE; + if (offer->source_actions & WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK) window->drop_operation.source_actions |= GLFW_DRAG_OPERATION_GENERIC; +} + static void drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *id) { debug_input("Drop entered\n"); @@ -2565,6 +2573,7 @@ drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint double xpos = wl_fixed_to_double(x); double ypos = wl_fixed_to_double(y); if (reset_copy_mimes(offer)) { + update_drop_source_actions(window, offer); size_t mime_count = _glfwInputDropEvent( window, GLFW_DROP_ENTER, xpos, ypos, offer->copy_mimes, offer->copy_mimes_count, offer->is_self_offer); @@ -2713,6 +2722,7 @@ motion(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t double xpos = wl_fixed_to_double(x); double ypos = wl_fixed_to_double(y); if (reset_copy_mimes(offer)) { + update_drop_source_actions(window, offer); size_t mime_count = _glfwInputDropEvent( window, GLFW_DROP_MOVE, xpos, ypos, offer->copy_mimes, offer->copy_mimes_count, offer->is_self_offer); update_drop_state(offer, window, mime_count); diff --git a/kitty/dnd.c b/kitty/dnd.c index c33730e1a..153c52db9 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -505,7 +505,7 @@ drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz) { } void -drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop, int allowed_ops) { +drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop) { if (!w->drop.hovered) { reset_drop(w); w->drop.hovered = true; @@ -531,14 +531,13 @@ drop_move_on_child(Window *w, const char** mimes, size_t num_mimes, bool is_drop if (w->drop.pending.count && !is_drop) return; char buf[128]; int header_size; - if (allowed_ops) - header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=%c:x=%u:y=%u:X=%d:Y=%d:o=%d", DND_CODE, - is_drop ? 'M' : 'm', w->mouse_pos.cell_x, w->mouse_pos.cell_y, - (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y, allowed_ops); - else - header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=%c:x=%u:y=%u:X=%d:Y=%d", DND_CODE, - is_drop ? 'M' : 'm', w->mouse_pos.cell_x, w->mouse_pos.cell_y, - (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y); + int allowed_ops = 0; + if (global_state.drop_dest.allowed_ops & GLFW_DRAG_OPERATION_COPY) allowed_ops |= 1; + if (global_state.drop_dest.allowed_ops & GLFW_DRAG_OPERATION_MOVE) allowed_ops |= 2; + if (global_state.drop_dest.allowed_ops & GLFW_DRAG_OPERATION_GENERIC) allowed_ops |= 3; + header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=%c:x=%u:y=%u:X=%d:Y=%d:o=%d", DND_CODE, + is_drop ? 'M' : 'm', w->mouse_pos.cell_x, w->mouse_pos.cell_y, + (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y, allowed_ops); if (w->drop.offered_mimes_total_size) { const size_t mimes_total_size = 1 + w->drop.offered_mimes_total_size; RAII_ALLOC(char, mbuf, malloc(mimes_total_size)); @@ -2434,7 +2433,9 @@ dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) { } if (x > -1) w->mouse_pos.cell_x = x; if (y > -1) w->mouse_pos.cell_y = y; - drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false, allowed_ops); + global_state.drop_dest.allowed_ops = allowed_ops; + drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false); + global_state.drop_dest.allowed_ops = 0; Py_RETURN_NONE; } diff --git a/kitty/dnd.h b/kitty/dnd.h index 6e001c609..86833d549 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -12,7 +12,7 @@ void dnd_query(Window *w, uint32_t client_id); void drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id, bool more); void drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz); -void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes, bool is_drop, int allowed_ops); +void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes, bool is_drop); void drop_left_child(Window *w); void drop_free_data(Window *w); void drop_send_einval(Window *w, const char *desc); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index e5cefc6fc..38ea7ccde 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1135,7 +1135,7 @@ typedef struct GLFWDropEvent { // Positions are only valid for GLFW_DROP_ENTER and GLFW_DROP_MOVE. // They are in window co-ordinates same as for mouse events double xpos, ypos; - struct { GLFWDragOperationType preferred; int allowed; } operation; + struct { GLFWDragOperationType preferred; int allowed, source_actions; } operation; bool from_self; // Only valid upto GLFW_DROP_DROP ssize_t (*read_data)(GLFWwindow *w, struct GLFWDropEvent* ev, char *buffer, size_t sz); // Only valid for GLFW_DROP_DATA_AVAILABLE void (*finish_drop)(GLFWwindow *w, GLFWDragOperationType op); // Only valid for GLFW_DROP_DROP and GLFW_DROP_DATA_AVAILABLE diff --git a/kitty/glfw.c b/kitty/glfw.c index c163ef50e..ba19ab58e 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -800,12 +800,13 @@ drop_dest_callback(GLFWwindow *window, GLFWDropEvent *ev) { os_window->last_drag_event.x = (int)(ev->xpos * os_window->viewport_x_ratio); os_window->last_drag_event.y = (int)(ev->ypos * os_window->viewport_y_ratio); on_mouse_position_update(ev->xpos, ev->ypos); + global_state.drop_dest.allowed_ops = ev->operation.source_actions; if (is_client_drop) { - if (ev->type == GLFW_DROP_ENTER) w->drop.accepted_operation = GLFW_DRAG_OPERATION_NONE; - int kitty_allowed_ops = 0; - if (ev->operation.allowed & GLFW_DRAG_OPERATION_COPY) kitty_allowed_ops |= 1; - if (ev->operation.allowed & GLFW_DRAG_OPERATION_MOVE) kitty_allowed_ops |= 2; - drop_move_on_child(w, ev->mimes, ev->num_mimes, false, kitty_allowed_ops); + if (!w->drop.initialized) { + w->drop.initialized = true; + w->drop.accepted_operation = GLFW_DRAG_OPERATION_NONE; + } + drop_move_on_child(w, ev->mimes, ev->num_mimes, false); ev->num_mimes = drop_update_mimes(w, ev->mimes, ev->num_mimes); ev->operation.allowed = w->drop.accepted_operation; ev->operation.preferred = w->drop.accepted_operation; @@ -854,10 +855,7 @@ drop_dest_callback(GLFWwindow *window, GLFWDropEvent *ev) { global_state.drop_dest.client_window_data_request = 0; global_state.drop_dest.os_window_id = os_window->id; if (is_client_drop) { - int kitty_allowed_ops = 0; - if (ev->operation.allowed & GLFW_DRAG_OPERATION_COPY) kitty_allowed_ops |= 1; - if (ev->operation.allowed & GLFW_DRAG_OPERATION_MOVE) kitty_allowed_ops |= 2; - drop_move_on_child(w, ev->mimes, ev->num_mimes, true, kitty_allowed_ops); + drop_move_on_child(w, ev->mimes, ev->num_mimes, true); ev->num_mimes = 0; // we wait for the client to request MIMEs } else { if (ev->from_self) { @@ -3121,7 +3119,10 @@ start_window_drag(Window *w, bool in_test_mode) { global_state.drag_source.thumbnail_idx = w->drag_source.img_idx < num_images ? (int)w->drag_source.img_idx : -1; global_state.tracked_drag_in_window = 0; // this is now an OS global drag if (in_test_mode) return 0; - int ret = glfwStartDrag(osw->handle, items, w->drag_source.num_mimes, thumbnail.pixels ? &thumbnail : NULL, w->drag_source.allowed_operations, true); + GLFWDragOperationType ops = GLFW_DRAG_OPERATION_NONE; + if (w->drag_source.allowed_operations & 1) ops |= GLFW_DRAG_OPERATION_COPY; + if (w->drag_source.allowed_operations & 2) ops |= GLFW_DRAG_OPERATION_MOVE; + int ret = glfwStartDrag(osw->handle, items, w->drag_source.num_mimes, thumbnail.pixels ? &thumbnail : NULL, ops, true); if (ret != 0) free_drag_source(); return ret; } diff --git a/kitty/state.h b/kitty/state.h index d0393f2e3..c50f11f2a 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -290,7 +290,7 @@ typedef struct Window { bool is_hovering; } scrollbar; struct { - bool wanted, hovered, dropped, is_remote_client; + bool wanted, hovered, dropped, is_remote_client, initialized; uint32_t client_id; char *registered_mimes; char *uri_list; size_t uri_list_sz; @@ -299,7 +299,7 @@ typedef struct Window { const char **offerred_mimes; size_t num_offerred_mimes, offered_mimes_total_size; char *accepted_mimes; size_t accepted_mimes_sz; - int accepted_operation; bool accept_in_progress; + int accepted_operation, allowed_operations; bool accept_in_progress; char *getting_data_for_mime; DirHandle *dir_handles; size_t num_dir_handles, dir_handles_capacity; @@ -507,6 +507,7 @@ typedef struct GlobalState { double x, y; size_t num_left; bool drop_has_happened; + int allowed_ops; } drop_dest; struct { diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 1b7b9e04d..dae659ddd 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -197,35 +197,6 @@ def tearDown(self): self.screen = None self.pty = None - def test_dnd_kitten_drop_allowed_ops(self): - # Test that the drop destination respects the drag source's allowed operations. - # When the drag source only allows copy (allowed_ops=1), dropping on the move - # box must be rejected, and vice versa. - self.finish_setup() - copy, move = self.get_button_geometry() - mimes = ['text/uri-list'] - wid = self.capture.window_id - # allowed_ops=1 means copy-only in kitten format - dnd_test_fake_drop_event(wid, False, mimes, copy[0] + 1, copy[1] + 1, 1) - self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY) - dnd_test_fake_drop_event(wid, False) - dnd_test_fake_drop_event(wid, False, mimes, move[0] + 1, move[1] + 1, 1) - self.wait_for_state('drop_action', 0) # GLFW_DRAG_OPERATION_NONE: source allows copy only, move box must reject - dnd_test_fake_drop_event(wid, False) - # allowed_ops=2 means move-only in kitten format - dnd_test_fake_drop_event(wid, False, mimes, move[0] + 1, move[1] + 1, 2) - self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_MOVE) - dnd_test_fake_drop_event(wid, False) - dnd_test_fake_drop_event(wid, False, mimes, copy[0] + 1, copy[1] + 1, 2) - self.wait_for_state('drop_action', 0) # GLFW_DRAG_OPERATION_NONE: source allows move only, copy box must reject - dnd_test_fake_drop_event(wid, False) - # allowed_ops=3 means both copy and move allowed (default) - for b, expected in ((copy, GLFW_DRAG_OPERATION_COPY), (move, GLFW_DRAG_OPERATION_MOVE)): - dnd_test_fake_drop_event(wid, False, mimes, b[0] + 1, b[1] + 1, 3) - self.wait_for_state('drop_action', expected) - dnd_test_fake_drop_event(wid, False) - self.exit_kitten() - def test_dnd_kitten_drop(self): img_drop_path = 'images/image.png' self.finish_setup(cli_args=(f'--drop=image/png:{img_drop_path}', '--confirm-drop-overwrite'))