*<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" />*++
## 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
<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 -->
--- /dev/null
+/* 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 */
LAB_PLACE_INVALID = 0,
LAB_PLACE_CENTER,
LAB_PLACE_CURSOR,
- LAB_PLACE_AUTOMATIC
+ LAB_PLACE_AUTOMATIC,
+ LAB_PLACE_CASCADE,
};
enum adaptive_sync_mode {
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;
--- /dev/null
+// 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;
+}
labwc_sources += files(
+ 'box.c',
'buf.c',
'dir.c',
'fd-util.c',
}
} 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")) {
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;
#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"
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;
}
#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"
}
}
+/*
+ * 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, ¢er.x, ¢er.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)
view_move(view, geometry.x, geometry.y);
return;
}
+ } else if (policy == LAB_PLACE_CASCADE) {
+ view_cascade(view);
+ return;
}
view_center(view, NULL);
* 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) {
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;