feat: automatic window placement

With automatic placement, new top-level windows will be placed to
minimize overlap with other windows already on screen.
This commit is contained in:
Andrew J. Hesford 2023-12-24 13:57:46 -05:00 committed by Johan Malm
parent ef62d47ad1
commit 52aafcc054
7 changed files with 548 additions and 5 deletions

View file

@ -134,10 +134,12 @@ this is for compatibility with Openbox.
## PLACEMENT
*<placement><policy>* [center|cursor]
*<placement><policy>* [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

View file

@ -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 {

10
include/placement.h Normal file
View file

@ -0,0 +1,10 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef LABWC_PLACEMENT_H
#define LABWC_PLACEMENT_H
#include <stdbool.h>
#include "view.h"
bool placement_find_best(struct view *view, int *x, int *y);
#endif /* LABWC_PLACEMENT_H */

View file

@ -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;

View file

@ -12,6 +12,7 @@ labwc_sources = files(
'node.c',
'osd.c',
'output.c',
'placement.c',
'regions.c',
'resistance.c',
'seat.c',

520
src/placement.c Normal file
View file

@ -0,0 +1,520 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#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;
}

View file

@ -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);