diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd
index 50f6ad33..6eb9b127 100644
--- a/docs/labwc-actions.5.scd
+++ b/docs/labwc-actions.5.scd
@@ -70,6 +70,14 @@ Actions are used in menus and keyboard/mouse bindings.
*direction* [left|up|right|down] Direction in which to shrink.
+**
+ Move focus to closest window in given direction.
+
+ *direction* [left|up|right|down] Direction in which to move.
+
+ *wrap* [yes|no] Wrap around from right-to-left or top-to-bottom,
+ and vice versa. Default no.
+
**
Move to position (x, y).
diff --git a/src/action.c b/src/action.c
index 80231b95..676944db 100644
--- a/src/action.c
+++ b/src/action.c
@@ -5,6 +5,8 @@
#include
#include
#include
+#include
+#include
#include
#include "action.h"
#include "common/macros.h"
@@ -73,6 +75,7 @@ enum action_type {
ACTION_TYPE_SNAP_TO_EDGE,
ACTION_TYPE_GROW_TO_EDGE,
ACTION_TYPE_SHRINK_TO_EDGE,
+ ACTION_TYPE_DIRECTIONAL_TARGET_WINDOW,
ACTION_TYPE_NEXT_WINDOW,
ACTION_TYPE_PREVIOUS_WINDOW,
ACTION_TYPE_RECONFIGURE,
@@ -132,6 +135,7 @@ const char *action_names[] = {
"SnapToEdge",
"GrowToEdge",
"ShrinkToEdge",
+ "DirectionalTargetWindow",
"NextWindow",
"PreviousWindow",
"Reconfigure",
@@ -310,6 +314,12 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char
goto cleanup;
}
break;
+ case ACTION_TYPE_DIRECTIONAL_TARGET_WINDOW:
+ if (!strcasecmp(argument, "wrap")) {
+ action_arg_add_bool(action, argument, parse_bool(content, false));
+ goto cleanup;
+ }
+ /* Falls through */
case ACTION_TYPE_MOVE_TO_EDGE:
if (!strcasecmp(argument, "snapWindows")) {
action_arg_add_bool(action, argument, parse_bool(content, true));
@@ -531,6 +541,7 @@ action_is_valid(struct action *action)
case ACTION_TYPE_EXECUTE:
arg_name = "command";
break;
+ case ACTION_TYPE_DIRECTIONAL_TARGET_WINDOW:
case ACTION_TYPE_MOVE_TO_EDGE:
case ACTION_TYPE_SNAP_TO_EDGE:
case ACTION_TYPE_GROW_TO_EDGE:
@@ -701,6 +712,131 @@ run_if_action(struct view *view, struct server *server, struct action *action)
return !strcmp(branch, "then");
}
+static int
+rightmost_visible_point(struct server *server, struct view *view)
+{
+ struct output *output = view->output;
+ struct wlr_box usable = output_usable_area_in_layout_coords(output);
+ struct wlr_box o_usable;
+ struct output *o;
+ int rightmost_point = usable.x + usable.width;
+ int cy = view->current.y + view->current.height/2;
+ wl_list_for_each(o, &server->outputs, link) {
+ if (!output_is_usable(o)) {
+ continue;
+ }
+ o_usable = output_usable_area_in_layout_coords(o);
+ if (o_usable.x >= rightmost_point
+ && o_usable.y <= cy
+ && o_usable.y + o_usable.height >= cy) {
+ rightmost_point = o_usable.x + o_usable.width;
+ }
+ }
+ return rightmost_point;
+}
+
+static int
+lowest_visible_point(struct server *server, struct view *view)
+{
+ struct output *output = view->output;
+ struct wlr_box usable = output_usable_area_in_layout_coords(output);
+ struct wlr_box o_usable;
+ struct output *o;
+ int lowest_point = usable.y + usable.height;
+ int cx = view->current.x + view->current.width/2;
+ wl_list_for_each(o, &server->outputs, link) {
+ if (!output_is_usable(o)) {
+ continue;
+ }
+ o_usable = output_usable_area_in_layout_coords(o);
+ if (o_usable.y >= lowest_point
+ && o_usable.x <= cx
+ && o_usable.x + o_usable.width >= cx) {
+ lowest_point = o_usable.y + o_usable.height;
+ }
+ }
+ return lowest_point;
+}
+
+static struct view*
+directional_target_window(struct view *view, struct server *server,
+ enum view_edge direction, bool wrap)
+{
+ assert(view);
+ struct view *v;
+ struct view *closest_view = NULL;
+ struct view *closest_view_wrap = NULL;
+ struct output *output = view->output;
+ struct wlr_box usable = output_usable_area_in_layout_coords(output);
+ float dx, dy;
+ int distance, distance_wrap;
+ int min_distance = INT_MAX;
+ int min_distance_wrap = INT_MAX;
+ for_each_view(v, &server->views,
+ LAB_VIEW_CRITERIA_CURRENT_WORKSPACE) {
+ if (v->minimized) {
+ continue;
+ }
+ distance = INT_MAX;
+ distance_wrap = INT_MAX;
+ dx = v->current.x + v->current.width/2.
+ - view->current.x - view->current.width/2.;
+ dy = v->current.y + v->current.height/2.
+ - view->current.y - view->current.height/2.;
+ switch (direction) {
+ case VIEW_EDGE_LEFT:
+ if (dx < 0 && dy*dy/(dx*dx) < 2) {
+ distance = (dx*dx + dy*dy) * (1 + dy*dy/(dx*dx));
+ } else if (dx > 0 && dy*dy/(dx*dx) < 2) {
+ dx = rightmost_visible_point(server, v) - dx;
+ distance_wrap = (dx*dx + dy*dy) * (1 + dy*dy/(dx*dx));
+ }
+ break;
+ case VIEW_EDGE_RIGHT:
+ if (dx > 0 && dy*dy/(dx*dx) < 2) {
+ distance = (dx*dx + dy*dy) * (1 + dy*dy/(dx*dx));
+ } else if (dx < 0 && dy*dy/(dx*dx) < 2) {
+ dx = usable.x + usable.width + dx;
+ distance_wrap = (dx*dx + dy*dy) * (1 + dy*dy/(dx*dx));
+ }
+
+ break;
+ case VIEW_EDGE_UP:
+ if (dy < 0 && dx*dx/(dy*dy) < 2) {
+ distance = (dx * dx + dy * dy) * (1 + dx*dx/(dy*dy));
+ } else if (dy > 0 && dx*dx/(dy*dy) < 2) {
+ dy = lowest_visible_point(server, v) - dy;
+ distance_wrap = (dx*dx + dy*dy) * (1 + dx*dx/(dy*dy));
+ }
+ break;
+ case VIEW_EDGE_DOWN:
+ if (dy > 0 && dx*dx/(dy*dy) < 2) {
+ distance = (dx*dx + dy*dy) * (1 + dx*dx/(dy*dy));
+ } else if (dy < 0 && dx*dx/(dy*dy) < 2) {
+ dy = usable.y + usable.height + dy;
+ distance_wrap = (dx*dx + dy*dy) * (1 + dx*dx/(dy*dy));
+ }
+ break;
+ default:
+ return NULL;
+ }
+ if (distance < min_distance) {
+ min_distance = distance;
+ closest_view = v;
+ } else if (distance_wrap < min_distance_wrap) {
+ min_distance_wrap = distance_wrap;
+ closest_view_wrap = v;
+ }
+ }
+ if (closest_view) {
+ return closest_view;
+ }
+ if (wrap && closest_view_wrap) {
+ return closest_view_wrap;
+ }
+ return NULL;
+}
+
void
actions_run(struct view *activator, struct server *server,
struct wl_list *actions, uint32_t resize_edges)
@@ -790,6 +926,18 @@ actions_run(struct view *activator, struct server *server,
view_shrink_to_edge(view, edge);
}
break;
+ case ACTION_TYPE_DIRECTIONAL_TARGET_WINDOW:
+ if (view) {
+ /* Config parsing makes sure that direction is a valid direction */
+ enum view_edge direction = action_get_int(action, "direction", 0);
+ bool wrap = action_get_bool(action, "wrap", false);
+ struct view *closest_view = directional_target_window(view, server,
+ direction, wrap);
+ if (closest_view) {
+ desktop_focus_view(closest_view, /*raise*/ true);
+ }
+ }
+ break;
case ACTION_TYPE_NEXT_WINDOW:
server->osd_state.cycle_view = desktop_cycle_view(server,
server->osd_state.cycle_view, LAB_CYCLE_DIR_FORWARD);