]> git.mdlowis.com Git - proto/labwc.git/commitdiff
feat: automatic window placement
authorAndrew J. Hesford <ajh@sideband.org>
Sun, 24 Dec 2023 18:57:46 +0000 (13:57 -0500)
committerJohan Malm <johanmalm@users.noreply.github.com>
Tue, 26 Dec 2023 19:20:06 +0000 (19:20 +0000)
With automatic placement, new top-level windows will be placed to
minimize overlap with other windows already on screen.

docs/labwc-config.5.scd
include/config/rcxml.h
include/placement.h [new file with mode: 0644]
src/config/rcxml.c
src/meson.build
src/placement.c [new file with mode: 0644]
src/view.c

index c37dddc2fea58543455391edc19cfa52a94272ba..fac994d0445de0a2b396e41806504a1cb2d3d728 100644 (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
 
index b6a116dafa4989f50906a10f236df09e54917817..068baa9eb16ba590962b20cec1faa040bf941e7a 100644 (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 {
diff --git a/include/placement.h b/include/placement.h
new file mode 100644 (file)
index 0000000..ccbbe4b
--- /dev/null
@@ -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 */
index 5ff069eff3153831d36ca861ff350ce5cbe5cbdf..50b8f85c4e61a9f75da1549fbeb8efc8a97a8088 100644 (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;
index 9a04034a641956fc8c51302aa3c5dceb9e81f271..a14dc83e8e1c2d607b8071ad8e4b844dc6779fa5 100644 (file)
@@ -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 (file)
index 0000000..1643cdb
--- /dev/null
@@ -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;
+}
index 232e1dca05bb638550b8c0fad45becdc12e84937..d2b3bb50043328a7496909150be03ebd55817ede 100644 (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);