feat: add composable window snapping

This commit is contained in:
retoor 2025-12-28 11:21:03 +01:00
parent 395583aea9
commit beda6ec573
17 changed files with 210 additions and 64 deletions

BIN
bin/dwn

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -18,7 +18,8 @@ build/client.o: src/client.c include/client.h include/dwn.h \
/usr/include/dbus-1.0/dbus/dbus-server.h \
/usr/include/dbus-1.0/dbus/dbus-signature.h \
/usr/include/dbus-1.0/dbus/dbus-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/layout.h
/usr/include/dbus-1.0/dbus/dbus-threads.h include/layout.h \
include/panel.h
include/client.h:
include/dwn.h:
include/atoms.h:
@ -46,3 +47,4 @@ include/notifications.h:
/usr/include/dbus-1.0/dbus/dbus-syntax.h:
/usr/include/dbus-1.0/dbus/dbus-threads.h:
include/layout.h:
include/panel.h:

Binary file not shown.

View File

@ -18,7 +18,8 @@ build/keys.o: src/keys.c include/keys.h include/dwn.h include/client.h \
/usr/include/dbus-1.0/dbus/dbus-signature.h \
/usr/include/dbus-1.0/dbus/dbus-syntax.h \
/usr/include/dbus-1.0/dbus/dbus-threads.h include/news.h \
include/applauncher.h include/decorations.h include/demo.h
include/applauncher.h include/decorations.h include/demo.h \
include/layout.h
include/keys.h:
include/dwn.h:
include/client.h:
@ -49,3 +50,4 @@ include/news.h:
include/applauncher.h:
include/decorations.h:
include/demo.h:
include/layout.h:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -62,6 +62,26 @@ typedef enum {
CLIENT_MAXIMIZED = (1 << 5)
} ClientFlags;
typedef enum {
SNAP_H_NONE = 0,
SNAP_H_LEFT,
SNAP_H_RIGHT,
SNAP_H_FULL
} SnapHorizontal;
typedef enum {
SNAP_V_NONE = 0,
SNAP_V_TOP,
SNAP_V_BOTTOM,
SNAP_V_FULL
} SnapVertical;
typedef struct {
SnapHorizontal horizontal;
SnapVertical vertical;
long timestamp;
} SnapConstraint;
typedef struct Client Client;
typedef struct Workspace Workspace;
typedef struct Monitor Monitor;
@ -80,6 +100,7 @@ struct Client {
unsigned int workspace;
char title[256];
char class[64];
SnapConstraint snap;
Client *next;
Client *prev;
Client *mru_next;

View File

@ -83,6 +83,8 @@ void key_show_shortcuts(void);
void key_start_tutorial(void);
void key_snap_left(void);
void key_snap_right(void);
void key_snap_up(void);
void key_snap_down(void);
void key_start_demo(void);
void tutorial_start(void);

View File

@ -17,6 +17,9 @@ void layout_arrange_monocle(int workspace);
int layout_get_usable_area(int *x, int *y, int *width, int *height);
int layout_count_tiled_clients(int workspace);
void layout_apply_snap_constraint(Client *c, int area_x, int area_y,
int area_w, int area_h, int gap);
const char *layout_get_name(LayoutType layout);
const char *layout_get_symbol(LayoutType layout);

View File

@ -250,13 +250,37 @@
<td><kbd>Super</kbd> + <kbd>D</kbd></td>
<td>Decrease master window count</td>
</tr>
</tbody>
</table>
</div>
<h2 id="snapping">Window Snapping (Composable)</h2>
<p style="color: var(--text-muted); margin-bottom: 1rem;">
Snapping shortcuts are composable: press Super+Left then Super+Up for top-left quarter. Press the same key twice to expand to full in that axis.
</p>
<div class="table-wrapper">
<table class="shortcuts-table">
<thead>
<tr>
<th style="width: 40%;">Shortcut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Super</kbd> + <kbd>Left</kbd></td>
<td>Snap window to left half (50% width)</td>
<td>Snap left 50% (press twice for full width)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Right</kbd></td>
<td>Snap window to right half (50% width)</td>
<td>Snap right 50% (press twice for full width)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Up</kbd></td>
<td>Snap top 50% (press twice for full height)</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Down</kbd></td>
<td>Snap bottom 50% (press twice for full height)</td>
</tr>
</tbody>
</table>
@ -291,7 +315,7 @@
</div>
<h2 id="news">News Ticker</h2>
<p style="color: var(--text-muted); margin-bottom: 1rem;">
Navigate the scrolling news ticker displayed in the bottom panel.
The scrolling news ticker is displayed in the bottom panel.
</p>
<div class="table-wrapper">
<table class="shortcuts-table">
@ -302,14 +326,6 @@
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Super</kbd> + <kbd>Down</kbd></td>
<td>Next news article</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Up</kbd></td>
<td>Previous news article</td>
</tr>
<tr>
<td><kbd>Super</kbd> + <kbd>Return</kbd></td>
<td>Open current article in browser</td>
@ -380,7 +396,7 @@
<kbd>Super</kbd>+<kbd>H</kbd>/<kbd>L</kbd> Resize master
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Super</kbd>+<kbd>Left</kbd>/<kbd>Right</kbd> Snap 50%
<kbd>Super</kbd>+Arrows Composable snap
</li>
<li style="padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<kbd>Shift</kbd>+<kbd>F1-9</kbd> Move to workspace

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

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

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) {