interactive: allow snapping to corner edges

In addition to <snapping><range>, <snapping><cornerRange> configures the
distance from the screen corner to trigger quater window snapping.

Also, new values "up-left", "up-right", "down-left" and "down-right" are
allowed for <action name="(Toggle)SnapToEdge" direction="[value]"> and
<query tiled="[value]">.
This commit is contained in:
tokyo4j 2025-08-02 21:35:51 +09:00 committed by Johan Malm
parent b0ff2911b6
commit 2f183cdcb6
14 changed files with 147 additions and 89 deletions

View file

@ -95,7 +95,8 @@ Actions are used in menus and keyboard/mouse bindings.
*<action name="ToggleSnapToEdge" direction="value" />*++ *<action name="ToggleSnapToEdge" direction="value" />*++
*<action name="SnapToEdge" direction="value" />* *<action name="SnapToEdge" direction="value" />*
Resize window to fill half the output in the given direction. Supports Resize window to fill half the output in the given direction. Supports
directions "left", "up", "right", "down" and "center". directions "left", "up", "right", "down", "up-left", "up-right", "down-left",
"down-right" and "center".
ToggleSnapToEdge additionally toggles the active window between ToggleSnapToEdge additionally toggles the active window between
tiled to the given direction and its untiled position. tiled to the given direction and its untiled position.
@ -474,7 +475,7 @@ Actions that execute other actions. Used in keyboard/mouse bindings.
The "left" , "right", "left-occupied" and The "left" , "right", "left-occupied" and
"right-occupied" directions will not wrap. "right-occupied" directions will not wrap.
*tiled* [up|right|down|left|center|any] *tiled* [up|right|down|left|top-left|top-right|down-left|down-right|center|any]
Whether the client is tiled (snapped) along the the Whether the client is tiled (snapped) along the the
indicated screen edge. indicated screen edge.

View file

@ -412,11 +412,13 @@ activated with SnapToEdge actions or, optionally, by dragging windows to the
edges of an output. Edge snapping causes a window to occupy half of its output, edges of an output. Edge snapping causes a window to occupy half of its output,
extending outward from the snapped edge. extending outward from the snapped edge.
*<snapping><range>* *<snapping><range>*++
If an interactive move ends with the cursor a maximum distance *range*, *<snapping><cornerRange>*
(in pixels) from the edge of an output, the move will trigger a If an interactive move ends with the cursor within *<range>* pixels of an
SnapToEdge action for that edge. A *range* of 0 disables snapping via output edge, the window is snapped to the edge. If it's also within
interactive moves. Default is 10. *<cornerRange>* pixels of an output corner, the window is snapped to the
corner instead. A *<range>* of 0 disables snapping.
Default is 10 for *<range>* and 50 for *<cornerRange>*.
*<snapping><overlay><enabled>* [yes|no] *<snapping><overlay><enabled>* [yes|no]
Show an overlay when snapping to a window to an edge. Default is yes. Show an overlay when snapping to a window to an edge. Default is yes.

View file

@ -155,6 +155,7 @@
<snapping> <snapping>
<!-- Set range to 0 to disable window snapping completely --> <!-- Set range to 0 to disable window snapping completely -->
<range>10</range> <range>10</range>
<cornerRange>50</cornerRange>
<overlay enabled="yes"> <overlay enabled="yes">
<delay inner="500" outer="500" /> <delay inner="500" outer="500" />
</overlay> </overlay>

View file

@ -2,9 +2,10 @@
#ifndef LABWC_DIRECTION_H #ifndef LABWC_DIRECTION_H
#define LABWC_DIRECTION_H #define LABWC_DIRECTION_H
#include <wlr/types/wlr_output_layout.h>
#include "view.h" #include "view.h"
enum wlr_direction direction_from_view_edge(enum view_edge edge); bool direction_from_view_edge(enum view_edge edge, enum wlr_direction *direction);
enum wlr_direction direction_get_opposite(enum wlr_direction direction); enum wlr_direction direction_get_opposite(enum wlr_direction direction);
#endif /* LABWC_DIRECTION_H */ #endif /* LABWC_DIRECTION_H */

View file

@ -145,6 +145,7 @@ struct rcxml {
/* window snapping */ /* window snapping */
int snap_edge_range; int snap_edge_range;
int snap_edge_corner_range;
bool snap_overlay_enabled; bool snap_overlay_enabled;
int snap_overlay_delay_inner; int snap_overlay_delay_inner;
int snap_overlay_delay_outer; int snap_overlay_delay_outer;

View file

@ -429,7 +429,16 @@ void interactive_anchor_to_cursor(struct server *server, struct wlr_box *geo);
void interactive_begin(struct view *view, enum input_mode mode, uint32_t edges); void interactive_begin(struct view *view, enum input_mode mode, uint32_t edges);
void interactive_finish(struct view *view); void interactive_finish(struct view *view);
void interactive_cancel(struct view *view); void interactive_cancel(struct view *view);
enum view_edge edge_from_cursor(struct seat *seat, struct output **dest_output);
/**
* Returns the edge to snap a window to.
* For example, if the output-relative cursor position (x,y) fulfills
* x <= (<snapping><cornerRange>) and y <= (<snapping><range>),
* then edge1=VIEW_EDGE_UP and edge2=VIEW_EDGE_LEFT.
* The value of (edge1|edge2) can be passed to view_snap_to_edge().
*/
bool edge_from_cursor(struct seat *seat, struct output **dest_output,
enum view_edge *edge1, enum view_edge *edge2);
void handle_tearing_new_object(struct wl_listener *listener, void *data); void handle_tearing_new_object(struct wl_listener *listener, void *data);

View file

@ -73,6 +73,10 @@ enum view_edge {
VIEW_EDGE_DOWN = (1 << 3), VIEW_EDGE_DOWN = (1 << 3),
VIEW_EDGE_CENTER = (1 << 4), VIEW_EDGE_CENTER = (1 << 4),
VIEW_EDGE_ANY = (1 << 5), VIEW_EDGE_ANY = (1 << 5),
VIEW_EDGE_UPLEFT = (VIEW_EDGE_UP | VIEW_EDGE_LEFT),
VIEW_EDGE_UPRIGHT = (VIEW_EDGE_UP | VIEW_EDGE_RIGHT),
VIEW_EDGE_DOWNLEFT = (VIEW_EDGE_DOWN | VIEW_EDGE_LEFT),
VIEW_EDGE_DOWNRIGHT = (VIEW_EDGE_DOWN | VIEW_EDGE_RIGHT),
}; };
enum view_wants_focus { enum view_wants_focus {
@ -524,6 +528,9 @@ bool view_is_focusable(struct view *view);
*/ */
void view_offer_focus(struct view *view); void view_offer_focus(struct view *view);
struct wlr_box view_get_edge_snap_box(struct view *view, struct output *output,
enum view_edge edge);
void mappable_connect(struct mappable *mappable, struct wlr_surface *surface, void mappable_connect(struct mappable *mappable, struct wlr_surface *surface,
wl_notify_func_t notify_map, wl_notify_func_t notify_unmap); wl_notify_func_t notify_map, wl_notify_func_t notify_unmap);
void mappable_disconnect(struct mappable *mappable); void mappable_disconnect(struct mappable *mappable);

View file

@ -4,22 +4,24 @@
#include <wlr/types/wlr_output_layout.h> #include <wlr/types/wlr_output_layout.h>
#include "view.h" #include "view.h"
enum wlr_direction bool
direction_from_view_edge(enum view_edge edge) direction_from_view_edge(enum view_edge edge, enum wlr_direction *direction)
{ {
switch (edge) { switch (edge) {
case VIEW_EDGE_LEFT: case VIEW_EDGE_LEFT:
return WLR_DIRECTION_LEFT; *direction = WLR_DIRECTION_LEFT;
return true;
case VIEW_EDGE_RIGHT: case VIEW_EDGE_RIGHT:
return WLR_DIRECTION_RIGHT; *direction = WLR_DIRECTION_RIGHT;
return true;
case VIEW_EDGE_UP: case VIEW_EDGE_UP:
return WLR_DIRECTION_UP; *direction = WLR_DIRECTION_UP;
return true;
case VIEW_EDGE_DOWN: case VIEW_EDGE_DOWN:
return WLR_DIRECTION_DOWN; *direction = WLR_DIRECTION_DOWN;
case VIEW_EDGE_CENTER: return true;
case VIEW_EDGE_INVALID:
default: default:
return WLR_DIRECTION_UP; return false;
} }
} }

View file

@ -1175,6 +1175,8 @@ entry(xmlNode *node, char *nodename, char *content)
rc.unmaximize_threshold = atoi(content); rc.unmaximize_threshold = atoi(content);
} else if (!strcasecmp(nodename, "range.snapping")) { } else if (!strcasecmp(nodename, "range.snapping")) {
rc.snap_edge_range = atoi(content); rc.snap_edge_range = atoi(content);
} else if (!strcasecmp(nodename, "cornerRange.snapping")) {
rc.snap_edge_corner_range = atoi(content);
} else if (!strcasecmp(nodename, "enabled.overlay.snapping")) { } else if (!strcasecmp(nodename, "enabled.overlay.snapping")) {
set_bool(content, &rc.snap_overlay_enabled); set_bool(content, &rc.snap_overlay_enabled);
} else if (!strcasecmp(nodename, "inner.delay.overlay.snapping")) { } else if (!strcasecmp(nodename, "inner.delay.overlay.snapping")) {
@ -1411,6 +1413,7 @@ rcxml_init(void)
rc.unmaximize_threshold = 150; rc.unmaximize_threshold = 150;
rc.snap_edge_range = 10; rc.snap_edge_range = 10;
rc.snap_edge_corner_range = 50;
rc.snap_overlay_enabled = true; rc.snap_overlay_enabled = true;
rc.snap_overlay_delay_inner = 500; rc.snap_overlay_delay_inner = 500;
rc.snap_overlay_delay_outer = 500; rc.snap_overlay_delay_outer = 500;

View file

@ -164,22 +164,26 @@ interactive_begin(struct view *view, enum input_mode mode, uint32_t edges)
} }
} }
enum view_edge bool
edge_from_cursor(struct seat *seat, struct output **dest_output) edge_from_cursor(struct seat *seat, struct output **dest_output,
enum view_edge *edge1, enum view_edge *edge2)
{ {
*dest_output = NULL;
*edge1 = VIEW_EDGE_INVALID;
*edge2 = VIEW_EDGE_INVALID;
if (!view_is_floating(seat->server->grabbed_view)) { if (!view_is_floating(seat->server->grabbed_view)) {
return VIEW_EDGE_INVALID; return false;
} }
int snap_range = rc.snap_edge_range; if (rc.snap_edge_range == 0) {
if (!snap_range) { return false;
return VIEW_EDGE_INVALID;
} }
struct output *output = output_nearest_to_cursor(seat->server); struct output *output = output_nearest_to_cursor(seat->server);
if (!output_is_usable(output)) { if (!output_is_usable(output)) {
wlr_log(WLR_ERROR, "output at cursor is unusable"); wlr_log(WLR_ERROR, "output at cursor is unusable");
return VIEW_EDGE_INVALID; return false;
} }
*dest_output = output; *dest_output = output;
@ -190,18 +194,39 @@ edge_from_cursor(struct seat *seat, struct output **dest_output)
output->wlr_output, &cursor_x, &cursor_y); output->wlr_output, &cursor_x, &cursor_y);
struct wlr_box *area = &output->usable_area; struct wlr_box *area = &output->usable_area;
if (cursor_x <= area->x + snap_range) {
return VIEW_EDGE_LEFT; int top = cursor_y - area->y;
} else if (cursor_x >= area->x + area->width - snap_range) { int bottom = area->y + area->height - cursor_y;
return VIEW_EDGE_RIGHT; int left = cursor_x - area->x;
} else if (cursor_y <= area->y + snap_range) { int right = area->x + area->width - cursor_x;
return VIEW_EDGE_UP;
} else if (cursor_y >= area->y + area->height - snap_range) { if (top < rc.snap_edge_range) {
return VIEW_EDGE_DOWN; *edge1 = VIEW_EDGE_UP;
} else if (bottom < rc.snap_edge_range) {
*edge1 = VIEW_EDGE_DOWN;
} else if (left < rc.snap_edge_range) {
*edge1 = VIEW_EDGE_LEFT;
} else if (right < rc.snap_edge_range) {
*edge1 = VIEW_EDGE_RIGHT;
} else { } else {
/* Not close to any edge */ return false;
return VIEW_EDGE_INVALID;
} }
if (*edge1 == VIEW_EDGE_UP || *edge1 == VIEW_EDGE_DOWN) {
if (left < rc.snap_edge_corner_range) {
*edge2 = VIEW_EDGE_LEFT;
} else if (right < rc.snap_edge_corner_range) {
*edge2 = VIEW_EDGE_RIGHT;
}
} else if (*edge1 == VIEW_EDGE_LEFT || *edge1 == VIEW_EDGE_RIGHT) {
if (top < rc.snap_edge_corner_range) {
*edge2 = VIEW_EDGE_UP;
} else if (bottom < rc.snap_edge_corner_range) {
*edge2 = VIEW_EDGE_DOWN;
}
}
return true;
} }
/* Returns true if view was snapped to any edge */ /* Returns true if view was snapped to any edge */
@ -209,10 +234,11 @@ static bool
snap_to_edge(struct view *view) snap_to_edge(struct view *view)
{ {
struct output *output; struct output *output;
enum view_edge edge = edge_from_cursor(&view->server->seat, &output); enum view_edge edge1, edge2;
if (edge == VIEW_EDGE_INVALID) { if (!edge_from_cursor(&view->server->seat, &output, &edge1, &edge2)) {
return false; return false;
} }
enum view_edge edge = edge1 | edge2;
view_set_output(view, output); view_set_output(view, output);
/* /*

View file

@ -979,6 +979,11 @@ output_get_adjacent(struct output *output, enum view_edge edge, bool wrap)
return NULL; return NULL;
} }
enum wlr_direction direction;
if (!direction_from_view_edge(edge, &direction)) {
return NULL;
}
struct wlr_box box = output_usable_area_in_layout_coords(output); struct wlr_box box = output_usable_area_in_layout_coords(output);
int lx = box.x + box.width / 2; int lx = box.x + box.width / 2;
int ly = box.y + box.height / 2; int ly = box.y + box.height / 2;
@ -987,7 +992,6 @@ output_get_adjacent(struct output *output, enum view_edge edge, bool wrap)
struct wlr_output *new_output = NULL; struct wlr_output *new_output = NULL;
struct wlr_output *current_output = output->wlr_output; struct wlr_output *current_output = output->wlr_output;
struct wlr_output_layout *layout = output->server->output_layout; struct wlr_output_layout *layout = output->server->output_layout;
enum wlr_direction direction = direction_from_view_edge(edge);
new_output = wlr_output_layout_adjacent_output(layout, direction, new_output = wlr_output_layout_adjacent_output(layout, direction,
current_output, lx, ly); current_output, lx, ly);

View file

@ -2,6 +2,7 @@
#include "overlay.h" #include "overlay.h"
#include <assert.h> #include <assert.h>
#include <wlr/types/wlr_scene.h> #include <wlr/types/wlr_scene.h>
#include "common/direction.h"
#include "common/lab-scene-rect.h" #include "common/lab-scene-rect.h"
#include "labwc.h" #include "labwc.h"
#include "output.h" #include "output.h"
@ -137,42 +138,27 @@ handle_edge_overlay_timeout(void *data)
return 0; return 0;
} }
static enum wlr_direction
get_wlr_direction(enum view_edge edge)
{
switch (edge) {
case VIEW_EDGE_LEFT:
return WLR_DIRECTION_LEFT;
case VIEW_EDGE_RIGHT:
return WLR_DIRECTION_RIGHT;
case VIEW_EDGE_UP:
case VIEW_EDGE_CENTER:
return WLR_DIRECTION_UP;
case VIEW_EDGE_DOWN:
return WLR_DIRECTION_DOWN;
default:
/* not reached */
assert(false);
return 0;
}
}
static bool static bool
edge_has_adjacent_output_from_cursor(struct seat *seat, struct output *output, edge_has_adjacent_output_from_cursor(struct seat *seat, struct output *output,
enum view_edge edge) enum view_edge edge)
{ {
enum wlr_direction dir;
if (!direction_from_view_edge(edge, &dir)) {
return false;
}
return wlr_output_layout_adjacent_output( return wlr_output_layout_adjacent_output(
seat->server->output_layout, get_wlr_direction(edge), seat->server->output_layout, dir,
output->wlr_output, seat->cursor->x, seat->cursor->y); output->wlr_output, seat->cursor->x, seat->cursor->y);
} }
static void static void
show_edge_overlay(struct seat *seat, enum view_edge edge, show_edge_overlay(struct seat *seat, enum view_edge edge1, enum view_edge edge2,
struct output *output) struct output *output)
{ {
if (!rc.snap_overlay_enabled) { if (!rc.snap_overlay_enabled) {
return; return;
} }
uint32_t edge = edge1 | edge2;
if (seat->overlay.active.edge == edge if (seat->overlay.active.edge == edge
&& seat->overlay.active.output == output) { && seat->overlay.active.output == output) {
return; return;
@ -182,7 +168,7 @@ show_edge_overlay(struct seat *seat, enum view_edge edge,
seat->overlay.active.output = output; seat->overlay.active.output = output;
int delay; int delay;
if (edge_has_adjacent_output_from_cursor(seat, output, edge)) { if (edge_has_adjacent_output_from_cursor(seat, output, edge1)) {
delay = rc.snap_overlay_delay_inner; delay = rc.snap_overlay_delay_inner;
} else { } else {
delay = rc.snap_overlay_delay_outer; delay = rc.snap_overlay_delay_outer;
@ -219,9 +205,9 @@ overlay_update(struct seat *seat)
/* Edge-snapping overlay */ /* Edge-snapping overlay */
struct output *output; struct output *output;
enum view_edge edge = edge_from_cursor(seat, &output); enum view_edge edge1, edge2;
if (edge != VIEW_EDGE_INVALID) { if (edge_from_cursor(seat, &output, &edge1, &edge2)) {
show_edge_overlay(seat, edge, output); show_edge_overlay(seat, edge1, edge2, output);
return; return;
} }

View file

@ -451,35 +451,29 @@ view_get_edge_snap_box(struct view *view, struct output *output,
enum view_edge edge) enum view_edge edge)
{ {
struct wlr_box usable = output_usable_area_in_layout_coords(output); struct wlr_box usable = output_usable_area_in_layout_coords(output);
int x_offset = edge == VIEW_EDGE_RIGHT int x1 = rc.gap;
? (usable.width + rc.gap) / 2 : rc.gap; int y1 = rc.gap;
int y_offset = edge == VIEW_EDGE_DOWN int x2 = usable.width - rc.gap;
? (usable.height + rc.gap) / 2 : rc.gap; int y2 = usable.height - rc.gap;
int base_width, base_height; if (edge & VIEW_EDGE_RIGHT) {
switch (edge) { x1 = (usable.width + rc.gap) / 2;
case VIEW_EDGE_LEFT: }
case VIEW_EDGE_RIGHT: if (edge & VIEW_EDGE_LEFT) {
base_width = (usable.width - 3 * rc.gap) / 2; x2 = (usable.width - rc.gap) / 2;
base_height = usable.height - 2 * rc.gap; }
break; if (edge & VIEW_EDGE_DOWN) {
case VIEW_EDGE_UP: y1 = (usable.height + rc.gap) / 2;
case VIEW_EDGE_DOWN: }
base_width = usable.width - 2 * rc.gap; if (edge & VIEW_EDGE_UP) {
base_height = (usable.height - 3 * rc.gap) / 2; y2 = (usable.height - rc.gap) / 2;
break;
default:
case VIEW_EDGE_CENTER:
base_width = usable.width - 2 * rc.gap;
base_height = usable.height - 2 * rc.gap;
break;
} }
struct wlr_box dst = { struct wlr_box dst = {
.x = x_offset + usable.x, .x = x1 + usable.x,
.y = y_offset + usable.y, .y = y1 + usable.y,
.width = base_width, .width = x2 - x1,
.height = base_height, .height = y2 - y1,
}; };
if (view) { if (view) {
@ -2149,6 +2143,14 @@ view_edge_parse(const char *direction, bool tiled, bool any)
if (tiled) { if (tiled) {
if (!strcasecmp(direction, "center")) { if (!strcasecmp(direction, "center")) {
return VIEW_EDGE_CENTER; return VIEW_EDGE_CENTER;
} else if (!strcasecmp(direction, "up-left")) {
return VIEW_EDGE_UPLEFT;
} else if (!strcasecmp(direction, "up-right")) {
return VIEW_EDGE_UPRIGHT;
} else if (!strcasecmp(direction, "down-left")) {
return VIEW_EDGE_DOWNLEFT;
} else if (!strcasecmp(direction, "down-right")) {
return VIEW_EDGE_DOWNRIGHT;
} }
} }

View file

@ -660,6 +660,19 @@ xdg_toplevel_view_notify_tiled(struct view *view)
case VIEW_EDGE_DOWN: case VIEW_EDGE_DOWN:
edge = WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | WLR_EDGE_RIGHT; edge = WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | WLR_EDGE_RIGHT;
break; break;
case VIEW_EDGE_UPLEFT:
edge = WLR_EDGE_TOP | WLR_EDGE_LEFT;
break;
case VIEW_EDGE_UPRIGHT:
edge = WLR_EDGE_TOP | WLR_EDGE_RIGHT;
break;
case VIEW_EDGE_DOWNLEFT:
edge = WLR_EDGE_BOTTOM | WLR_EDGE_LEFT;
break;
case VIEW_EDGE_DOWNRIGHT:
edge = WLR_EDGE_BOTTOM | WLR_EDGE_RIGHT;
break;
/* TODO: VIEW_EDGE_CENTER? */
default: default:
edge = WLR_EDGE_NONE; edge = WLR_EDGE_NONE;
} }