Add CycleInRegion action

CycleInRegion will cycle focus between all windows/views that are within
the given region, to support position-based focusing.

The window/region matching supports:
* when a window is explicitly assigned to a region
* an optional `windowOverlapPercentage` arg, when at least the given
  percentage of the window's geometry is within the region's geometry.
* an optional `regionOverlapPercentage` arg, when at least the given
  percentage of the region's geometry is within the window's geometry.

To support immediate focus cycling and to avoid ping-pong'ing between the
top two windows, we cycle through windows within a region in
reverse-order.  When we are not focused within a region, we instead focus
the top-most (first) window, as that is what the user will expect.

If the `region` arg is not specified, the action will cycle through all
windows on the given desktop instead.
This commit is contained in:
Robert M. Thomson 2024-05-22 07:43:57 +02:00
parent 16c5373be5
commit 6a4d937050
4 changed files with 225 additions and 1 deletions

View file

@ -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 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. position if it had been maximized or tiled to a direction or region.
*<action name="CycleInRegion" region="value" windowOverlapPercent="value" regionOverlapPercent="value" />*
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.
*<action name="NextWindow" workspace="current" output="all" identifier="all" />*++ *<action name="NextWindow" workspace="current" output="all" identifier="all" />*++
*<action name="PreviousWindow" workspace="current" output="all" identifier="all" />* *<action name="PreviousWindow" workspace="current" output="all" identifier="all" />*
Cycle focus to next/previous window, respectively. Cycle focus to next/previous window, respectively.

View file

@ -2,6 +2,7 @@
#ifndef LABWC_H #ifndef LABWC_H
#define LABWC_H #define LABWC_H
#include "config.h" #include "config.h"
#include "config/types.h"
#include <wlr/util/box.h> #include <wlr/util/box.h>
#include <wlr/util/log.h> #include <wlr/util/log.h>
#include "common/set.h" #include "common/set.h"
@ -359,6 +360,30 @@ void desktop_focus_output(struct output *output);
*/ */
void desktop_update_top_layer_visibility(struct server *server); 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 * desktop_focus_topmost_view() - focus the topmost view on the current
* workspace, skipping views that claim not to want focus (those can * workspace, skipping views that claim not to want focus (those can

View file

@ -112,6 +112,7 @@ enum action_type {
ACTION_TYPE_TOGGLE_SNAP_TO_REGION, ACTION_TYPE_TOGGLE_SNAP_TO_REGION,
ACTION_TYPE_SNAP_TO_REGION, ACTION_TYPE_SNAP_TO_REGION,
ACTION_TYPE_UNSNAP, ACTION_TYPE_UNSNAP,
ACTION_TYPE_CYCLE_IN_REGION,
ACTION_TYPE_TOGGLE_KEYBINDS, ACTION_TYPE_TOGGLE_KEYBINDS,
ACTION_TYPE_FOCUS_OUTPUT, ACTION_TYPE_FOCUS_OUTPUT,
ACTION_TYPE_MOVE_TO_OUTPUT, ACTION_TYPE_MOVE_TO_OUTPUT,
@ -182,6 +183,7 @@ const char *action_names[] = {
"ToggleSnapToRegion", "ToggleSnapToRegion",
"SnapToRegion", "SnapToRegion",
"UnSnap", "UnSnap",
"CycleInRegion",
"ToggleKeybinds", "ToggleKeybinds",
"FocusOutput", "FocusOutput",
"MoveToOutput", "MoveToOutput",
@ -516,6 +518,17 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char
goto cleanup; goto cleanup;
} }
break; 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_FOCUS_OUTPUT:
case ACTION_TYPE_MOVE_TO_OUTPUT: case ACTION_TYPE_MOVE_TO_OUTPUT:
if (!strcmp(argument, "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); view_toggle_visible_on_all_workspaces(view);
} }
break; 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: case ACTION_TYPE_FOCUS:
if (view) { if (view) {
desktop_focus_view(view, /*raise*/ false); desktop_focus_view(view, /*raise*/ false);

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only // SPDX-License-Identifier: GPL-2.0-only
#include "config.h" #include "config.h"
#include <assert.h> #include <assert.h>
#include <string.h>
#include <wlr/types/wlr_cursor.h> #include <wlr/types/wlr_cursor.h>
#include <wlr/types/wlr_layer_shell_v1.h> #include <wlr/types/wlr_layer_shell_v1.h>
#include <wlr/types/wlr_output_layout.h> #include <wlr/types/wlr_output_layout.h>
@ -8,6 +9,7 @@
#include <wlr/types/wlr_seat.h> #include <wlr/types/wlr_seat.h>
#include <wlr/types/wlr_subcompositor.h> #include <wlr/types/wlr_subcompositor.h>
#include <wlr/types/wlr_xdg_shell.h> #include <wlr/types/wlr_xdg_shell.h>
#include "common/macros.h"
#include "common/scene-helpers.h" #include "common/scene-helpers.h"
#include "dnd.h" #include "dnd.h"
#include "labwc.h" #include "labwc.h"
@ -16,6 +18,7 @@
#include "output.h" #include "output.h"
#include "ssd.h" #include "ssd.h"
#include "view.h" #include "view.h"
#include "regions.h"
#include "workspaces.h" #include "workspaces.h"
#if HAVE_XWAYLAND #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, &region->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 * static struct view *
desktop_topmost_focusable_view(struct server *server) desktop_topmost_focusable_view(struct server *server)
{ {
@ -384,4 +510,3 @@ get_cursor_context(struct server *server)
*/ */
return ret; return ret;
} }