view: implement cascade placement policy

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:
df9f8f8346/src/placement.cpp (L589)

Also added some helper functions to manipulate `wlr_box`.
This commit is contained in:
tokyo4j 2024-05-04 06:26:27 +09:00 committed by Johan Malm
parent 3be8fe25f3
commit 46ec513630
10 changed files with 199 additions and 17 deletions

View 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" />*++

View 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

View file

@ -18,6 +18,11 @@
<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 -->

17
include/common/box.h Normal file
View file

@ -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 */

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

49
src/common/box.c Normal file
View file

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

View file

@ -1,4 +1,5 @@
labwc_sources += files(
'box.c',
'buf.c',
'dir.c',
'fd-util.c',

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

View 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;
}

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