]> git.mdlowis.com Git - proto/labwc.git/commitdiff
snap: cache and ignore last-snapped edge when growing or shrinking
authorAndrew J. Hesford <ajh@sideband.org>
Tue, 2 Apr 2024 19:58:50 +0000 (15:58 -0400)
committerJohan Malm <johanmalm@users.noreply.github.com>
Wed, 10 Apr 2024 22:30:28 +0000 (23:30 +0100)
When growing or shrinking a view by snapping to an edge, a client may
ignore the requested size and instead keep its original size or
substitute a different (possibly constrained) size. In this case, the
view may not actually contact the snapped edge, and a subsequent snap
attempt will just keep re-trying (and failing) to contact the same ege.

To mitigate this, remember the last-snapped view, snapping direction and
offset of the snapping edge in snap.c; when re-attempting a snap for the
same view in the same direction, ignore the edge that was last "hit", to
allow snapping to progress beyond the problematic edge.

include/edges.h
include/snap-constraints.h [new file with mode: 0644]
include/snap.h
src/edges.c
src/interactive.c
src/meson.build
src/resistance.c
src/snap-constraints.c [new file with mode: 0644]
src/snap.c
src/view.c
src/xdg.c

index 6ce029c32b76a6aca202cdfddcc6208d4e1feebb..47dd6f1b37dab0fb2f3f2166721c13aa6314893e 100644 (file)
@@ -105,12 +105,12 @@ void edges_adjust_geom(struct view *view, struct border edges,
        uint32_t resize_edges, struct wlr_box *geom);
 
 void edges_find_neighbors(struct border *nearest_edges, struct view *view,
-       struct wlr_box target, struct output *output,
-       edge_validator_t validator, bool use_pending, bool ignore_hidden);
+       struct wlr_box origin, struct wlr_box target,
+       struct output *output, edge_validator_t validator, bool ignore_hidden);
 
 void edges_find_outputs(struct border *nearest_edges, struct view *view,
-       struct wlr_box target, struct output *output,
-       edge_validator_t validator, bool use_pending);
+       struct wlr_box origin, struct wlr_box target,
+       struct output *output, edge_validator_t validator);
 
 void edges_adjust_move_coords(struct view *view, struct border edges,
        int *x, int *y, bool use_pending);
diff --git a/include/snap-constraints.h b/include/snap-constraints.h
new file mode 100644 (file)
index 0000000..294c53b
--- /dev/null
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef LABWC_SNAP_CONSTRAINTS_H
+#define LABWC_SNAP_CONSTRAINTS_H
+
+#include "common/border.h"
+#include "view.h"
+
+struct wlr_box;
+
+void snap_constraints_set(struct view *view,
+       enum view_edge direction, struct wlr_box geom);
+
+void snap_constraints_invalidate(struct view *view);
+
+void snap_constraints_update(struct view *view);
+
+struct wlr_box snap_constraints_effective(struct view *view,
+       enum view_edge direction);
+
+#endif /* LABWC_SNAP_CONSTRAINTS_H */
index 342e56f4efcb970eb2e064fdb244c5f63a877327..4ac8025d4c8498eb8a0b164549c7ae4b75372703 100644 (file)
@@ -16,4 +16,7 @@ void snap_grow_to_next_edge(struct view *view,
 void snap_shrink_to_next_edge(struct view *view,
        enum view_edge direction, struct wlr_box *geo);
 
+void snap_invalidate_edge_cache(struct view *view);
+void snap_update_cache_geometry(struct view *view);
+
 #endif /* LABWC_SNAP_H */
index 725356913f7dca60bd4e4dd1d9cf3aedad5ce21d..bd96c5a05d9ca2eda515b7b301a6cce8360fa497 100644 (file)
@@ -376,8 +376,8 @@ edges_calculate_visibility(struct server *server, struct view *ignored_view)
 
 void
 edges_find_neighbors(struct border *nearest_edges, struct view *view,
-               struct wlr_box target, struct output *output,
-               edge_validator_t validator, bool use_pending, bool ignore_hidden)
+               struct wlr_box origin, struct wlr_box target,
+               struct output *output, edge_validator_t validator, bool ignore_hidden)
 {
        assert(view);
        assert(validator);
@@ -391,10 +391,7 @@ edges_find_neighbors(struct border *nearest_edges, struct view *view,
        struct border view_edges = { 0 };
        struct border target_edges = { 0 };
 
-       struct wlr_box *view_geom =
-               use_pending ? &view->pending : &view->current;
-
-       edges_for_target_geometry(&view_edges, view, *view_geom);
+       edges_for_target_geometry(&view_edges, view, origin);
        edges_for_target_geometry(&target_edges, view, target);
 
        struct view *v;
@@ -437,8 +434,8 @@ edges_find_neighbors(struct border *nearest_edges, struct view *view,
 
 void
 edges_find_outputs(struct border *nearest_edges, struct view *view,
-               struct wlr_box target, struct output *output,
-               edge_validator_t validator, bool use_pending)
+               struct wlr_box origin, struct wlr_box target,
+               struct output *output, edge_validator_t validator)
 {
        assert(view);
        assert(validator);
@@ -453,10 +450,7 @@ edges_find_outputs(struct border *nearest_edges, struct view *view,
        struct border view_edges = { 0 };
        struct border target_edges = { 0 };
 
-       struct wlr_box *view_geom =
-               use_pending ? &view->pending : &view->current;
-
-       edges_for_target_geometry(&view_edges, view, *view_geom);
+       edges_for_target_geometry(&view_edges, view, origin);
        edges_for_target_geometry(&target_edges, view, target);
 
        struct output *o;
@@ -473,7 +467,7 @@ edges_find_outputs(struct border *nearest_edges, struct view *view,
                        output_usable_area_in_layout_coords(o);
 
                struct wlr_box ol;
-               if (!wlr_box_intersection(&ol, view_geom, &usable) &&
+               if (!wlr_box_intersection(&ol, &origin, &usable) &&
                                !wlr_box_intersection(&ol, &target, &usable)) {
                        continue;
                }
index 243aaa6a6e1baef6aa1725f23e89da40679ae1b2..d4679f3a22343d7635c3d6e37fcaeebabf58e77d 100644 (file)
@@ -4,6 +4,7 @@
 #include "labwc.h"
 #include "regions.h"
 #include "resize_indicator.h"
+#include "snap.h"
 #include "view.h"
 #include "window-rules.h"
 
index e08afb822099e826a5bc0be11f046997de11fbab..f3ff141237b91a8035b5c333628c39d37695a2dd 100644 (file)
@@ -21,6 +21,7 @@ labwc_sources = files(
   'seat.c',
   'server.c',
   'session-lock.c',
+  'snap-constraints.c',
   'snap.c',
   'tearing.c',
   'theme.c',
index 7d317de81cf67ccd30cf8a24bdff1a1339e19ea4..ce2efbdd3e0be4a2ce7b51b5644ab816ad96a66e 100644 (file)
@@ -108,15 +108,14 @@ resistance_move_apply(struct view *view, double *x, double *y)
 
        if (rc.screen_edge_strength != 0) {
                /* Find any relevant output edges encountered by this move */
-               edges_find_outputs(&next_edges, view, target, NULL,
-                       check_edge_output, /* use_pending */ false);
+               edges_find_outputs(&next_edges, view,
+                       view->current, target, NULL, check_edge_output);
        }
 
        if (rc.window_edge_strength != 0) {
                /* Find any relevant window edges encountered by this move */
-               edges_find_neighbors(&next_edges,
-                       view, target, NULL, check_edge_window,
-                       /* use_pending */ false, /* ignore_hidden */ true);
+               edges_find_neighbors(&next_edges, view, view->current, target,
+                       NULL, check_edge_window, /* ignore_hidden */ true);
        }
 
        /* If any "best" edges were encountered during this move, snap motion */
@@ -138,15 +137,14 @@ resistance_resize_apply(struct view *view, struct wlr_box *new_geom)
 
        if (rc.screen_edge_strength != 0) {
                /* Find any relevant output edges encountered by this move */
-               edges_find_outputs(&next_edges, view, *new_geom, NULL,
-                       check_edge_output, /* use_pending */ false);
+               edges_find_outputs(&next_edges, view,
+                       view->current, *new_geom, NULL, check_edge_output);
        }
 
        if (rc.window_edge_strength != 0) {
                /* Find any relevant window edges encountered by this move */
-               edges_find_neighbors(&next_edges,
-                       view, *new_geom, NULL, check_edge_window,
-                       /* use_pending */ false, /* ignore_hidden */ true);
+               edges_find_neighbors(&next_edges, view, view->current, *new_geom,
+                       NULL, check_edge_window, /* ignore_hidden */ true);
        }
 
        /* If any "best" edges were encountered during this move, snap motion */
diff --git a/src/snap-constraints.c b/src/snap-constraints.c
new file mode 100644 (file)
index 0000000..dbd7a54
--- /dev/null
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <assert.h>
+#include <wlr/util/box.h>
+#include "common/macros.h"
+#include "labwc.h"
+#include "snap-constraints.h"
+#include "view.h"
+
+/*
+ * When snapping to edges during a resize action, XDG clients can override the
+ * desired size to honor arbitrary sizing constraints (*e.g.*, honoring some
+ * aspect ratio or ensuring that a terminal window consists of an integer
+ * number of character cells). In that case, the final position of the view
+ * edge may fall short of the edge to which it's snapped.
+ *
+ * This can prevent a subsequent resize action in the same direction from ever
+ * crossing the "missed" edge, because the next action will keep trying to hit
+ * the edge and the client will overriding the desired size. To compensate,
+ * every snapped resize will update a "last snapped" cache, recording the
+ * view that was resized, the position (and orientation) of the resized edge,
+ * and the expected geometry resulting from the snapped resize. At first, the
+ * expected geometry is the "pending" geometry that will be sent in a configure
+ * request. However, if the client overrides this pending geometry with some
+ * other, constrained value, the expectation should be updated (only once!) to
+ * reflect the size that the client chooses to honor.
+ *
+ * In subsequent snapped resize actions, if:
+ *
+ * 1. The view is the same as in the last attemped snapped resize;
+ * 2. The direction of resizing is the same as in the last attempt; and
+ * 3. The geometry of the view matches that expected from the last attempt;
+ *
+ * then the view geometry will be modified to reflect the *original* intended
+ * geometry from last snapped resize, which will allow the current attempt to
+ * progress beyond the "sticky" edge.
+ */
+static struct {
+       struct view *view;
+       bool pending;
+       int offset;
+       enum view_edge direction;
+       struct wlr_box geom;
+} last_snap_hit;
+
+static void
+snap_constraints_reset(void)
+{
+       last_snap_hit.view = NULL;
+       last_snap_hit.pending = false;
+       last_snap_hit.offset = INT_MIN;
+       last_snap_hit.direction = VIEW_EDGE_INVALID;
+       memset(&last_snap_hit.geom, 0, sizeof(last_snap_hit.geom));
+}
+
+static bool
+snap_constraints_are_valid(struct view *view, enum view_edge direction)
+{
+       assert(view);
+
+       /* Cache is not valid if view has changed */
+       if (view != last_snap_hit.view) {
+               return false;
+       }
+
+       /* Cache is not valid if direction has changed */
+       if (direction != last_snap_hit.direction) {
+               return false;
+       }
+
+       /* Cache is not valid if offset is unbounded */
+       if (!BOUNDED_INT(last_snap_hit.offset)) {
+               return false;
+       }
+
+       /* Cache is valid iff pending view geometry matches expectation */
+       return wlr_box_equal(&view->pending, &last_snap_hit.geom);
+}
+
+void
+snap_constraints_set(struct view *view,
+               enum view_edge direction, struct wlr_box geom)
+{
+       assert(view);
+
+       int offset = INT_MIN;
+       switch (direction) {
+       case VIEW_EDGE_LEFT:
+               offset = geom.x;
+               break;
+       case VIEW_EDGE_RIGHT:
+               offset = geom.x + geom.width;
+               break;
+       case VIEW_EDGE_UP:
+               offset = geom.y;
+               break;
+       case VIEW_EDGE_DOWN:
+               offset = geom.y + geom.height;
+               break;
+       default:
+               break;
+       }
+
+       if (!BOUNDED_INT(offset)) {
+               snap_constraints_reset();
+               return;
+       }
+
+       /* Capture the current geometry and effective snapped edge */
+       last_snap_hit.view = view;
+       last_snap_hit.offset = offset;
+       last_snap_hit.direction = direction;
+       last_snap_hit.geom = geom;
+
+       /*
+        * Client geometry change is pending, and XDG clients may adjust the
+        * geometry to match arbitrary constraints. Allow the client to update
+        * our concept of constraints exactly once after the configure request.
+        */
+       last_snap_hit.pending = true;
+}
+
+void
+snap_constraints_invalidate(struct view *view)
+{
+       if (view == last_snap_hit.view) {
+               snap_constraints_reset();
+       }
+}
+
+void
+snap_constraints_update(struct view *view)
+{
+       assert(view);
+
+       /* Ignore update if this is the wrong view */
+       if (view != last_snap_hit.view) {
+               return;
+       }
+
+       /* Never update constraints more than once */
+       if (!last_snap_hit.pending) {
+               return;
+       }
+
+       /* Only update constraints when view geometry matches expectation */
+       if (!wlr_box_equal(&view->pending, &last_snap_hit.geom)) {
+               return;
+       }
+
+       last_snap_hit.geom = view->current;
+       last_snap_hit.pending = false;
+}
+
+struct wlr_box
+snap_constraints_effective(struct view *view, enum view_edge direction)
+{
+       assert(view);
+
+       /* Use actual geometry when constraints do not apply */
+       if (!snap_constraints_are_valid(view, direction)) {
+               return view->pending;
+       }
+
+       /* Override changing edge with constrained value */
+       struct wlr_box geom = view->pending;
+       switch (last_snap_hit.direction) {
+       case VIEW_EDGE_LEFT:
+               geom.x = last_snap_hit.offset;
+               break;
+       case VIEW_EDGE_RIGHT:
+               geom.width = last_snap_hit.offset - geom.x;
+               break;
+       case VIEW_EDGE_UP:
+               geom.y = last_snap_hit.offset;
+               break;
+       case VIEW_EDGE_DOWN:
+               geom.height = last_snap_hit.offset - geom.y;
+               break;
+       default:
+               return view->pending;
+       }
+
+       /* Fall back to actual geometry when constrained geometry is nonsense */
+       if (geom.width <= 0 || geom.height <= 0) {
+               return view->pending;
+       }
+
+       return geom;
+}
index 0db5d11ce2b971f971ec34c19d72eaf10aa2f94a..ca5186cf7e083a046e82bf62f0d4c61e17105ec6 100644 (file)
@@ -4,10 +4,9 @@
 #include <wlr/util/box.h>
 #include "common/border.h"
 #include "common/macros.h"
-#include "config/rcxml.h"
 #include "edges.h"
 #include "labwc.h"
-#include "resistance.h"
+#include "snap-constraints.h"
 #include "snap.h"
 #include "view.h"
 
@@ -121,9 +120,8 @@ snap_move_to_edge(struct view *view, enum view_edge direction,
                struct border next_edges;
                edges_initialize(&next_edges);
 
-               edges_find_neighbors(&next_edges,
-                       view, target, output, check_edge,
-                       /* use_pending */ true, /* ignore_hidden */ false);
+               edges_find_neighbors(&next_edges, view, view->pending, target,
+                       output, check_edge, /* ignore_hidden */ false);
 
                /* If any "best" edges were encountered, limit motion */
                edges_adjust_move_coords(view, next_edges,
@@ -135,8 +133,8 @@ snap_move_to_edge(struct view *view, enum view_edge direction,
 }
 
 void
-snap_grow_to_next_edge(struct view *view, enum view_edge direction,
-               struct wlr_box *geo)
+snap_grow_to_next_edge(struct view *view,
+               enum view_edge direction, struct wlr_box *geo)
 {
        assert(view);
        assert(!view->shaded);
@@ -196,17 +194,26 @@ snap_grow_to_next_edge(struct view *view, enum view_edge direction,
        struct border next_edges;
        edges_initialize(&next_edges);
 
+       /* Use a constrained, effective geometry for snapping if appropriate */
+       struct wlr_box origin = snap_constraints_effective(view, direction);
+
        /* Limit motion to any intervening edge of other views on this output */
-       edges_find_neighbors(&next_edges,
-               view, *geo, output, check_edge,
-               /* use_pending */ true, /* ignore_hidden */ false);
+       edges_find_neighbors(&next_edges, view, origin, *geo,
+               output, check_edge, /* ignore_hidden */ false);
+
        edges_adjust_resize_geom(view, next_edges,
                resize_edges, geo, /* use_pending */ true);
+
+       /*
+        * Record effective geometry after snapping in case the client opts to
+        * ignore or modify the configured geometry
+        */
+       snap_constraints_set(view, direction, *geo);
 }
 
 void
-snap_shrink_to_next_edge(struct view *view, enum view_edge direction,
-               struct wlr_box *geo)
+snap_shrink_to_next_edge(struct view *view,
+               enum view_edge direction, struct wlr_box *geo)
 {
        assert(view);
        assert(!view->shaded);
@@ -252,15 +259,23 @@ snap_shrink_to_next_edge(struct view *view, enum view_edge direction,
        struct border next_edges;
        edges_initialize(&next_edges);
 
+       /* Use a constrained, effective geometry for snapping if appropriate */
+       struct wlr_box origin = snap_constraints_effective(view, direction);
+
        /* Snap to output edges if the moving edge started off-screen */
-       edges_find_outputs(&next_edges, view, *geo,
-               view->output, check_edge, /* use_pending */ true);
+       edges_find_outputs(&next_edges, view,
+               origin, *geo, view->output, check_edge);
 
        /* Limit motion to any intervening edge of ther views on this output */
-       edges_find_neighbors(&next_edges,
-               view, *geo, view->output, check_edge,
-               /* use_pending */ true, /* ignore_hidden */ false);
+       edges_find_neighbors(&next_edges, view, origin, *geo,
+               view->output, check_edge, /* ignore_hidden */ false);
 
        edges_adjust_resize_geom(view, next_edges,
                resize_edges, geo, /* use_pending */ true);
+
+       /*
+        * Record effective geometry after snapping in case the client opts to
+        * ignore or modify the configured geometry
+        */
+       snap_constraints_set(view, direction, *geo);
 }
index 6e8a77691b79fd2a5078f6b1554cf877b2f3417b..98ecf4de9c30844f906c19ea201689cd15f7a99c 100644 (file)
@@ -13,6 +13,7 @@
 #include "placement.h"
 #include "regions.h"
 #include "resize_indicator.h"
+#include "snap-constraints.h"
 #include "snap.h"
 #include "ssd.h"
 #include "view.h"
@@ -2203,6 +2204,8 @@ view_destroy(struct view *view)
        assert(view);
        struct server *server = view->server;
 
+       snap_constraints_invalidate(view);
+
        if (view->mappable.connected) {
                mappable_disconnect(&view->mappable);
        }
index 1ad535547c858f06fe110a72d9a9054b3553c63d..4e6d6dad4923631276ec41b8bed31adf3cde8410 100644 (file)
--- a/src/xdg.c
+++ b/src/xdg.c
@@ -8,6 +8,7 @@
 #include "decorations.h"
 #include "labwc.h"
 #include "node.h"
+#include "snap-constraints.h"
 #include "view.h"
 #include "view-impl-common.h"
 #include "window-rules.h"
@@ -141,6 +142,7 @@ handle_commit(struct wl_listener *listener, void *data)
                 * actual view.
                 */
                if (!view->pending_configure_serial) {
+                       snap_constraints_update(view);
                        view->pending = view->current;
 
                        /*
@@ -188,6 +190,7 @@ handle_configure_timeout(void *data)
                view->current.width, view->current.height);
 
        /* Re-sync pending view with current state */
+       snap_constraints_update(view);
        view->pending = view->current;
 
        return 0; /* ignored per wl_event_loop docs */