]> git.mdlowis.com Git - proto/labwc.git/commitdiff
view: implement `cascade` placement policy
authortokyo4j <hrak1529@gmail.com>
Fri, 3 May 2024 21:26:27 +0000 (06:26 +0900)
committerJohan Malm <johanmalm@users.noreply.github.com>
Sat, 20 Jul 2024 07:59:46 +0000 (08:59 +0100)
Adds following settings:
<placement>
  <policy>cascade</policy>
  <cascadeOffset x="40" y="30" />
</placement>

"Cascade" policy places a new window at the center of the screen like
"center" policy, but possibly shifts its position to bottom-right so the
new window doesn't cover existing windows.

The algorithm is copied from KWin's implementation:
https://github.com/KDE/kwin/blob/df9f8f8346b5b7645578e37365dabb1a7b02ca5a/src/placement.cpp#L589

Also added some helper functions to manipulate `wlr_box`.

docs/labwc-actions.5.scd
docs/labwc-config.5.scd
docs/rc.xml.all
include/common/box.h [new file with mode: 0644]
include/config/rcxml.h
src/common/box.c [new file with mode: 0644]
src/common/meson.build
src/config/rcxml.c
src/edges.c
src/view.c

index e2edf387d3d09bfef1ef633a0161b7a3674ea34d..9ccfb7988c887eec19d7c292942073c89fe59e19 100644 (file)
@@ -276,9 +276,9 @@ Actions are used in menus and keyboard/mouse bindings.
 *<action name="AutoPlace" policy="value"/>*
        Reposition the window according to the desired placement policy.
 
-       *policy* [automatic|cursor|center] Use the specified policy, which has
-       the same meaning as the corresponding value for *<placement><policy>*.
-       Default is automatic.
+       *policy* [automatic|cursor|center|cascade] Use the specified policy,
+       which has the same meaning as the corresponding value for
+       *<placement><policy>*. Default is automatic.
 
 *<action name="Shade" />*++
 *<action name="Unshade" />*++
index 70f705c61195e0d9ba2a230a01ff47975e9db61d..f2d6e925feaa7f5468bbc8861026e7fbdde7ae5b 100644 (file)
@@ -203,12 +203,22 @@ this is for compatibility with Openbox.
 
 ## PLACEMENT
 
-*<placement><policy>* [center|automatic|cursor]
+*<placement><policy>* [center|automatic|cursor|cascade]
        Specify a placement policy for new windows. The "center" policy will
        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".
+       center new windows under the cursor. The "cascade" policy will try to
+       place new windows at the center of the active output, but possibly
+       shifts its position to bottom-right not to cover existing windows.
+       Default is "center".
+
+*<placement><cascadeOffset><x>*++
+*<placement><cascadeOffset><y>*
+       Specify the offset by which a new window can be shifted from an existing
+       window when <placement><policy> is "cascade". These values must be positive.
+       Default is the height of titlebar (the sum of *titlebar.height* and
+       *border.width* from theme) plus 5 for both *x* and *y*.
 
 ## WINDOW SWITCHER
 
index f481c09137fd3ebe31dc90cfbdd603098cf23455..c537b03944140dfb3575ddf81cfb73da175bb1f0 100644 (file)
 
   <placement>
     <policy>center</policy>
+    <!--
+      When <placement><policy> is "cascade", the offset for cascading new
+      windows can be overwritten like this:
+      <cascadeOffset x="40" y="30" />
+    -->
   </placement>
 
   <!-- <font><theme> can be defined without an attribute to set all places -->
diff --git a/include/common/box.h b/include/common/box.h
new file mode 100644 (file)
index 0000000..105ff29
--- /dev/null
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef LABWC_BOX_H
+#define LABWC_BOX_H
+
+#include <wlr/util/box.h>
+
+bool
+box_contains(struct wlr_box *box_super, struct wlr_box *box_sub);
+
+bool
+box_intersects(struct wlr_box *box_a, struct wlr_box *box_b);
+
+/* Returns the bounding box of 2 boxes */
+void
+box_union(struct wlr_box *box_dest, struct wlr_box *box_a, struct wlr_box *box_b);
+
+#endif /* LABWC_BOX_H */
index 1a2bb203af73c9dcc4a38029e613e1f4ac81b3fd..53263df766d6c297cb80f2ab4ff266a9155d8d24 100644 (file)
@@ -20,7 +20,8 @@ enum view_placement_policy {
        LAB_PLACE_INVALID = 0,
        LAB_PLACE_CENTER,
        LAB_PLACE_CURSOR,
-       LAB_PLACE_AUTOMATIC
+       LAB_PLACE_AUTOMATIC,
+       LAB_PLACE_CASCADE,
 };
 
 enum adaptive_sync_mode {
@@ -57,6 +58,8 @@ struct rcxml {
        bool reuse_output_mode;
        enum view_placement_policy placement_policy;
        bool xwayland_persistence;
+       int placement_cascade_offset_x;
+       int placement_cascade_offset_y;
 
        /* focus */
        bool focus_follow_mouse;
diff --git a/src/common/box.c b/src/common/box.c
new file mode 100644 (file)
index 0000000..1a56cfe
--- /dev/null
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <assert.h>
+#include "common/box.h"
+#include "common/macros.h"
+
+bool
+box_contains(struct wlr_box *box_super, struct wlr_box *box_sub)
+{
+       if (wlr_box_empty(box_super) || wlr_box_empty(box_sub)) {
+               return false;
+       }
+       return box_super->x <= box_sub->x
+               && box_super->x + box_super->width >= box_sub->x + box_sub->width
+               && box_super->y <= box_sub->y
+               && box_super->y + box_super->height >= box_sub->y + box_sub->height;
+}
+
+bool
+box_intersects(struct wlr_box *box_a, struct wlr_box *box_b)
+{
+       if (wlr_box_empty(box_a) || wlr_box_empty(box_b)) {
+               return false;
+       }
+       return box_a->x < box_b->x + box_b->width
+               && box_b->x < box_a->x + box_a->width
+               && box_a->y < box_b->y + box_b->height
+               && box_b->y < box_a->y + box_a->height;
+}
+
+void
+box_union(struct wlr_box *box_dest, struct wlr_box *box_a, struct wlr_box *box_b)
+{
+       if (wlr_box_empty(box_a)) {
+               *box_dest = *box_b;
+               return;
+       }
+       if (wlr_box_empty(box_b)) {
+               *box_dest = *box_a;
+               return;
+       }
+       int x1 = MIN(box_a->x, box_b->x);
+       int y1 = MIN(box_a->y, box_b->y);
+       int x2 = MAX(box_a->x + box_a->width, box_b->x + box_b->width);
+       int y2 = MAX(box_a->y + box_a->height, box_b->y + box_b->height);
+       box_dest->x = x1;
+       box_dest->y = y1;
+       box_dest->width = x2 - x1;
+       box_dest->height = y2 - y1;
+}
index 441e92c0c834735a8748978846af149a2c04d00f..1569f77555088d45e459554ffde1a8539f5bde87 100644 (file)
@@ -1,4 +1,5 @@
 labwc_sources += files(
+       'box.c',
   'buf.c',
   'dir.c',
   'fd-util.c',
index 9ad0268aa82b06a74f0948e70aa4e7fd49fd92ee..28eff198d70ce6793cab0cb19e8bb8354c148f6c 100644 (file)
@@ -901,6 +901,10 @@ entry(xmlNode *node, char *nodename, char *content)
                }
        } else if (!strcasecmp(nodename, "xwaylandPersistence.core")) {
                set_bool(content, &rc.xwayland_persistence);
+       } else if (!strcasecmp(nodename, "x.cascadeOffset.placement")) {
+               rc.placement_cascade_offset_x = atoi(content);
+       } else if (!strcasecmp(nodename, "y.cascadeOffset.placement")) {
+               rc.placement_cascade_offset_y = atoi(content);
        } else if (!strcmp(nodename, "name.theme")) {
                rc.theme_name = xstrdup(content);
        } else if (!strcmp(nodename, "cornerradius.theme")) {
@@ -1234,6 +1238,8 @@ rcxml_init(void)
        has_run = true;
 
        rc.placement_policy = LAB_PLACE_CENTER;
+       rc.placement_cascade_offset_x = 0;
+       rc.placement_cascade_offset_y = 0;
 
        rc.xdg_shell_server_side_deco = true;
        rc.ssd_keep_border = true;
index bd96c5a05d9ca2eda515b7b301a6cce8360fa497..c9c881845580cd55ed0a097d04b2eb5d276b202f 100644 (file)
@@ -5,6 +5,7 @@
 #include <wlr/util/edges.h>
 #include <wlr/util/box.h>
 #include "common/border.h"
+#include "common/box.h"
 #include "common/macros.h"
 #include "config/rcxml.h"
 #include "edges.h"
@@ -466,9 +467,8 @@ edges_find_outputs(struct border *nearest_edges, struct view *view,
                struct wlr_box usable =
                        output_usable_area_in_layout_coords(o);
 
-               struct wlr_box ol;
-               if (!wlr_box_intersection(&ol, &origin, &usable) &&
-                               !wlr_box_intersection(&ol, &target, &usable)) {
+               if (!box_intersects(&origin, &usable)
+                               && !box_intersects(&target, &usable)) {
                        continue;
                }
 
index 6a0f9806761b9064682be8a51b87400cf26a69d2..d06251aa389a6d96d6939f0806379840b14370fb 100644 (file)
@@ -4,6 +4,7 @@
 #include <strings.h>
 #include <wlr/types/wlr_output_layout.h>
 #include <wlr/types/wlr_security_context_v1.h>
+#include "common/box.h"
 #include "common/macros.h"
 #include "common/match.h"
 #include "common/mem.h"
@@ -873,6 +874,94 @@ view_center(struct view *view, const struct wlr_box *ref)
        }
 }
 
+/*
+ * Algorithm based on KWin's implementation:
+ * https://github.com/KDE/kwin/blob/df9f8f8346b5b7645578e37365dabb1a7b02ca5a/src/placement.cpp#L589
+ */
+static void
+view_cascade(struct view *view)
+{
+       /* "cascade" policy places a new view at center by default */
+       struct wlr_box center = view->pending;
+       view_compute_centered_position(view, NULL,
+               center.width, center.height, &center.x, &center.y);
+       struct border margin = ssd_get_margin(view->ssd);
+       center.x -= margin.left;
+       center.y -= margin.top;
+       center.width += margin.left + margin.right;
+       center.height += margin.top + margin.bottom;
+
+       /* Candidate geometry to which the view is moved */
+       struct wlr_box candidate = center;
+
+       struct wlr_box usable = output_usable_area_in_layout_coords(view->output);
+
+       /* TODO: move this logic to rcxml.c */
+       int offset_x = rc.placement_cascade_offset_x;
+       int offset_y = rc.placement_cascade_offset_y;
+       struct theme *theme = view->server->theme;
+       int default_offset = theme->title_height + theme->border_width + 5;
+       if (offset_x <= 0) {
+               offset_x = default_offset;
+       }
+       if (offset_y <= 0) {
+               offset_y = default_offset;
+       }
+
+       /*
+        * Keep updating the candidate until it doesn't cover any existing views
+        * or doesn't fit within the usable area.
+        */
+       bool candidate_updated = true;
+       while (candidate_updated) {
+               candidate_updated = false;
+               struct wlr_box covered = {0};
+
+               /* Iterate over views from top to bottom */
+               struct view *other_view;
+               for_each_view(other_view, &view->server->views,
+                               LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) {
+                       struct wlr_box other = ssd_max_extents(other_view);
+                       if (other_view == view
+                                       || view->minimized
+                                       || !box_intersects(&candidate, &other)) {
+                               continue;
+                       }
+                       /*
+                        * If the candidate covers an existing view whose
+                        * top-left corner is not covered by other views,
+                        * shift the candidate to bottom-right.
+                        */
+                       if (box_contains(&candidate, &other)
+                                       && !wlr_box_contains_point(
+                                               &covered, other.x, other.y)) {
+                               candidate.x = other.x + offset_x;
+                               candidate.y = other.y + offset_y;
+                               if (!box_contains(&usable, &candidate)) {
+                                       /*
+                                        * If the candidate doesn't fit within
+                                        * the usable area, fall back to center
+                                        * and finish updating the candidate.
+                                        */
+                                       candidate = center;
+                                       break;
+                               } else {
+                                       /* Repeat with the new candidate */
+                                       candidate_updated = true;
+                                       break;
+                               }
+                       }
+                       /*
+                        * We use just a bounding box to represent the covered
+                        * area, which would be fine for our use-case.
+                        */
+                       box_union(&covered, &covered, &other);
+               }
+       }
+
+       view_move(view, candidate.x + margin.left, candidate.y + margin.top);
+}
+
 void
 view_place_by_policy(struct view *view, bool allow_cursor,
                enum view_placement_policy policy)
@@ -886,6 +975,9 @@ view_place_by_policy(struct view *view, bool allow_cursor,
                        view_move(view, geometry.x, geometry.y);
                        return;
                }
+       } else if (policy == LAB_PLACE_CASCADE) {
+               view_cascade(view);
+               return;
        }
 
        view_center(view, NULL);
@@ -1075,14 +1167,11 @@ view_apply_maximized_geometry(struct view *view)
         * center the unmaximized axis.
         */
        struct wlr_box natural = view->natural_geometry;
-       if (view->maximized != VIEW_AXIS_BOTH) {
-               struct wlr_box intersect;
-               wlr_box_intersection(&intersect, &box, &natural);
-               if (wlr_box_empty(&intersect)) {
-                       view_compute_centered_position(view, NULL,
-                               natural.width, natural.height,
-                               &natural.x, &natural.y);
-               }
+       if (view->maximized != VIEW_AXIS_BOTH
+                       && !box_intersects(&box, &natural)) {
+               view_compute_centered_position(view, NULL,
+                       natural.width, natural.height,
+                       &natural.x, &natural.y);
        }
 
        if (view->ssd_enabled) {
@@ -2004,6 +2093,8 @@ view_placement_parse(const char *policy)
                return LAB_PLACE_CURSOR;
        } else if (!strcasecmp(policy, "center")) {
                return LAB_PLACE_CENTER;
+       } else if (!strcasecmp(policy, "cascade")) {
+               return LAB_PLACE_CASCADE;
        }
 
        return LAB_PLACE_INVALID;