diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd
index fe468cd1..d9bdcb6f 100644
--- a/docs/labwc-actions.5.scd
+++ b/docs/labwc-actions.5.scd
@@ -125,6 +125,23 @@ Actions are used in menus and keyboard/mouse bindings.
Resize and move the active window back to its untiled or unmaximized
position if it had been maximized or tiled to a direction or region.
+**
+ Cycle focus to the next window in the given region.
+
+ If _windowOverlapPercent_ is set and > 0, it is the minimum percentage of
+ the window area that must lie inside the region.
+
+ If _regionOverlapPercent_ is set and > 0, it is the minimum percentage of
+ the region area that must lie inside the window.
+
+ If both are omitted or set to 0, a window matches only if it is explicitly
+ assigned to the region (e.g. tiled to it), regardless of window size.
+
+ If _region_ is omitted, cycle focus to the next window on the entire
+ screen (still honoring the standard window-switcher criteria such as
+ current-workspace filtering).
+ See labwc-config(5) for further information on how to define regions.
+
**++
**
Cycle focus to next/previous window, respectively.
diff --git a/include/labwc.h b/include/labwc.h
index 728deaf9..0f52edfe 100644
--- a/include/labwc.h
+++ b/include/labwc.h
@@ -2,6 +2,7 @@
#ifndef LABWC_H
#define LABWC_H
#include "config.h"
+#include "config/types.h"
#include
#include
#include "common/set.h"
@@ -359,6 +360,30 @@ void desktop_focus_output(struct output *output);
*/
void desktop_update_top_layer_visibility(struct server *server);
+/**
+ * desktop_cycle_view_in_region() - return a view to "cycle" to,
+ * filtered to views that are mostly inside a region.
+ *
+ * @active_view: Reference point for finding the next view.
+ * @region: If non-NULL, only return views mostly inside this region.
+ * @criteria: Base filtering (e.g. current workspace).
+ * @view_threshold: Fraction of the view area that must lie inside @region.
+ * If <= 0, this check is disabled.
+ * @region_threshold: Fraction of the region area that must lie inside the
+ * view. If <= 0, this check is disabled.
+ *
+ * If both thresholds are disabled, a view only matches if it is explicitly
+ * assigned to the region (e.g. tiled to it).
+ *
+ * Note: If @active_view is not in-region, the topmost matching view is
+ * returned first. If @active_view is in-region, iteration reverses to
+ * avoid bouncing between the two most recent views when focusing raises.
+ */
+struct view *desktop_cycle_view_in_region(struct server *server,
+ struct view *active_view, struct region *region,
+ enum lab_view_criteria criteria, double view_threshold,
+ double region_threshold);
+
/**
* desktop_focus_topmost_view() - focus the topmost view on the current
* workspace, skipping views that claim not to want focus (those can
diff --git a/src/action.c b/src/action.c
index 6d8c8419..3e2ad469 100644
--- a/src/action.c
+++ b/src/action.c
@@ -112,6 +112,7 @@ enum action_type {
ACTION_TYPE_TOGGLE_SNAP_TO_REGION,
ACTION_TYPE_SNAP_TO_REGION,
ACTION_TYPE_UNSNAP,
+ ACTION_TYPE_CYCLE_IN_REGION,
ACTION_TYPE_TOGGLE_KEYBINDS,
ACTION_TYPE_FOCUS_OUTPUT,
ACTION_TYPE_MOVE_TO_OUTPUT,
@@ -182,6 +183,7 @@ const char *action_names[] = {
"ToggleSnapToRegion",
"SnapToRegion",
"UnSnap",
+ "CycleInRegion",
"ToggleKeybinds",
"FocusOutput",
"MoveToOutput",
@@ -516,6 +518,17 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char
goto cleanup;
}
break;
+ case ACTION_TYPE_CYCLE_IN_REGION:
+ if (!strcasecmp(argument, "region")) {
+ action_arg_add_str(action, argument, content);
+ goto cleanup;
+ }
+ if (!strcasecmp(argument, "windowOverlapPercent")
+ || !strcasecmp(argument, "regionOverlapPercent")) {
+ action_arg_add_int(action, argument, atoi(content));
+ goto cleanup;
+ }
+ break;
case ACTION_TYPE_FOCUS_OUTPUT:
case ACTION_TYPE_MOVE_TO_OUTPUT:
if (!strcmp(argument, "output")) {
@@ -1249,6 +1262,50 @@ run_action(struct view *view, struct server *server, struct action *action,
view_toggle_visible_on_all_workspaces(view);
}
break;
+ case ACTION_TYPE_CYCLE_IN_REGION:
+ {
+ struct view *active_view = view ? view : server->active_view;
+ enum lab_view_criteria criteria =
+ LAB_VIEW_CRITERIA_NO_SKIP_WINDOW_SWITCHER
+ | LAB_VIEW_CRITERIA_NO_DIALOG;
+ int window_overlap_percent = action_get_int(action, "windowOverlapPercent", 0);
+ int region_overlap_percent = action_get_int(action, "regionOverlapPercent", 0);
+
+ if (rc.window_switcher.workspace_filter == CYCLE_WORKSPACE_CURRENT) {
+ criteria |= LAB_VIEW_CRITERIA_CURRENT_WORKSPACE;
+ }
+
+ window_overlap_percent = CLAMP(window_overlap_percent, 0, 100);
+ region_overlap_percent = CLAMP(region_overlap_percent, 0, 100);
+
+ double view_threshold = (double)window_overlap_percent / 100.0;
+ double region_threshold = (double)region_overlap_percent / 100.0;
+
+ const char *region_name = action_get_str(action, "region", NULL);
+ struct region *region = NULL;
+ if (region_name) {
+ if (!active_view) {
+ break;
+ }
+ struct output *output = active_view->output;
+ if (!output_is_usable(output)) {
+ break;
+ }
+ region = regions_from_name(region_name, output);
+ if (!region) {
+ wlr_log(WLR_ERROR, "Invalid CycleInRegion region: '%s'",
+ region_name);
+ break;
+ }
+ }
+
+ struct view *new_view = desktop_cycle_view_in_region(
+ server, active_view, region, criteria, view_threshold, region_threshold);
+ if (new_view) {
+ desktop_focus_view(new_view, /*raise*/ true);
+ }
+ break;
+ }
case ACTION_TYPE_FOCUS:
if (view) {
desktop_focus_view(view, /*raise*/ false);
diff --git a/src/desktop.c b/src/desktop.c
index e5a33a78..e4c02328 100644
--- a/src/desktop.c
+++ b/src/desktop.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only
#include "config.h"
#include
+#include
#include
#include
#include
@@ -8,6 +9,7 @@
#include
#include
#include
+#include "common/macros.h"
#include "common/scene-helpers.h"
#include "dnd.h"
#include "labwc.h"
@@ -16,6 +18,7 @@
#include "output.h"
#include "ssd.h"
#include "view.h"
+#include "regions.h"
#include "workspaces.h"
#if HAVE_XWAYLAND
@@ -134,6 +137,129 @@ desktop_focus_view_or_surface(struct seat *seat, struct view *view,
}
}
+static bool
+view_is_assigned_to_region(struct view *view, struct region *region)
+{
+ if (!view || !region) {
+ return false;
+ }
+
+ if (view->tiled_region && view->tiled_region->name
+ && !strcmp(view->tiled_region->name, region->name)) {
+ return true;
+ }
+
+ if (view->tiled_region_evacuate
+ && !strcmp(view->tiled_region_evacuate, region->name)) {
+ return true;
+ }
+
+ return false;
+}
+
+static bool
+view_matches_region(struct view *view, struct region *region,
+ double view_threshold, double region_threshold)
+{
+ if (!view || !region) {
+ return false;
+ }
+
+ if (view_is_assigned_to_region(view, region)) {
+ return true;
+ }
+
+ if (view_threshold <= 0 && region_threshold <= 0) {
+ return false;
+ }
+
+ struct wlr_box overlap;
+ wlr_box_intersection(&overlap, &view->current, ®ion->geo);
+ double overlap_area = (double)overlap.height * (double)overlap.width;
+ if (overlap_area <= 0) {
+ return false;
+ }
+
+ if (view_threshold > 0) {
+ double view_area = (double)view->current.height * (double)view->current.width;
+ if (view_area > 0 && overlap_area >= view_threshold * view_area) {
+ return true;
+ }
+ }
+
+ if (region_threshold > 0) {
+ double region_area = (double)region->geo.height * (double)region->geo.width;
+ if (region_area > 0 && overlap_area >= region_threshold * region_area) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static struct view *
+cycle_prev_wrap(struct wl_list *head, struct view *from,
+ enum lab_view_criteria criteria)
+{
+ struct view *view = view_prev(head, from, criteria);
+ return view ? view : view_prev(head, NULL, criteria);
+}
+
+struct view *
+desktop_cycle_view_in_region(struct server *server, struct view *active_view,
+ struct region *region, enum lab_view_criteria criteria,
+ double view_threshold, double region_threshold)
+{
+ if (!active_view) {
+ struct view *cur;
+ for_each_view(cur, &server->views, criteria) {
+ if (cur->minimized) {
+ continue;
+ }
+ if (!region || view_matches_region(cur, region,
+ view_threshold, region_threshold)) {
+ return cur;
+ }
+ }
+ return NULL;
+ }
+
+ if (region && (!active_view
+ || !view_matches_region(active_view, region,
+ view_threshold, region_threshold))) {
+ /*
+ * If the currently focused view is not in-region, always focus the
+ * topmost matching view first.
+ */
+ struct view *cur;
+ for_each_view(cur, &server->views, criteria) {
+ if (!cur->minimized && view_matches_region(cur, region,
+ view_threshold, region_threshold)) {
+ return cur;
+ }
+ }
+ return NULL;
+ }
+
+ /*
+ * If the currently active/focused window is already in-region, cycle
+ * through in reverse order so that we cycle through all windows, and
+ * not just the two most recent (focusing raises).
+ */
+ struct view *start = active_view;
+ struct view *cur = cycle_prev_wrap(&server->views, start, criteria);
+ while (cur && cur != start) {
+ if (!cur->minimized
+ && (!region
+ || view_matches_region(cur, region,
+ view_threshold, region_threshold))) {
+ return cur;
+ }
+ cur = cycle_prev_wrap(&server->views, cur, criteria);
+ }
+ return NULL;
+}
+
static struct view *
desktop_topmost_focusable_view(struct server *server)
{
@@ -384,4 +510,3 @@ get_cursor_context(struct server *server)
*/
return ret;
}
-