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.

Reply via email to