This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch wl/browser-all
in repository enlightenment.

View the commit online.

commit c65d2669b4b4f01ed6450d412760599519c398ea
Author: Cedric BAIL <[email protected]>
AuthorDate: Tue Aug 18 18:32:02 2026 -0600

    e_comp_wl - xdg_toplevel_drag_v1: carry a window with a drag
    
    Chrome tab tear-off, and the same gesture in anything with detachable
    panels. Ordinary drag-and-drop moves a picture of what is being dragged;
    this moves a real window alongside it, so a tab pulled out of a window
    becomes a window that follows the cursor and can be dropped into another.
    
    Split across two files, which looks arbitrary and is not. `attach` takes
    an xdg_toplevel, and xdg_toplevel_interface is defined by the
    wl_desktop_shell module - the main binary cannot even link a protocol
    that references it, which is a link error and not a matter of taste. So
    the objects live in the module and the motion lives in e_comp_wl_data.c
    next to the drag it rides on, joined by three fields on
    E_Comp_Wl_Data_Source.
    
    The XML is vendored to src/protocol/ because wayland-protocols 1.33 does
    not carry it, with provenance and a checksum in a README beside it. That
    directory already held one upstream file; this is the second.
    
    e_drag_move_cb_set is the only addition to shared drag-and-drop code: a
    callback invoked after the drag has been repositioned, NULL for X11,
    which has no such thing.
    
    Tested: the window follows the cursor and keeps following it - two moves,
    because being placed once and then stopping is the failure a single
    assertion would miss. Both protocol errors are asserted too, ongoing_drag
    and invalid_source.
    
    Getting there needed two things fixed first, both on their own branches:
    E crashed on any drag started without an icon surface
    (e_client_has_xwindow(NULL)), and the harness did not deliver input to
    E's ecore handlers at all.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01FtoiXoSKUmZb6Aix6U3GZS
---
 src/bin/e_comp_wl_data.c                         |  63 ++++++++
 src/bin/e_comp_wl_data.h                         |  12 ++
 src/bin/e_dnd.c                                  |  12 ++
 src/bin/e_dnd.h                                  |   6 +
 src/modules/wl_desktop_shell/e_mod_main.c        |   5 +
 src/modules/wl_desktop_shell/e_mod_main.h        |   1 +
 src/modules/wl_desktop_shell/meson.build         |   6 +
 src/modules/wl_desktop_shell/xdg_toplevel_drag.c | 188 +++++++++++++++++++++++
 src/protocol/README                              |  43 ++++++
 src/protocol/xdg-toplevel-drag-v1.xml            | 142 +++++++++++++++++
 src/tests/wayland/e_wl_testkit.c                 |   6 +
 src/tests/wayland/e_wl_testkit.h                 |   4 +
 src/tests/wayland/globals.expected               |   1 +
 src/tests/wayland/meson.build                    |   2 +
 src/tests/wayland/test_toplevel_drag.c           | 186 ++++++++++++++++++++++
 15 files changed, 677 insertions(+)

diff --git a/src/bin/e_comp_wl_data.c b/src/bin/e_comp_wl_data.c
index fe64ed09f..513b8af26 100644
--- a/src/bin/e_comp_wl_data.c
+++ b/src/bin/e_comp_wl_data.c
@@ -624,6 +624,10 @@ _e_comp_wl_data_device_drag_key(E_Drag *drag EINA_UNUSED, Ecore_Event_Key *ev)
      data_offer_update_action(drag_source->offer);
 }
 
+/* Defined with the rest of xdg_toplevel_drag_v1 below; needed here because
+ * the drag it rides on is created in drag_start. */
+static void _toplevel_drag_move(E_Drag *drag, int x, int y);
+
 static void
 _e_comp_wl_data_device_cb_drag_start(struct wl_client *client, struct wl_resource *resource EINA_UNUSED, struct wl_resource *source_resource, struct wl_resource *origin_resource, struct wl_resource *icon_resource, uint32_t serial)
 {
@@ -679,6 +683,10 @@ _e_comp_wl_data_device_cb_drag_start(struct wl_client *client, struct wl_resourc
                                 _e_comp_wl_data_device_drag_finished);
    e_drag_key_down_cb_set(e_comp_wl->drag, _e_comp_wl_data_device_drag_key);
    e_drag_key_up_cb_set(e_comp_wl->drag, _e_comp_wl_data_device_drag_key);
+   /* Carry an attached toplevel along with the drag. Set unconditionally:
+    * the callback returns immediately when there is no xdg_toplevel_drag,
+    * which is every drag but a tab tear-off. */
+   e_drag_move_cb_set(e_comp_wl->drag, _toplevel_drag_move);
    e_comp_wl->drag->button_mask =
      evas_pointer_button_down_mask_get(e_comp->evas);
    if (ec)
@@ -714,6 +722,19 @@ _e_comp_wl_data_device_cb_selection_set(struct wl_client *client EINA_UNUSED, st
                                "cannot set drag-and-drop source as selection");
         return;
      }
+   /* Same rule, arrived at from the other direction: a source that an
+    * xdg_toplevel_drag_v1 was made from is drag-and-drop only, and the
+    * protocol names set_selection as the case to refuse. Reported on the
+    * source, beside the existing refusal above, rather than on the manager -
+    * the source is the object the client got wrong. */
+   if (source->dnd_only)
+     {
+        wl_resource_post_error(source_resource,
+                               WL_DATA_SOURCE_ERROR_INVALID_SOURCE,
+                               "cannot set a source with an xdg_toplevel_drag "
+                               "as selection");
+        return;
+     }
 
    _e_comp_wl_data_device_selection_set(e_comp->wl_comp_data, source, serial);
 }
@@ -1500,6 +1521,48 @@ e_comp_wl_data_device_keyboard_focus_set(void)
    wl_data_device_send_selection(data_device_res, offer_res);
 }
 
+
+/* xdg_toplevel_drag_v1 - the compositor half.
+ *
+ * Chrome tab tear-off, and the same gesture in any application with detachable
+ * panels. Ordinary drag-and-drop moves a *picture* of what is being dragged;
+ * this moves a real window at the same time, so a tab pulled out of a window
+ * becomes a window that follows the cursor and can be dropped into another.
+ *
+ * The protocol object itself lives in the wl_desktop_shell module, because
+ * `attach` takes an xdg_toplevel and `xdg_toplevel_interface` is defined
+ * there - the main binary cannot link a protocol that references it. The
+ * module sets source->drag_toplevel and the offsets; everything below is the
+ * part that has to live here, next to the drag it rides on.
+ */
+
+/* Follow the cursor. The offset is where the client asked for the window to
+ * sit relative to the pointer, so it is subtracted rather than added.
+ *
+ * The mapped check is the protocol's auto-detach rule - "if the toplevel is
+ * unmapped while it is attached, it is automatically detached" - done here
+ * rather than on an unmap hook because here is the only place that has to be
+ * right: a window that is not on screen cannot be seen to lag behind. */
+static void
+_toplevel_drag_move(E_Drag *drag EINA_UNUSED, int x, int y)
+{
+   E_Comp_Wl_Data_Source *source = e_comp_wl->drag_source;
+   E_Client *ec;
+
+   if (!source) return;
+   if (!(ec = source->drag_toplevel)) return;
+
+   if (e_object_is_del(E_OBJECT(ec)) || (!ec->comp_data) ||
+       (!ec->comp_data->mapped))
+     {
+        source->drag_toplevel = NULL;
+        return;
+     }
+
+   evas_object_move(ec->frame, x - source->drag_x_offset,
+                    y - source->drag_y_offset);
+}
+
 EINTERN Eina_Bool
 e_comp_wl_data_manager_init(void)
 {
diff --git a/src/bin/e_comp_wl_data.h b/src/bin/e_comp_wl_data.h
index bc76b5c75..72f19dff9 100644
--- a/src/bin/e_comp_wl_data.h
+++ b/src/bin/e_comp_wl_data.h
@@ -53,8 +53,20 @@ struct _E_Comp_Wl_Data_Source
    enum wl_data_device_manager_dnd_action compositor_action;
    uint32_t serial;
 
+   /* xdg_toplevel_drag_v1: the toplevel travelling with this drag, and where
+    * the client asked for it to sit relative to the cursor. Set by the
+    * wl_desktop_shell module, which owns the protocol object because it owns
+    * xdg_toplevel; read by the drag motion handler here, which owns the drag.
+    * NULL for every drag that is not a tab tear-off, which is almost all. */
+   E_Client *drag_toplevel;
+   int drag_x_offset, drag_y_offset;
+
    Eina_Bool accepted E_BITFIELD;
    Eina_Bool actions_set E_BITFIELD;
+   /* Set once an xdg_toplevel_drag_v1 has been made from this source: the
+    * protocol says such a source may only ever be used for drag-and-drop, so
+    * set_selection has to refuse it. */
+   Eina_Bool dnd_only E_BITFIELD;
 };
 
 struct _E_Comp_Wl_Data_Offer
diff --git a/src/bin/e_dnd.c b/src/bin/e_dnd.c
index 981840acc..de8d27f95 100644
--- a/src/bin/e_dnd.c
+++ b/src/bin/e_dnd.c
@@ -571,6 +571,13 @@ e_drag_key_up_cb_set(E_Drag *drag, void (*func)(E_Drag *drag, Ecore_Event_Key *e
    drag->cb.key_up = func;
 }
 
+E_API void
+e_drag_move_cb_set(E_Drag *drag, void (*func)(E_Drag *drag, int x, int y))
+{
+   EINA_SAFETY_ON_NULL_RETURN(drag);
+   drag->cb.move = func;
+}
+
 /* from ecore_x_selection.c */
 E_API Eina_List *
 e_dnd_util_text_uri_list_convert(char *data, int size)
@@ -641,6 +648,11 @@ _e_drag_move(E_Drag *drag, int x, int y)
    drag->x = x - drag->dx;
    drag->y = y - drag->dy;
    evas_object_move(drag->comp_object, drag->x, drag->y);
+
+   /* After the move, not instead of it: a toplevel travelling with the drag
+    * is positioned relative to where the drag now is. NULL for X11
+    * drag-and-drop, which has no such thing. */
+   if (drag->cb.move) drag->cb.move(drag, x, y);
 }
 
 static void
diff --git a/src/bin/e_dnd.h b/src/bin/e_dnd.h
index e36fb2571..9ded1577b 100644
--- a/src/bin/e_dnd.h
+++ b/src/bin/e_dnd.h
@@ -36,6 +36,11 @@ struct _E_Drag
       void  (*finished)(E_Drag *drag, int dropped);
       void  (*key_down)(E_Drag *drag, Ecore_Event_Key *e);
       void  (*key_up)(E_Drag *drag, Ecore_Event_Key *e);
+      /* Called after the drag has been repositioned, with the new pointer
+       * position. Exists for xdg_toplevel_drag_v1, where a real window has to
+       * travel with the cursor alongside the drag icon; nothing else sets it,
+       * and X11 drag-and-drop leaves it NULL. */
+      void  (*move)(E_Drag *drag, int x, int y);
    } cb;
 
    Evas              *evas;
@@ -128,6 +133,7 @@ E_API void            e_drag_move(E_Drag *drag, int x, int y);
 E_API void            e_drag_resize(E_Drag *drag, int w, int h);
 E_API void            e_drag_key_down_cb_set(E_Drag *drag, void (*func)(E_Drag *drag, Ecore_Event_Key *e));
 E_API void            e_drag_key_up_cb_set(E_Drag *drag, void (*func)(E_Drag *drag, Ecore_Event_Key *e));
+E_API void            e_drag_move_cb_set(E_Drag *drag, void (*func)(E_Drag *drag, int x, int y));
 
 /* x and y are the coords where the mouse is when dragging starts */
 E_API int             e_drag_start(E_Drag *drag, int x, int y);
diff --git a/src/modules/wl_desktop_shell/e_mod_main.c b/src/modules/wl_desktop_shell/e_mod_main.c
index c14b1cca0..abbddbe7f 100644
--- a/src/modules/wl_desktop_shell/e_mod_main.c
+++ b/src/modules/wl_desktop_shell/e_mod_main.c
@@ -178,6 +178,11 @@ e_modapi_init(E_Module *m)
      }
 
    have_shell = e_xdg_shell_v6_init() | e_xdg_shell_init();
+   /* After the shells, and not part of have_shell: a compositor with no
+    * xdg-shell has nothing to attach, but failing to advertise the drag
+    * manager is not a reason to refuse to load the module. */
+   if (have_shell && (!e_xdg_toplevel_drag_init()))
+     ERR("Could not create global for xdg_toplevel_drag_manager_v1");
    if (!have_shell) return NULL;
 
 #ifdef HAVE_WL_TEXT_INPUT
diff --git a/src/modules/wl_desktop_shell/e_mod_main.h b/src/modules/wl_desktop_shell/e_mod_main.h
index 6e7ed64f8..46e0cbb5d 100644
--- a/src/modules/wl_desktop_shell/e_mod_main.h
+++ b/src/modules/wl_desktop_shell/e_mod_main.h
@@ -17,6 +17,7 @@ EINTERN E_Shell_Data *e_shell_data_new(unsigned int version);
 
 EINTERN Eina_Bool e_xdg_shell_v6_init(void);
 EINTERN Eina_Bool e_xdg_shell_init(void);
+EINTERN Eina_Bool e_xdg_toplevel_drag_init(void);
 EINTERN void wl_shell_cb_bind(struct wl_client *client, void *data EINA_UNUSED, uint32_t version, uint32_t id);
 
 struct E_Shell_Data
diff --git a/src/modules/wl_desktop_shell/meson.build b/src/modules/wl_desktop_shell/meson.build
index 83803afaf..7365968de 100644
--- a/src/modules/wl_desktop_shell/meson.build
+++ b/src/modules/wl_desktop_shell/meson.build
@@ -7,6 +7,7 @@ else
     'wl_shell.c',
     'xdg6.c',
     'xdg.c',
+    'xdg_toplevel_drag.c',
     'e_mod_main.h'
    )
 
@@ -15,6 +16,11 @@ else
     '@0@/stable/xdg-shell/xdg-shell.xml'.format(dir_wayland_protocols),
     '@0@/unstable/input-method/input-method-unstable-v1.xml'.format(dir_wayland_protocols),
     '@0@/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml'.format(dir_wayland_protocols),
+    # Vendored, not from wayland-protocols - see src/protocol/README. It lives
+    # here rather than with the compositor's own protocols because it takes an
+    # xdg_toplevel argument, and xdg_toplevel_interface is defined by this
+    # module: the main binary cannot link a protocol that references it.
+    '../../protocol/xdg-toplevel-drag-v1.xml',
   ]
     src += gen_scanner_server.process(p)
     src += gen_scanner_impl.process(p)
diff --git a/src/modules/wl_desktop_shell/xdg_toplevel_drag.c b/src/modules/wl_desktop_shell/xdg_toplevel_drag.c
new file mode 100644
index 000000000..f808fb6c8
--- /dev/null
+++ b/src/modules/wl_desktop_shell/xdg_toplevel_drag.c
@@ -0,0 +1,188 @@
+/* xdg_toplevel_drag_v1 - the protocol half.
+ *
+ * A window that travels with a drag-and-drop operation: Chrome tab tear-off,
+ * and the same gesture in anything with detachable panels. The client starts
+ * a normal drag, decides for itself when the thing has come loose, maps a new
+ * toplevel with the contents, and attaches it. From then the compositor's job
+ * is "keep this window under the cursor until the drag ends", and the protocol
+ * says the window then settles exactly where an xdg_toplevel.move would have
+ * left it.
+ *
+ * Split across two files, which needs explaining because it looks arbitrary.
+ * `attach` takes an **xdg_toplevel**, and `xdg_toplevel_interface` is defined
+ * by this module - the main binary cannot even link a protocol that references
+ * it. But the drag itself, and the motion handler that has to move the window,
+ * live in `e_comp_wl_data.c`. So the objects are here and the movement is
+ * there, joined by three plain fields on E_Comp_Wl_Data_Source.
+ *
+ * The attached window deliberately takes no part in choosing the drop target -
+ * it is being carried, not hovered - and E gets that for free, because drop
+ * targets are resolved against the pointer position rather than against
+ * whatever happens to be under it.
+ */
+#define E_COMP_WL
+#include "e.h"
+#include "e_mod_main.h"
+#include "xdg-toplevel-drag-v1-server-protocol.h"
+
+typedef struct Toplevel_Drag
+{
+   struct wl_resource *res;
+   E_Comp_Wl_Data_Source *source;
+} Toplevel_Drag;
+
+/* Is the drag this object belongs to still running? E clears drag_source when
+ * the drag finishes, which is the same moment the client is told by
+ * wl_data_source.dnd_drop_performed or .cancelled - so the two agree. */
+static Eina_Bool
+_drag_ongoing(const Toplevel_Drag *td)
+{
+   return td->source && (e_comp_wl->drag_source == td->source);
+}
+
+static void
+_e_xdg_toplevel_drag_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+   Toplevel_Drag *td = wl_resource_get_user_data(resource);
+
+   /* "This request must only be called after the underlying wl_data_source
+    * drag has ended, as indicated by the dnd_drop_performed or cancelled
+    * events. In any other case an ongoing_drag error is raised." */
+   if (td && _drag_ongoing(td))
+     {
+        wl_resource_post_error(resource, XDG_TOPLEVEL_DRAG_V1_ERROR_ONGOING_DRAG,
+                               "xdg_toplevel_drag_v1 destroyed while its drag "
+                               "is still running");
+        return;
+     }
+
+   wl_resource_destroy(resource);
+}
+
+static void
+_e_xdg_toplevel_drag_cb_attach(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, struct wl_resource *toplevel, int32_t x_offset, int32_t y_offset)
+{
+   Toplevel_Drag *td = wl_resource_get_user_data(resource);
+   E_Client *ec, *attached;
+
+   if ((!td) || (!td->source)) return;
+   if (!(ec = wl_resource_get_user_data(toplevel))) return;
+   if (e_object_is_del(E_OBJECT(ec)) || (!ec->comp_data)) return;
+
+   /* "This request can be called multiple times but issuing it while a
+    * toplevel with an active role is attached raises a toplevel_attached
+    * error."
+    *
+    * A window that has since been unmapped is not one of those - the protocol
+    * detaches it for us - so re-attaching after an unmap is legal, and is
+    * exactly what a client does when the user drags a tab out, back in, and
+    * out again. Re-attaching the *same* window is legal too: that is how the
+    * offset gets updated. */
+   attached = td->source->drag_toplevel;
+   if (attached && (attached != ec) &&
+       (!e_object_is_del(E_OBJECT(attached))) && attached->comp_data &&
+       attached->comp_data->mapped)
+     {
+        wl_resource_post_error(resource,
+                               XDG_TOPLEVEL_DRAG_V1_ERROR_TOPLEVEL_ATTACHED,
+                               "a mapped toplevel is already attached to this "
+                               "xdg_toplevel_drag_v1");
+        return;
+     }
+
+   td->source->drag_toplevel = ec;
+   td->source->drag_x_offset = x_offset;
+   td->source->drag_y_offset = y_offset;
+}
+
+static const struct xdg_toplevel_drag_v1_interface _e_xdg_toplevel_drag_interface =
+{
+   _e_xdg_toplevel_drag_cb_destroy,
+   _e_xdg_toplevel_drag_cb_attach,
+};
+
+static void
+_e_xdg_toplevel_drag_res_destroy(struct wl_resource *resource)
+{
+   Toplevel_Drag *td = wl_resource_get_user_data(resource);
+
+   if (!td) return;
+   /* Leave the source's dnd_only flag set: it records that this source was
+    * once made into a toplevel drag, and the protocol never lets such a
+    * source become a selection afterwards. */
+   if (td->source) td->source->drag_toplevel = NULL;
+   free(td);
+}
+
+static void
+_e_xdg_toplevel_drag_manager_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+   wl_resource_destroy(resource);
+}
+
+static void
+_e_xdg_toplevel_drag_manager_cb_get(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *source_resource)
+{
+   E_Comp_Wl_Data_Source *source;
+   Toplevel_Drag *td;
+   struct wl_resource *res;
+
+   if (!(source = wl_resource_get_user_data(source_resource))) return;
+
+   td = E_NEW(Toplevel_Drag, 1);
+   if (!td)
+     {
+        wl_resource_post_no_memory(resource);
+        return;
+     }
+
+   res = wl_resource_create(client, &xdg_toplevel_drag_v1_interface,
+                            wl_resource_get_version(resource), id);
+   if (!res)
+     {
+        free(td);
+        wl_resource_post_no_memory(resource);
+        return;
+     }
+
+   td->res = res;
+   td->source = source;
+   wl_resource_set_implementation(res, &_e_xdg_toplevel_drag_interface, td,
+                                  _e_xdg_toplevel_drag_res_destroy);
+
+   /* From here the source is a drag-and-drop source and nothing else:
+    * "Attempting to use the source other than for drag-and-drop such as in
+    * wl_data_device.set_selection will raise an invalid_source error." The
+    * refusal itself is in _e_comp_wl_data_device_cb_selection_set. */
+   source->dnd_only = 1;
+}
+
+static const struct xdg_toplevel_drag_manager_v1_interface _e_xdg_toplevel_drag_manager_interface =
+{
+   _e_xdg_toplevel_drag_manager_cb_destroy,
+   _e_xdg_toplevel_drag_manager_cb_get,
+};
+
+static void
+_e_xdg_toplevel_drag_manager_cb_bind(struct wl_client *client, void *data EINA_UNUSED, uint32_t version, uint32_t id)
+{
+   struct wl_resource *res;
+
+   res = wl_resource_create(client, &xdg_toplevel_drag_manager_v1_interface,
+                            version, id);
+   if (!res)
+     {
+        wl_client_post_no_memory(client);
+        return;
+     }
+   wl_resource_set_implementation(res, &_e_xdg_toplevel_drag_manager_interface,
+                                  NULL, NULL);
+}
+
+EINTERN Eina_Bool
+e_xdg_toplevel_drag_init(void)
+{
+   return !!wl_global_create(e_comp_wl->wl.disp,
+                             &xdg_toplevel_drag_manager_v1_interface, 1,
+                             NULL, _e_xdg_toplevel_drag_manager_cb_bind);
+}
diff --git a/src/protocol/README b/src/protocol/README
new file mode 100644
index 000000000..4d5418311
--- /dev/null
+++ b/src/protocol/README
@@ -0,0 +1,43 @@
+Protocol XML in this directory
+==============================
+
+Two different kinds of file live here, and the difference matters when one of
+them needs updating.
+
+E's own protocols
+-----------------
+
+    action_route.xml
+    efl-aux-hints.xml
+    session-recovery.xml
+
+Ours. Edit them here; there is no upstream to track.
+
+Vendored from wayland-protocols
+-------------------------------
+
+    xdg-foreign-unstable-v1.xml
+    xdg-toplevel-drag-v1.xml    upstream staging/, fetched 2026-08-18
+                                sha256 687d7a1db0ced0e2794dc2d5be1a80b1dc71c46a055aedd882838586345ee1dc
+                                https://gitlab.freedesktop.org/wayland/wayland-protocols
+                                /-/raw/main/staging/xdg-toplevel-drag/xdg-toplevel-drag-v1.xml
+
+Copies of upstream files, carried because the wayland-protocols a build machine
+has is not ours to choose. `xdg-toplevel-drag-v1.xml` is not in 1.33, which is
+what this tree is built against; without a vendored copy the feature would
+silently vanish on any machine below whatever version first ships it, and a
+browser would lose tab tear-off for reasons nobody could see from here.
+
+Two rules for these:
+
+* **Keep them byte-identical to upstream.** They are not ours to improve. If
+  one needs a local change, that is a conversation with upstream, not a diff
+  here. The checksum above is how to tell whether that rule still holds.
+* **The build uses the vendored copy, always** - never
+  `dependency('wayland-protocols')` for the same protocol. If both were wired
+  up, which one a given machine compiled against would depend on what it had
+  installed, and two machines could disagree about the protocol while both
+  looking fine.
+
+To update one: fetch the new file, replace it whole, update the checksum and
+date above, and re-run the tests. Do not merge changes into it by hand.
diff --git a/src/protocol/xdg-toplevel-drag-v1.xml b/src/protocol/xdg-toplevel-drag-v1.xml
new file mode 100644
index 000000000..e14497df6
--- /dev/null
+++ b/src/protocol/xdg-toplevel-drag-v1.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="xdg_toplevel_drag_v1">
+
+  <copyright>
+    Copyright 2023 David Redondo
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice (including the next
+    paragraph) shall be included in all copies or substantial portions of the
+    Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+  </copyright>
+
+  <interface name="xdg_toplevel_drag_manager_v1" version="1">
+    <description summary="Move a window during a drag">
+      This protocol enhances normal drag and drop with the ability to move a
+      window at the same time. This allows having detachable parts of a window
+      that when dragged out of it become a new window and can be dragged over
+      an existing window to be reattached.
+
+      A typical workflow would be when the user starts dragging on top of a
+      detachable part of a window, the client would create a wl_data_source and
+      a xdg_toplevel_drag_v1 object and start the drag as normal via
+      wl_data_device.start_drag. Once the client determines that the detachable
+      window contents should be detached from the originating window, it creates
+      a new xdg_toplevel with these contents and issues a
+      xdg_toplevel_drag_v1.attach request before mapping it. From now on the new
+      window is moved by the compositor during the drag as if the client called
+      xdg_toplevel.move.
+
+      Dragging an existing window is similar. The client creates a
+      xdg_toplevel_drag_v1 object and attaches the existing toplevel before
+      starting the drag.
+
+      Clients use the existing drag and drop mechanism to detect when a window
+      can be docked or undocked. If the client wants to snap a window into a
+      parent window it should delete or unmap the dragged top-level. If the
+      contents should be detached again it attaches a new toplevel as described
+      above. If a drag operation is cancelled without being dropped, clients
+      should revert to the previous state, deleting any newly created windows
+      as appropriate. When a drag operation ends as indicated by
+      wl_data_source.dnd_drop_performed the dragged toplevel window's final
+      position is determined as if a xdg_toplevel_move operation ended.
+
+      Warning! The protocol described in this file is currently in the testing
+      phase. Backward compatible changes may be added together with the
+      corresponding interface version bump. Backward incompatible changes can
+      only be done by creating a new major version of the extension.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_source" value="0"
+             summary="data_source already used for toplevel drag"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the xdg_toplevel_drag_manager_v1 object">
+        Destroy this xdg_toplevel_drag_manager_v1 object. Other objects,
+        including xdg_toplevel_drag_v1 objects created by this factory, are not
+        affected by this request.
+      </description>
+    </request>
+
+    <request name="get_xdg_toplevel_drag">
+      <description summary="get an xdg_toplevel_drag for a wl_data_source">
+        Create an xdg_toplevel_drag for a drag and drop operation that is going
+        to be started with data_source.
+
+        This request can only be made on sources used in drag-and-drop, so it
+        must be performed before wl_data_device.start_drag. Attempting to use
+        the source other than for drag-and-drop such as in
+        wl_data_device.set_selection will raise an invalid_source error.
+
+        Destroying data_source while a toplevel is attached to the
+        xdg_toplevel_drag is undefined.
+      </description>
+
+      <arg name="id" type="new_id" interface="xdg_toplevel_drag_v1"/>
+      <arg name="data_source" type="object" interface="wl_data_source"/>
+    </request>
+  </interface>
+
+  <interface name="xdg_toplevel_drag_v1" version="1">
+    <description summary="Object representing a toplevel move during a drag">
+    </description>
+
+    <enum name="error">
+      <entry name="toplevel_attached" value="0"
+             summary="valid toplevel already attached"/>
+      <entry name="ongoing_drag" value="1"
+             summary="drag has not ended" />
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy an xdg_toplevel_drag_v1 object">
+        Destroy this xdg_toplevel_drag_v1 object. This request must only be
+        called after the underlying wl_data_source drag has ended, as indicated
+        by the dnd_drop_performed or cancelled events. In any other case an
+        ongoing_drag error is raised.
+      </description>
+    </request>
+
+    <request name="attach">
+      <description summary="Move a toplevel with the drag operation">
+        Request that the window will be moved with the cursor during the drag
+        operation. The offset is a hint to the compositor how the toplevel
+        should be positioned relative to the cursor hotspot in surface local
+        coordinates and relative to the geometry of the toplevel being attached.
+        See xdg_surface.set_window_geometry. For example it might only
+        be used when an unmapped window is attached. The attached window
+        does not participate in the selection of the drag target.
+
+        If the toplevel is unmapped while it is attached, it is automatically
+        detached from the drag. In this case this request has to be called again
+        if the window should be attached after it is remapped.
+
+        This request can be called multiple times but issuing it while a
+        toplevel with an active role is attached raises a toplevel_attached
+        error.
+      </description>
+
+      <arg name="toplevel" type="object" interface="xdg_toplevel"/>
+      <arg name="x_offset" type="int" summary="dragged surface x offset"/>
+      <arg name="y_offset" type="int" summary="dragged surface y offset"/>
+    </request>
+
+  </interface>
+</protocol>
+
diff --git a/src/tests/wayland/e_wl_testkit.c b/src/tests/wayland/e_wl_testkit.c
index 60aefe193..8e1b98f08 100644
--- a/src/tests/wayland/e_wl_testkit.c
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -478,6 +478,12 @@ tk_toplevel_surface(Tk_Toplevel *top)
    return top->surface;
 }
 
+struct xdg_toplevel *
+tk_toplevel_xdg(Tk_Toplevel *top)
+{
+   return top->toplevel;
+}
+
 int
 tk_check_error(Tk *tk)
 {
diff --git a/src/tests/wayland/e_wl_testkit.h b/src/tests/wayland/e_wl_testkit.h
index e2653b401..71d5f54d4 100644
--- a/src/tests/wayland/e_wl_testkit.h
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -73,6 +73,10 @@ Tk_Toplevel *tk_toplevel_new(Tk *tk, const char *app_id, const char *title,
  * owns it and destroys it with the connection. */
 struct wl_surface *tk_toplevel_surface(Tk_Toplevel *top);
 
+/* The xdg_toplevel behind one, for the protocols that take the role object
+ * rather than the surface - xdg_toplevel_drag_v1.attach is the first. */
+struct xdg_toplevel *tk_toplevel_xdg(Tk_Toplevel *top);
+
 /* A toplevel whose surface is `shadow` pixels larger than its window geometry
  * on every side, declared with xdg_surface.set_window_geometry - what every
  * client that draws its own shadow looks like on the wire.
diff --git a/src/tests/wayland/globals.expected b/src/tests/wayland/globals.expected
index d3489d992..37d26dd33 100644
--- a/src/tests/wayland/globals.expected
+++ b/src/tests/wayland/globals.expected
@@ -13,6 +13,7 @@ wp_presentation	1
 wp_single_pixel_buffer_manager_v1	1
 wp_viewporter	1
 xdg_activation_v1	1
+xdg_toplevel_drag_manager_v1	1
 xdg_wm_base	6
 zwp_e_session_recovery	1
 zwp_idle_inhibit_manager_v1	1
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 040e89b94..b313c0e34 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -44,6 +44,7 @@ foreach p: [
   '@0@/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml'.format(dir_wayland_protocols),
   '@0@/unstable/xdg-foreign/xdg-foreign-unstable-v2.xml'.format(dir_wayland_protocols),
   '@0@/staging/content-type/content-type-v1.xml'.format(dir_wayland_protocols),
+  '../../protocol/xdg-toplevel-drag-v1.xml',
 ]
   test_proto_src += gen_scanner_client.process(p)
   test_proto_src += gen_scanner_impl.process(p)
@@ -88,6 +89,7 @@ wl_protocol_tests = [
   ['presentation-time', 'test_presentation_time.c'],
   ['xdg-foreign', 'test_xdg_foreign.c'],
   ['content-type', 'test_content_type.c'],
+  ['toplevel-drag', 'test_toplevel_drag.c'],
 ]
 
 # Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_toplevel_drag.c b/src/tests/wayland/test_toplevel_drag.c
new file mode 100644
index 000000000..badf0473c
--- /dev/null
+++ b/src/tests/wayland/test_toplevel_drag.c
@@ -0,0 +1,186 @@
+/* xdg_toplevel_drag_v1: a real window carried by a drag-and-drop operation.
+ *
+ * Chrome tab tear-off. The client starts an ordinary drag, decides for itself
+ * that the tab has come loose, maps a toplevel with the tab's contents and
+ * attaches it; from then the compositor keeps that window under the cursor
+ * until the drag ends.
+ *
+ * The window actually moving is the feature, and it is the one thing here that
+ * cannot be checked by watching for a protocol error - so it is checked
+ * directly: start a drag, warp the pointer, and read the window's position
+ * back from the compositor over the wl_test back door. Everything else in this
+ * file is the contract around that movement.
+ *
+ * Each error case gets its own connection, because a protocol error ends the
+ * one it happens on, and "the compositor disconnected us" is the only way a
+ * client can observe one.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "e_wl_testkit.h"
+#include "xdg-toplevel-drag-v1-client-protocol.h"
+
+#define PROG "test-toplevel-drag"
+#define DRAGGED "toplevel-drag-window"
+
+#define SETTLE_MS 5000
+
+static struct xdg_toplevel_drag_manager_v1 *
+_manager_get(Tk *tk)
+{
+   struct xdg_toplevel_drag_manager_v1 *mgr;
+
+   if (tk_global_version(tk, "xdg_toplevel_drag_manager_v1") < 1)
+     tk_fail(tk, "no xdg_toplevel_drag_manager_v1 - dragging a tab out of a "
+                 "browser window has nothing to move, so the tab detaches and "
+                 "the new window sits wherever it was mapped while the cursor "
+                 "goes on without it");
+
+   mgr = tk_bind(tk, &xdg_toplevel_drag_manager_v1_interface, 1);
+   if (!mgr) tk_fail(tk, "advertised but would not bind");
+   return mgr;
+}
+
+/* A drag needs a real button press behind it: start_drag is validated against
+ * a serial from an actual pointer event, and E's drag follows the pointer it
+ * was started from. */
+static void
+_drag_begin(Tk *tk, struct wl_data_device *dev, struct wl_data_source *src,
+            struct wl_surface *origin, int x, int y)
+{
+   tk_pointer_warp(tk, x, y);
+   tk_sync(tk);
+   tk_pointer_button(tk, TK_BTN_LEFT, 1);
+   tk_sync(tk);
+
+   wl_data_source_offer(src, "text/plain");
+   wl_data_device_start_drag(dev, src, origin, NULL, 0);
+   tk_sync(tk);
+}
+
+int
+main(void)
+{
+   Tk *tk;
+   Tk_Toplevel *origin, *dragged;
+   Tk_Client *c;
+   struct xdg_toplevel_drag_manager_v1 *mgr;
+   struct xdg_toplevel_drag_v1 *drag;
+   struct wl_data_device_manager *ddm;
+   struct wl_data_device *dev;
+   struct wl_data_source *src;
+   struct wl_seat *seat;
+   int x0, y0;
+
+   /* --------------------------------------------- the window moves */
+
+   tk = tk_connect(PROG);
+   mgr = _manager_get(tk);
+
+   ddm = tk_bind(tk, &wl_data_device_manager_interface, 3);
+   seat = tk_bind(tk, &wl_seat_interface, 5);
+   if ((!ddm) || (!seat))
+     tk_fail(tk, "no wl_data_device_manager or wl_seat to drive a drag with");
+   dev = wl_data_device_manager_get_data_device(ddm, seat);
+
+   /* The dragged window first, the origin last. E hands keyboard focus to the
+    * most recently mapped toplevel, and start_drag is silently dropped when
+    * the origin surface is not the focused one
+    * (_e_comp_wl_data_device_cb_drag_start) - so mapping them the other way
+    * round produces a drag that never begins and a test that blames the
+    * feature for it. */
+   dragged = tk_toplevel_new(tk, DRAGGED, "dragged", 200, 150);
+   origin = tk_toplevel_new(tk, "drag-origin", "origin", 300, 200);
+   tk_settle(tk);
+
+   c = tk_expect(tk, DRAGGED);
+   x0 = c->x; y0 = c->y;
+   printf(PROG ": dragged window starts at +%d+%d\n", x0, y0);
+
+   src = ""
+   drag = xdg_toplevel_drag_manager_v1_get_xdg_toplevel_drag(mgr, src);
+
+   _drag_begin(tk, dev, src, tk_toplevel_surface(origin), 400, 400);
+
+   /* Offset 0,0: the window's top-left goes to the cursor, which makes the
+    * expected position arithmetic-free and the failure message readable. */
+   xdg_toplevel_drag_v1_attach(drag, tk_toplevel_xdg(dragged), 0, 0);
+   tk_sync(tk);
+
+   tk_pointer_warp(tk, 600, 500);
+   tk_settle(tk);
+
+   c = tk_expect(tk, DRAGGED);
+   if ((c->x == x0) && (c->y == y0))
+     tk_fail(tk, "the pointer moved from 400,400 to 600,500 and the attached "
+                 "window is still at +%d+%d, exactly where it started. It is "
+                 "not being carried by the drag at all", c->x, c->y);
+   if ((c->x != 600) || (c->y != 500))
+     tk_fail(tk, "attached at offset 0,0 with the pointer at 600,500, so the "
+                 "window should be at +600+500; it is at +%d+%d. It moves, "
+                 "but not to where the client asked", c->x, c->y);
+   printf(PROG ": window followed the cursor to +%d+%d\n", c->x, c->y);
+
+   /* Move again, to be sure it tracks rather than having been positioned once
+    * by the attach itself. */
+   tk_pointer_warp(tk, 300, 250);
+   tk_settle(tk);
+   c = tk_expect(tk, DRAGGED);
+   if ((c->x != 300) || (c->y != 250))
+     tk_fail(tk, "second move: pointer at 300,250, window at +%d+%d. It was "
+                 "placed once and then stopped tracking", c->x, c->y);
+   printf(PROG ": and again to +%d+%d\n", c->x, c->y);
+
+   tk_pointer_button(tk, TK_BTN_LEFT, 0);
+   tk_settle(tk);
+   tk_disconnect(tk);
+
+   /* ------------------------------ destroy while the drag is running */
+
+   tk = tk_connect(PROG);
+   mgr = _manager_get(tk);
+   ddm = tk_bind(tk, &wl_data_device_manager_interface, 3);
+   seat = tk_bind(tk, &wl_seat_interface, 5);
+   dev = wl_data_device_manager_get_data_device(ddm, seat);
+   origin = tk_toplevel_new(tk, "drag-origin", "origin", 300, 200);
+   tk_settle(tk);
+
+   src = ""
+   drag = xdg_toplevel_drag_manager_v1_get_xdg_toplevel_drag(mgr, src);
+   _drag_begin(tk, dev, src, tk_toplevel_surface(origin), 400, 400);
+
+   xdg_toplevel_drag_v1_destroy(drag);
+   if (!tk_check_error(tk))
+     tk_fail(tk, "destroying an xdg_toplevel_drag_v1 mid-drag was accepted. "
+                 "The protocol names this ongoing_drag, and a compositor that "
+                 "allows it is left moving a window on behalf of an object "
+                 "the client believes is gone");
+   printf(PROG ": destroy during the drag is refused\n");
+   tk_disconnect(tk);
+
+   /* ------------------------------ the source is drag-and-drop only */
+
+   tk = tk_connect(PROG);
+   mgr = _manager_get(tk);
+   ddm = tk_bind(tk, &wl_data_device_manager_interface, 3);
+   seat = tk_bind(tk, &wl_seat_interface, 5);
+   dev = wl_data_device_manager_get_data_device(ddm, seat);
+
+   src = ""
+   wl_data_source_offer(src, "text/plain");
+   xdg_toplevel_drag_manager_v1_get_xdg_toplevel_drag(mgr, src);
+   wl_data_device_set_selection(dev, src, 0);
+
+   if (!tk_check_error(tk))
+     tk_fail(tk, "a source with an xdg_toplevel_drag was accepted as the "
+                 "selection. The protocol says such a source may only ever be "
+                 "used for drag-and-drop, and names set_selection as the case "
+                 "to refuse");
+   printf(PROG ": a toplevel-drag source cannot become the selection\n");
+   tk_disconnect(tk);
+
+   printf(PROG ": ok\n");
+   return 0;
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to