macOS-style polish: fix seam, bidirectional Alt+Tab, minimized windows in switcher, rounded corners, deskbar context menu

Fix #1: Replace hardcoded 185px deskbar-offset in client.c and events.c with
DESKBAR_W constant (200) to eliminate the 15px gap between the WM frame and
deskbar edge.

Fix #2: Add Shift+Alt+Tab reverse-direction support — grab Mod1+Shift+XK_Tab
in wm.c, detect ShiftMask in events.c, and implement switcher_prev() in
switcher.c.

Fix #3: Include minimized windows in the Alt+Tab switcher. swl_populate() no
longer skips minimized clients; swl_mini[] tracks state so switcher_draw()
can dim them with bracketed titles and darker card backgrounds.

Fix #4: Rewrite themes/picom.conf with macOS-inspired settings — 10px corner
radius on normal windows, 8px on menus, soft downward shadow (radius 18,
offset-y 8, opacity 0.35), smooth fading (step 0.08), no dock/tray rounding.

Fix #5: Add per-app right-click context menu to the deskbar. Right-clicking
an app row opens a popup with Raise / Minimize / Close actions (Close shown
in red). Actions are sent as EWMH client messages (_NET_ACTIVE_WINDOW,
WM_CHANGE_STATE+IconicState, _NET_CLOSE_WINDOW) with matching handlers added
to the WM's on_client_message. Pointer grab closes the menu on any outside
click.
This commit is contained in:
Claude committed 2026-08-30 19:51:54 +00:00
1 parent 0d874617dd
commit 36641e77e2
8 files changed
+311 -42

No files matched your search

+3
View File
@@ -29,6 +29,7 @@
#define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */
/* ── geometry ────────────────────────────────────────────────────────── */
#define DESKBAR_W 200 /* must match DB_W in deskbar/include/deskbar.h */
#define TITLE_HEIGHT 28 /* macOS-style height */
#define TITLE_TAB_W 200
#define BORDER_W 1 /* thin 1px macOS-style border */
@@ -130,6 +131,7 @@ typedef struct WM {
int swl_count;
Window swl_wins[64];
char swl_titles[64][128];
int swl_mini[64]; /* 1 if corresponding entry is minimized */
} WM;
/* core */
@@ -175,6 +177,7 @@ int menu_item_at(int py);
/* switcher */
void switcher_show(WM *wm);
void switcher_next(WM *wm);
void switcher_prev(WM *wm);
void switcher_hide(WM *wm, int commit);
void switcher_draw(WM *wm);
+5 -5
View File
@@ -65,9 +65,9 @@ Client *client_add(WM *wm, Window win) {
c->x = wa.x;
c->y = wa.y;
}
/* never land behind deskbar (top-right panel ~180px wide) */
/* never land behind deskbar */
int sw = DisplayWidth(wm->dpy, wm->screen);
if (c->x + (int)c->w > sw - 185 && c->y < 200)
if (c->x + (int)c->w > sw - DESKBAR_W && c->y < 200)
c->y = 40;
/* fetch title */
@@ -228,7 +228,7 @@ void client_maximize(WM *wm, Client *c) {
int sh = DisplayHeight(wm->dpy, wm->screen);
/* leave room for deskbar on the right */
int deskbar_w = 185;
int deskbar_w = DESKBAR_W;
int new_w = sw - deskbar_w - BORDER_W * 2;
int new_h = sh - TITLE_HEIGHT - BORDER_W;
@@ -280,7 +280,7 @@ void client_snap_left(WM *wm, Client *c) {
if (c->maximized) client_unmaximize(wm, c);
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
int deskbar_w = 185;
int deskbar_w = DESKBAR_W;
int half_w = (sw - deskbar_w) / 2;
c->x = 0;
@@ -299,7 +299,7 @@ void client_snap_right(WM *wm, Client *c) {
if (c->maximized) client_unmaximize(wm, c);
int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(wm->dpy, wm->screen);
int deskbar_w = 185;
int deskbar_w = DESKBAR_W;
int half_w = (sw - deskbar_w) / 2;
c->x = half_w;
+38 -5
View File
@@ -33,7 +33,7 @@ static void check_snap(WM *wm, Client *c) {
} else {
client_snap_left(wm, c);
}
} else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - 185 - SNAP_DIST) {
} else if (c->x + (int)(c->w + BORDER_W * 2) >= sw - DESKBAR_W - SNAP_DIST) {
client_snap_right(wm, c);
} else if (c->y <= SNAP_DIST) {
client_maximize(wm, c);
@@ -95,10 +95,34 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) {
}
void on_client_message(WM *wm, XClientMessageEvent *e) {
Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False);
Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False);
Atom net_close = XInternAtom(wm->dpy, "_NET_CLOSE_WINDOW", False);
Atom wm_change_state = XInternAtom(wm->dpy, "WM_CHANGE_STATE", False);
if (e->message_type == net_active) {
Client *c = client_find_by_win(wm, e->window);
if (c) client_restore(wm, c);
return;
}
if (e->message_type == net_close) {
Client *c = client_find_by_win(wm, e->window);
if (c) {
XEvent ev = {0};
ev.xclient.type = ClientMessage;
ev.xclient.window = c->win;
ev.xclient.message_type = wm->wm_protocols;
ev.xclient.format = 32;
ev.xclient.data.l[0] = wm->wm_delete_window;
ev.xclient.data.l[1] = CurrentTime;
XSendEvent(wm->dpy, c->win, False, NoEventMask, &ev);
}
return;
}
/* WM_CHANGE_STATE IconicState → minimize */
if (e->message_type == wm_change_state && e->data.l[0] == 3) {
Client *c = client_find_by_win(wm, e->window);
if (c && !c->minimized) client_minimize(wm, c);
return;
}
}
@@ -263,10 +287,19 @@ void on_key_press(WM *wm, XKeyEvent *e) {
if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused)
send_delete(wm, wm->focused->win);
/* Alt+Tab — visual switcher */
/* Alt+Tab / Shift+Alt+Tab — visual switcher */
if ((e->state & Mod1Mask) && sym == XK_Tab) {
if (wm->swl_visible) switcher_next(wm);
else if (wm->clients) switcher_show(wm);
int backward = (e->state & ShiftMask) != 0;
if (wm->swl_visible) {
if (backward) switcher_prev(wm);
else switcher_next(wm);
} else if (wm->clients) {
switcher_show(wm);
if (backward && wm->swl_visible) {
wm->swl_idx = wm->swl_count - 1;
switcher_draw(wm);
}
}
return;
}
+34 -12
View File
@@ -8,17 +8,19 @@
static void swl_populate(WM *wm) {
wm->swl_count = 0;
/* current focused window goes first */
/* current focused (non-minimized) window goes first */
if (wm->focused && !wm->focused->minimized) {
wm->swl_wins[0] = wm->focused->win;
wm->swl_wins[0] = wm->focused->win;
wm->swl_mini[0] = 0;
strncpy(wm->swl_titles[0], wm->focused->title, 127);
wm->swl_titles[0][127] = '\0';
wm->swl_count = 1;
}
/* all other windows, including minimized ones */
for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) {
if (c->minimized) continue;
if (wm->focused && c == wm->focused) continue;
wm->swl_wins[wm->swl_count] = c->win;
wm->swl_wins[wm->swl_count] = c->win;
wm->swl_mini[wm->swl_count] = c->minimized;
strncpy(wm->swl_titles[wm->swl_count], c->title, 127);
wm->swl_titles[wm->swl_count][127] = '\0';
wm->swl_count++;
@@ -61,29 +63,34 @@ void switcher_draw(WM *wm) {
for (int vi = 0; vi < vis; vi++) {
int i = vstart + vi;
int selected = (i == wm->swl_idx);
int mini = wm->swl_mini[i];
int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD);
int iy = SWL_PAD;
unsigned long card_bg = color_alloc(wm, selected ? COLOR_TITLE_ACTIVE : 0x1C2338);
unsigned long card_bg = color_alloc(wm,
selected ? COLOR_TITLE_ACTIVE : (mini ? 0x141820 : 0x1C2338));
XSetForeground(wm->dpy, wm->gc, card_bg);
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H);
XSetForeground(wm->dpy, wm->gc,
color_alloc(wm, selected ? COLOR_BORDER_LIGHT : 0x242C3E));
color_alloc(wm, selected ? COLOR_BORDER_LIGHT : (mini ? 0x202028 : 0x242C3E)));
XDrawRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W - 1, SWL_ITEM_H - 1);
/* letter icon */
/* letter icon — dimmed for minimized */
int icon_x = ix + (SWL_ITEM_W - SWL_ICON_SZ) / 2;
int icon_y = iy + 8;
XSetForeground(wm->dpy, wm->gc,
color_alloc(wm, selected ? 0x285898 : 0x182538));
color_alloc(wm, selected ? 0x285898 : (mini ? 0x1A1E28 : 0x182538)));
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, icon_x, icon_y, SWL_ICON_SZ, SWL_ICON_SZ);
char letter[2] = { wm->swl_titles[i][0] ? wm->swl_titles[i][0] : '?', '\0' };
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32;
unsigned long icon_rgb = selected ? COLOR_TITLE_TEXT_ACT
: (mini ? 0x3A4860
: COLOR_TITLE_TEXT_INACT);
XftColor icon_col;
xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &icon_col);
xft_color(wm, icon_rgb, &icon_col);
XGlyphInfo ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext);
int lx = icon_x + (SWL_ICON_SZ - (int)ext.width) / 2;
@@ -91,10 +98,19 @@ void switcher_draw(WM *wm) {
XftDrawStringUtf8(xd, &icon_col, wm->font_title, lx, ly, (const FcChar8 *)letter, 1);
XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col);
/* title below icon */
const char *title = wm->swl_titles[i];
/* title below icon — prefix [..] for minimized */
char title_buf[132];
const char *base_title = wm->swl_titles[i];
if (mini && !selected)
snprintf(title_buf, sizeof(title_buf), "[%s]", base_title);
else
snprintf(title_buf, sizeof(title_buf), "%s", base_title);
const char *title = title_buf;
XftColor title_col;
xft_color(wm, selected ? COLOR_TITLE_TEXT_ACT : COLOR_TITLE_TEXT_INACT, &title_col);
unsigned long title_rgb = selected ? COLOR_TITLE_TEXT_ACT
: (mini ? 0x3A4860
: COLOR_TITLE_TEXT_INACT);
xft_color(wm, title_rgb, &title_col);
XGlyphInfo text_ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title,
(const FcChar8 *)title, strlen(title), &text_ext);
@@ -163,6 +179,12 @@ void switcher_next(WM *wm) {
switcher_draw(wm);
}
void switcher_prev(WM *wm) {
if (!wm->swl_visible) return;
wm->swl_idx = (wm->swl_idx - 1 + wm->swl_count) % wm->swl_count;
switcher_draw(wm);
}
void switcher_hide(WM *wm, int commit) {
if (!wm->swl_visible) return;
XUngrabKeyboard(wm->dpy, CurrentTime);
+1
View File
@@ -90,6 +90,7 @@ void wm_init(WM *wm) {
/* global key grabs */
struct { KeySym sym; unsigned mod; } grabs[] = {
{ XK_Tab, Mod1Mask },
{ XK_Tab, Mod1Mask | ShiftMask }, /* Shift+Alt+Tab — backwards */
{ XK_F4, Mod1Mask },
{ XK_F9, Mod1Mask }, /* minimize */
{ XK_F10, Mod1Mask }, /* maximize */