From 46ec513630f60a4ab14b5677c1a5b10f23e60db2 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sat, 4 May 2024 06:26:27 +0900 Subject: [PATCH] view: implement `cascade` placement policy Adds following settings: cascade "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 | 6 +-- docs/labwc-config.5.scd | 14 ++++- docs/rc.xml.all | 5 ++ include/common/box.h | 17 +++++++ include/config/rcxml.h | 5 +- src/common/box.c | 49 ++++++++++++++++++ src/common/meson.build | 1 + src/config/rcxml.c | 6 +++ src/edges.c | 6 +-- src/view.c | 107 ++++++++++++++++++++++++++++++++++++--- 10 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 include/common/box.h create mode 100644 src/common/box.c diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index e2edf387..9ccfb798 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -276,9 +276,9 @@ Actions are used in menus and keyboard/mouse bindings. ** 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 **. - Default is automatic. + *policy* [automatic|cursor|center|cascade] Use the specified policy, + which has the same meaning as the corresponding value for + **. Default is automatic. **++ **++ diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 70f705c6..f2d6e925 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -203,12 +203,22 @@ this is for compatibility with Openbox. ## PLACEMENT -** [center|automatic|cursor] +** [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". + +**++ +** + Specify the offset by which a new window can be shifted from an existing + window when 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 diff --git a/docs/rc.xml.all b/docs/rc.xml.all index f481c091..c537b039 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -18,6 +18,11 @@ center + diff --git a/include/common/box.h b/include/common/box.h new file mode 100644 index 00000000..105ff297 --- /dev/null +++ b/include/common/box.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_BOX_H +#define LABWC_BOX_H + +#include + +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 */ diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 1a2bb203..53263df7 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -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 index 00000000..1a56cfe2 --- /dev/null +++ b/src/common/box.c @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#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; +} diff --git a/src/common/meson.build b/src/common/meson.build index 441e92c0..1569f775 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -1,4 +1,5 @@ labwc_sources += files( + 'box.c', 'buf.c', 'dir.c', 'fd-util.c', diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 9ad0268a..28eff198 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -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; diff --git a/src/edges.c b/src/edges.c index bd96c5a0..c9c88184 100644 --- a/src/edges.c +++ b/src/edges.c @@ -5,6 +5,7 @@ #include #include #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; } diff --git a/src/view.c b/src/view.c index 6a0f9806..d06251aa 100644 --- a/src/view.c +++ b/src/view.c @@ -4,6 +4,7 @@ #include #include #include +#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, ¢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) @@ -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;