// SPDX-License-Identifier: GPL-2.0-only #include "snap-constraints.h" #include #include #include "common/macros.h" #include "labwc.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 attempted 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 vertical_offset; int horizontal_offset; enum lab_edge resize_edges; 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.horizontal_offset = INT_MIN; last_snap_hit.vertical_offset = INT_MIN; last_snap_hit.resize_edges = LAB_EDGE_NONE; memset(&last_snap_hit.geom, 0, sizeof(last_snap_hit.geom)); } static bool snap_constraints_are_valid(struct view *view, enum lab_edge resize_edges) { assert(view); /* Cache is not valid if view has changed */ if (view != last_snap_hit.view) { return false; } /* Cache is not valid if expected edges do not match */ if (resize_edges != last_snap_hit.resize_edges) { return false; } /* Cache is not valid if edge offsets are invalid */ if (resize_edges & LAB_EDGES_LEFT_RIGHT) { if (!BOUNDED_INT(last_snap_hit.horizontal_offset)) { return false; } if ((resize_edges & LAB_EDGE_LEFT) && (resize_edges & LAB_EDGE_RIGHT)) { return false; } } else if (resize_edges & LAB_EDGES_TOP_BOTTOM) { if (!BOUNDED_INT(last_snap_hit.vertical_offset)) { return false; } if ((resize_edges & LAB_EDGE_TOP) && (resize_edges & LAB_EDGE_BOTTOM)) { return false; } } else { return false; } /* Cache is valid iff view geometry matches expectation */ return wlr_box_equal(&view->pending, &last_snap_hit.geom); } void snap_constraints_set(struct view *view, enum lab_edge resize_edges, struct wlr_box geom) { assert(view); /* Set horizontal offset when resizing horizontal edges */ last_snap_hit.horizontal_offset = INT_MIN; if (resize_edges & LAB_EDGE_LEFT) { last_snap_hit.horizontal_offset = geom.x; } else if (resize_edges & LAB_EDGE_RIGHT) { last_snap_hit.horizontal_offset = geom.x + geom.width; } /* Set vertical offset when resizing vertical edges */ last_snap_hit.vertical_offset = INT_MIN; if (resize_edges & LAB_EDGE_TOP) { last_snap_hit.vertical_offset = geom.y; } else if (resize_edges & LAB_EDGE_BOTTOM) { last_snap_hit.vertical_offset = geom.y + geom.height; } /* Capture the current geometry and effective snapped edge */ last_snap_hit.view = view; last_snap_hit.resize_edges = resize_edges; 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 pending view dimensions match expectation */ if (view->pending.width != last_snap_hit.geom.width) { return; } if (view->pending.height != last_snap_hit.geom.height) { return; } last_snap_hit.geom = view->current; last_snap_hit.pending = false; } struct wlr_box snap_constraints_effective(struct view *view, enum lab_edge resize_edges, bool use_pending) { assert(view); struct wlr_box real_geom = use_pending ? view->pending : view->current; /* Use actual geometry when constraints do not apply */ if (!snap_constraints_are_valid(view, resize_edges)) { return real_geom; } /* Override changing edge with constrained value */ struct wlr_box geom = real_geom; if (resize_edges & LAB_EDGE_LEFT) { geom.x = last_snap_hit.horizontal_offset; } else if (resize_edges & LAB_EDGE_RIGHT) { geom.width = last_snap_hit.horizontal_offset - geom.x; } if (resize_edges & LAB_EDGE_TOP) { geom.y = last_snap_hit.vertical_offset; } else if (resize_edges & LAB_EDGE_BOTTOM) { geom.height = last_snap_hit.vertical_offset - geom.y; } /* Fall back to actual geometry when constrained geometry is nonsense */ if (!BOUNDED_INT(geom.x) || !BOUNDED_INT(geom.y)) { return real_geom; } if (geom.width <= 0 || geom.height <= 0) { return real_geom; } return geom; }