From: Andrew J. Hesford Date: Sun, 24 Dec 2023 18:57:46 +0000 (-0500) Subject: feat: automatic window placement X-Git-Url: https://git.mdlowis.com/?a=commitdiff_plain;h=52aafcc054230f8467bd05fea77bd9bb20dafabd;p=proto%2Flabwc.git feat: automatic window placement With automatic placement, new top-level windows will be placed to minimize overlap with other windows already on screen. --- diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index c37dddc2..fac994d0 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -134,10 +134,12 @@ this is for compatibility with Openbox. ## PLACEMENT -** [center|cursor] +** [center|automatic|cursor] Specify a placement policy for new windows. The "center" policy will - always place windows at the center of the active output. The "cursor" - policy will center new windows under the cursor. Default is "center". + always place windows at the center of the active output. The "automatic" + policy will try to place new windows in such a way that they will + have minimal overlap with existing windows. The "cursor" policy will + center new windows under the cursor. Default is "center". ## WINDOW SWITCHER diff --git a/include/config/rcxml.h b/include/config/rcxml.h index b6a116da..068baa9e 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -22,7 +22,8 @@ enum window_switcher_field_content { enum view_placement_policy { LAB_PLACE_CENTER = 0, - LAB_PLACE_CURSOR + LAB_PLACE_CURSOR, + LAB_PLACE_AUTOMATIC }; struct usable_area_override { diff --git a/include/placement.h b/include/placement.h new file mode 100644 index 00000000..ccbbe4b9 --- /dev/null +++ b/include/placement.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_PLACEMENT_H +#define LABWC_PLACEMENT_H + +#include +#include "view.h" + +bool placement_find_best(struct view *view, int *x, int *y); + +#endif /* LABWC_PLACEMENT_H */ diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 5ff069ef..50b8f85c 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -704,7 +704,9 @@ entry(xmlNode *node, char *nodename, char *content) } else if (!strcasecmp(nodename, "reuseOutputMode.core")) { set_bool(content, &rc.reuse_output_mode); } else if (!strcmp(nodename, "policy.placement")) { - if (!strcmp(content, "cursor")) { + if (!strcmp(content, "automatic")) { + rc.placement_policy = LAB_PLACE_AUTOMATIC; + } else if (!strcmp(content, "cursor")) { rc.placement_policy = LAB_PLACE_CURSOR; } else { rc.placement_policy = LAB_PLACE_CENTER; diff --git a/src/meson.build b/src/meson.build index 9a04034a..a14dc83e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,6 +12,7 @@ labwc_sources = files( 'node.c', 'osd.c', 'output.c', + 'placement.c', 'regions.c', 'resistance.c', 'seat.c', diff --git a/src/placement.c b/src/placement.c new file mode 100644 index 00000000..1643cdb8 --- /dev/null +++ b/src/placement.c @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include "common/macros.h" +#include "common/mem.h" +#include "labwc.h" +#include "placement.h" +#include "ssd.h" +#include "view.h" + +#define overlap_bitmap_index(bmp, i, j) \ + (bmp)->grid[i * ((bmp)->nr_cols - 1) + j] + +struct overlap_bitmap { + int nr_rows; + int nr_cols; + int *rows; + int *cols; + int *grid; +}; + +static int +compare_ints(const void *a, const void *b) +{ + return *(const int *)a - *(const int *)b; +} + +static void +destroy_bitmap(struct overlap_bitmap *bmp) +{ + assert(bmp); + + zfree(bmp->rows); + zfree(bmp->cols); + zfree(bmp->grid); + + bmp->nr_rows = 0; + bmp->nr_cols = 0; +} + +/* Count the number of views on view->output, excluding *view itself */ +static int +count_views(struct view *view) +{ + assert(view); + + struct server *server = view->server; + assert(server); + + struct output *output = view->output; + if (!output_is_usable(output)) { + return 0; + } + + int nviews = 0; + + struct view *v; + for_each_view(v, &server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { + /* Ignore the target view or anything on a different output */ + if (v == view || v->output != output) { + continue; + } + + nviews++; + } + + return nviews; +} + +/* Sort and de-deplicate a list of points that define a 1-D grid */ +static int +order_grid(int *edges, int nedges) +{ + /* Sort grid edges */ + qsort(edges, nedges, sizeof(int), compare_ints); + + /* Skip over non-unique edges, counting the unique ones */ + /* This is taken almost verbatim from Openbox. */ + int i = 0; + int j = 0; + + while (j < nedges) { + int last = edges[j++]; + edges[i++] = last; + while (j < nedges && edges[j] == last) { + ++j; + } + } + + return i; +} + +/* + * Construct an irregular grid that divides the usable area of view->output + * by extending the edges of every view on the output (except for *view itself) + * to infinity. The resulting grid will consist of rectangular intervals that + * are either completely uncovered by any view, or entirely covered. + * Furthermore, when any view intersects any interval on the grid, that view + * overlaps the whole interval: no view ever partially intersects any interval. + */ +static void +build_grid(struct overlap_bitmap *bmp, struct view *view) +{ + assert(bmp); + assert(view); + + struct server *server = view->server; + assert(server); + + /* Always start with a fresh bitmap */ + destroy_bitmap(bmp); + + struct output *output = view->output; + if (!output_is_usable(output)) { + return; + } + + int nviews = count_views(view); + if (nviews < 1) { + return; + } + + /* Number of rows/columns is bounded by two per view plus screen edges */ + int max_rc = 2 * nviews + 2; + + bmp->rows = xzalloc(max_rc * sizeof(int)); + bmp->cols = xzalloc(max_rc * sizeof(int)); + if (!bmp->rows || !bmp->cols) { + destroy_bitmap(bmp); + return; + } + + /* First edges of grid are start of usable area of output */ + struct wlr_box usable = output_usable_area_in_layout_coords(output); + int usable_right = usable.x + usable.width; + int usable_bottom = usable.y + usable.height; + + bmp->cols[0] = usable.x; + bmp->rows[0] = usable.y; + + bmp->cols[1] = usable_right; + bmp->rows[1] = usable_bottom; + + int nr_rows = 2; + int nr_cols = 2; + + struct view *v; + for_each_view(v, &server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { + if (v == view || v->output != output) { + continue; + } + + struct border margin = ssd_get_margin(v->ssd); + int x = v->pending.x - margin.left; + int y = v->pending.y - margin.top; + + /* Add a column if the left view edge is in the usable region */ + if (x > usable.x && x < usable_right) { + assert(nr_cols < max_rc); + bmp->cols[nr_cols++] = x; + } + + /* Add a row if the top view edge is in the usable region */ + if (y > usable.y && y < usable_bottom) { + assert(nr_rows < max_rc); + bmp->rows[nr_rows++] = y; + } + + x = v->pending.x + v->pending.width + margin.right; + y = v->pending.y + v->pending.height + margin.bottom; + + /* Add a column if the right view edge is in the usable region */ + if (x > usable.x && x < usable_right) { + assert(nr_cols < max_rc); + bmp->cols[nr_cols++] = x; + } + + /* Add a row if the bottom view edge is in the usable region */ + if (y > usable.y && y < usable_bottom) { + assert(nr_rows < max_rc); + bmp->rows[nr_rows++] = y; + } + } + + bmp->nr_rows = order_grid(bmp->rows, nr_rows); + bmp->nr_cols = order_grid(bmp->cols, nr_cols); + + int grid_size = (bmp->nr_rows - 1) * (bmp->nr_cols - 1); + + bmp->grid = xzalloc(grid_size * sizeof(int)); + if (!bmp->grid) { + destroy_bitmap(bmp); + return; + } +} + +/* + * Perform a rightmost binary search along a list of edges in a 1-D grid for + * the maximum index j such that edges[j] <= val. The list of edges must be + * sorted in increasing order. + * + * For a returned index j: + * + * - The index j == -1 implies that val < edges[0]. + * - An index 0 <= j < (nedges - 1) implies that edges[j] <= val < edges[j + 1]. + * - The index j == (nedges - 1) implies that edges[nedges - 1] <= val. + */ +static int +find_interval(int *edges, int nedges, double val) +{ + int l = 0; + int r = nedges; + + while (l < r) { + int m = (l + r) / 2; + if (edges[m] > val) { + r = m; + } else { + l = m + 1; + } + } + + return r - 1; +} + +/* + * Construct an overlap bitmap for the irregular grid, computed by + * build_grid(), that spans view->output. The overlap bitmap maps + * each interval to the number of views on the output (excluding *view) + * that overlap that interval. + */ +static void +build_overlap(struct overlap_bitmap *bmp, struct view *view) +{ + assert(bmp); + assert(view); + + struct server *server = view->server; + assert(server); + + if (bmp->nr_rows < 1 || bmp->nr_cols < 1) { + return; + } + + struct output *output = view->output; + if (!output_is_usable(output)) { + return; + } + + struct view *v; + for_each_view(v, &server->views, LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) { + if (v == view || v->output != output) { + continue; + } + + /* Find boundaries of the window */ + struct border margin = ssd_get_margin(v->ssd); + int lx = v->pending.x - margin.left; + int ly = v->pending.y - margin.top; + int hx = v->pending.x + v->pending.width + margin.right; + int hy = v->pending.y + v->pending.height + margin.bottom; + + /* + * Find the first and last row and column intervals spanned by + * this view. We want the left and top edges to fall in a + * half-open interval [low, high) but the right and bottom + * edges to fall in a half-open interval (low, high] to ensure + * that the results do not include intervals adjacent to the + * view. View edges are guaranteed by construction to fall + * exactly on the grid points, so we perturb the left and top + * edges by +0.5 units, and the right and bottom edges by -0.5 + * units, to ensure that we are always searching in the + * interior of an interval. + */ + + /* First row and column overlapping the view */ + int fc = find_interval(bmp->cols, bmp->nr_cols, lx + 0.5); + int fr = find_interval(bmp->rows, bmp->nr_rows, ly + 0.5); + + /* Clip first row/column to start of usable grid */ + fc = MAX(fc, 0); + fr = MAX(fr, 0); + + /* Last row and column overlapping the view */ + int lc = find_interval(bmp->cols, bmp->nr_cols, hx - 0.5); + int lr = find_interval(bmp->rows, bmp->nr_rows, hy - 0.5); + + /* + * Increment the last indices to convert them to strict upper + * bounds, then clip them to the limits of the usable grid. + */ + lc = MIN(bmp->nr_cols - 1, lc + 1); + lr = MIN(bmp->nr_rows - 1, lr + 1); + + /* + * Every interval in the region [fr, lr) x [fc, lc) is + * completely covered by the view. Increment the overlap + * counters these intervals to account for the view. + */ + for (int i = fr; i < lr; ++i) { + for (int j = fc; j < lc; ++j) { + overlap_bitmap_index(bmp, i, j) += 1; + } + } + } +} + +/* + * Find the total overlap of an arbitrary region of a given width and height + * with intervals in a pre-computed overlap bitmap. The starting interval for + * the region is (i, j) in the bitmap grid. If the region is larger than + * interval (i, j), neighboring regions will be considered width-wise rightward + * (when right is true) or leftward (otherwise) and height-wise downward (when + * down is true) or upward (otherwise). + * + * If the region would extend beyond the edges of the grid (i.e., beyond the + * usable region of an output) in the prescribed directions, an overlap of + * INT_MAX is returned. Otherwise, the overlap is the sum of the areas of each + * interval covered by the region multiplied by its overlap count. For example, + * an interval currently covered by three windows will be triply counted in the + * overlap sum. + */ +static int +compute_overlap(struct overlap_bitmap *bmp, int i, int j, + int width, int height, bool right, bool down, bool *single) +{ + /* + * The number of row or column intervals is one less than corresponding + * number of row or column grid points. + */ + int nri = bmp->nr_rows - 1; + int nci = bmp->nr_cols - 1; + + int i_incr = down ? 1 : -1; + int j_incr = right ? 1 : -1; + + int overlap = 0; + int count = 0; + + /* Walk up or down along rows according to preference */ + for (int ii = i; ii >= 0 && ii < nri && height > 0; ii += i_incr) { + /* Height of this row */ + int rh = bmp->rows[ii + 1] - bmp->rows[ii]; + + /* Height of overlap between this row and test region */ + int mh = MAX(0, MIN(height, rh)); + + /* Remaining height to consider for next row */ + height -= rh; + + /* Walk left or right along columns according to preference */ + int ww = width; + for (int jj = j; jj >= 0 && jj < nci && ww > 0; jj += j_incr) { + /* Width of this column */ + int cw = bmp->cols[jj + 1] - bmp->cols[jj]; + + /* Width of overlap between this column and test region */ + int mw = MAX(0, MIN(ww, cw)); + + /* Add overlap contribution for this interval */ + overlap += overlap_bitmap_index(bmp, ii, jj) * mh * mw; + + /* Count the number of overlapping intervals */ + count++; + + /* Remaining width to consider for next column */ + ww -= cw; + } + + /* + * If there is width left to consider after walking columns, + * the region extends out of bounds and placement is invalid. + */ + if (ww > 0) { + overlap = INT_MAX; + break; + } + } + + /* + * If there is height left ot consider after walking rows, the region + * extends out of bounds and placement is invalid. + */ + if (height > 0) { + overlap = INT_MAX; + } + + /* Indicate whether overlap is confined to a single region */ + if (single) { + *single = (count == 1); + } + + return overlap; +} + +/* + * Find, in (*x, *y), the placement of *view on its output that will minimize + * overlap with all other views. + */ +bool +placement_find_best(struct view *view, int *x, int *y) +{ + assert(view); + + struct server *server = view->server; + assert(server); + + struct border margin = ssd_get_margin(view->ssd); + + struct output *output = view->output; + if (!output_is_usable(output)) { + return false; + } + + /* Default placement is just the upper-left corner of the output */ + struct wlr_box usable = output_usable_area_in_layout_coords(output); + *x = usable.x + margin.left; + *y = usable.y + margin.top; + + /* Build the placement grid and overlap bitmap */ + struct overlap_bitmap bmp = { 0 }; + build_grid(&bmp, view); + build_overlap(&bmp, view); + + int height = view->pending.height + margin.top + margin.bottom; + int width = view->pending.width + margin.left + margin.right; + + int min_overlap = INT_MAX; + + int nri = bmp.nr_rows - 1; + int nci = bmp.nr_cols - 1; + + /* + * Convolve the view region with the overlap grid to determine the + * total overlap of the view in all possible positions on the grid. + * + * When the view starts in a particular interval and is wider than the + * interval, it can extend either rightward (by placing the left edge + * of the view on the left edge of the interval) or leftward (by + * placing the right edge of the view on the right edge of the + * interval) into adjoining intervals. Likewise, when the view is wider + * than the interval in which it starts, it can extend either upward + * (by placing the bottom edge of the view on the bottom edge of the + * interval) or downward (by placing the top edge of the view on the + * top edge of the interval). All four possibilities produce different + * overlap characteristics and need to be checked independently. + * + * If the view is no larger than the interval in which it starts, there + * is no need to check multiple directions---the overlap will be the + * same regardless of where in the interval the window is placed. + * + * The interval (and, when the view spans more than one interval, + * directions in which it should extend) that produces the smallest + * overlap with other windows will determine the view placement. + */ + for (int i = 0; i < nri; ++i) { + for (int j = 0; j < nci; ++j) { + /* + * Search all directions, as a two-bit field, starting + * from interval (i, j). + */ + for (int ii = 0; ii < 4; ++ii) { + /* Left/right is determined by first bit */ + bool rt = (ii & 0x1) == 0; + /* Up/down is determined by second bit */ + bool dn = (ii & 0x2) == 0; + + /* Track whether overlap comes from single region */ + bool single = false; + + /* Compute overlap in specified direction */ + int overlap = compute_overlap(&bmp, i, j, + width, height, rt, dn, &single); + + /* Move on if overlap isn't reduced */ + if (overlap >= min_overlap) { + continue; + } + + /* Place window in optimal direction */ + min_overlap = overlap; + + if (rt) { + /* Extend window right from left edge */ + *x = bmp.cols[j] + margin.left; + } else { + /* Extend window left from right edge */ + *x = bmp.cols[j + 1] - width + margin.left; + } + + if (dn) { + /* Extend window down from top edge */ + *y = bmp.rows[i] + margin.top; + } else { + /* Extend window up from bottom edge */ + *y = bmp.rows[i + 1] - height + margin.top; + } + + /* If there is no overlap, the search is done. */ + if (min_overlap <= 0) { + goto final_placement; + } + + /* + * Skip multi-directional searches when the + * view fits completely within one region. + */ + if (single) { + break; + } + } + } + } + +final_placement: + destroy_bitmap(&bmp); + return true; +} diff --git a/src/view.c b/src/view.c index 232e1dca..d2b3bb50 100644 --- a/src/view.c +++ b/src/view.c @@ -9,6 +9,7 @@ #include "input/keyboard.h" #include "labwc.h" #include "menu/menu.h" +#include "placement.h" #include "regions.h" #include "resize_indicator.h" #include "snap.h" @@ -684,6 +685,12 @@ view_place_initial(struct view *view) if (rc.placement_policy == LAB_PLACE_CURSOR) { view_move_to_cursor(view); return; + } else if (rc.placement_policy == LAB_PLACE_AUTOMATIC) { + int x = 0, y = 0; + if (placement_find_best(view, &x, &y)) { + view_move(view, x, y); + return; + } } view_center(view, NULL);