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 b6301ad9b083e718599ea46cd8dd9aff6c773180
Author: Cedric BAIL <[email protected]>
AuthorDate: Sun Aug 16 22:47:56 2026 -0600
e_client, e_comp_wl - a drag has to know where it started
Moving and resizing a window from anything other than a mouse binding was
broken in two different ways, and both come from the same missing line.
e_client_act_move_begin and e_client_act_resize_begin gather the drag's origin
only when they are handed a mouse event:
if (ev) { snprintf(source, ..., "mouse,down,%i", ev->button);
_e_client_moveinfo_gather(ec, source); }
ACT_FN_GO(window_move) passes NULL, and so does every key binding, signal
binding and test. With no gather, moveinfo.down keeps whatever it held - zero
on a window that has never been dragged - so:
* a move put the window *at* the pointer instead of moving it *by* how far
the pointer had travelled. Grab a window anywhere and its top-left corner
jumps under the cursor. Measured: a 60,45 drag from the middle of a
320x240 window at the origin left it at +220+165 instead of +60+45.
* a resize grew the window from a starting size of zero, so the first motion
collapsed it to its minimum. Measured: a 70,50 corner drag turned a
420x320 window into 1x1.
_e_client_moveinfo_gather already has the right answer for the no-button case -
start from where the pointer is now - so both call it with a source naming no
button. It was not recording the window's size in that branch, which is the
other half of what a resize needs, so it does now; the button case reads that
out of mouse.last_down[], which a press fills in.
Which leaves the array. _e_comp_wl_evas_cb_resize indexed
mouse.last_down[moveinfo.down.button - 1] four times with no guard, and button
is 0 for a drag that did not start from a press, so [-1]. e_client.c's own
resize handler has guarded this for as long as it has existed and falls back to
moveinfo.down; do the same, resolving the press position once instead of
indexing four more times.
test_client_move.c covers both ways a user moves a window and both ways a user
resizes one, and asserts the two things that go wrong in practice: a move must
not resize, and a resize must reach the client - E growing its own idea of the
frame while the client keeps painting at the old size is the split that had a
browser sitting in a corner. The interactive cases go through E's own grab
rather than window_move_by, because the grab is where the arithmetic is.
Driving a grab needs both halves of an action, so wl_test grows
client_action_end: E's bindings run the first half on press and the second on
release, and window_move takes a grab that nothing else will ever end.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
src/bin/e_client.c | 30 ++++++++
src/bin/e_comp_wl.c | 36 +++++++---
src/modules/wl_test/e_mod_main.c | 22 ++++++
src/modules/wl_test/wl-test.xml | 22 ++++++
src/tests/wayland/e_wl_testkit.c | 28 ++++++++
src/tests/wayland/e_wl_testkit.h | 17 +++++
src/tests/wayland/meson.build | 1 +
src/tests/wayland/test_client_move.c | 133 +++++++++++++++++++++++++++++++++++
8 files changed, 280 insertions(+), 9 deletions(-)
diff --git a/src/bin/e_client.c b/src/bin/e_client.c
index 427c45b1c..1206e8021 100644
--- a/src/bin/e_client.c
+++ b/src/bin/e_client.c
@@ -997,6 +997,14 @@ _e_client_moveinfo_gather(E_Client *ec, const char *source)
{
ec->moveinfo.down.mx = ec->mouse.current.mx;
ec->moveinfo.down.my = ec->mouse.current.my;
+ /* And how big it was. With a button, _e_client_resize_handle reads
+ * this out of mouse.last_down[], which the press filled in. Without
+ * one nothing fills it, so a resize started from a key binding, a
+ * signal or a test grew the window from whatever down.w happened to
+ * hold - zero on a window that has never been dragged - and the first
+ * motion collapsed it to its minimum size. */
+ ec->moveinfo.down.w = ec->w;
+ ec->moveinfo.down.h = ec->h;
}
}
@@ -5099,6 +5107,21 @@ e_client_act_move_begin(E_Client *ec, E_Binding_Event_Mouse_Button *ev)
snprintf(source, sizeof(source) - 1, "mouse,down,%i", ev->button);
_e_client_moveinfo_gather(ec, source);
}
+ else
+ {
+ /* A move has to know where it started from, and with no button event
+ * there was nothing to record it. Skipping the gather left
+ * moveinfo.down at whatever it held - 0,0 on a window that has never
+ * been dragged - so the first motion moved the window to the pointer
+ * instead of by the distance the pointer had travelled: grab a window
+ * anywhere and its top-left corner jumps under the cursor.
+ *
+ * _e_client_moveinfo_gather already has the right answer for this
+ * case. A source naming no button takes its else branch and starts the
+ * drag from where the pointer is now, which is what every caller
+ * passing NULL means - a key binding, a signal, a test. */
+ _e_client_moveinfo_gather(ec, "no,button");
+ }
if (!_e_client_move_begin(ec))
return;
@@ -5134,6 +5157,13 @@ e_client_act_resize_begin(E_Client *ec, E_Binding_Event_Mouse_Button *ev)
snprintf(source, sizeof(source) - 1, "mouse,down,%i", ev->button);
_e_client_moveinfo_gather(ec, source);
}
+ else
+ {
+ /* Same as e_client_act_move_begin: with no button event there is
+ * nothing to record where the drag started from or how big the window
+ * was, and the gather is where both come from. */
+ _e_client_moveinfo_gather(ec, "no,button");
+ }
if ((ec->mouse.current.mx > (ec->x + ec->w / 5)) &&
(ec->mouse.current.mx < (ec->x + ec->w * 4 / 5)))
{
diff --git a/src/bin/e_comp_wl.c b/src/bin/e_comp_wl.c
index 16f857de1..33134f56e 100644
--- a/src/bin/e_comp_wl.c
+++ b/src/bin/e_comp_wl.c
@@ -1189,10 +1189,30 @@ _e_comp_wl_evas_cb_resize(void *data, Evas_Object *obj EINA_UNUSED, void *event
if (e_client_util_resizing_get(ec) && e_comp_wl->resize.edges)
{
- int x, y;
+ int x, y, dmx, dmy;
- x = ec->mouse.last_down[ec->moveinfo.down.button - 1].w;
- y = ec->mouse.last_down[ec->moveinfo.down.button - 1].h;
+ /* Where the drag started, and how big the window was when it did.
+ * button is 0 for a resize that did not begin with a button press - a
+ * key binding, a signal, a test - and last_down[-1] is not a place to
+ * read from. moveinfo.down carries the same four numbers for that
+ * case, which is what _e_client_resize_handle already uses. Resolve
+ * them once here rather than indexing an array four more times. */
+ if ((ec->moveinfo.down.button >= 1) && (ec->moveinfo.down.button <= 3))
+ {
+ int b = ec->moveinfo.down.button - 1;
+
+ x = ec->mouse.last_down[b].w;
+ y = ec->mouse.last_down[b].h;
+ dmx = ec->mouse.last_down[b].mx;
+ dmy = ec->mouse.last_down[b].my;
+ }
+ else
+ {
+ x = ec->moveinfo.down.w;
+ y = ec->moveinfo.down.h;
+ dmx = ec->moveinfo.down.mx;
+ dmy = ec->moveinfo.down.my;
+ }
if (e_comp_object_frame_exists(ec->frame))
e_comp_object_frame_wh_unadjust(ec->frame, x, y, &x, &y);
@@ -1201,13 +1221,12 @@ _e_comp_wl_evas_cb_resize(void *data, Evas_Object *obj EINA_UNUSED, void *event
case E_POINTER_RESIZE_TL:
case E_POINTER_RESIZE_L:
case E_POINTER_RESIZE_BL:
- x += ec->mouse.last_down[ec->moveinfo.down.button - 1].mx -
- ec->mouse.current.mx;
+ x += dmx - ec->mouse.current.mx;
break;
case E_POINTER_RESIZE_TR:
case E_POINTER_RESIZE_R:
case E_POINTER_RESIZE_BR:
- x += ec->mouse.current.mx - ec->mouse.last_down[ec->moveinfo.down.button - 1].mx;
+ x += ec->mouse.current.mx - dmx;
break;
default:
break;;
@@ -1217,13 +1236,12 @@ _e_comp_wl_evas_cb_resize(void *data, Evas_Object *obj EINA_UNUSED, void *event
case E_POINTER_RESIZE_TL:
case E_POINTER_RESIZE_T:
case E_POINTER_RESIZE_TR:
- y += ec->mouse.last_down[ec->moveinfo.down.button - 1].my -
- ec->mouse.current.my;
+ y += dmy - ec->mouse.current.my;
break;
case E_POINTER_RESIZE_BL:
case E_POINTER_RESIZE_B:
case E_POINTER_RESIZE_BR:
- y += ec->mouse.current.my - ec->mouse.last_down[ec->moveinfo.down.button - 1].my;
+ y += ec->mouse.current.my - dmy;
break;
default:
break;
diff --git a/src/modules/wl_test/e_mod_main.c b/src/modules/wl_test/e_mod_main.c
index 136903d5a..aa6f531da 100644
--- a/src/modules/wl_test/e_mod_main.c
+++ b/src/modules/wl_test/e_mod_main.c
@@ -191,6 +191,27 @@ _wl_test_cb_client_action(struct wl_client *client EINA_UNUSED, struct wl_resour
act->func.go(E_OBJECT(ec), (params && params[0]) ? params : NULL);
}
+static void
+_wl_test_cb_client_action_end(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t id, const char *name, const char *params)
+{
+ E_Client *ec;
+ E_Action *act;
+
+ ec = _wl_test_client_by_id(resource, id);
+ if (!ec) return;
+
+ act = e_action_find(name);
+ if ((!act) || (!act->func.end))
+ {
+ wl_resource_post_error(resource, WL_TEST_ERROR_UNKNOWN_ACTION,
+ "no action named '%s' with a second half",
+ name ?: "");
+ return;
+ }
+
+ act->func.end(E_OBJECT(ec), (params && params[0]) ? params : NULL);
+}
+
static void
_wl_test_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
{
@@ -533,6 +554,7 @@ static const struct wl_test_interface _wl_test_implementation =
_wl_test_cb_screensaver_enable,
_wl_test_cb_get_clients,
_wl_test_cb_client_action,
+ _wl_test_cb_client_action_end,
};
static void
diff --git a/src/modules/wl_test/wl-test.xml b/src/modules/wl_test/wl-test.xml
index 6004f7be1..0d8bcb437 100644
--- a/src/modules/wl_test/wl-test.xml
+++ b/src/modules/wl_test/wl-test.xml
@@ -424,5 +424,27 @@
<arg name="name" type="string"/>
<arg name="params" type="string"/>
</request>
+
+ <request name="client_action_end" since="4">
+ <description summary="finish an action that holds a grab">
+ E's actions come in two halves. A binding runs the first on press and
+ the second on release, and the interactive ones live entirely in
+ between: window_move starts a move grab and does not end it, and
+ nothing else will, because the release that would have ended it belongs
+ to the binding the test did not use.
+
+ So a test that drives an interactive move or resize has to say when the
+ button came back up, and this is how. Between the two, pointer_move is
+ what the grab reacts to - which is the point of driving it this way
+ rather than calling window_move_by: it goes through the same grab a
+ user's drag does.
+
+ Posts unknown_action if the action exists but has no second half, since
+ that is a test asking for something the action cannot do.
+ </description>
+ <arg name="id" type="uint"/>
+ <arg name="name" type="string"/>
+ <arg name="params" type="string"/>
+ </request>
</interface>
</protocol>
diff --git a/src/tests/wayland/e_wl_testkit.c b/src/tests/wayland/e_wl_testkit.c
index e1ee07c15..a72b8214c 100644
--- a/src/tests/wayland/e_wl_testkit.c
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -404,6 +404,13 @@ tk_toplevel_configured(Tk_Toplevel *top, int *w, int *h, int *count)
if (count) *count = top->configures;
}
+void
+tk_toplevel_painted(Tk_Toplevel *top, int *w, int *h)
+{
+ if (w) *w = top->w;
+ if (h) *h = top->h;
+}
+
void
tk_settle(Tk *tk)
{
@@ -508,3 +515,24 @@ tk_action(Tk *tk, unsigned int id, const char *name, const char *params)
wl_test_client_action(tk->tester, id, name, params ?: "");
tk_settle(tk);
}
+
+void
+tk_action_end(Tk *tk, unsigned int id, const char *name, const char *params)
+{
+ wl_test_client_action_end(tk->tester, id, name, params ?: "");
+ tk_settle(tk);
+}
+
+void
+tk_pointer_warp(Tk *tk, int x, int y)
+{
+ wl_test_pointer_warp(tk->tester, x, y);
+ tk_settle(tk);
+}
+
+void
+tk_pointer_move(Tk *tk, int dx, int dy)
+{
+ wl_test_pointer_move(tk->tester, dx, dy);
+ tk_settle(tk);
+}
diff --git a/src/tests/wayland/e_wl_testkit.h b/src/tests/wayland/e_wl_testkit.h
index 59bc71004..a669f2f22 100644
--- a/src/tests/wayland/e_wl_testkit.h
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -74,6 +74,13 @@ Tk_Toplevel *tk_toplevel_new(Tk *tk, const char *app_id, const char *title,
* the bug - so a test that asserts geometry should be able to say which. */
void tk_toplevel_configured(Tk_Toplevel *top, int *w, int *h, int *count);
+/* The size this toplevel last actually painted at. Not the same question as
+ * the last configure: a configure of 0x0 means "you choose", which is what E
+ * sends once an interactive resize ends, so the last configure size is not
+ * where the window ended up. This is what to compare E's frame against when
+ * asking whether the compositor and the client agree. */
+void tk_toplevel_painted(Tk_Toplevel *top, int *w, int *h);
+
/* Round-trip through the compositor's own main loop, so effects E applies from
* a job or an idler have landed. Not wl_display.sync, which only proves the
* requests were read. */
@@ -113,4 +120,14 @@ Tk_Client *tk_wait_state(Tk *tk, const char *app_id, unsigned int mask,
/* Run one of E's own actions on a window. Empty or NULL params means none. */
void tk_action(Tk *tk, unsigned int id, const char *name, const char *params);
+/* Finish an action that took a grab - the half a binding would run on button
+ * release. window_move and window_resize need it; nothing else does. */
+void tk_action_end(Tk *tk, unsigned int id, const char *name, const char *params);
+
+/* Put the pointer somewhere, and move it by a delta. A grab drags by the
+ * delta from where the pointer was when it started, so a test driving an
+ * interactive move has to place the pointer first. */
+void tk_pointer_warp(Tk *tk, int x, int y);
+void tk_pointer_move(Tk *tk, int dx, int dy);
+
#endif
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index fdad19c04..d0ac6094f 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -77,6 +77,7 @@ wl_protocol_tests = [
['client-list', 'test_client_list.c'],
['client-action', 'test_client_action.c'],
['client-state', 'test_client_state.c'],
+ ['client-move', 'test_client_move.c'],
]
# Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_client_move.c b/src/tests/wayland/test_client_move.c
new file mode 100644
index 000000000..a12027b47
--- /dev/null
+++ b/src/tests/wayland/test_client_move.c
@@ -0,0 +1,133 @@
+/* Moving and resizing a window, the two ways a user does it.
+ *
+ * The interactive case is the one that matters and the one that is hard to
+ * fake: window_move takes a grab, pointer motion drags the window, and the
+ * release ends it. Driving it through the grab rather than through
+ * window_move_by is the whole point - it is the same path an alt-drag takes,
+ * and it is where a compositor gets the arithmetic wrong.
+ *
+ * Two things every case here checks, because they are the two ways this goes
+ * wrong in practice:
+ *
+ * - a move must not resize. A drag that quietly changes the window's size is
+ * the classic frame-offset bug, and it is invisible if you only assert the
+ * position.
+ * - a resize must reach the client. E growing its own idea of the frame
+ * while the client keeps painting at the old size is exactly the split
+ * that made a browser sit in a corner, so the size the client was told and
+ * the size E reports have to agree.
+ */
+#include <stdio.h>
+
+#include "e_wl_testkit.h"
+
+#define APP_ID "e.test.client_move"
+#define TITLE "moves under test"
+#define W 320
+#define H 240
+
+int
+main(void)
+{
+ Tk *tk;
+ Tk_Toplevel *top;
+ Tk_Client *c;
+ unsigned int id;
+ int x0, y0, w0, h0, cw, ch;
+
+ tk = tk_connect("test-client-move");
+ top = tk_toplevel_new(tk, APP_ID, TITLE, W, H);
+
+ c = tk_expect(tk, APP_ID);
+ id = c->id;
+ x0 = c->x; y0 = c->y; w0 = c->w; h0 = c->h;
+
+ /* ------------------------------------------------- programmatic move */
+
+ tk_action(tk, id, "window_move_by", "40 30");
+
+ c = tk_expect(tk, APP_ID);
+ if ((c->x != x0 + 40) || (c->y != y0 + 30))
+ tk_fail(tk, "window_move_by 40 30 put the window at +%d+%d, expected "
+ "+%d+%d", c->x, c->y, x0 + 40, y0 + 30);
+ if ((c->w != w0) || (c->h != h0))
+ tk_fail(tk, "moving resized the window to %dx%d, was %dx%d",
+ c->w, c->h, w0, h0);
+
+ tk_action(tk, id, "window_move_by", "-40 -30");
+ c = tk_expect(tk, APP_ID);
+ if ((c->x != x0) || (c->y != y0))
+ tk_fail(tk, "moving back put the window at +%d+%d, expected +%d+%d",
+ c->x, c->y, x0, y0);
+
+ /* -------------------------------------------------- interactive move */
+
+ /* The pointer has to be over the window before the grab starts: a move
+ * grab drags by the delta from where the pointer was when it began. */
+ tk_pointer_warp(tk, c->x + (c->w / 2), c->y + (c->h / 2));
+ tk_action(tk, id, "window_move", NULL);
+ tk_pointer_move(tk, 60, 45);
+ tk_action_end(tk, id, "window_move", NULL);
+
+ c = tk_expect(tk, APP_ID);
+ if ((c->x != x0 + 60) || (c->y != y0 + 45))
+ tk_fail(tk, "a 60,45 drag put the window at +%d+%d, expected +%d+%d",
+ c->x, c->y, x0 + 60, y0 + 45);
+ if ((c->w != w0) || (c->h != h0))
+ tk_fail(tk, "dragging resized the window to %dx%d, was %dx%d",
+ c->w, c->h, w0, h0);
+
+ tk_action(tk, id, "window_move_by", "-60 -45");
+ c = tk_expect(tk, APP_ID);
+ if ((c->x != x0) || (c->y != y0))
+ tk_fail(tk, "moving back after the drag put the window at +%d+%d, "
+ "expected +%d+%d", c->x, c->y, x0, y0);
+
+ /* ------------------------------------------------------------ resize */
+
+ tk_action(tk, id, "window_resize_by", "100 80");
+
+ c = tk_expect(tk, APP_ID);
+ if ((c->w != w0 + 100) || (c->h != h0 + 80))
+ tk_fail(tk, "window_resize_by 100 80 made the frame %dx%d, expected "
+ "%dx%d", c->w, c->h, w0 + 100, h0 + 80);
+
+ tk_toplevel_painted(top, &cw, &ch);
+ if ((cw != c->w) || (ch != c->h))
+ tk_fail(tk, "resized: E reports %dx%d and the client is painting %dx%d - "
+ "the compositor and the client disagree about the size",
+ c->w, c->h, cw, ch);
+
+ /* ------------------------------------------- interactive resize */
+
+ /* Bottom-right corner, so e_client_act_resize_begin picks RESIZE_BR and
+ * the drag grows the window rather than moving its top-left edge. */
+ c = tk_expect(tk, APP_ID);
+ x0 = c->x; y0 = c->y; w0 = c->w; h0 = c->h;
+ tk_pointer_warp(tk, c->x + c->w - 4, c->y + c->h - 4);
+ tk_action(tk, id, "window_resize", NULL);
+ tk_pointer_move(tk, 70, 50);
+ tk_action_end(tk, id, "window_resize", NULL);
+
+ c = tk_expect(tk, APP_ID);
+ if ((c->x != x0) || (c->y != y0))
+ tk_fail(tk, "a bottom-right resize moved the window to +%d+%d, was "
+ "+%d+%d - only the far corner should have moved",
+ c->x, c->y, x0, y0);
+ if ((c->w != w0 + 70) || (c->h != h0 + 50))
+ tk_fail(tk, "a 70,50 corner drag made the frame %dx%d, expected %dx%d",
+ c->w, c->h, w0 + 70, h0 + 50);
+
+ /* The painted size, not the last configure: E ends an interactive resize
+ * with a 0x0 configure meaning "keep what you have". */
+ tk_toplevel_painted(top, &cw, &ch);
+ if ((cw != c->w) || (ch != c->h))
+ tk_fail(tk, "after the drag E reports %dx%d and the client is painting "
+ "%dx%d", c->w, c->h, cw, ch);
+
+ printf("test-client-move: ok (id=%u dragged, moved and resized to "
+ "%dx%d+%d+%d)\n", id, c->w, c->h, c->x, c->y);
+
+ tk_disconnect(tk);
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.