From e7e6d292370fd9c4890a1fd4a0190fbd4b286e1e Mon Sep 17 00:00:00 2001 From: "Andrew J. Hesford" Date: Tue, 23 Jan 2024 13:44:40 -0500 Subject: [PATCH] edges, resistance, snap: unified resistance and snapping engine --- include/common/macros.h | 11 + include/edges.h | 110 ++++++++++ include/snap.h | 12 +- src/edges.c | 303 +++++++++++++++++++++++++ src/meson.build | 1 + src/resistance.c | 282 ++++++++---------------- src/snap.c | 474 +++++++++++++++++++--------------------- src/view.c | 24 +- 8 files changed, 748 insertions(+), 469 deletions(-) create mode 100644 include/edges.h create mode 100644 src/edges.c diff --git a/include/common/macros.h b/include/common/macros.h index 17ada31e..38ab45de 100644 --- a/include/common/macros.h +++ b/include/common/macros.h @@ -2,6 +2,8 @@ #ifndef LABWC_MACROS_H #define LABWC_MACROS_H +#include + /** * ARRAY_SIZE() - Get the number of elements in array. * @arr: array to be sized @@ -50,4 +52,13 @@ #define MAX(a, b) (((a) > (b)) ? (a) : (b)) #endif +/** + * BOUNDED_INT() - Returns true if an integer is not INT_MAX or INT_MIN + * + * @param val Value to test (integer) + */ +#ifndef BOUNDED_INT +#define BOUNDED_INT(a) ((a) < INT_MAX && (a) > INT_MIN) +#endif + #endif /* LABWC_MACROS_H */ diff --git a/include/edges.h b/include/edges.h new file mode 100644 index 00000000..fd401c43 --- /dev/null +++ b/include/edges.h @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_EDGES_H +#define LABWC_EDGES_H + +#include +#include "common/macros.h" + +struct border; +struct output; +struct view; + +static inline int +clipped_add(int a, int b) +{ + if (b > 0) { + return a >= (INT_MAX - b) ? INT_MAX : (a + b); + } else if (b < 0) { + return a <= (INT_MIN - b) ? INT_MIN : (a + b); + } + + return a; +} + +static inline int +clipped_sub(int a, int b) +{ + if (b > 0) { + return a <= (INT_MIN + b) ? INT_MIN : (a - b); + } else if (b < 0) { + return a >= (INT_MAX + b) ? INT_MAX : (a - b); + } + + return a; +} + +static inline int +edge_get_best(int next, int edge, bool decreasing) +{ + if (!BOUNDED_INT(next)) { + /* Any bounded edge beats an unbounded next */ + return BOUNDED_INT(edge) ? edge : next; + } + + /* No unbounded edge ever beats next */ + if (!BOUNDED_INT(edge)) { + return next; + } + + /* Max edge wins for decreasing moves, min edge for increasing */ + return decreasing ? MAX(next, edge) : MIN(next, edge); +} + +/* + * edge_validator_t - edge validator signature + * @best: pointer to the current "best" edge + * @current: current position of a moving edge + * @target: position to which the moving edge will be moved + * @oppose: opposing edge of encountered region + * @align: aligned edge of encountered region + * @lesser: true if moving edge is top or left edge; false otherwise + * + * This function will be used by edge_find_neighbors and edge_find_outputs to + * validate and select the "best" output or neighbor edge against which a + * moving edge should be snapped. The moving edge has current position + * "current" and desired position "target". The validator should determine + * whether motion of the crosses the given opposed and aligned edges of a trial + * region and should be considered a snap point. An edge is "lesser" if it + * occupies a smaller coordinate than the opposite edge of the view region + * (i.e., it is a top or left edge). + * + * Opposing edges are on the opposite side of the target region from the moving + * edge (i.e., left <-> right, top <-> bottom). When the moving edge snaps to + * an opposing edge, the view should maintain the configured gap. Aligned edges + * are on the same side of the target region from the moving edge (i.e., + * left <-> left, right <-> right, top <-> top, bottom <-> bottom). When the + * moving edge snaps to an aligned edge, the view should *not* include a gap. + * + * If window gaps are configured, all edges will be offset as appropriate to + * reflect the desired padding. Thus, the validator should generally compare + * the given current or target values directly to the opposing and aligned edge + * without regard for rc.gap. + * + * Any edge may take the values INT_MIN or INT_MAX to indicate that the edge + * should be effectively ignored. Should the validator decide that a given + * region edge (oppose or align) should be a preferred snap point, it should + * update the value of *best accordingly. + */ +typedef void (*edge_validator_t)(int *best, + int current, int target, int oppose, int align, bool lesser); + +void edges_initialize(struct border *edges); + +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); + +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); + +void edges_adjust_move_coords(struct view *view, struct border edges, + int *x, int *y, bool use_pending); + +void edges_adjust_resize_geom(struct view *view, struct border edges, + uint32_t resize_edges, struct wlr_box *geom, bool use_pending); + +#endif /* LABWC_EDGES_H */ diff --git a/include/snap.h b/include/snap.h index 20d9b9d6..342e56f4 100644 --- a/include/snap.h +++ b/include/snap.h @@ -7,11 +7,13 @@ struct wlr_box; -struct border snap_get_max_distance(struct view *view); +void snap_move_to_edge(struct view *view, + enum view_edge direction, bool snap_to_windows, int *dx, int *dy); -void snap_vector_to_next_edge(struct view *view, enum view_edge direction, int *dx, int *dy); -int snap_distance_to_next_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); -void snap_shrink_to_next_edge(struct view *view, enum view_edge direction, struct wlr_box *geo); +void snap_grow_to_next_edge(struct view *view, + enum view_edge direction, struct wlr_box *geo); + +void snap_shrink_to_next_edge(struct view *view, + enum view_edge direction, struct wlr_box *geo); #endif /* LABWC_SNAP_H */ diff --git a/src/edges.c b/src/edges.c new file mode 100644 index 00000000..965bf61e --- /dev/null +++ b/src/edges.c @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include "common/border.h" +#include "common/macros.h" +#include "config/rcxml.h" +#include "edges.h" +#include "labwc.h" +#include "view.h" + +static void +edges_for_target_geometry(struct border *edges, struct view *view, + struct wlr_box target) +{ + struct border border = ssd_get_margin(view->ssd); + + /* Use the effective height to properly handle shaded views */ + int eff_height = view->shaded ? 0 : target.height; + + edges->left = target.x - border.left - rc.gap; + edges->top = target.y - border.top - rc.gap; + edges->right = target.x + target.width + border.right + rc.gap; + edges->bottom = target.y + eff_height + border.bottom + rc.gap; +} + +void +edges_initialize(struct border *edges) +{ + assert(edges); + edges->top = INT_MIN; + edges->right = INT_MAX; + edges->bottom = INT_MAX; + edges->left = INT_MIN; +} + +static void +validate_edges(struct border *valid_edges, + struct border view, struct border target, + struct border region, edge_validator_t validator) +{ + /* When a view snaps to a region while moving to its target, it can do + * so in two ways: a view edge can snap to an "opposing" edge of the + * region (left <-> right, top <-> bottom) or to an "aligned" edge + * (left <-> left, right <-> right, top <-> top, bottom <-> bottom). + * + * When a view hits the opposing edge of a region, it should be + * separated by a gap; when a view hits the aligned edge, it should not + * be separated. The view and its target already include necessary + * padding to reflect the gap. The region does not. To make sure the + * "aligned" edges are properly aligned, add padding to the region + * borders for aligned edges only. + */ + + struct border region_pad = { + .top = clipped_sub(region.top, rc.gap), + .right = clipped_add(region.right, rc.gap), + .bottom = clipped_add(region.bottom, rc.gap), + .left = clipped_sub(region.left, rc.gap), + }; + + /* Check for edges encountered during movement of left edge */ + validator(&valid_edges->left, view.left, target.left, + region.right, region_pad.left, /* lesser */ true); + + /* Check for edges encountered during movement of right edge */ + validator(&valid_edges->right, view.right, target.right, + region.left, region_pad.right, /* lesser */ false); + + /* Check for edges encountered during movement of top edge */ + validator(&valid_edges->top, view.top, target.top, + region.bottom, region_pad.top, /* lesser */ true); + + /* Check for edges encountered during movement of bottom edge */ + validator(&valid_edges->bottom, view.bottom, target.bottom, + region.top, region_pad.bottom, /* lesser */ false); +} + +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) +{ + assert(view); + assert(validator); + assert(nearest_edges); + + struct border view_edges = { 0 }; + struct border target_edges = { 0 }; + + edges_for_target_geometry(&view_edges, view, + use_pending ? view->pending : view->current); + edges_for_target_geometry(&target_edges, view, target); + + struct view *v; + for_each_view(v, &view->server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { + if (v == view || !output_is_usable(v->output)) { + continue; + } + + if (output && v->output != output) { + continue; + } + + struct border border = ssd_get_margin(v->ssd); + + struct border win_edges = { + .top = v->current.y - border.top, + .left = v->current.x - border.left, + .bottom = v->current.y + border.bottom + + view_effective_height(v, /* use_pending */ false), + .right = v->current.x + v->current.width + border.right, + }; + + validate_edges(nearest_edges, view_edges, + target_edges, win_edges, validator); + } +} + +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) +{ + assert(view); + assert(validator); + assert(nearest_edges); + + 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(&target_edges, view, target); + + struct output *o; + wl_list_for_each(o, &view->server->outputs, link) { + if (!output_is_usable(o)) { + continue; + } + + if (output && o != output) { + continue; + } + + struct wlr_box usable = + output_usable_area_in_layout_coords(o); + + struct wlr_box ol; + if (!wlr_box_intersection(&ol, view_geom, &usable) && + !wlr_box_intersection(&ol, &target, &usable)) { + continue; + } + + /* + * Split a single "leaving output" problem into four "entering + * complementary region" problems, treating the view, its + * target and the screen boundaries as half planes. This + * prevents unexpected snapping behavior like the bottom of a + * window snapping above the top of an output, where it would + * become invisible. + */ + struct border screen; + struct border view_eff; + struct border target_eff; + + /* First problem: view toward upper half-plane */ + edges_initialize(&screen); + edges_initialize(&view_eff); + edges_initialize(&target_eff); + + screen.bottom = usable.y; + view_eff.top = view_edges.top; + target_eff.top = target_edges.top; + validate_edges(nearest_edges, view_eff, target_eff, screen, validator); + + /* Second problem: view toward lower half-plane */ + edges_initialize(&screen); + edges_initialize(&view_eff); + edges_initialize(&target_eff); + + screen.top = usable.y + usable.height; + view_eff.bottom = view_edges.bottom; + target_eff.bottom = target_edges.bottom; + validate_edges(nearest_edges, view_eff, target_eff, screen, validator); + + /* Third problem: view toward left half-plane */ + edges_initialize(&screen); + edges_initialize(&view_eff); + edges_initialize(&target_eff); + + screen.right = usable.x; + view_eff.left = view_edges.left; + target_eff.left = target_edges.left; + validate_edges(nearest_edges, view_eff, target_eff, screen, validator); + + /* Fourth problem: view toward right half-plane */ + edges_initialize(&screen); + edges_initialize(&view_eff); + edges_initialize(&target_eff); + + screen.left = usable.x + usable.width; + view_eff.right = view_edges.right; + target_eff.right = target_edges.right; + validate_edges(nearest_edges, view_eff, target_eff, screen, validator); + } +} + +static void +adjust_move_coords_1d(int *edge, int lesser, int lesser_offset, + int greater, int greater_offset, bool decreasing) +{ + /* Default best candidate is not valid */ + int best = INT_MAX; + + if (BOUNDED_INT(lesser)) { + /* A valid lesser edge is the always the first candidate */ + best = clipped_add(lesser, lesser_offset); + } + + if (BOUNDED_INT(greater)) { + /* Check if a valid greater edge is a better candidate */ + best = edge_get_best(best, + clipped_sub(greater, greater_offset), decreasing); + } + + if (BOUNDED_INT(best)) { + /* Replace the edge if a valid candidate was found */ + *edge = best; + } +} + +void +edges_adjust_move_coords(struct view *view, struct border edges, + int *x, int *y, bool use_pending) +{ + assert(view); + + struct border border = ssd_get_margin(view->ssd); + struct wlr_box *view_geom = + use_pending ? &view->pending : &view->current; + + /* When moving, limit motion to the best valid, intervening edge */ + + if (view_geom->x != *x) { + int lshift = border.left + rc.gap; + int rshift = border.right + rc.gap + view->pending.width; + + adjust_move_coords_1d(x, edges.left, lshift, + edges.right, rshift, *x < view_geom->x); + } + + if (view_geom->y != *y) { + int tshift = border.top + rc.gap; + int bshift = border.bottom + rc.gap + + view_effective_height(view, /* use_pending */ true); + + adjust_move_coords_1d(y, edges.top, tshift, + edges.bottom, bshift, *y < view_geom->y); + } +} + +void +edges_adjust_resize_geom(struct view *view, struct border edges, + uint32_t resize_edges, struct wlr_box *geom, bool use_pending) +{ + assert(view); + + struct border border = ssd_get_margin(view->ssd); + struct wlr_box *view_geom = + use_pending ? &view->pending : &view->current; + + /* + * When resizing along a given edge, limit the motion of that edge to + * any valid nearest edge in the corresponding direction. + */ + + if (resize_edges & WLR_EDGE_LEFT) { + if (BOUNDED_INT(edges.left)) { + geom->x = edges.left + border.left + rc.gap; + geom->width = view_geom->width + view_geom->x - geom->x; + } + } else if (resize_edges & WLR_EDGE_RIGHT) { + if (BOUNDED_INT(edges.right)) { + geom->width = edges.right + - view_geom->x - border.right - rc.gap; + } + } + + if (resize_edges & WLR_EDGE_TOP) { + if (BOUNDED_INT(edges.top)) { + geom->y = edges.top + border.top + rc.gap; + geom->height = view_geom->height + view_geom->y - geom->y; + } + } else if (resize_edges & WLR_EDGE_BOTTOM) { + if (BOUNDED_INT(edges.bottom)) { + geom->height = edges.bottom + - view_geom->y - border.bottom - rc.gap; + } + } +} diff --git a/src/meson.build b/src/meson.build index baf46cb8..b544e99f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ labwc_sources = files( 'debug.c', 'desktop.c', 'dnd.c', + 'edges.c', 'foreign.c', 'idle.c', 'interactive.c', diff --git a/src/resistance.c b/src/resistance.c index d187c44a..241441fb 100644 --- a/src/resistance.c +++ b/src/resistance.c @@ -4,164 +4,84 @@ #include "common/border.h" #include "common/macros.h" #include "config/rcxml.h" +#include "edges.h" #include "labwc.h" #include "resistance.h" #include "view.h" static void -is_within_resistance_range(struct border view, struct border target, - struct border other, struct border *flags, int strength) +check_edge(int *next, int current, int target, + int oppose, int align, bool lesser, int tolerance) { - if (view.left >= other.left) { - const int lo = other.left - abs(strength); - const int hi = other.left - MIN(strength, 0); - flags->left = target.left >= lo && target.left < hi; - } - - if (!flags->left && view.right <= other.right) { - const int lo = other.right + MIN(strength, 0); - const int hi = other.right + abs(strength); - flags->right = target.right > lo && target.right <= hi; - } - - if (view.top >= other.top) { - const int lo = other.top - abs(strength); - const int hi = other.top - MIN(strength, 0); - flags->top = target.top >= lo && target.top < hi; - } - - if (!flags->top && view.bottom <= other.bottom) { - const int lo = other.bottom + MIN(strength, 0); - const int hi = other.bottom + abs(strength); - flags->bottom = target.bottom > lo && target.bottom <= hi; - } -} - -static void -build_view_edges(struct view *view, struct wlr_box new_geom, - struct border *view_edges, struct border *target_edges, bool move) -{ - struct border border = ssd_get_margin(view->ssd); - - /* Use the effective height to properly snap shaded views */ - int eff_height = view_effective_height(view, /* use_pending */ false); - - view_edges->left = view->current.x - border.left + (move ? 1 : 0); - view_edges->top = view->current.y - border.top + (move ? 1 : 0); - view_edges->right = view->current.x + view->current.width + border.right; - view_edges->bottom = view->current.y + eff_height + border.bottom; - - target_edges->left = new_geom.x - border.left; - target_edges->top = new_geom.y - border.top; - target_edges->right = new_geom.x + new_geom.width + border.right; - target_edges->bottom = new_geom.y + border.bottom + - (view->shaded ? 0 : new_geom.height); -} - -static void -update_nearest_edge(struct border view_edges, struct border target_edges, - struct border region_edges, int strength, - struct border *next_edges) -{ - struct border flags = { 0 }; - is_within_resistance_range(view_edges, - target_edges, region_edges, &flags, strength); - - if (flags.left == 1) { - next_edges->left = MAX(region_edges.left, next_edges->left); - } else if (flags.right == 1) { - next_edges->right = MIN(region_edges.right, next_edges->right); - } - - if (flags.top == 1) { - next_edges->top = MAX(region_edges.top, next_edges->top); - } else if (flags.bottom == 1) { - next_edges->bottom = MIN(region_edges.bottom, next_edges->bottom); - } -} - -static void -find_neighbor_edges(struct view *view, struct wlr_box new_geom, - struct border *next_edges, bool move) -{ - if (rc.window_edge_strength == 0) { + /* Ignore non-moving edges */ + if (current == target) { return; } - struct border view_edges = { 0 }; - struct border target_edges = { 0 }; + /* + * The edge defined by current and moving to target may encounter two + * edges of another region: the opposing edge of the region is that in + * the opposite orientation of the moving edge (i.e., left <-> right or + * top <-> bottom); the aligned edge of the region is that in the same + * orientation as the moving edge (i.e., left <->left, top <-> top, + * right <-> right, bottom <-> bottom). + * + * Any opposing or aligned edge of a region is considered "valid" in + * this search if the resist/attract zone (defined by tolerance) of + * that edge contains the target position of the moving edge. + */ - build_view_edges(view, new_geom, &view_edges, &target_edges, move); + /* Direction of motion for the edge */ + const bool decreasing = target < current; - struct view *v; - for_each_view(v, &view->server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { - if (v == view || !output_is_usable(v->output)) { - continue; - } + /* Check the opposing edge */ + bool valid = false; + if (decreasing) { + const int lo = clipped_sub(oppose, abs(tolerance)); + const int hi = clipped_sub(oppose, MIN(tolerance, 0)); + valid = target >= lo && target < hi; + } else { + /* Check for increasing movement across opposing edge */ + const int lo = clipped_add(oppose, MIN(tolerance, 0)); + const int hi = clipped_add(oppose, abs(tolerance)); + valid = target > lo && target <= hi; + } - struct border border = ssd_get_margin(v->ssd); + if (valid) { + *next = edge_get_best(*next, oppose, decreasing); + } - /* - * The significance of window edges here is inverted with - * respect to the usual orientation, because the edges of the - * view v of interest are those that would be encountered by a - * change in geometry in view along the named edge of view. - * Hence, when moving or resizing view *left*, it is the - * *right* edge of v that would be encountered, and vice versa; - * when moving or resizing view *down* ("bottom"), it is the - * *top* edge of v that would be encountered, and vice versa. - */ - struct border win_edges = { - .top = v->current.y + border.bottom - + view_effective_height(v, /* use_pending */ false), - .right = v->current.x - border.left, - .bottom = v->current.y - border.top, - .left = v->current.x + v->current.width + border.right, - }; + /* Check the aligned edge */ + valid = false; + if (decreasing) { + const int lo = clipped_sub(align, abs(tolerance)); + const int hi = clipped_sub(align, MIN(tolerance, 0)); + valid = target >= lo && target < hi; + } else { + const int lo = clipped_add(align, MIN(tolerance, 0)); + const int hi = clipped_add(align, abs(tolerance)); + valid = target > lo && target <= hi; + } - update_nearest_edge(view_edges, target_edges, - win_edges, rc.window_edge_strength, next_edges); + if (valid) { + *next = edge_get_best(*next, align, decreasing); } } static void -find_screen_edges(struct view *view, struct wlr_box new_geom, - struct border *next_edges, bool move) +check_edge_output(int *next, int current, int target, + int oppose, int align, bool lesser) { - if (rc.screen_edge_strength == 0) { - return; - } + check_edge(next, current, target, + oppose, align, lesser, rc.screen_edge_strength); +} - struct border view_edges = { 0 }; - struct border target_edges = { 0 }; - - build_view_edges(view, new_geom, &view_edges, &target_edges, move); - - struct output *output; - wl_list_for_each(output, &view->server->outputs, link) { - if (!output_is_usable(output)) { - continue; - } - - struct wlr_box mgeom = - output_usable_area_in_layout_coords(output); - - struct wlr_box ol; - if (!wlr_box_intersection(&ol, &view->current, &mgeom) && - !wlr_box_intersection(&ol, &new_geom, &mgeom)) { - continue; - } - - struct border screen_edges = { - .top = mgeom.y, - .right = mgeom.x + mgeom.width, - .bottom = mgeom.y + mgeom.height, - .left = mgeom.x, - }; - - update_nearest_edge(view_edges, target_edges, - screen_edges, rc.screen_edge_strength, next_edges); - } +static void +check_edge_window(int *next, int current, int target, + int oppose, int align, bool lesser) +{ + check_edge(next, current, target, + oppose, align, lesser, rc.window_edge_strength); } void @@ -169,37 +89,34 @@ resistance_move_apply(struct view *view, double *x, double *y) { assert(view); - struct border border = ssd_get_margin(view->ssd); + struct border next_edges; + edges_initialize(&next_edges); - struct border next_edges = { - .top = INT_MIN, - .right = INT_MAX, - .bottom = INT_MAX, - .left = INT_MIN, - }; - - struct wlr_box new_geom = { + struct wlr_box target = { .x = *x, .y = *y, .width = view->current.width, .height = view->current.height, }; - find_screen_edges(view, new_geom, &next_edges, /* move */ true); - find_neighbor_edges(view, new_geom, &next_edges, /* move */ true); - - if (next_edges.left > INT_MIN) { - *x = next_edges.left + border.left; - } else if (next_edges.right < INT_MAX) { - *x = next_edges.right - view->current.width - border.right; + 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); } - if (next_edges.top > INT_MIN) { - *y = next_edges.top + border.top; - } else if (next_edges.bottom < INT_MAX) { - *y = next_edges.bottom - border.bottom - - view_effective_height(view, /* use_pending */ false); + 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); } + + /* If any "best" edges were encountered during this move, snap motion */ + edges_adjust_move_coords(view, next_edges, + &target.x, &target.y, /* use_pending */ false); + + *x = target.x; + *y = target.y; } void @@ -208,41 +125,22 @@ resistance_resize_apply(struct view *view, struct wlr_box *new_geom) assert(view); assert(!view->shaded); - struct border border = ssd_get_margin(view->ssd); + struct border next_edges; + edges_initialize(&next_edges); - struct border next_edges = { - .top = INT_MIN, - .right = INT_MAX, - .bottom = INT_MAX, - .left = INT_MIN, - }; - - find_screen_edges(view, *new_geom, &next_edges, /* move */ false); - find_neighbor_edges(view, *new_geom, &next_edges, /* move */ false); - - if (view->server->resize_edges & WLR_EDGE_LEFT) { - if (next_edges.left > INT_MIN) { - new_geom->x = next_edges.left + border.left; - new_geom->width = view->current.width - + view->current.x - new_geom->x; - } - } else if (view->server->resize_edges & WLR_EDGE_RIGHT) { - if (next_edges.right < INT_MAX) { - new_geom->width = next_edges.right - - view->current.x - border.right; - } + 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); } - if (view->server->resize_edges & WLR_EDGE_TOP) { - if (next_edges.top > INT_MIN) { - new_geom->y = next_edges.top + border.top; - new_geom->height = view->current.height - + view->current.y - new_geom->y; - } - } else if (view->server->resize_edges & WLR_EDGE_BOTTOM) { - if (next_edges.bottom < INT_MAX) { - new_geom->height = next_edges.bottom - - view->current.y - border.bottom; - } + 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); } + + /* If any "best" edges were encountered during this move, snap motion */ + edges_adjust_resize_geom(view, next_edges, + view->server->resize_edges, new_geom, /* use_pending */ false); } diff --git a/src/snap.c b/src/snap.c index fd60cad0..5d0e943d 100644 --- a/src/snap.c +++ b/src/snap.c @@ -1,285 +1,259 @@ // SPDX-License-Identifier: GPL-2.0-only -#include +#include +#include #include +#include "common/border.h" +#include "common/macros.h" +#include "config/rcxml.h" +#include "edges.h" #include "labwc.h" +#include "resistance.h" #include "snap.h" #include "view.h" -#include "workspaces.h" - -/* We cannot use MIN/MAX macros, as they may call functions twice, and - * can be overridden by previous #define. - */ -static inline int -min(int a, int b) { - return a < b ? a : b; -} - -static inline int -max(int a, int b) { - return a > b ? a : b; -} - -static inline int -min3(int a, int b, int c) { - return min(min(a, b), c); -} - -enum snap_mode { - SNAP_MODE_MOVE = 0, - SNAP_MODE_GROW, - SNAP_MODE_SHRINK, -}; - -static struct border -snap_get_view_edge(struct view *view) -{ - struct border margin = ssd_get_margin(view->ssd); - struct border edge = { - .left = view->pending.x - margin.left, - .top = view->pending.y - margin.top, - .right = view->pending.x + margin.right + view->pending.width, - .bottom = view->pending.y + margin.bottom - + view_effective_height(view, /* use_pending */ true) - }; - return edge; -} - -struct border -snap_get_max_distance(struct view *view) -{ - struct output *output = view->output; - struct border margin = ssd_get_margin(view->ssd); - struct wlr_box usable = output_usable_area_scaled(output); - struct border distance = { - .left = usable.x + margin.left + rc.gap - view->pending.x, - .top = usable.y + margin.top + rc.gap - view->pending.y, - .right = usable.x + usable.width - view->pending.width - - margin.right - rc.gap - view->pending.x, - .bottom = usable.y + usable.height - - view_effective_height(view, /* use_pending */ true) - - margin.bottom - rc.gap - view->pending.y - }; - return distance; -} - -struct snap_search { - const int search_dir; /* -1: left/up, 1: right/down */ - - const int add_view_x; - const int add_view_y; - const int add_view_width; - const int add_view_height; - - const int add_margin_left; - const int add_margin_top; - const int add_margin_right; - const int add_margin_bottom; -}; - -/* near/far is the left, right, top or bottom border of a window, - * depending on the search direction: - * - near_right: search to the right, snap to left (near) border of a window. - * - far_right: search to the right, snap to right (far) border of a window. - * - near_left: search to the left, snap to right (near) border of a window. - * - far_left: search to the left, snap to left (far) border of a window. - * - * structs below define what coordinates and margins to take into - * account depending near/far, and direction. - */ -static const struct snap_search near_left = { -1, 1, 0, 1, 0, 0, 0, 1, 0 }; -static const struct snap_search near_up = { -1, 0, 1, 0, 1, 0, 0, 0, 1 }; -static const struct snap_search near_right = { 1, 1, 0, 0, 0, -1, 0, 0, 0 }; -static const struct snap_search near_down = { 1, 0, 1, 0, 0, 0, -1, 0, 0 }; -static const struct snap_search far_left = { -1, 1, 0, 0, 0, -1, 0, 0, 0 }; -static const struct snap_search far_up = { -1, 0, 1, 0, 0, 0, -1, 0, 0 }; -static const struct snap_search far_right = { 1, 1, 0, 1, 0, 0, 0, 1, 0 }; -static const struct snap_search far_down = { 1, 0, 1, 0, 1, 0, 0, 1, 0 }; - -static inline int -_snap_next_edge(struct view *view, int start_pos, const struct snap_search def, int max, int gap) -{ - struct output *output = view->output; - struct server *server = output->server; - struct view *v; - int p = max; - for_each_view(v, &server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { - if (v == view || v->output != output || v->minimized - || v->maximized == VIEW_AXIS_BOTH) { - continue; - } - - struct border margin = ssd_get_margin(v->ssd); - int vp = -start_pos; - vp += def.add_margin_left * margin.left; - vp += def.add_margin_top * margin.top; - vp += def.add_margin_right * margin.right; - vp += def.add_margin_bottom * margin.bottom; - vp += def.add_view_x * v->pending.x; - vp += def.add_view_y * v->pending.y; - vp += def.add_view_width * v->pending.width; - vp += def.add_view_height - * view_effective_height(v, /* use_pending */ true); - vp += gap; - - if (def.search_dir * vp > 0 && def.search_dir * (vp - p) < 0) { - p = vp; - } - } - return p; -} static void -_snap_move_resize_to_edge(struct view *view, enum view_edge direction, enum snap_mode mode, - struct wlr_box *delta) +check_edge(int *next, int current, int target, int oppose, int align, bool lesser) { - struct border edge = snap_get_view_edge(view); - struct border dmax; - - if (mode == SNAP_MODE_SHRINK) { - /* limit to half of current size */ - int eff_height = - view_effective_height(view, /* use_pending */ true); - int width_max_dx = max(view->pending.width - LAB_MIN_VIEW_WIDTH, 0); - int height_max_dy = max(eff_height - LAB_MIN_VIEW_HEIGHT, 0); - dmax.right = min(width_max_dx, view->pending.width / 2); - dmax.bottom = min(height_max_dy, eff_height / 2); - dmax.left = -dmax.right; - dmax.top = -dmax.bottom; - } else { - dmax = snap_get_max_distance(view); + if (current == target) { + return; } + /* + * The edge defined by current and moving to target may encounter two + * edges of another region: the opposing edge of the region is that in + * the opposite orientation of the moving edge (i.e., left <-> right or + * top <-> bottom); the aligned edge of the region is that in the same + * orientation as the moving edge (i.e., left <-> left, top <-> top, + * right <-> right, bottom <-> bottom). + * + * Any opposing or aligned edge of a region is considered "valid" in + * this search if the edge sits between the current and target + * positions of the moving edge (including the target position itself). + */ + + /* Direction of motion for the edge */ + const bool decreasing = target < current; + + /* Check the opposing edge */ + if ((target <= oppose && oppose < current) || + (current < oppose && oppose <= target)) { + *next = edge_get_best(*next, oppose, decreasing); + } + + /* Check the aligned edge */ + if ((target <= align && align < current) || + (current < align && align <= target)) { + *next = edge_get_best(*next, align, decreasing); + } +} + +void +snap_move_to_edge(struct view *view, enum view_edge direction, + bool snap_to_windows, int *dx, int *dy) +{ + assert(view); + + *dx = 0; + *dy = 0; + + struct output *output = view->output; + if (!output_is_usable(output)) { + wlr_log(WLR_ERROR, "view has no output, not snapping to edge"); + return; + } + + struct wlr_box target = view->pending; + struct border ssd = ssd_thickness(view); + struct wlr_box usable = output_usable_area_in_layout_coords(output); + + /* + * First try to move the view to the relevant edge of its output. If + * the view is off-screen, such a move might actually run contrary to + * the commanded direction (e.g., a view off the screen to the left, + * when moved to the left edge, will actually move rightward). This is + * counter-intuitive, so abandon any such movements. + * + * In addition, any view that is already at the desired screen edge + * needs no further consideration. + */ switch (direction) { case VIEW_EDGE_LEFT: - if (mode == SNAP_MODE_MOVE) { - delta->x += max( - /* left edge to left/right edges */ - _snap_next_edge(view, edge.left, near_left, dmax.left, rc.gap), - _snap_next_edge(view, edge.left, far_left, dmax.left, 0) - ); - } else if (mode == SNAP_MODE_GROW) { - int dx = max( - /* left edge to left/right edges */ - _snap_next_edge(view, edge.left, near_left, dmax.left, rc.gap), - _snap_next_edge(view, edge.left, far_left, dmax.left, 0) - ); - delta->x += dx; - delta->width += -dx; - } else if (mode == SNAP_MODE_SHRINK) { - delta->width += max( - /* right edge to left/right edges */ - _snap_next_edge(view, edge.right, near_left, dmax.left, 0), - _snap_next_edge(view, edge.right, far_left, dmax.left, -rc.gap) - ); - } - break; - case VIEW_EDGE_UP: - if (mode == SNAP_MODE_MOVE) { - delta->y += max( - /* top edge to top/bottom edges */ - _snap_next_edge(view, edge.top, near_up, dmax.top, rc.gap), - _snap_next_edge(view, edge.top, far_up, dmax.top, 0) - ); - } else if (mode == SNAP_MODE_GROW) { - int dy = max( - /* top edge to top/bottom edges */ - _snap_next_edge(view, edge.top, near_up, dmax.top, rc.gap), - _snap_next_edge(view, edge.top, far_up, dmax.top, 0) - ); - delta->y += dy; - delta->height += -dy; - } else if (mode == SNAP_MODE_SHRINK) { - delta->height += max( - /* bottom edge to top/bottom edges */ - _snap_next_edge(view, edge.bottom, near_up, dmax.top, 0), - _snap_next_edge(view, edge.bottom, far_up, dmax.top, -rc.gap) - ); + target.x = usable.x + ssd.left + rc.gap; + if (target.x >= view->pending.x) { + return; } break; case VIEW_EDGE_RIGHT: - if (mode == SNAP_MODE_MOVE) { - delta->x += min3( - /* left edge to left/right edges */ - _snap_next_edge(view, edge.left, near_right, dmax.right, 0), - _snap_next_edge(view, edge.left, far_right, dmax.right, rc.gap), - /* right edge to left edge */ - _snap_next_edge(view, edge.right, near_right, dmax.right, -rc.gap) - ); - } else if (mode == SNAP_MODE_GROW) { - delta->width += min( - /* right edge to left/right edges */ - _snap_next_edge(view, edge.right, near_right, dmax.right, -rc.gap), - _snap_next_edge(view, edge.right, far_right, dmax.right, 0) - ); - } else if (mode == SNAP_MODE_SHRINK) { - delta->x += min( - /* left edge to left/right edges */ - _snap_next_edge(view, edge.left, near_right, dmax.right, 0), - _snap_next_edge(view, edge.left, far_right, dmax.right, rc.gap) - ); - delta->width += -(delta->x); + target.x = usable.x + usable.width + - rc.gap - target.width - ssd.right; + if (target.x <= view->pending.x) { + return; + } + break; + case VIEW_EDGE_UP: + target.y = usable.y + ssd.top + rc.gap; + if (target.y >= view->pending.y) { + return; } break; case VIEW_EDGE_DOWN: - if (mode == SNAP_MODE_MOVE) { - delta->y += min3( - /* top edge to top/bottom edges */ - _snap_next_edge(view, edge.top, near_down, dmax.bottom, 0), - _snap_next_edge(view, edge.top, far_down, dmax.bottom, rc.gap), - /* bottom edge to top edge */ - _snap_next_edge(view, edge.bottom, near_down, dmax.bottom, -rc.gap) - ); - } else if (mode == SNAP_MODE_GROW) { - delta->height += min( - /* bottom edge to top/bottom edges */ - _snap_next_edge(view, edge.bottom, near_down, dmax.bottom, -rc.gap), - _snap_next_edge(view, edge.bottom, far_down, dmax.bottom, 0) - ); - } else if (mode == SNAP_MODE_SHRINK) { - delta->y += min( - /* top edge to top/bottom edges */ - _snap_next_edge(view, edge.top, near_down, dmax.bottom, 0), - _snap_next_edge(view, edge.top, far_down, dmax.bottom, rc.gap) - ); - delta->height += -(delta->y); + target.y = usable.y + usable.height - rc.gap - ssd.bottom + - view_effective_height(view, /* use_pending */ true); + if (target.y <= view->pending.y) { + return; } break; default: return; } -} -void -snap_vector_to_next_edge(struct view *view, enum view_edge direction, int *dx, int *dy) -{ - struct wlr_box delta = {0}; - _snap_move_resize_to_edge(view, direction, SNAP_MODE_MOVE, &delta); - *dx = delta.x; - *dy = delta.y; -} + /* + * Because the target has been updated to put the view at the edge of + * an output, there is no need to check snapping to output edges. If + * snapping to view is desired, check for snapping against any view on + * the same output. + */ + if (snap_to_windows) { + struct border next_edges; + edges_initialize(&next_edges); -int -snap_distance_to_next_edge(struct view *view, enum view_edge direction) -{ - struct wlr_box delta = {0}; - _snap_move_resize_to_edge(view, direction, SNAP_MODE_MOVE, &delta); - switch (direction) { - case VIEW_EDGE_LEFT: return -delta.x; - case VIEW_EDGE_UP: return -delta.y; - case VIEW_EDGE_RIGHT: return delta.x; - case VIEW_EDGE_DOWN: return delta.y; - default: return 0; + edges_find_neighbors(&next_edges, view, target, + output, check_edge, /* use_pending */ true); + + /* If any "best" edges were encountered, limit motion */ + edges_adjust_move_coords(view, next_edges, + &target.x, &target.y, /* use_pending */ true); } + + *dx = target.x - view->pending.x; + *dy = target.y - view->pending.y; } 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) { - _snap_move_resize_to_edge(view, direction, SNAP_MODE_GROW, geo); + assert(view); + assert(!view->shaded); + + *geo = view->pending; + + struct output *output = view->output; + if (!output_is_usable(output)) { + wlr_log(WLR_ERROR, "view has no output, not growing to edge"); + return; + } + + struct border ssd = ssd_thickness(view); + struct wlr_box usable = output_usable_area_in_layout_coords(output); + uint32_t resize_edges; + + /* First try to grow the view to the relevant edge of its output. */ + switch (direction) { + case VIEW_EDGE_LEFT: + geo->x = usable.x + ssd.left + rc.gap; + geo->width = view->pending.x + view->pending.width - geo->x; + resize_edges = WLR_EDGE_LEFT; + break; + case VIEW_EDGE_RIGHT: + geo->width = usable.x + usable.width + - rc.gap - ssd.right - view->pending.x; + resize_edges = WLR_EDGE_RIGHT; + break; + case VIEW_EDGE_UP: + geo->y = usable.y + ssd.top + rc.gap; + geo->height = view->pending.y + view->pending.height - geo->y; + resize_edges = WLR_EDGE_TOP; + break; + case VIEW_EDGE_DOWN: + geo->height = usable.y + usable.height + - rc.gap - ssd.bottom - view->pending.y; + resize_edges = WLR_EDGE_BOTTOM; + break; + default: + return; + } + + /* No grow operation should ever shrink the view */ + if (geo->width < view->pending.width || + geo->height < view->pending.height) { + *geo = view->pending; + return; + } + + /* If the view doesn't change size, there is no need for snap checks */ + if (geo->width == view->pending.width && + geo->height == view->pending.height) { + *geo = view->pending; + return; + } + + struct border next_edges; + edges_initialize(&next_edges); + + /* 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); + edges_adjust_resize_geom(view, next_edges, + resize_edges, geo, /* use_pending */ true); } 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) { - _snap_move_resize_to_edge(view, direction, SNAP_MODE_SHRINK, geo); + assert(view); + assert(!view->shaded); + + *geo = view->pending; + uint32_t resize_edges; + + /* + * First shrink the view along the relevant edge. The maximum shrink + * allowed is half the current size, but the window must also meet + * minimum size requirements. + */ + switch (direction) { + case VIEW_EDGE_RIGHT: + geo->width = MAX(geo->width / 2, LAB_MIN_VIEW_WIDTH); + geo->x = view->pending.x + view->pending.width - geo->width; + resize_edges = WLR_EDGE_LEFT; + break; + case VIEW_EDGE_LEFT: + geo->width = MAX(geo->width / 2, LAB_MIN_VIEW_WIDTH); + resize_edges = WLR_EDGE_RIGHT; + break; + case VIEW_EDGE_DOWN: + geo->height = MAX(geo->height / 2, LAB_MIN_VIEW_HEIGHT); + geo->y = view->pending.y + view->pending.height - geo->height; + resize_edges = WLR_EDGE_TOP; + break; + case VIEW_EDGE_UP: + geo->height = MAX(geo->height / 2, LAB_MIN_VIEW_HEIGHT); + resize_edges = WLR_EDGE_BOTTOM; + break; + default: + return; + } + + /* If the view doesn't change size, abandon the shrink */ + if (geo->width == view->pending.width && + geo->height == view->pending.height) { + *geo = view->pending; + return; + } + + struct border next_edges; + edges_initialize(&next_edges); + + /* 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); + + /* 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); + + edges_adjust_resize_geom(view, next_edges, + resize_edges, geo, /* use_pending */ true); } diff --git a/src/view.c b/src/view.c index 180ae189..424e921e 100644 --- a/src/view.c +++ b/src/view.c @@ -1550,27 +1550,7 @@ view_move_to_edge(struct view *view, enum view_edge direction, bool snap_to_wind } int dx = 0, dy = 0; - if (snap_to_windows) { - snap_vector_to_next_edge(view, direction, &dx, &dy); - } else { - struct border distance = snap_get_max_distance(view); - switch (direction) { - case VIEW_EDGE_LEFT: - dx = distance.left; - break; - case VIEW_EDGE_UP: - dy = distance.top; - break; - case VIEW_EDGE_RIGHT: - dx = distance.right; - break; - case VIEW_EDGE_DOWN: - dy = distance.bottom; - break; - default: - return; - } - } + snap_move_to_edge(view, direction, snap_to_windows, &dx, &dy); if (dx != 0 || dy != 0) { /* Move the window if a change was discovered */ @@ -1665,7 +1645,7 @@ view_grow_to_edge(struct view *view, enum view_edge direction) view_set_shade(view, false); - struct wlr_box geo = view->pending; + struct wlr_box geo; snap_grow_to_next_edge(view, direction, &geo); view_move_resize(view, geo); }