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.