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