diff --git a/include/edges.h b/include/edges.h index 6ce029c3..47dd6f1b 100644 --- a/include/edges.h +++ b/include/edges.h @@ -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 index 00000000..294c53b6 --- /dev/null +++ b/include/snap-constraints.h @@ -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 */ diff --git a/include/snap.h b/include/snap.h index 342e56f4..4ac8025d 100644 --- a/include/snap.h +++ b/include/snap.h @@ -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 */ diff --git a/src/edges.c b/src/edges.c index 72535691..bd96c5a0 100644 --- a/src/edges.c +++ b/src/edges.c @@ -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; } diff --git a/src/interactive.c b/src/interactive.c index 243aaa6a..d4679f3a 100644 --- a/src/interactive.c +++ b/src/interactive.c @@ -4,6 +4,7 @@ #include "labwc.h" #include "regions.h" #include "resize_indicator.h" +#include "snap.h" #include "view.h" #include "window-rules.h" diff --git a/src/meson.build b/src/meson.build index e08afb82..f3ff1412 100644 --- a/src/meson.build +++ b/src/meson.build @@ -21,6 +21,7 @@ labwc_sources = files( 'seat.c', 'server.c', 'session-lock.c', + 'snap-constraints.c', 'snap.c', 'tearing.c', 'theme.c', diff --git a/src/resistance.c b/src/resistance.c index 7d317de8..ce2efbdd 100644 --- a/src/resistance.c +++ b/src/resistance.c @@ -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 index 00000000..dbd7a54d --- /dev/null +++ b/src/snap-constraints.c @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#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; +} diff --git a/src/snap.c b/src/snap.c index 0db5d11c..ca5186cf 100644 --- a/src/snap.c +++ b/src/snap.c @@ -4,10 +4,9 @@ #include #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); } diff --git a/src/view.c b/src/view.c index 6e8a7769..98ecf4de 100644 --- a/src/view.c +++ b/src/view.c @@ -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); } diff --git a/src/xdg.c b/src/xdg.c index 1ad53554..4e6d6dad 100644 --- 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 */