feat: implement composable window snapping with dual-axis constraints and toggle-to-full behavior

Add SnapHorizontal/SnapVertical enums and SnapConstraint struct to Client for tracking snap state. Introduce layout_apply_snap_constraint() to position windows into quadrants or full axes based on stored snap flags. Extend key_snap_left/right with toggle logic that expands to full width on second press, and add new key_snap_up/down handlers for vertical snapping. Update shortcut documentation and help overlay to describe composable two-key sequences (e.g. Super+Left then Super+Up for top-left quarter).
This commit is contained in:
2025-12-28 10:21:03 +00:00
parent 64f9761116
commit ab36b7dd2c
17 changed files with 210 additions and 64 deletions
+12
View File
@@ -12,6 +12,7 @@
#include "decorations.h"
#include "notifications.h"
#include "layout.h"
#include "panel.h"
#include <string.h>
#include <stdlib.h>
@@ -46,6 +47,9 @@ Client *client_create(Window window)
client->prev = NULL;
client->mru_next = NULL;
client->mru_prev = NULL;
client->snap.horizontal = SNAP_H_NONE;
client->snap.vertical = SNAP_V_NONE;
client->snap.timestamp = 0;
XWindowAttributes wa;
int orig_width = 640, orig_height = 480;
@@ -396,6 +400,7 @@ void client_raise(Client *client)
XRaiseWindow(dwn->display, win);
notifications_raise_all();
panel_raise_all();
}
void client_lower(Client *client)
@@ -443,6 +448,13 @@ void client_move(Client *client, int x, int y)
return;
}
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
if (y < area_y) {
y = area_y;
}
client->x = x;
client->y = y;
client_configure(client);
+93 -50
View File
@@ -15,6 +15,7 @@
#include "applauncher.h"
#include "decorations.h"
#include "demo.h"
#include "layout.h"
#include <stdio.h>
#include <string.h>
@@ -530,9 +531,9 @@ void keys_setup_defaults(void)
keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial");
keys_bind(MOD_SUPER, XK_Down, key_news_next, "Next news article");
keys_bind(MOD_SUPER, XK_Down, key_snap_down, "Snap window bottom/full");
keys_bind(MOD_SUPER, XK_Up, key_news_prev, "Previous news article");
keys_bind(MOD_SUPER, XK_Up, key_snap_up, "Snap window top/full");
keys_bind(MOD_SUPER, XK_Return, key_news_open, "Open news article");
@@ -802,8 +803,12 @@ void key_show_shortcuts(void)
"Super+L Expand master area\n"
"Super+I Add to master\n"
"Super+D Remove from master\n"
"Super+Left Snap window left (50%)\n"
"Super+Right Snap window right (50%)\n"
"\n"
"=== Window Snapping (composable) ===\n"
"Super+Left Left 50% (2x=full width)\n"
"Super+Right Right 50% (2x=full width)\n"
"Super+Up Top 50% (2x=full height)\n"
"Super+Down Bottom 50% (2x=full height)\n"
"\n"
"=== AI Features ===\n"
"Super+A Show AI context\n"
@@ -851,10 +856,6 @@ void key_screenshot(void)
void key_snap_left(void)
{
if (dwn == NULL) {
return;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
@@ -862,42 +863,30 @@ void key_snap_left(void)
Client *c = ws->focused;
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
int work_height = dwn->screen_height;
if (dwn->config != NULL) {
if (dwn->config->top_panel_enabled) {
work_y += config_get_panel_height();
work_height -= config_get_panel_height();
}
if (dwn->config->bottom_panel_enabled) {
work_height -= config_get_panel_height();
}
}
int gap = config_get_gap();
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
c->x = work_x + gap;
c->y = work_y + gap;
c->width = (work_width / 2) - (gap * 2);
c->height = work_height - (gap * 2);
if (c->snap.horizontal == SNAP_H_LEFT) {
c->snap.horizontal = SNAP_H_FULL;
} else {
c->snap.horizontal = SNAP_H_LEFT;
}
if (c->snap.vertical == SNAP_V_NONE) {
c->snap.vertical = SNAP_V_FULL;
}
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
int gap = config_get_gap();
layout_apply_snap_constraint(c, area_x, area_y, area_w, area_h, gap);
client_configure(c);
decorations_render(c, true);
}
void key_snap_right(void)
{
if (dwn == NULL) {
return;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
@@ -905,32 +894,86 @@ void key_snap_right(void)
Client *c = ws->focused;
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
int work_height = dwn->screen_height;
if (dwn->config != NULL) {
if (dwn->config->top_panel_enabled) {
work_y += config_get_panel_height();
work_height -= config_get_panel_height();
}
if (dwn->config->bottom_panel_enabled) {
work_height -= config_get_panel_height();
}
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
if (c->snap.horizontal == SNAP_H_RIGHT) {
c->snap.horizontal = SNAP_H_FULL;
} else {
c->snap.horizontal = SNAP_H_RIGHT;
}
if (c->snap.vertical == SNAP_V_NONE) {
c->snap.vertical = SNAP_V_FULL;
}
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
int gap = config_get_gap();
layout_apply_snap_constraint(c, area_x, area_y, area_w, area_h, gap);
client_configure(c);
decorations_render(c, true);
}
void key_snap_up(void)
{
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
Client *c = ws->focused;
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
c->x = work_x + (work_width / 2) + gap;
c->y = work_y + gap;
c->width = (work_width / 2) - (gap * 2);
c->height = work_height - (gap * 2);
if (c->snap.vertical == SNAP_V_TOP) {
c->snap.vertical = SNAP_V_FULL;
} else {
c->snap.vertical = SNAP_V_TOP;
}
if (c->snap.horizontal == SNAP_H_NONE) {
c->snap.horizontal = SNAP_H_FULL;
}
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
int gap = config_get_gap();
layout_apply_snap_constraint(c, area_x, area_y, area_w, area_h, gap);
client_configure(c);
decorations_render(c, true);
}
void key_snap_down(void)
{
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
Client *c = ws->focused;
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
if (c->snap.vertical == SNAP_V_BOTTOM) {
c->snap.vertical = SNAP_V_FULL;
} else {
c->snap.vertical = SNAP_V_BOTTOM;
}
if (c->snap.horizontal == SNAP_H_NONE) {
c->snap.horizontal = SNAP_H_FULL;
}
int area_x, area_y, area_w, area_h;
layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);
int gap = config_get_gap();
layout_apply_snap_constraint(c, area_x, area_y, area_w, area_h, gap);
client_configure(c);
decorations_render(c, true);
}
+45
View File
@@ -229,6 +229,51 @@ int layout_count_tiled_clients(int workspace)
return count;
}
void layout_apply_snap_constraint(Client *c, int area_x, int area_y,
int area_w, int area_h, int gap)
{
if (c == NULL) {
return;
}
int half_w = area_w / 2;
int half_h = area_h / 2;
switch (c->snap.horizontal) {
case SNAP_H_LEFT:
c->x = area_x + gap;
c->width = half_w - gap * 2;
break;
case SNAP_H_RIGHT:
c->x = area_x + half_w + gap;
c->width = half_w - gap * 2;
break;
case SNAP_H_FULL:
c->x = area_x + gap;
c->width = area_w - gap * 2;
break;
case SNAP_H_NONE:
break;
}
switch (c->snap.vertical) {
case SNAP_V_TOP:
c->y = area_y + gap;
c->height = half_h - gap * 2;
break;
case SNAP_V_BOTTOM:
c->y = area_y + half_h + gap;
c->height = half_h - gap * 2;
break;
case SNAP_V_FULL:
c->y = area_y + gap;
c->height = area_h - gap * 2;
break;
case SNAP_V_NONE:
break;
}
}
const char *layout_get_name(LayoutType layout)
{
if (layout >= 0 && layout < LAYOUT_COUNT) {